Compare commits

...
318 Commits
Author SHA1 Message Date
GitHub Actions a0706d9b51 chore: release v4.8.2
Post release / release (push) Canceled after 0s
2024-12-17 17:05:26 +00:00
Omar LópezandKeith Williams 2eb385af48 feat: allows forcing/skipping calendar cache serving (#18224)
* feat: allows forcing/skipping calendar cache serving

Signed-off-by: Omar López <zomars@me.com>

* Update features.repository.ts

* Added to HNB

* type fixes

* Update packages/prisma/migrations/20241216000000_add_calendar_cache_serve/migration.sql

Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* Update packages/prisma/zod-utils.ts

Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* Update selectedCalendar.ts

---------

Signed-off-by: Omar López <zomars@me.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-12-17 16:54:55 +00:00
Lauris Skraucis a0f986f949 feat: v2 teams CRUD (#18019)
* feat: teams endpoints

* refactor: organization teams reuse team types

* refactor: move team specific functions out of organizations-teams.repository

* supporting files

* regenerate docs

* feat: stripe service to generate team creation checkout

* refactor: request payment when creating a team

* fix: tests

* fix env

* driveby: event type name and email booking fields

* finish merge main
2024-12-17 14:19:57 +00:00
Keith Williams b0094bad2d perf: More tracing for handleNewBooking (#18233) 2024-12-17 12:29:41 +00:00
Rajiv Sahal c732c06835 fix: bookerEmbed atom (#18168)
* update translations

* add custom hook for platform embed

* update event type ouput DTO response

* fixup

* display terms and conditions for embed

* update v2 docs

* PR feedback

* update docs

* PR feedback

* update docs

* update output DTO

* update docs

* update output typing

* docs update

* resolve merge conflicts

* merge conflicts resolving

* resolve merge conflicts

* fixup

* update docs
2024-12-17 12:07:47 +00:00
Anik Dhabal Babu c1334475c5 fix: private team does not hide team members from manged event type (#18166) 2024-12-17 17:05:15 +05:30
Lauris Skraucis b35dc43011 feat: v2 wildcard redirectURIs (#18165)
* feat: isOriginAllowed function

* feat: use isOriginAllowed in v2

* refactor: shorten isOriginAllowed
2024-12-17 11:21:59 +00:00
Morgan c7dc3ecc49 chore: add TLD in username of managed users (#18229)
* chore: add TLD in username of managed users

* fixup! chore: add TLD in username of managed users

* bump platform libraries
2024-12-17 12:45:48 +02:00
Somay Chauhan a613c975d3 refactor: added ApiPropertyOptional decorator for optional fields in api for better documentation (#18208)
* update decorators 1

* update decorators 2

* update decorators 3

* update decorators 4

* update decorators 4

* update decorators 6

* update decorators 7

* added documentation,json and openapi.json

* implemented feedback

* remove default values from update dto's

* update

* removed default from redirectUris
2024-12-17 10:29:07 +00:00
Morgan 5fad7c9478 fix: redirect on calendar oauth denied auth (#18228) 2024-12-17 09:05:10 +00:00
Udit Takkar 318470d866 fix: webhook improvement (#18220) 2024-12-17 14:32:38 +05:30
Morgan a0ce42e375 chore: improve prisma exception filter messages (#18227)
* chore: improve prisma exception filter messages

* fixup! chore: improve prisma exception filter messages
2024-12-17 08:40:19 +00:00
Anik Dhabal Babu 5293a5cc54 fix: column f.userid does not exist (#18215) 2024-12-17 08:37:48 +00:00
Benny Joo 42aefc9032 perf: limit # of host members for OG image (#18219)
* fetch only 5 hosts and only first user

* refactor

* limit to 3
2024-12-17 08:15:11 +00:00
Keith Williams 14acf4e9bc chore: New booking perf tracing (#18212)
* chore: Add perf tracing for handleNewBooking

* Added a few more to the event manager

* Added the sentry wrapper that enables perf tracing

* Revert "Added a few more to the event manager"

This reverts commit ccf3ee5200466b8b1d51f5663363fbb9dfd9a06f.

* Fixed where we wrap sentry

* Reverse sentry wrapper
2024-12-17 08:51:00 +01:00
Lauris Skraucis 80815bd8a5 feat: Booker atom slots callbacks (#18203) 2024-12-17 12:58:18 +05:30
Benny Joo 9ed8face03 Revert "feat: remove setup-availability step in onboarding flow (#17799)" (#18205)
This reverts commit a466e8eef7.
2024-12-16 22:03:41 +00:00
Udit Takkar 18a107d081 fix: trigger guest no show bug (#18164)
* fix: trigger guest no show

* test: add tests for trigger guest no show
2024-12-16 11:43:03 -05:00
Alex van Andel 04d7537e2f perf: improve insights where query and type fix (#18211)
* perf: insights perf improvement

* removed additional const
2024-12-16 15:50:20 +00:00
Udit Takkar 423b17a819 refactor: getContacts (#18137) 2024-12-16 10:17:59 -05:00
GitHub Actions 0466b7ebd6 chore: release v4.8.1
Post release / release (push) Canceled after 0s
2024-12-16 14:35:27 +00:00
Lauris Skraucis 09329707c1 fix: bookings parsing errors (#18200) 2024-12-16 16:31:04 +02:00
Anik Dhabal Babu 1773aba378 fix: fix data expose issue in routing form insights (#18207) 2024-12-16 15:30:25 +01:00
Somay ChauhanandMorgan f8be2b3833 feat: added office 365 video to conferencing atoms (#18067)
* feat: added office 365 video to conferencing atoms

* added documentation for conferencing atoms

* added props table to the documentation

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-12-16 16:20:59 +02:00
Eunjae Leeandsean-brydon 79d488faca feat: insights filter refactoring (#18167)
* Reapply "feat: add text & numeric filters to insights routing responses (#18016)"

This reverts commit 49070b96ae.

* support SINGLE_SELECT

* fix type error

* put back ts-expect-error

* remove playground

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-12-16 13:47:28 +00:00
Venkata Rohan KambhampatiandUdit Takkar da2c57c302 fix: Booking questions order correction in confirmation email (#18192)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-16 09:33:04 +00:00
Keith Williams abaf03adde fix: Next.js bundle analysis build error (#18193) 2024-12-15 17:40:59 +05:30
GitHub Actions 5b353c3d0c chore: release v4.8.0
Post release / release (push) Canceled after 0s
2024-12-15 11:29:06 +00:00
Benny Joo 1f4cda31ac update app layout (#17126) 2024-12-14 18:42:43 -05:00
Calcom BotandReplexica 73d1a6f468 feat: update translations via @replexica (#18158)
Co-authored-by: Replexica <support@replexica.com>
2024-12-14 19:05:59 +00:00
Benny JooandAlex van Andel f72748b6f2 fix: trpc/react-query — only one query client should exist (#18179)
* use prefetch function in ScheduleListItem and refactor

* modify app router trpc provider to match the original configs

* revert unneeded change

* move trpc provider up and do refactors

* remove log

* add comment

* fix: Some typing style fixes

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-13 20:26:51 -05:00
Benny Joo d12fc78fea chore: add viewport / favicon metadata (#18177)
* fix favicon name

* add viewport metadata for PageWrapperAppDir

* fix

* fix

* refactor
2024-12-13 20:25:03 +00:00
Carina WollendorferandCarinaWolli 3d26332443 fix: change trigger time for form submitted not booked webhook (#18176)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-12-13 20:16:11 +00:00
Omar López bdb490c894 Revert "fix: correct line-breaks in calendar event description" (#18175) 2024-12-13 20:03:56 +00:00
Morgan 01dbc686b9 chore: bump platform libraries version for api-v2 (#18174) 2024-12-13 19:35:18 +00:00
Carina WollendorferandCarinaWolli 376961f30d fix: don't send reassingment email to attendee for past events (#18173)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-12-13 19:02:54 +00:00
Morgan 18a0eb9736 fix: improve getAllUserBooking filter by teamIds (#18171)
* fix: improve getAllUserBooking filted by teamIds

* fixup! fix: improve getAllUserBooking filted by teamIds
2024-12-13 20:18:58 +02:00
Carina WollendorferandCarinaWolli 84fb4f68b2 fix: wrong workflow notifications when enabling workflows (#18169)
* only schedule workflow emails not send

* fix if statement

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-12-13 11:58:30 -05:00
Udit Takkar b5ab6fe3fa fix: add no show guest (#18075)
* fix: add booker no show

* chore: update name
2024-12-13 16:00:17 +00:00
Anik Dhabal BabuandUdit Takkar 645985c783 fix: Invalid time zone specified: Etc/Unknown (#18121)
* fix: Invalid time zone specified: Etc/Unknown

* update

* Update PaymentPage.tsx

* update

* update one more file

* address review

* update test

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-13 15:29:02 +00:00
GitHub Actions a84513eda5 chore: release v4.7.25
Post release / release (push) Canceled after 0s
2024-12-13 14:19:52 +00:00
Benny Joo b97331aeb3 chore: /availability - Remove Pages Router / Render App Router (#16517)
* /availability: Remove Pages Router / Render App Router

* comment out server side fetching

* fix

* refactor for safer code

* add to config.matcher
2024-12-13 12:44:57 +00:00
Morgan 851d96423b chore: small changes for platform booker embed (#18159) 2024-12-13 13:24:19 +01:00
Anik Dhabal BabuandPeer Richelsen ef830f11ec fix: Hyperlinks not highlighted in eventmeta (#18120)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-12-13 13:23:25 +01:00
Keith Williams 1ab4ec9c22 chore: Upgrade actions/upload-artifact to v4 (#18160) 2024-12-13 11:32:50 +00:00
Benny Joo 656a5bf423 chore: remove hardcoded strings (#18156) 2024-12-13 09:54:28 +01:00
Benny Joo 8a6450e854 chore: use getTranslate instead (#18148) 2024-12-13 13:06:47 +05:30
Benny Joo 51841e5e9c fix: ensure migrated pages are added to config.matcher in middleware (#18153) 2024-12-13 12:43:08 +05:30
Benny Joo 59dfcaa2fa chore: fix misleading test names (#18154) 2024-12-13 04:32:52 +00:00
Omar López 0d24706380 refactor: improved observability in google calendar service (#18147) 2024-12-13 03:47:43 +00:00
Alex van Andel 87b6e0b49c fix: Prisma validation error in generateMetadata (workflows) (#18149) 2024-12-13 03:33:19 +00:00
Calcom BotandReplexica 4244ca3117 feat: update translations via @replexica (#18139)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2024-12-12 20:48:07 -05:00
Carina WollendorferandCarinaWolli 8d0f0d4899 fix: default to 100 when attribute weight undefined (#18145)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-12-12 22:57:43 +00:00
Anik Dhabal Babu 6463983d55 fix: Searching users in admin page not working (#18140) 2024-12-12 19:13:00 +00:00
Rajiv Sahal c8661563c8 fix: update get booking output for v2 (#18082)
* also include email field for attendees

* update tests

* update tests

* updating more tests

* fixup

* fixup

* fixup fixup
2024-12-12 18:55:28 +00:00
Sanchit Tewari 2262d56491 fix: show tooltip below the delete button (#18135) 2024-12-12 18:53:25 +00:00
GitHub Actions 6b1c747c0e chore: release v4.7.24
Post release / release (push) Canceled after 0s
2024-12-12 17:22:03 +00:00
Benny Joo 04036c32a7 fix: filling users logic (#18128)
* fix filling users logic

* fix type check

* refactor
2024-12-12 17:12:56 +00:00
Alex van Andel 49070b96ae Revert "feat: add text & numeric filters to insights routing responses (#18016)"
This reverts commit a7e7561efa.
2024-12-12 17:10:49 +00:00
GitHub Actions 237fea0a41 chore: release v4.7.23
Post release / release (push) Canceled after 0s
2024-12-12 16:40:04 +00:00
a7e7561efa feat: add text & numeric filters to insights routing responses (#18016)
* feat: improve text filters (WIP)

* move function to bottom

* apply some styles

* fix selection of TextFilterOptions

* rename value to operand

* remove unused file

* merge filters/filters into filters/utils

* fix regression of not putting url params correctly

* move makeWhereClause to filters/utils

* fix negative, empty, and not empty operators

* fix initial filtering from search state (url)

* fix type errors

* do not send an empty array to query

* update yarn.lock

* i18n for text filter operators

* extract logic as useColumnFilters()

* add missing import

* fix type error

* revert yarn.lock

* use i18n

* insensitive text match

* move data-table to @calcom/features

* fix type errors

* fix type errors

* fix type errors

* feat: support DataTable filters for Insights Routing WIP

* remove unused filters

* remove additionalFilters and fix types

* clean up filter components

* support icons for ActiveFilters

* support filters on json

* fix filter ui

* fix type error and clean up

* revert changes

* revert change

* clean up

* revert change

* fix compatibility with insights booking page

* remove unused params

* fix type errors

* update yarn.lock

* fix field filter and adjust ui

* chore: update yarn.lock

* fix text filter

* add Clear Filters button

* add more test data

* feat: support numeric filter WIP

* feat: support numeric filter

* feat: move People to an external filter

* fixing query states

* rename state to activeFilters

* add useDataTable

* fix ResponseCellValue bug and enable resizing for RoutingFormResponsesTable

* fix response values

* truncate attribute text

* seed booking attendees

* fix accessor (no actual change though)

* add a gap

* rename

* use pathname as default identifier for DataTable

* fix type error

* support text filter for assignment reason

* Apply suggestions from code review

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* rename fields to headers

* use safeParse

* fix type error

* push yarn.lock with faker

* fix MemberList

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: sean-brydon <sean@cal.com>
2024-12-12 16:06:30 +00:00
Alex van Andel e366c10521 Convert update statement into unique select in getServerSession 2024-12-12 15:50:44 +00:00
Morgan 2ace1d1017 chore: add successRedirectUrl to event-types api v2 (#18136)
* chore: add successRedirectUrl to event-types api v2

* chore: add successRedirectUrl to event-types api v2 patch

* chore: add successRedirectUrl to event-types api v2 patch
2024-12-12 16:32:35 +02:00
sean-brydon fbe31ad82d early return if no users are found to prevent prisma error (#18133) 2024-12-12 13:37:39 +01:00
sean-brydonandUdit Takkar ee3c08918a feat: weights per attribute UI (#18109)
* wip attribut weights

* edit sheet and display in table

* add shrink so badges dont split

* push changes to support weights on csv export

* fix typing issue

* type error

* fall back to 100 weight

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-12 11:54:10 +00:00
Lauris Skraucis b6d6b7cdb4 fix: store default booking fields when creating event (#18108)
* refactor: store default booking fields when creating event

* Revert "refactor: store default booking fields when creating event"

This reverts commit 78b7c4eda2be61420b4410bc80a3d77b5557a967.

* refactor: v2 return default booking fields if they are null

* test: v2 returning default booking fields if they are null

* chore: platform-libraries export getBookingFieldsWithSystemFields
2024-12-12 10:58:45 +01:00
Hariom BalharaandUdit Takkar 1b82552e76 Fix crm error log (#18105)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-12 13:15:14 +05:30
Joe Au-Yeung 13a90aba30 Fix logic (#18131) 2024-12-11 21:03:51 -07:00
Alex van Andel de0de64a16 build: Node alpine libssl symlink fix (#18127) 2024-12-12 00:39:36 +00:00
GitHub Actions 8e772d9046 chore: release v4.7.22
Post release / release (push) Canceled after 0s
2024-12-11 23:04:54 +00:00
Omar López 7ac7a14beb refactor: improved observability for Salesforce (#18125)
* refactor: better observability for Salesforce

* Update crmManager.ts

* Update CrmService.d.ts
2024-12-11 22:57:43 +00:00
Alex van Andel 9c127cbfc2 Revert "feat: migrate /apps/routing-forms/forms to /routing (#18001)"
This reverts commit 1de007ad21.
2024-12-11 22:16:50 +00:00
GitHub Actions 187a2c576e chore: release v4.7.22 2024-12-11 21:54:50 +00:00
5e8d994ac1 feat: Change 'add override' to 'save override' (#17434)
* feat: Change 'add override' to 'save override'

* fix: type check

* fix: type check

* autosave in availability edit sheet

* remove unused translation key

* added requested changes

* added requested changes

---------

Co-authored-by: Prashant Varma <prashantvarma5083@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-12-11 21:20:42 +00:00
VishnuandPrashant Varma 1de007ad21 feat: migrate /apps/routing-forms/forms to /routing (#18001)
* feat: add getServerSideProps and getStaticProps

* feat: add routing pages view

* feat: add routing and related pages

* feat: setup redirect

* feat: remove routing configs in getStaticProps and getServerSideProps

* feat: add redirects in next.config and middleware

* feat: add split icon

* feat: rename href to /routing/forms

* feat: fix redirects

* feat: finish routing page

* feat: remove /apps/routing-forms/forms redirect in next-config

* feat: revert back redirects in serverSideProps

* fix: minor issues

* fix: routing-form url cloning

* chore: remove comments

* fix: routing page link

* fix: routing hover link

---------

Co-authored-by: Prashant Varma <prashantvarma5083@gmail.com>
2024-12-11 21:18:46 +00:00
michaelvaletteandYounes Henni 06494a6999 fix: correct line-breaks in calendar event description (#18077)
* fix: correct line-breaks in calendar event description

* Update packages/lib/CalEventParser.ts

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

* fix: review comments

* fix: better add spacing on who

---------

Co-authored-by: Younes Henni <youneshenniwrites@gmail.com>
2024-12-11 21:18:29 +00:00
Benny Joo 016c47d535 fix: do not export client components directly in app routes (#18122) 2024-12-11 21:17:53 +00:00
Alex van Andel 2aeb290bda fix: Error on webhooks page exporting client only comp directly (#18118)
* fix: Error on webhooks page exporting client only comp directly

* Wrap correctly, WebhooksView contains dynamic logic in the SettingsHeader
2024-12-11 19:42:04 +00:00
Keith Williams 95b4f67842 chore: Add CODEOWNERS (#18116)
* chore: Add CODEOWNERS

* Added the calcom org

* Removed non-critical calendar apps
2024-12-11 18:29:41 +00:00
Benny JooandUdit Takkar 1a4afe06c8 chore: Remove Pages Router / Render App Router - /video (#16518)
* /video: Remove Pages Router / Render App Router

* fix

* add meeting_unavailable i18n

* remove <Head> from videos view

* add correct metadata to video/[uid]

* add safety to videos-single-view logic

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-11 18:51:30 +01:00
Keith Williams 0eb72d66a7 revert: "fix: Request permissions to allow events to be created on shared Office365/Outlook calendars (#17760)" (#18111)
This reverts commit 1680cba5a5.
2024-12-11 14:57:57 +00:00
Eunjae Lee 7d2523e823 fix: prevent infinite rerendering of DataTable (#18087) 2024-12-11 14:03:17 +00:00
Benny JooandKeith Williams 6739ed3a87 feat: Title - AI translation for booking pages (#17794)
* feat: AI description - DB model + frontend + backend (fetch only)

* fix types and add validation to backend

* improve log

* improve

* import type

* add EventTypeTranslationRepository

* fix replexica error

* fix replexica error

* fix

* fix test

* wip

* wip

* add createManyDescriptionTranslations method

* improve logic

* refactor

* update replexica type

* improve typing

* Renamed descriptionTranslations to fieldTranslations

* log errors in replexica service

* handle translations in background

* don't create, upsert instead

* make sure that description is updated

* simplify

* fix

* Update packages/trpc/server/routers/viewer/eventTypes/update.handler.ts

* improve filter logic in frontend

* implement background job using Tasker

* update schema

* update schema again

* rename

* rename id to uid

* add migration

* fix type

* update

* update schema and migration sql

* fix migration

* fix type check

* fix type check 2

* improve find logic in EventMeta

* add more validation

* add logger

* rename

* upgrade replexica/sdk to 0.7.0

* upgrade replexica to 0.7.0

* use batchLocalizeText

* add comments

* override browser locale if user is a signed in calcom user

* fix type in replexica

* fix migration sql

* fix type

* use dynamic import

* do not use batchLocalizeText

* chore: drop sourceLang, targetLang and id columns

* fix

* wip

* add more validations

* fix type error

* address comment

* refactor

* update booking page to use translated title

* fix type error

* fix

* revert yarn.lock changes

* remove unused import

* remove duplication migrate.sql file

* fix test

* remove test tags

* fix function name

* refactor

* fix type check

* use Promise.all

* remove unneeded code

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-12-11 14:18:32 +01:00
Anik Dhabal Babu dd3505ad8b fix: Insights team filter behavior (#18084)
* fix: Insights team filter behavior

* fix type error
2024-12-11 14:43:25 +05:30
Benny Joo ce9df65b5d chore: Remove Pages Router / Use App Router - /workflows (#16519)
* /workflows: Remove Pages Router / Render App Router

* fix

* unneeded changes

* disable server side fetching for now

* revert unneeded changes
2024-12-11 09:51:22 +01:00
sean-brydon fc6e39c78a feat: assigned bookings per period (#17940)
* wip

* bring in users median status for selected range

* merge

* i18n

* routed to talbe period scroll x-y infinite

* insights i18n + hover card on user

* add search by name or email

* wip download button

* fix type error

* type fix

* type errors + status

* type error
2024-12-11 08:12:06 +00:00
Calcom BotandReplexica 931f3d3396 feat: update translations via @replexica (#18100)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2024-12-11 00:12:17 +00:00
GitHub Actions 8a6fac6748 chore: release v4.7.21
Post release / release (push) Canceled after 0s
2024-12-11 00:07:34 +00:00
Benny Joo af43ede38e remove pages router and use app router for insights pages (#18088) 2024-12-10 16:56:56 -07:00
Benny Joo a66eb9dfac chore: remove extra fetching and get user locale from headers in settings pages (#18048)
* add settings pages to matcher

* fetch user locale from headers in settings pages
2024-12-10 16:56:12 -07:00
Benny Joo 5ba676b014 chore: app router migration - /enterprise and d/[link]/[slug] (#18101)
* use app router for /enterprise page

* use app router for /d page
2024-12-10 23:48:24 +00:00
Benny Joo c2c082c105 chore: Remove Pages Router / Use App Router - more, maintenance, upgrade, connect-and-join (#16531)
* Remove Pages Router / Render App Router

* remove head component from maintenance view

* use i18n metadata
2024-12-10 14:37:25 -07:00
Joe Au-Yeung 44d6c1f3fb feat: Show routing reason on booking page [CAL-4814] (#18053) 2024-12-10 14:31:48 -07:00
Benny Joo a466e8eef7 feat: remove setup-availability step in onboarding flow (#17799)
* feat: make the onboarding flow 3-step for google signups

* fix

* refactor

* rename

* improve tests

* remove test tag

* fix test and type error

* fix

* feat: remove setup-availability step in onboarding flow

* update test

* revert unneeded change

* add safety

* update create schedule options
2024-12-10 14:30:23 -07:00
VijayandPrashant Varma 370345da22 fix: Not able to create consecutive ooo (#17388)
* fix: able to create consecutive ooo

* added e2e test

* chore

* update to allow overlapping ooo and test for same

* refactor to reduce repeating code in tests

* nit

* added testcases for consecutive and reverse redirects

* fix:duplicate OOO creation

* prevent duplicate entries, also added test

* chore

* update after merges

* correct datetype in prisma query

* update to changes in rdp after merge

---------

Co-authored-by: Prashant Varma <prashantvarma5083@gmail.com>
2024-12-10 19:46:28 +05:30
Kartik SainiandKeith Williams a1fc6668d5 feat: ability to require an email (#18057)
* feat: ability to require an email

* fix: requireEmails empty bug

* Update apps/web/public/static/locales/en/common.json

* Update apps/web/public/static/locales/en/common.json

* add tests

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-12-10 17:27:32 +05:30
Calcom BotandReplexica 351ab0790f feat: update translations via @replexica (#18078)
Co-authored-by: Replexica <support@replexica.com>
2024-12-10 08:49:46 +00:00
Alex van Andel 504600b151 Change alpine image to one compatible with Prisma (#18080) 2024-12-10 02:50:00 +00:00
GitHub Actions 8b222490c2 chore: release v4.7.20
Post release / release (push) Canceled after 0s
2024-12-10 01:14:21 +00:00
Anwar SadathandAnik Dhabal Babu 0657a96795 feat: Add OAuth support for closecom app linking (#17936)
* feat: Add OAuth support for closecom app linking

* Fix types

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-12-10 00:18:30 +00:00
Kartik Saini 9c9d7fe0c3 fix: can't edit ooo (#18020)
* fix: can't edit ooo

* fix: unauthorized users can edit ooo
2024-12-09 23:39:56 +00:00
sean-brydon 950879b9c7 fix: sorting insights table for booking status (#17982)
* Revert "merge columns (#17938)"

This reverts commit a3ceefc00e.

* sort order by status
2024-12-09 23:39:10 +00:00
Peer Richelsen 55dc4bddb6 fixed days of trial (#18074) 2024-12-09 19:04:45 +00:00
Anik Dhabal Babu cbb1b285f5 fix: not able to mark no show (#17980)
* fix: not able to mark no show

* add test

* Update bookings-list.e2e.ts
2024-12-09 14:02:45 -05:00
Anik Dhabal Babu 45f8ecbab7 chore: Add location to workflow reminder template (#17449)
* chore: Add location to workflow reminder template

* update

* remove log
2024-12-09 18:58:17 +00:00
Toon ToetenelandToon Toetenel 1680cba5a5 fix: Request permissions to allow events to be created on shared Office365/Outlook calendars (#17760)
Co-authored-by: Toon Toetenel <toon@breakingbits.nl>
2024-12-09 16:17:48 +00:00
Benny Joo 046042a877 feat: remove 2 steps from the onboarding flow for google signups (#17787) 2024-12-09 15:44:42 +00:00
Morgan a926e0b9d6 fix: public booking page serialization error (#18070)
* fix: public booking page serialization error

* fixup! fix: public booking page serialization error
2024-12-09 14:48:06 +00:00
9334b67f2c feat: banner and avatars for Booker atom (#17821)
* init

* added bannerUrl to output

* cleanup

* fixup

* fix response DTOs

* remove logs

* send back avatar url for user

* enable passing avatar urls for managed user

* dont include urls for platform

* remove logs

* add team banner

* update docs

* nit

* PR feedback

* fix type error

* add prop to pass in bannerUrl

* update DTOs

* update tests

* fix tests

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-12-09 14:35:57 +00:00
Hariom Balhara ae1b844bea chore: Remove pages/settings (#18064)
* Remove unused org mkigration pages and delete pages/settings

* Remove more unused code causing ts errors

* Remove SettingsLayout
2024-12-09 15:19:12 +01:00
Morgan 15e93af2ac chore: enable markdown for platform atoms (#18069)
* chore: enable markdown for platform atoms

* fixup! chore: enable markdown for platform atoms
2024-12-09 16:11:02 +02:00
Rajiv Sahal 49c97c6380 rearrange order of platform tabs (#18068) 2024-12-09 18:50:59 +05:30
Keith Williams 8679d21d78 docs: Fix OAuth casing (#18066) 2024-12-09 13:18:44 +01:00
Vincent Lam 382f00e178 - Set stripe payment element to accordion layout (#18056)
- Removed hardcoded payment_method_types for checkouts and immediate payments
2024-12-09 11:37:46 +01:00
Anik Dhabal Babu e8eab10d17 fix: team event leading to a 404 page due slug conflict (#18055) 2024-12-09 11:36:15 +01:00
Eunjae Lee 7a7282a7ab fix: add members page back to settings (#18065) 2024-12-09 10:23:27 +00:00
Calcom BotandReplexica 19b3d6a633 feat: update translations via @replexica (#18063)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2024-12-09 10:07:43 +00:00
Lauris Skraucis 046b7a0d08 feat: v2 teams memberships endpoints (#18041) 2024-12-09 10:52:01 +01:00
Benny Joo 30acf6b196 chore: use app router - /apps main page (#16980)
* chore: apps main page remove pages router

* revert unneeded changes
2024-12-09 09:48:22 +00:00
Eunjae Lee 5e95018870 feat: resizing in DataTable (#17973)
* feat: support resizing in data table

* wip

* clean up colors

* feat: resizable DataTable

* update implementation

* fix

* remove unused code

* improve dependency array

* enable resizing at DataTable level

* hide header label

* fix type error
2024-12-09 08:26:02 +00:00
Morgan e559e7a46f fix: add onTouchEnd for mobile apps (#18040) 2024-12-09 10:21:52 +02:00
Morgan 3b1123a29a chore: add user profile to org user endpoints apiv2 (#18039) 2024-12-09 10:21:28 +02:00
Udit TakkarandHariom be3e580107 feat: domain wide delegation UI (#18013)
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-12-09 08:27:16 +05:30
Ignite_99 b325cf8d00 Fixes #17863: When crating a new event from a team event list, the cancel button redirects to /event-types (#18058) 2024-12-08 17:19:04 +00:00
Hariom Balhara 6ce9defecf fix: Do not consider salesforce ownership when making rerouting decision (#18034) 2024-12-07 02:36:41 +00:00
Anik Dhabal Babu 8d05656b7b fix: Schedule a timezone change only allows you to select one day not a range (#18045) 2024-12-06 16:53:55 +00:00
Anik Dhabal Babu 091400b7d5 chore: Add host default language to workflow reminder weekdays (#18029)
* chore: Add host default language to workflow reminder weekdays

* update
2024-12-06 15:56:03 +00:00
Hariom BalharaandJoe Au-Yeung 6de26b3c43 feat: Enable SCIM logging that can be configured through constant (#18043)
* Enable SCIM logging that can be configured through constants

* Remove example

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2024-12-06 15:48:16 +00:00
Somay ChauhanandMorgan 063400fcbc feat: conferencing atoms (#17988)
* added api/v2 for zoom connect

* Update conferencing.controller.ts

* fix: added api for ConferencingAppsViewPlatformWrapper

* type fixes

* added getBulkEventTypes api for bulk-update default location

* added bulk-update-to-default-location

* fix:bulk-update-to-default-location

* invalidated

* fixed returnTo and onErrorReturnTo

* make logos work for conferencing atoms

* smalll improvements

* fixup

* share common types

* type fix, and fixed a typo

* fix: type-error

* fix: json.parse(json.parse())

* fix: invalidate query

* fix: removing app throws error in the main app

* undo platform-libraries-1.2.3

* update platform-libraries version

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-12-06 19:03:04 +05:30
Syed Ali Shahbaz 44115ac7fa feat: v1 booking "created at" field in response (#18038)
* Update booking.ts

* Update booking.ts
2024-12-06 10:22:47 +00:00
Hariom Balhara 7e76770ba6 feat: Okta(SCIM2.0) -> Attributes Sync and Group Option support (#17805) 2024-12-06 15:17:50 +05:30
GitHub Actions d5ef3e1d72 chore: release v4.7.19
Post release / release (push) Canceled after 0s
2024-12-06 09:31:35 +00:00
Benny Joo 0c19606e19 chore: technical debt - remove instances of testing /future routes (#18032) 2024-12-06 02:53:00 +00:00
Benny Joo b8219c273d feat: Reduce bundle size via better imports + install gcal + gvideo by default for google signups (#17810)
* wip

* use light imports

* fix

* perf: Reduce bundle size via better imports with `googleapis` (#17811)

* fix test

* refactor

* update vite.config.js

* fix

* fix unit test failing
2024-12-06 02:51:48 +00:00
Calcom BotandReplexica f75415e414 feat: update translations via @replexica (#18033)
Co-authored-by: Replexica <support@replexica.com>
2024-12-06 01:14:40 +00:00
Syed Ali ShahbazandCarinaWolli 4bf5e3cdf5 feat: Webhook support for Managed Events (#17986)
* managed event webhooks

* more fixes

* fix types --WIP

* fix types

* fix test

* add test

* remove remnant comments

* address feedback

* fix test

* fix test

* fix tests

* change of plans \(00)/

* remove console log

* remove unnecessary comments

* revert tests

* list active webhooks in child event types

* add translations

* allow editing own event types

* add locked info for children

* secure create handler

* fix for isLocked check

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-12-05 23:20:34 +00:00
Benny Joo 4181b5988a chore: remove unused files (#18025) 2024-12-05 22:01:24 +00:00
Keith Williams c41ba9d984 revert: "feat: refactor filters on insights response table (#17796)" (#18024)
* revert: "feat: refactor filters on insights response table (#17796)"

This reverts commit 5d88858995.

* Put back the search columns and options english terms
2024-12-05 17:29:09 +00:00
Benny Joo 2a578ea9d5 chore: drop sourceLang, targetLang and id columns (#17792)
* chore: drop sourceLang, targetLang and id columns

* remove copying for sourceLocale and targetLocale
2024-12-05 17:23:07 +01:00
GitHub Actions c96827e1c0 chore: release v4.7.18
Post release / release (push) Canceled after 0s
2024-12-05 16:18:37 +00:00
Calcom BotandReplexica 3f5363038c feat: update translations via @replexica (#18022)
Co-authored-by: Replexica <support@replexica.com>
2024-12-05 16:12:27 +00:00
Kartik SainiandPrashant Varma e2de28b933 feat: prevent rescheduling past bookings (#18003)
* feat: prevent rescheduling past bookings

* hide reschedule option on frontend

* fix

* addd null check for booking.endTime

---------

Co-authored-by: Prashant Varma <prashantvarma5083@gmail.com>
2024-12-05 16:36:00 +01:00
Eunjae LeeandUdit Takkar 5d88858995 feat: refactor filters on insights response table (#17796)
* feat: improve text filters (WIP)

* move function to bottom

* apply some styles

* fix selection of TextFilterOptions

* rename value to operand

* remove unused file

* merge filters/filters into filters/utils

* fix regression of not putting url params correctly

* move makeWhereClause to filters/utils

* fix negative, empty, and not empty operators

* fix initial filtering from search state (url)

* fix type errors

* do not send an empty array to query

* update yarn.lock

* i18n for text filter operators

* extract logic as useColumnFilters()

* add missing import

* fix type error

* revert yarn.lock

* use i18n

* insensitive text match

* move data-table to @calcom/features

* fix type errors

* fix type errors

* fix type errors

* feat: support DataTable filters for Insights Routing WIP

* remove unused filters

* remove additionalFilters and fix types

* clean up filter components

* support icons for ActiveFilters

* support filters on json

* fix filter ui

* fix type error and clean up

* revert changes

* revert change

* clean up

* revert change

* fix compatibility with insights booking page

* remove unused params

* fix type errors

* update yarn.lock

* fix field filter and adjust ui

* chore: update yarn.lock

* fix text filter

* add Clear Filters button

* add more test data

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-12-05 16:32:52 +01:00
Anik Dhabal Babu 82a500aeaa fix: days of the week in different language in the workflow (#17850)
* fix: days of the week in different language in the workflow

* Update
2024-12-05 15:19:45 +00:00
Hariom Balhara 2408337c78 fix: Don't force reschedule with same RR host if reschedule is actually rerouting (#17511)
* Dont force reschedule with same RR host if routed there

* Add unit tests
2024-12-05 09:30:22 -05:00
Rajiv Sahal 119edb5389 fix: localStorage not found for Booker atom (#18014) 2024-12-05 19:50:09 +05:30
Anwar SadathandPraash ae4474cd42 feat: Add slotFormat query parameter to slots API V2 endpoint to return start and end time of a slot (#17873)
* feat: Add formatAsStartAndEndTime query parameter to slots API V2 endpoint to return start and end time of a slot

* Update apps/api/v2/src/modules/slots/controllers/slots.controller.ts

Co-authored-by: Praash <99237795+Praashh@users.noreply.github.com>

* Fix duration was allowed to be any number

* Refactor code

* Show slotFormat property as optional in swagger doc

---------

Co-authored-by: Praash <99237795+Praashh@users.noreply.github.com>
2024-12-05 19:12:17 +05:30
Morgan 3290ae526a fix: custom implem markdownToSafeHTML for platform (#18018) 2024-12-05 12:18:45 +00:00
Morgan 16a7cf6d04 fix: fetching event-types of org users apiv2 (#18004) 2024-12-05 06:02:19 +00:00
Benny Joo ab2ab31eef chore: named export / default export same time (#18012) 2024-12-04 23:21:29 +01:00
GitHub Actions a08ae43533 chore: release v4.7.17
Post release / release (push) Canceled after 0s
2024-12-04 18:14:31 +00:00
Benny Joo d60b6c6a6d chore: settings/teams and settings/billing - remove pages router and use app router (#16751)
* remove pages router and use app router

* revert yarn.lock

* remove line break from yarn lock

* Add booking limits page to app dir

* revert yarn.lock

* do not render bookingLimits from pages router

* clean up code

* remove isAppDir

* revert unneeded change

* add layout to billing page

* remove files not needed

* rename imports

* remove APP_ROUTER_SETTINGS_DEVELOPER

* remove layout
2024-12-04 12:31:45 -05:00
Keith Williams 9085287fc6 revert: "chore: Move pre-commit commands to pre-push (#17996)" (#18009)
This reverts commit f63c06d68a.
2024-12-04 17:17:58 +00:00
Benny Joo 7e2b5b12b4 chore: settings/developer - remove pages router and use app router (#16794)
* remove pages router and use app router

* remove Meta / improve code quality

* remove isAppDir

* refactor code for better code

* fix test

* remove meta

* improve tests
2024-12-04 11:00:37 -05:00
sean-brydon 3211c5bcff fix data loading state (#18002) 2024-12-04 15:15:01 +00:00
Christoffer Bjelke a75a5b72b2 chore: enable eslint rule react/no-danger (#17983)
* enable react/no-danger and disable rule all places

* remove wild rule
2024-12-04 15:07:30 +00:00
Rajdeep Ghosh 509fc4b289 style: rename [...nextauth].tsx to use .ts file extention (#17992) 2024-12-04 14:52:31 +00:00
Keith Williams f63c06d68a chore: Move pre-commit commands to pre-push (#17996)
* chore: Move pre-commit commands to pre-push

* Is this fast

* Removed test commit

* Adding logging
2024-12-04 14:51:26 +00:00
Anik Dhabal Babu 3dff77fe59 chore: don't show parent managed eventType as event redirectionoption (#17993) 2024-12-04 20:08:52 +05:30
Calcom BotandReplexica a6f9952d75 feat: update translations via @replexica (#18000)
Co-authored-by: Replexica <support@replexica.com>
2024-12-04 13:13:51 +00:00
Benny JooandKeith Williams 03704b2e73 feat: fetch SEO data for booking pages in server (#17987)
* feat: fetch public event data for booking pages server-side

* fix types

* refactor

* Replace the getPublicEvent with other smaller calls

* Change username to teamSlug

* Fixing types

* Type checks

* Pushing latest for Benny - this still has type errors

* fix type checks

* fill data for team pages too

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-12-04 14:08:27 +01:00
sean-brydon 982a715c4b feat: add assignment reason to routing insights (#17984)
* add assignment reason

* add routing reason to csv export
2024-12-04 12:39:20 +00:00
Morgan eaff9e8c89 fix: disable mandatory reminder for platform (#17998)
* fix: disable mandatory reminder for platform

* bump libraries version

* fixup! bump libraries version
2024-12-04 12:33:53 +00:00
e2938ac39a refactor: v2 OAuth webhooks & workflows (#17959)
* refactor: handleMarkNoShow OAuth webhook handling

* refactor: confirm / decline booking OAuth webhook handling

* refactor: pass oauth client to calendar event

* refactor: pass oauth params

* fix import

* fix tests

* chore: bump libraries

* chore: bump libraries

* chore: republish platform libraries

* chore: republish platform libraries

* fix: use replexica key in v2 and unit test CI

* lock file

* fixup! Merge branch 'main' into lauris/cal-4807-platform-refactor-oauth-webhooks

* Revert "fix: use replexica key in v2 and unit test CI"

This reverts commit 0bd6c364535f9e11a4d1497a6bd8909885562080.

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-12-04 11:25:20 +01:00
Morgan b92db55380 chore: add REPLEXICA env to ci tests (#17970)
* chore: add REPLEXICA env to ci tests

* fixup! chore: add REPLEXICA env to ci tests
2024-12-04 09:37:25 +00:00
Somay Chauhan 3015c5a15e fix: trying to update the timeFormat throws error (#17991)
* timeFormat should be an intiger
2024-12-04 09:29:51 +01:00
Joe Au-YeungandUdit Takkar 2444598e38 Fuzzy search account website, fallback to contacts (#17989)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-04 07:46:51 +00:00
Benny Joo 6eadadec73 fix: remove duplicate header in attributes page (#17985) 2024-12-03 20:32:56 +00:00
sean-brydon 145449e7b4 fix:z index hover card on routing form responses (#17981)
* Revert "merge columns (#17938)"

This reverts commit a3ceefc00e.

* fix hovercard on responses
2024-12-03 19:00:56 +00:00
1d8326ed83 feat: watchlist (#17947)
* lock users on signup if their email is in blacklist

* add to turbo env list

* prevent locked user or blocked email domain from using API

* Refactored to only run 1 query to find the user

* Fixing tests

* WIP

* WIP

* WIP

* Discard changes to turbo.json

* Fixed tests

* Update isLockedOrBlocked.test.ts

* Update isAdmin.integration-test.ts

* Update tsconfig.json

* chore: rename to watchlist

Signed-off-by: Omar López <zomars@me.com>

---------

Signed-off-by: Omar López <zomars@me.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-12-03 18:43:01 +01:00
GitHub Actions 59993891b8 chore: release v4.7.16
Post release / release (push) Canceled after 0s
2024-12-03 17:36:59 +00:00
Alex van Andel a31d140761 fix: handling of defaultLabel (#17976) 2024-12-03 17:28:02 +00:00
Calcom BotandReplexica b055fe182a feat: update translations via @replexica (#17965)
Co-authored-by: Replexica <support@replexica.com>
2024-12-03 17:19:32 +00:00
392757fb55 fix: Remove user from managed event type when removing from org (#17369)
* Remove user from managed event type when removing from org

* added test for admin check, correct changes for membership, user info, profiles

* remove host assignment for removed members

* expand test to cover  event types removal

* chore: use var

* chore

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-12-03 17:54:04 +01:00
3222be9171 docs: v2 rolling window and authentication (#17825)
* docs: booking window rolling description

* docs: v2 auth and navigating endpoints

* Update apps/api/v2/swagger/documentation.json

* Update docs/api-reference/v2/introduction.mdx

* Update docs/api-reference/v2/introduction.mdx

* Update docs/api-reference/v2/introduction.mdx

* docs: refresh and force-refresh endpoints

* docs: booking fields and toasts guides

* fix docs

* fix: bookings docs

* docs: booking-redirects usebooking

* Update booking-fields.mdx

* Update booking-fields.mdx

* Update booking-fields.mdx

* Update booking-window.input.ts

* Update replacing-toasts.mdx

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-12-03 17:53:14 +01:00
Benny Joo 6378729962 chore: use markdownToSafeHTML in EventMeta (#17975) 2024-12-03 16:45:48 +00:00
Udit Takkar 1c5786b66f fix: org bio (#17971)
* fix: org bio

* chore: use in bookings
2024-12-03 16:30:03 +00:00
Peer Richelsen e61a0d8081 chore: safeMarkdown (#17972) 2024-12-03 15:47:58 +00:00
Rajiv SahalandMorgan Vernay 0cb955cc0b fix: build failure for v4.7.15 (#17968)
* fix: type error causing broken build

* fix typings

* fixup! fix typings

* fixup! fixup! fix typings

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-12-03 15:44:42 +00:00
Hariom BalharaandAlex van Andel 00689fda0a fix: remove platform and console submodule (#17939)
* Fix: remove platform submodule

* Update git-setup.sh

* Update git-setup.sh

* Update git-setup.sh

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-03 11:49:52 +00:00
Joe Au-Yeung bd83b36b75 feat: Salesforce - add option to ignore guests (#17956)
* Enable option to ignore guests

* Ignore guests in CRM Manager
2024-12-03 11:19:57 +00:00
Eunjae Lee 05e2029bc9 fix: change case for column size vars (#17961)
* fix: change case for column size vars

* fix wrong import
2024-12-03 09:50:00 +00:00
Udit Takkar 9e3d369ac3 feat: return participants (#17950)
* feat: return participants

* fix: update tests
2024-12-03 09:36:59 +00:00
sean-brydon 356c603e3f Revert "merge columns (#17938)" (#17960)
This reverts commit a3ceefc00e.
2024-12-03 09:23:28 +00:00
5afe3aa54d feat: update translations via @replexica (#17952)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-03 06:56:08 +00:00
Alex van Andel 985f216a43 test: fix organisation settings e2e (#17957) 2024-12-03 09:41:20 +05:30
Anwar SadathandHariom 3a472997ff fix: brand colors set by user not respected in embed (#17399)
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-12-03 04:10:58 +00:00
GitHub Actions 6dcb6aece4 chore: release v4.7.15
Post release / release (push) Canceled after 0s
2024-12-02 22:51:37 +00:00
GitHub Actions dc7ee1aba2 chore: release vv4.7.15 2024-12-02 22:41:39 +00:00
Joe Au-Yeung 2f9f29511b Reduce banner height on mobile booker (#17954) 2024-12-02 22:30:16 +00:00
Anik Dhabal BabuandPeer Richelsen 0aa58c1095 fix: Date Range Picker behavior (#17914)
* fix: Date Range Picker behaviour

* remove console log

* update

* update

* update css

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-12-03 02:10:23 +05:30
Eunjae LeeandAlex van Andel 58b77b2a16 fix: fix date comparison for OOO (#17756)
* fix: fix date comparison for OOO

* fix date comparison with today, and add tests

* remove wrong code

* remove logic that is no longer in use

* fix type error

* mock more methods

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-02 19:39:44 +00:00
Peer Richelsen caeaf9053b chore: fixed colored event margin (#17932) 2024-12-02 19:31:07 +00:00
fe209df2be fix: ai translations description for atoms (#17875)
* remove ai translations description for atoms

* PR feedback

* fixup

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-12-02 19:27:54 +00:00
CodeWithAdnanandPrashant Varma 2e7a5a84a6 feat: Show add to calendar section for customers after booking an event (#17761)
* Show 'Add to calendar' section for customers after booking an event

* removed comment

---------

Co-authored-by: Prashant Varma <prashantvarma5083@gmail.com>
2024-12-02 19:25:56 +00:00
Anik Dhabal Babu 410558f822 fix: Display option label in the routing form response email (#17898)
* fix: display option label int the routing form response email

* Update

* fix type error

* type error
2024-12-02 19:24:11 +00:00
sean-brydon a3ceefc00e merge columns (#17938) 2024-12-02 19:21:42 +00:00
Kartik SainiandAlex van Andel c524a3c1ef fix: host entry not created when user added to team in org (#17927)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-02 19:19:05 +00:00
a5ba2682f6 fix: /members improvements (#17911)
* remove heading for more space

* fix: mobile support for mass attribute assigning bar

* change the order of Members item on Navigation

* refactor NavigationItem to support moreOnMobile property

* fix logic for nav

* decrease column widths on member list

* improve mobile design for mass attribute assign

* adjust mobile ui for DataTableSelectionBar

* add key to EventTypesList

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-12-02 19:13:08 +00:00
Anik Dhabal Babu 51c91e41da fix: navigation items weird behavior (#17929) 2024-12-02 19:08:13 +00:00
Alex van Andel cf6654dd1f Extract some refactor work from util.ts to improve error handling (#17912) 2024-12-02 19:06:45 +00:00
Benny Joo d057d7d37f fix: refactor OG link builder (#17897)
* fix: manually encode special characters

* fix test

* fix

* fix
2024-12-02 19:01:09 +00:00
Anik Dhabal BabuandAlex van Andel 262738f288 fix: Meeting URL does not save when changing the location to Gmeet or any static app (#17840)
* fix: meetingurl does not save when we change location

* update

* fix type error

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-02 19:00:43 +00:00
Eunjae Lee 27cd2457e8 fix: fix UI regression related to DataTable (#17943)
* fix: fix UI regression regarding DataTable

* update comment
2024-12-02 18:54:58 +00:00
Vishnu b33ca2f937 feat: no show guests on insights (#17919)
* feat: return noShowGuest data from eventsByStatus & eventsTimeline procedures

* feat: add noShowGuest Data in BookingKPICards

* feat: add noShowGuest Data in BookingStatusLineChart

* feat: track noShowGuests in outer query

* chore: fix noShowGuest naming
2024-12-02 18:53:32 +00:00
Alex van Andel 82f64f7d6a fix: Broken integration test due to month change (#17949) 2024-12-02 18:26:45 +00:00
Amit Sharma 8052d27d0b feat: show attributes in profile (#17920) 2024-11-30 22:53:05 +05:30
Aditya Pandey 5720ec8038 fix : Issue on docs (#17895) 2024-11-29 19:50:57 +00:00
Vincent Lam 01b1f5bcc8 fix: Added proper updates/interactivity on the "Getting started" app integ… (#17903)
* Added proper updates/interactivity on the "Getting started" app integrations.

- InstallAppButtonWithoutPlanCheck properly vends the mutation data in the render's props
- Fixed the improper props wiring in the AppConnectionItem
- Added a loading state for reactivity

* -Added toaster for error/success confirmation
2024-11-29 19:14:01 +00:00
Jesse KlotzandHariom 953a8119db build: Reduce bundle size from unnecessary bundeling react/jsx-runtime in embed-react package (#17790)
* Update vite.config.js

* linting fix

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-11-29 22:19:16 +05:30
Eunjae Lee e8daa4f92f fix: bulk attribute assignment (#17896)
remove comment
2024-11-29 10:00:20 +00:00
f631abfdc4 fix: #16028 Org Settings - SEOIndexing,Disable Org Url (#16075)
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2024-11-29 10:21:14 +05:30
GitHub Actions b5c6d7af91 chore: release v4.7.14
Post release / release (push) Canceled after 0s
2024-11-28 21:50:59 +00:00
Omar López 249bcaca68 fix: locked users admin list and bypass query (#17902) 2024-11-28 21:45:40 +00:00
Joe Au-Yeung f2efd6dc28 feat: Salesforce - send routing form responses to Salesforce (#17890) 2024-11-28 12:37:45 -07:00
Joe Au-Yeung be2f0f986e fix: getAttributeRoutingConfig find the route based on the chosenRouteId (#17867)
## 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. -->

We were parsing routes based on `eventTypeId` but routes could be pointing to the same event type. This was causing misroutings.

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

<!-- Please provide a loom video for visual changes to speed up reviews
 Loom Video: https://www.loom.com/
-->

## Mandatory Tasks (DO NOT REMOVE)

- [x] 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.
2024-11-28 12:31:06 -07:00
Udit TakkarandAlex van Andel 589c1dccb9 fix: remove from host when removed from org (#17893)
* fix: remove from host when removed from org

* test: add test for removing members

* fix: type errors, some unused params

* fix: test

* refactor: use prisma.count

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-11-28 15:56:17 +00:00
Eunjae Lee 4eac6c755d fix: revert bg color for pinned column at data table (#17892) 2024-11-28 10:04:53 +01:00
Lauris Skraucis 131b3d0984 refactor: v2 validate booking fields (#17878)
* refactor: v2 validate booking fields

* test
2024-11-28 10:37:42 +02:00
Omar López 693356d3c4 fix: Exclude locked users by default (#17889) 2024-11-27 17:03:58 -07:00
GitHub Actions 4b717108b0 chore: release v4.7.13
Post release / release (push) Canceled after 0s
2024-11-27 22:46:23 +00:00
Peer Richelsen ad8c888b77 chore: temporarily remove push notifications button until we have proper support (#17887) 2024-11-27 19:27:07 +00:00
Benny Joo d6fe955ea8 feat: hide the update button in case of no permission (#17883)
* feat: hide button if not owner of org

* fix
2024-11-27 11:43:46 -05:00
Benny Joo ab9b6024bb feat: add tooltip to button for better UX (#17881) 2024-11-27 15:47:08 +00:00
Benny Joo a7e99f3674 fix: Remove double encoding for OG image URLs (#17872)
* fix: do not use vercel image api for meeting link preview image

* fix

* fix

* remove double encoding

* fix

* fix test

* fix

* fix

* fix test

* fix

* fix

* remove test tag

* fix
2024-11-27 15:45:54 +00:00
Lauris Skraucis 999019c6f0 refactor: v2 return host username (#17880)
* fix: v2 return booking with host username

* auto gen docs

* auto gen docs
2024-11-27 15:15:12 +00:00
Lauris Skraucis 240ab60c5f fix: v2 return booking with split name attendee (#17877) 2024-11-27 16:22:58 +02:00
Benny Joo e8f84779f5 chore: make i18n.json the single source of truth (#17876) 2024-11-27 15:18:38 +01:00
Eunjae Lee 08a92b46f9 fix: load more rows if the existing ones do not fill the space (#17874) 2024-11-27 07:08:28 -07:00
Eunjae LeeandOmar López 877e4199e7 fix: move organization members list (#17531)
Co-authored-by: Omar López <zomars@me.com>
2024-11-27 13:53:36 +00:00
Hariom Balhara afd730e1c8 fix: (Hotfix)any group of rules in routing (#17869) 2024-11-27 10:44:45 +05:30
Morgan 471f5bd7eb chore: add Free to platform plan guard (#17862) 2024-11-27 01:40:39 +00:00
Joe Au-Yeung c676d47b7e fix: Routing form report filters (#17865) 2024-11-27 01:34:48 +00:00
GitHub Actions b3536728e0 chore: release v4.7.12
Post release / release (push) Canceled after 0s
2024-11-26 20:22:01 +00:00
Eunjae LeeandOmar López e22fec93b1 fix: provide fixed widths for DataTable columns (#17853)
Co-authored-by: Omar López <zomars@me.com>
2024-11-26 20:17:33 +00:00
Joe Au-Yeung f81716fbb2 feat: Salesforce - pass company booking response to the Lead field (#17860)
* Catch undefined app option properties

* Add `getCompanyNameFromBookingResponse` method

* Pass bookingResponse when Lead is being created

* Pass bookingResponse from CRMManager

* Handle empty company response

* Return the company value
2024-11-26 19:14:44 +00:00
Anik Dhabal Babu 0b66ddc6db fix: unable to remove banner (#17857) 2024-11-26 18:42:41 +00:00
Somay Chauhan 382a27f134 fix: round robin host reschedule error (#17855) 2024-11-26 18:08:58 +00:00
Calcom BotandReplexica ac5ee225d5 feat: update translations via @replexica (#17854)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2024-11-26 16:46:54 +00:00
Anik Dhabal Babu d8fd6f5c25 chore: exclude emails form field refactor (#17824)
* chore: exclude emails refactor

* Update
2024-11-26 21:30:28 +05:30
Benny Joo 0e333aa71c fix: use tags icon for attr (#17841) 2024-11-26 12:52:21 -03:00
Carina WollendorferandCarinaWolli ae70786b2b fix: send attendee email with event updates of reassignment (#17836)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-11-26 10:33:00 -05:00
Benny Joo 2df553b58b chore: use app router for settings/platform/members page (#17849)
* use i18n strings

* migrate platform/members page
2024-11-26 15:33:01 +01:00
Morgan d4afa1436d chore: make throttler logs verbose (#17845) 2024-11-26 09:42:27 +00:00
Morgan 88efa0a2b2 fix: oauth client users controller permissions (#17822)
* fix: oauth client users controller permissions

* fixup! fix: oauth client users controller permissions

* fixup! fixup! fix: oauth client users controller permissions

* fixup! fixup! fixup! fix: oauth client users controller permissions
2024-11-26 09:41:03 +00:00
Calcom BotandReplexica b563e41623 feat: update translations via @replexica (#17842)
Co-authored-by: Replexica <support@replexica.com>
2024-11-25 22:17:54 +00:00
Benny Joo f097f852ad chore: settings/organizations - remove pages router and use app router (#16774)
* remove pages router and use app router

* remove future/settings/organizations files

* fix billing page

* sync SettingsLayoutAppDirClient and fix members page

* fix attribute pages

* remove isAppDir where not needed anymore

* add CTA button for members view
2024-11-25 16:49:00 -05:00
Joe Au-Yeung aea8588a21 fix: Salesforce owner check, handle no contact email (#17834) 2024-11-25 19:38:06 +00:00
Benny Joo 7e0c684348 fix: UI issue in Phone Input dropdown (#17837) 2024-11-25 19:35:09 +00:00
Keith Williams 21956f096d chore: Add check to skip DB migrations as part of build (#17827) 2024-11-25 16:33:51 -03:00
Benny Joo d1adfa3bf9 fix (#17812) 2024-11-25 14:07:55 -05:00
Benny JooandKeith Williams 62976a4c77 feat: AI description - Backend for adding translation objects to EventType model (#17657)
* feat: AI description - DB model + frontend + backend (fetch only)

* fix types and add validation to backend

* improve log

* improve

* import type

* add EventTypeTranslationRepository

* fix replexica error

* fix replexica error

* fix

* fix test

* wip

* wip

* add createManyDescriptionTranslations method

* improve logic

* refactor

* update replexica type

* improve typing

* Renamed descriptionTranslations to fieldTranslations

* log errors in replexica service

* handle translations in background

* don't create, upsert instead

* make sure that description is updated

* simplify

* fix

* Update packages/trpc/server/routers/viewer/eventTypes/update.handler.ts

* improve filter logic in frontend

* implement background job using Tasker

* update schema

* update schema again

* rename

* rename id to uid

* add migration

* fix type

* update

* update schema and migration sql

* fix migration

* fix type check

* fix type check 2

* improve find logic in EventMeta

* add more validation

* add logger

* rename

* upgrade replexica/sdk to 0.7.0

* upgrade replexica to 0.7.0

* use batchLocalizeText

* add comments

* override browser locale if user is a signed in calcom user

* fix type in replexica

* fix migration sql

* fix type

* use dynamic import

* do not use batchLocalizeText

* fix

* add more validations

* fix type error

* address comment

* update locales

* update locales

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-11-25 18:37:42 +00:00
GitHub Actions 93489709a8 chore: release v4.7.11
Post release / release (push) Canceled after 0s
2024-11-25 16:35:44 +00:00
Amit Sharma 050102fde2 fix: OOO redirect, email wrong formatting (#17829) 2024-11-25 16:11:44 +00:00
Morgan c1112ab2ab fix: extract clientId header manually apiv2 (Revert) (#17832)
This reverts commit b0bf87f3ed.
2024-11-25 15:59:39 +00:00
Morgan 34d4c3d775 fix: api v2 webhook issue (#17831) 2024-11-25 15:52:00 +00:00
Somay Chauhan e477e5892c feat: added customClassnames for event-setup tab (#17240)
* feat: added customClassnames for event-setup tab

* Update EventTypePlatformWrapper.tsx

* better classnames naming

* added classNames to common types

* updated classnames to use the common exported types

* udpate

* better classnames

* better classname names

* classnames -> classNames

* added default styling

* Update Locations.tsx

* Update event-types.tsx

* update

* locationSelectCustomClassNames -> LocationSelectCustomClassNames

* Update documentation.json

* fix: better classnames

* Update types.ts
2024-11-25 09:59:09 +00:00
ad44ca9f2a fix: change order of eventtypes and routingforms (#17615)
* change order of eventtypes and routingforms

* for workflows

* for filtered workflows

* update slug of first expected event-type

* updated due to latest merges

* test to ensure new eventtype added is first in the list

* test for workflows

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-11-25 06:25:05 -03:00
Somay ChauhanandMorgan 868cf91c35 refactor: conferencing apps atoms web wrapper (#17732)
* conferencing apps atoms web wrapper

* Update DisconnectIntegrationModal.tsx

* formating changes

* handleBulkUpdateDefaultLocation -> bulkUpdateFunction

* fix: type error for handleRemoveApp

* fix: type error for handleRemoveApp

* refactor: removed ConferencingView component and migrated its functionality to ConferencingAppsViewWebWrapper

* refactor: moved AppList, AppListCard to packages/features

* fix: type changes

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-11-25 09:53:38 +02:00
Lauris Skraucis 8b9693ea60 fix: v2 booking location (#17817)
* fix: create-booking.input dont require location to be url

* fix: return video call url for booking location

* try to debug test

* remove test file
2024-11-25 07:34:59 +00:00
Somay Chauhan 935115d7c1 feat: added custom classnames for event-limits tab (#17478)
* added custom classnames for event limits tab

* added classNames to common types

* updated classnames to use the common exported types

* undo

* Update EventTypePlatformWrapper.tsx

* better clasnames naming

* added example classnames for demo

* fix: better calssnames

bookingFrequencyLimitSection-> bookingFrequencyLimit
totalDurationLimitSection-> totalDurationLimit
futureBookingLimitSection-> futureBookingLimit
offsetStartTimesSection-> offsetStartTimes

* suffix->addOn

* Update types.ts
2024-11-25 07:13:58 +00:00
Somay Chauhan eae74c568c feat: added custom classnames for event-advanced tab (#17479)
* feat: added custom classnames to event-advanced tab

* added more calssnames

* added classNames to common types

* undo extra changes

* Update EventTypePlatformWrapper.tsx

* Update types.ts

* Update EventTypePlatformWrapper.tsx

* better classnames

* Update EventTypePlatformWrapper.tsx

* addeed CustomEventTypeModalClassNames

* added example classnames for demo and some changes

* fix: better classnames

* timezoneLockToggle -> timezoneLock

* Update types.ts

* Update CustomEventTypeModal.tsx
2024-11-25 12:11:27 +05:30
Somay Chauhan 9e9dd6070d feat: added custom classnames to event-assignment tab atom (#17527)
* added classNames to common types

* Update types.ts

* feat: added custom classnames to event-assignment tab atom

* Update EventTypePlatformWrapper.tsx

* update: better classnames

* added example clasnames for demo

* fix: classnames

* fix: type check

* fixed classnames

* fix: better calssnames

assingAllTeamMembersToggle -> assingAllTeamMembers
enableWeightsToggle -> enableWeights
assingAllTeamMembersToggle -> assingAllTeamMembers
2024-11-25 06:10:22 +01:00
Amit SharmaandPeer Richelsen 5af10a2f61 feat: past and overlapping ooo (#17373)
* feat: past and overlapping ooo

* use existing props

* added e2e test

* disallow duplicates added tests

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-11-24 18:58:22 +00:00
Peer Richelsen 3936a0ae94 target blank for settings link (#17815) 2024-11-23 21:12:44 +00:00
Morgan c2a3d8b8df fix: extract trpc and react-awesome-query-builder from atoms (#17801)
* wip

* fix: externalize react-awesome-query-builder

* fix: move useSession to webWrapper

* fixup! Merge branch 'main' into fix-platform-event-types-segment

* fixup! fixup! Merge branch 'main' into fix-platform-event-types-segment

* fixup! fixup! fixup! Merge branch 'main' into fix-platform-event-types-segment

* fixup! fixup! fixup! fixup! Merge branch 'main' into fix-platform-event-types-segment
2024-11-23 12:29:16 +00:00
GitHub Actions 9bb66035c8 chore: release v4.7.10
Post release / release (push) Canceled after 0s
2024-11-22 23:01:45 +00:00
Joe Au-Yeung 226325062e Conditionally get the first assignment reason (#17808) 2024-11-22 22:23:15 +00:00
Carina Wollendorfer b61661214a fix: improvements for canceling workflow reminders (#17807) 2024-11-22 13:55:30 -07:00
Omar López 9f2047f203 fix: Got back to access token for release action
Signed-off-by: Omar López <zomars@me.com>
2024-11-22 13:53:19 -07:00
Omar López 9b0db345b0 Revert "Add new release actions"
This reverts commit 05c8b53444.
2024-11-22 13:50:27 -07:00
GitHub Actions a84a2b3152 chore: release 4.7.9
Post release / release (push) Canceled after 0s
2024-11-22 20:43:40 +00:00
Omar López 087dbefec2 fix: add missing version plugin for release action 2024-11-22 13:35:52 -07:00
Omar López 46ba793521 fix: draft release setup 2024-11-22 13:32:29 -07:00
Omar López 21ab9bf109 fix: draft release setup 2024-11-22 13:30:15 -07:00
Omar López 05c8b53444 Add new release actions
Signed-off-by: Omar López <zomars@me.com>
2024-11-22 13:27:33 -07:00
Joe Au-Yeung e7495e311b feat: Render assignment reason on routing form report (#17791) 2024-11-22 10:57:16 -07:00
Omar López 9c10ecf8ea ci: Add new release actions (#17789) 2024-11-22 10:08:26 -07:00
Lauris Skraucis 9fa73aca86 fix: v2 booking confirmation emails not sent (#17764)
* fix: round robin recurring don't send email if noEmail

* fix: noEmail: false for non platform requests

* fix: booking e2e tests

* refactor: move bookings e2e tests in folder and uniform name

* test: emails

* test: team emails

* reschedule and cancel emails with api key

* test: round robin reassign emails

* test: confirm and decline emails

* test: confirm and decline emails

* fix tests

* test: recurring booking emails

* remove unused imports and spies

* fix test

* remove log

* chore: bump libraries

* try to fix ci tests
2024-11-22 18:19:56 +02:00
Alex van Andel 8e7ce1216c Revert "feat: install gcal + gvideo by default for google signups (#17773)" (#17803)
release.yml / release (push) Canceled after 0s
This reverts commit fcd06c80e3.
2024-11-22 15:55:11 +00:00
Alex van Andel 4ddbdde553 chore: Release v4.7.8 2024-11-22 15:54:42 +00:00
Keith Williams cd24067f17 chore: Add ability to disable Sentry source maps (#17699)
* chore: Remove Sentry source maps

* Using .env to control Sentry source maps

* Trying with sourcemaps property

* Turned silent off for testing

* Completely turned off devtool
2024-11-22 15:19:52 +00:00
Alex van Andel 040adb2ddf chore: bump lockfile (#17802) 2024-11-22 15:14:39 +00:00
Somay Chauhan 4d578d2cdf feat: added customClassnames to event-availability tab (#17268)
* feat: added customClassnames to event-availability tab

* added classNames to common types

* use common classnames type

* Update types.ts

* update name

* fix: better classnames

* better calssnames

* Update types.ts
2024-11-22 10:35:20 +01:00
Anwar Sadath 1cf84a270c feat: Expose booking created at in v2 API response (#17452)
* feat: Expose booking created at in booking detailed view and v2 API response

* Fix tests

* Fix spacing

* Add API v2 tests to verify createdAt is present while creating a booking and fetching a booking

* Add test to validate createdAt after rescheduling a booking
2024-11-22 13:50:11 +05:30
Somay Chauhan d03651312e feat: added customClassnames for event-recurring tab (#17551)
* added classNames to common types

* Update types.ts

* feat: added custom classnames to event-recurring tab

* Update EventTypePlatformWrapper.tsx

* customClassnames -> customClassNames

* added example classnames for demo

* fixed classnames
2024-11-22 12:30:02 +05:30
Alex van Andel 4b34aec13d chore: Release v4.7.7 (#17786)
release.yml / release (push) Canceled after 0s
2024-11-21 22:47:11 +00:00
Anik Dhabal Babu 7ab8fdbcb4 fix: short text booking questions does not save we we click add (#17778) 2024-11-21 22:46:44 +00:00
Morgan b0bf87f3ed fix: extract clientId header manually apiv2 (#17783) 2024-11-21 22:46:13 +00:00
Joe Au-Yeung 1490642260 feat: Record Salesforce ownership assignment (#17775) 2024-11-21 14:22:39 -07:00
Udit Takkar 04b49acbb0 chore: add rescheduled by property (#17631)
* refactor: add rescheduledBy

* chore

* fix: bug

* test: add test for rescheduled booking

* fix: test
2024-11-21 11:51:19 -05:00
Joe Au-Yeung ff79e5c70e feat: Record assignment reasons for routing form and reassignment (#17767)
* Add assignment reason table

* Increment id

* Update migration

* Record assignment when manual reassignment is used

* Add comment to `getAttributesQueryValue`

* Add `getUsersAttributes`

* Record routing form routing reason

* Type fixes

* Remove unique property from bookingId

* Handle nested values
2024-11-21 16:27:19 +00:00
Alex van Andel 5d51c79739 fix: temporarily revert stricter schema so we can handle invalid DB e… (#17774) 2024-11-20 21:35:43 -07:00
Carina WollendorferandCarinaWolli 8bc2c08288 fix: no availability on reschedule with load balancing (#17770)
* don't filter if reschedule with same host

* check for reschedule

* fix filtering in handleNewBooking

* fix type error

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-11-21 01:22:43 +00:00
Omar López ade144aa20 ci: Re-drafts PR when changes are requested (#17137) 2024-11-21 00:59:11 +00:00
Benny Joo fcd06c80e3 feat: install gcal + gvideo by default for google signups (#17773)
* feat: install gcal + gvideo by default for google signups

* add upsertSelectedCalendar

* fix type check
2024-11-20 23:54:11 +00:00
Omar López 57bbba3d95 chore: v4.7.6 (#17766)
release.yml / release (push) Canceled after 0s
2024-11-20 13:22:47 -07:00
Amit Sharma d952e77b78 feat: Don't remove hosts when disabling 'Add all team members' (#17747)
* feat: Don't remove hosts when disabling 'Add all team members'

* retain existing host weights and priorities

* fix: merge changes, revert onInactive removal

* lint fix

* fix: weight priority dialog value
2024-11-20 17:54:42 +00:00
Anik Dhabal Babu 73989da3b7 fix: common schedule toggle for team event in never uncheckable (#17759) 2024-11-20 11:25:58 -05:00
Hariom Balhara a534d39351 Log error correctly (#17755) 2024-11-20 12:03:07 -03:00
Calcom BotandReplexica c16ef798f5 feat: update translations via @replexica (#17757)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2024-11-20 14:03:09 +00:00
Lauris Skraucis 58cac84f4a feat: v2 expose booking metadata (#17708)
* feat: accept metadata and store it in database

* feat: accept metadata and store it in database

* feat: add BookingSeat.metadata column

* feat: handleNewBooking: store booking seat metadata when first time seated booking

* feat: handleSeats, createNewSeat store metadata when 2nd+ seated booking

* feat: return booking metadata in output

* tests: metadata

* test: seated bookings

* swagger

* fix: reassign bookings test

* test: use different metadata in recurring

* fix: test

* test invalid metadata

* refactor: output service

* readd new line

* chore: bump platform libraries

* refactor: metadata doc
2024-11-20 15:15:49 +02:00
Eunjae LeeandUdit Takkar 1404581f78 feat: add text filter on org member list (#17632)
* feat: improve text filters (WIP)

* move function to bottom

* apply some styles

* fix selection of TextFilterOptions

* rename value to operand

* remove unused file

* merge filters/filters into filters/utils

* fix regression of not putting url params correctly

* move makeWhereClause to filters/utils

* fix negative, empty, and not empty operators

* fix initial filtering from search state (url)

* fix type errors

* do not send an empty array to query

* update yarn.lock

* i18n for text filter operators

* extract logic as useColumnFilters()

* add missing import

* fix type error

* revert yarn.lock

* use i18n

* insensitive text match

* move data-table to @calcom/features

* fix type errors

* fix type errors

* fix type errors

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-11-20 09:38:10 +00:00
Anwar Sadath ff18b9a733 chore: Add support for new docker compose command to run V2 API (#17744) 2024-11-20 08:47:37 +00:00
Hariom BalharaandCarinaWolli 82e52a1948 feat: Attribute filter/Segment for Round Robin hosts (#17361)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-11-20 10:22:53 +05:30
Vijay e3abd35ba7 fix: upgrade prismock to 1.33.4 (#17735)
* upgrade prismock, removed 2 hacks, update 3 tests

* yarnlock changes
2024-11-19 23:20:27 +00:00
sean-brydon dfdb04acf4 chore: routing table nits (#17738)
* portal date preset selection with a high zindex

* increase height of data-table

* skeleton fixes
2024-11-19 23:04:45 +00:00
Omar López 352a6ba8cf fix: Better error handling for user session (#17753) 2024-11-19 22:57:37 +00:00
Eunjae Lee 997d6a75e8 fix: fix wrong dates on OOO edit dialog (#17736)
* fix: fix wrong dates on OOO edit dialog

* do not use utc() on the browser side
2024-11-19 19:54:12 -03:00
NizzyandKeith Williams 757e3a2faf fix: PostHog event tracking (#17653)
* posthog provider fix

* posthog fixes w/ trpc changes

* Update yarn.lock

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-11-19 22:52:18 +00:00
Omar López 1b457f82a1 chore: v4.7.5 (#17750)
release.yml / release (push) Canceled after 0s
2024-11-19 20:44:58 +00:00
Omar López 16573717b4 fix: identify org-wide features per user (#17010) 2024-11-19 15:21:38 -05:00
Omar LópezandAnik Dhabal Babu 97b920744e feat: adds last active timestamp (#17261)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-11-20 00:38:20 +05:30
Joe Au-Yeung 6cfe9ff88d feat: Salesforce: Write booking question values to attendee record in Salesforce [CAL-4731] (#17650) 2024-11-19 10:30:34 -07:00
Anik Dhabal Babu 7c5292e13b fix (#17734) 2024-11-19 18:37:39 +05:30
Morgan 3cd4e34406 fix: platform libraries build vite.config (#17743) 2024-11-19 12:05:28 +00:00
sean-brydon a99eda21af chore: booking status reporting (#17733)
* move booking status into new column

* update i18n

* nowrap + badge for submitedAt to provide consistant feel to other dates
2024-11-19 11:24:45 +00:00
Omar López 03e2c6278d fix: cron api key checks (#17730) 2024-11-19 00:18:53 +00:00
1122 changed files with 43416 additions and 14297 deletions
+9 -13
View File
@@ -119,7 +119,7 @@ NEXT_PUBLIC_FRESHCHAT_HOST=
# Google OAuth credentials
# To enable Login with Google you need to:
# 1. Set `GOOGLE_API_CREDENTIALS` above
# 1. Set `GOOGLE_API_CREDENTIALS` below
# 2. Set `GOOGLE_LOGIN_ENABLED` to `true`
# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
@@ -147,6 +147,8 @@ NEXT_PUBLIC_SENDGRID_SENDER_NAME=
# Used for capturing exceptions and logging messages
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_DEBUG=
SENTRY_DISABLE_CLIENT_SOURCE_MAPS=
SENTRY_DISABLE_SERVER_SOURCE_MAPS=
SENTRY_MAX_SPANS=
SENTRY_TRACES_SAMPLE_RATE=
@@ -237,7 +239,8 @@ CLOUDFLARE_TURNSTILE_SECRET=
NEXT_PUBLIC_TEAM_IMPERSONATION=false
# Close.com internal CRM
CLOSECOM_API_KEY=
CLOSECOM_CLIENT_ID=
CLOSECOM_CLIENT_SECRET=
# Sendgrid internal sync service
SENDGRID_SYNC_API_KEY=
@@ -345,8 +348,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
AB_TEST_BUCKET_PROBABILITY=50
# whether we redirect to the future/event-types from event-types or not
APP_ROUTER_EVENT_TYPES_ENABLED=0
APP_ROUTER_SETTINGS_DEVELOPER_ENABLED=0
APP_ROUTER_SETTINGS_ORG_ENABLED=0
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
APP_ROUTER_APPS_SLUG_ENABLED=0
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
@@ -356,14 +357,10 @@ APP_ROUTER_APPS_CATEGORIES_ENABLED=0
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
APP_ROUTER_BOOKING_ENABLED=0
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
APP_ROUTER_WORKFLOWS_ENABLED=0
APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
APP_ROUTER_APPS_ENABLED=0
APP_ROUTER_VIDEO_ENABLED=0
APP_ROUTER_TEAM_ENABLED=0
APP_ROUTER_TEAMS_ENABLED=0
APP_ROUTER_AVAILABILITY_ENABLED=0
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
APP_ROUTER_AUTH_LOGIN_ENABLED=0
APP_ROUTER_AUTH_LOGOUT_ENABLED=0
@@ -372,10 +369,6 @@ APP_ROUTER_AUTH_SAML_ENABLED=0
APP_ROUTER_AUTH_ERROR_ENABLED=0
APP_ROUTER_AUTH_PLATFORM_ENABLED=0
APP_ROUTER_AUTH_OAUTH2_ENABLED=0
APP_ROUTER_MORE_ENABLED=0
APP_ROUTER_MAINTENANCE_ENABLED=0
APP_ROUTER_UPGRADE_ENABLED=0
APP_ROUTER_CONNECT_AND_JOIN_ENABLED=0
# disable setry server source maps
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
@@ -414,4 +407,7 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL=
NEXT_PUBLIC_LOGGER_LEVEL=
# Used to use Replexica SDK, a tool for real-time AI-powered localization
REPLEXICA_API_KEY=
REPLEXICA_API_KEY=
# Comma-separated list of DSyncData.directoryId to log SCIM API requests for. It can be enabled temporarily for debugging the requests being sent to SCIM server.
DIRECTORY_IDS_TO_LOG=
+11
View File
@@ -0,0 +1,11 @@
/packages/app-store/applecalendar/**/* @calcom/Foundation
/packages/app-store/caldavcalendar/**/* @calcom/Foundation
/packages/app-store/exchange2013calendar/**/* @calcom/Foundation
/packages/app-store/exchange2016calendar/**/* @calcom/Foundation
/packages/app-store/exchangecalendar/**/* @calcom/Foundation
/packages/app-store/feishucalendar/**/* @calcom/Foundation
/packages/app-store/googlecalendar/**/* @calcom/Foundation
/packages/app-store/ics-feedcalendar/**/* @calcom/Foundation
/packages/app-store/larkcalendar/**/* @calcom/Foundation
/packages/app-store/office365calendar/**/* @calcom/Foundation
/packages/app-store/zohocalendar/**/* @calcom/Foundation
+50
View File
@@ -0,0 +1,50 @@
name: Draft release
run-name: Draft release ${{ inputs.next_version }}
on:
workflow_dispatch:
inputs:
next_version:
required: true
type: string
description: 'Version name'
permissions:
contents: write
jobs:
draft_release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: 'main'
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Configure git
run: |
git config --local user.email "github-actions@github.com"
git config --local user.name "GitHub Actions"
- uses: ./.github/actions/yarn-install
- name: Bump version
run: |
cd apps/web
yarn version ${{ inputs.next_version }}
- name: Commit changes
run: |
git add .
git commit -m "chore: release v${{ inputs.next_version }}"
git push
- name: Draft release
run: gh release create v$VERSION --generate-notes --draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.next_version }}
+1
View File
@@ -17,6 +17,7 @@ env:
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NODE_OPTIONS: --max-old-space-size=29000
REDIS_URL: "redis://localhost:6379"
REPLEXICA_API_KEY: ${{ secrets.CI_REPLEXICA_API_KEY }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
+3 -2
View File
@@ -17,7 +17,6 @@ env:
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
@@ -51,7 +50,8 @@ env:
jobs:
analyze:
if: always()
runs-on: buildjet-2vcpu-ubuntu-2204
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
@@ -60,6 +60,7 @@ jobs:
- name: Analyze bundle
run: |
cd apps/web
export NODE_OPTIONS="--max_old_space_size=8192"
npx -p nextjs-bundle-analysis@0.5.0 report
- name: Upload bundle
@@ -0,0 +1,21 @@
name: Changes Requested
on:
pull_request_review:
types: [submitted]
jobs:
changes-requested:
if: github.event.review.state == 'changes_requested'
runs-on: ubuntu-latest
steps:
- name: Explanation
run: echo "This workflow triggers a workflow_run; it's necessary because otherwise the repo secrets aren't available for 'pull_request_review' events from externally forked pull requests"
- name: Save PR number to context.json
run: |
printf '{
"pr_number": ${{ github.event.pull_request.number }}
}' >> context.json
- uses: actions/upload-artifact@v4
with:
name: context.json
path: ./
+25
View File
@@ -0,0 +1,25 @@
name: Post release
on:
workflow_dispatch:
push:
# Pattern matched against refs/tags
tags:
- "*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: 'main'
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Configure git
run: |
git config --local user.email "github-actions@github.com"
git config --local user.name "GitHub Actions"
- run: git push origin +main:production
+104
View File
@@ -0,0 +1,104 @@
name: Re-draft
on:
workflow_run:
workflows:
- Changes Requested
types:
- completed
permissions:
contents: write
issues: write
pull-requests: write
statuses: write
jobs:
download-context:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: 'Download artifact'
uses: actions/github-script@v7
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "context.json"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/context.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip context.zip
- name: 'Return Parsed JSON'
uses: actions/github-script@v7
id: return-parsed-json
with:
script: |
let fs = require('fs');
let data = fs.readFileSync('./context.json');
return JSON.parse(data);
outputs:
pr_number: ${{fromJSON(steps.return-parsed-json.outputs.result).pr_number}}
re-draft:
needs:
- download-context
permissions:
contents: write
issues: write
pull-requests: write
statuses: write
runs-on: ubuntu-latest
steps:
- run: echo "This PR was rejected"
- name: Convert PR to draft when changes are requested
uses: actions/github-script@v7
with:
script: |
async function getPullRequestId() {
const pull_number = ${{ needs.download-context.outputs.pr_number }};
const pullRequest = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number,
});
if (!pullRequest.data.node_id) throw new Error(`pullRequestId no found for '${pull_number}'`);
return pullRequest.data.node_id;
}
const query = `
mutation($id: ID!) {
convertPullRequestToDraft(input: { pullRequestId: $id }) {
pullRequest {
id
number
isDraft
}
}
}
`;
const pullRequestId = await getPullRequestId();
const variables = {
id: pullRequestId,
}
const response = await github.graphql(query, variables)
if (!response.convertPullRequestToDraft) {
throw new Error("Failed to convert pull request to draft");
}
console.info("Pull request successfully converted to draft.");
console.info(`Draft conversion response: ${JSON.stringify(response, null, 2)}`);
-17
View File
@@ -1,17 +0,0 @@
on:
workflow_dispatch:
push:
# Pattern matched against refs/tags
tags:
- "*" # Push events to every tag not containing /
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main # Always checkout main even for tagged releases
fetch-depth: 0
token: ${{ secrets.GH_ACCESS_TOKEN }}
- run: git push origin +main:production
+2
View File
@@ -1,6 +1,8 @@
name: Unit
on:
workflow_call:
env:
REPLEXICA_API_KEY: ${{ secrets.CI_REPLEXICA_API_KEY }}
permissions:
contents: read
jobs:
File diff suppressed because one or more lines are too long
+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"
View File
View File
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"
+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"
View File
+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"
View File
View File
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"
+7
View File
@@ -0,0 +1,7 @@
undecided:
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
View File
+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"
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"
+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"
View File
+2
View File
@@ -5,5 +5,7 @@ plugins:
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: "@yarnpkg/plugin-version"
yarnPath: .yarn/releases/yarn-3.4.1.cjs
+22 -9
View File
@@ -6,6 +6,7 @@ import { IS_PRODUCTION } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { isAdminGuard } from "../utils/isAdmin";
import { isLockedOrBlocked } from "../utils/isLockedOrBlocked";
import { ScopeOfAdmin } from "../utils/scopeOfAdmin";
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
@@ -24,23 +25,35 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => {
if (!hasValidLicense && IS_PRODUCTION) {
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
}
// Check if the apiKey query param is provided.
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
// remove the prefix from the user provided api_key. If no env set default to "cal_"
const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", "");
// Hash the key again before matching against the database records.
const hashedKey = hashAPIKey(strippedApiKey);
// Check if the hashed api key exists in database.
const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey } });
// If cannot find any api key. Throw a 401 Unauthorized.
if (!apiKey) return res.status(401).json({ error: "Your apiKey is not valid" });
const apiKey = await prisma.apiKey.findUnique({
where: { hashedKey },
include: {
user: {
select: { role: true, locked: true, email: true },
},
},
});
if (!apiKey) return res.status(401).json({ error: "Your API key is not valid." });
if (apiKey.expiresAt && dateNotInPast(apiKey.expiresAt)) {
return res.status(401).json({ error: "This apiKey is expired" });
return res.status(401).json({ error: "This API key is expired." });
}
if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" });
if (!apiKey.userId || !apiKey.user)
return res.status(404).json({ error: "No user found for this API key." });
// save the user id in the request for later use
req.userId = apiKey.userId;
req.user = apiKey.user;
const { isAdmin, scope } = await isAdminGuard(req);
const userIsLockedOrBlocked = await isLockedOrBlocked(req);
if (userIsLockedOrBlocked)
return res.status(403).json({ error: "You are not authorized to perform this request." });
req.isSystemWideAdmin = isAdmin && scope === ScopeOfAdmin.SystemWide;
req.isOrganizationOwnerOrAdmin = isAdmin && scope === ScopeOfAdmin.OrgOwnerOrAdmin;
+3 -4
View File
@@ -6,8 +6,7 @@ import { UserPermissionRole, MembershipRole } from "@calcom/prisma/enums";
import { ScopeOfAdmin } from "./scopeOfAdmin";
export const isAdminGuard = async (req: NextApiRequest) => {
const { userId } = req;
const user = await prisma.user.findUnique({ where: { id: userId }, select: { role: true } });
const { user, userId } = req;
if (!user) return { isAdmin: false, scope: null };
const { role: userRole } = user;
@@ -34,7 +33,7 @@ export const isAdminGuard = async (req: NextApiRequest) => {
},
},
});
if (!orgOwnerOrAdminMemberships.length) return { isAdmin: false, scope: null };
if (orgOwnerOrAdminMemberships.length > 0) return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin };
return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin };
return { isAdmin: false, scope: null };
};
@@ -0,0 +1,9 @@
import type { NextApiRequest } from "next";
import { checkIfEmailInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
export async function isLockedOrBlocked(req: NextApiRequest) {
const user = req.user;
if (!user?.email) return false;
return user.locked || (await checkIfEmailInWatchlistController(user.email));
}
+2
View File
@@ -23,6 +23,7 @@ const schemaBookingBaseBodyParams = Booking.pick({
status: true,
rescheduledBy: true,
cancelledBy: true,
createdAt: true,
}).partial();
export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(schemaQueryUserId.partial());
@@ -119,4 +120,5 @@ export const schemaBookingReadPublic = Booking.extend({
fromReschedule: true,
cancelledBy: true,
rescheduledBy: true,
createdAt: true,
});
+1
View File
@@ -8,6 +8,7 @@ export declare module "next" {
session?: Session | null;
userId: number;
user?: { role: string; locked: boolean; email: string } | null;
method: string;
// session: { user: { id: number } };
// query: Partial<{ [key: string]: string | string[] }>;
@@ -1,17 +1,17 @@
import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock";
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, vi, it, expect, afterEach, beforeEach } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
import prisma from "@calcom/prisma";
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
import { isAdminGuard } from "~/lib/utils/isAdmin";
import { hashAPIKey } from "~/../../../packages/features/ee/api-keys/lib/apiKeys";
import { verifyApiKey } from "../../../lib/helpers/verifyApiKey";
import { ScopeOfAdmin } from "../../../lib/utils/scopeOfAdmin";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
@@ -20,11 +20,6 @@ afterEach(() => {
vi.resetAllMocks();
});
vi.mock("@calcom/prisma");
vi.mock("~/lib/utils/isAdmin", () => ({
isAdminGuard: vi.fn(),
}));
describe("Verify API key", () => {
let service: LicenseKeyService;
@@ -45,7 +40,6 @@ describe("Verify API key", () => {
};
vi.mocked(service.checkLicense).mockResolvedValue(false);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null });
const serverNext = vi.fn((next: void) => Promise.resolve(next));
@@ -69,7 +63,6 @@ describe("Verify API key", () => {
};
vi.mocked(service.checkLicense).mockResolvedValue(true);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null });
const serverNext = vi.fn((next: void) => Promise.resolve(next));
@@ -91,10 +84,18 @@ describe("Verify API key", () => {
},
prisma,
});
prismaMock.apiKey.findUnique.mockResolvedValue({
id: 1,
userId: 2,
const hashedKey = hashAPIKey("test_key");
await prismock.apiKey.create({
data: {
hashedKey,
user: {
create: {
email: "admin@example.com",
role: UserPermissionRole.ADMIN,
locked: false,
},
},
},
});
const middleware = {
@@ -102,7 +103,6 @@ describe("Verify API key", () => {
};
vi.mocked(service.checkLicense).mockResolvedValue(true);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.SystemWide });
const serverNext = vi.fn((next: void) => Promise.resolve(next));
@@ -125,10 +125,36 @@ describe("Verify API key", () => {
},
prisma,
});
prismaMock.apiKey.findUnique.mockResolvedValue({
id: 1,
userId: 2,
const hashedKey = hashAPIKey("test_key");
await prismock.apiKey.create({
data: {
hashedKey,
user: {
create: {
email: "org-admin@acme.com",
role: UserPermissionRole.USER,
locked: false,
teams: {
create: {
accepted: true,
role: MembershipRole.OWNER,
team: {
create: {
name: "ACME",
isOrganization: true,
organizationSettings: {
create: {
isAdminAPIEnabled: true,
orgAutoAcceptEmail: "acme.com",
},
},
},
},
},
},
},
},
},
});
const middleware = {
@@ -136,7 +162,6 @@ describe("Verify API key", () => {
};
vi.mocked(service.checkLicense).mockResolvedValue(true);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin });
const serverNext = vi.fn((next: void) => Promise.resolve(next));
@@ -149,4 +174,44 @@ describe("Verify API key", () => {
expect(req.isSystemWideAdmin).toBe(false);
expect(req.isOrganizationOwnerOrAdmin).toBe(true);
});
it("should return 403 if user is locked or blocked", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {},
query: {
apiKey: "cal_test_key",
},
prisma,
});
const hashedKey = hashAPIKey("test_key");
await prismock.apiKey.create({
data: {
hashedKey,
user: {
create: {
email: "locked@example.com",
role: UserPermissionRole.USER,
locked: true,
},
},
},
});
const middleware = {
fn: verifyApiKey,
};
vi.mocked(service.checkLicense).mockResolvedValue(true);
const serverNext = vi.fn((next: void) => Promise.resolve(next));
const middlewareSpy = vi.spyOn(middleware, "fn");
await middleware.fn(req, res, serverNext);
expect(middlewareSpy).toBeCalled();
expect(res.statusCode).toBe(403);
expect(JSON.parse(res._getData())).toEqual({ error: "You are not authorized to perform this request." });
expect(serverNext).not.toHaveBeenCalled();
});
});
@@ -19,6 +19,7 @@ describe("isAdmin guard", () => {
});
req.userId = 0;
req.user = undefined;
const { isAdmin, scope } = await isAdminGuard(req);
@@ -35,6 +36,7 @@ describe("isAdmin guard", () => {
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
req.userId = memberUser.id;
req.user = memberUser;
const { isAdmin, scope } = await isAdminGuard(req);
@@ -51,6 +53,7 @@ describe("isAdmin guard", () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "admin@example.com" } });
req.userId = adminUser.id;
req.user = adminUser;
const { isAdmin, scope } = await isAdminGuard(req);
@@ -67,6 +70,8 @@ describe("isAdmin guard", () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
req.userId = adminUser.id;
req.user = adminUser;
const { isAdmin, scope } = await isAdminGuard(req);
expect(isAdmin).toBe(true);
expect(scope).toBe(ScopeOfAdmin.OrgOwnerOrAdmin);
@@ -81,6 +86,8 @@ describe("isAdmin guard", () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-dunder@example.com" } });
req.userId = adminUser.id;
req.user = adminUser;
const { isAdmin } = await isAdminGuard(req);
expect(isAdmin).toBe(false);
});
@@ -0,0 +1,88 @@
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it, beforeEach } from "vitest";
import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked";
describe("isLockedOrBlocked", () => {
beforeEach(async () => {
await prismock.watchlist.createMany({
data: [
{
type: "DOMAIN",
value: "spam.com",
createdById: 1,
},
{
type: "DOMAIN",
value: "blocked.com",
createdById: 1,
},
],
});
});
it("should return false if no user in request", async () => {
const req = { userId: null, user: null } as any;
const result = await isLockedOrBlocked(req);
expect(result).toBe(false);
});
it("should return false if user has no email", async () => {
const req = { userId: 123, user: { email: null } } as any;
const result = await isLockedOrBlocked(req);
expect(result).toBe(false);
});
it("should return true if user is locked", async () => {
const req = {
userId: 123,
user: {
locked: true,
email: "test@example.com",
},
} as any;
const result = await isLockedOrBlocked(req);
expect(result).toBe(true);
});
it("should return true if user email domain is watchlisted", async () => {
const req = {
userId: 123,
user: {
locked: false,
email: "test@blocked.com",
},
} as any;
const result = await isLockedOrBlocked(req);
expect(result).toBe(true);
});
it("should return false if user is not locked and email domain is not watchlisted", async () => {
const req = {
userId: 123,
user: {
locked: false,
email: "test@example.com",
},
} as any;
const result = await isLockedOrBlocked(req);
expect(result).toBe(false);
});
it("should handle email domains case-insensitively", async () => {
const req = {
userId: 123,
user: {
locked: false,
email: "test@BLOCKED.COM",
},
} as any;
const result = await isLockedOrBlocked(req);
expect(result).toBe(true);
});
});
+1
View File
@@ -12,6 +12,7 @@
},
"include": [
"next-env.d.ts",
"./next.d.ts",
"**/*.ts",
"**/*.tsx",
"../../../packages/types/*.d.ts",
+1
View File
@@ -6,6 +6,7 @@ ARG DATABASE_URL
WORKDIR /calcom
RUN set -eux;
RUN ln -s /usr/lib/libssl.so.3 /lib/libssl.so.3
ENV NODE_ENV="production"
ENV NODE_OPTIONS="--max-old-space-size=8192"
+1 -1
View File
@@ -10,7 +10,7 @@
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"setupFiles": ["<rootDir>/test/setEnvVars.ts"],
"setupFiles": ["<rootDir>/test/setEnvVars.ts", "jest-date-mock"],
"reporters": ["default", "jest-summarizing-reporter"],
"workerIdleMemoryLimit": "512MB",
"maxWorkers": 2
+3 -2
View File
@@ -11,7 +11,7 @@
"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": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
"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",
"start:prod": "node ./dist/apps/api/v2/src/main.js",
@@ -29,7 +29,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.61",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.73",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -87,6 +87,7 @@
"@types/passport-jwt": "^3.0.13",
"@types/supertest": "^2.0.12",
"jest": "^29.7.0",
"jest-date-mock": "^1.0.10",
"prettier": "^2.8.6",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
+35
View File
@@ -0,0 +1,35 @@
import { execSync } from "child_process";
function checkCommandExists(command: string): boolean {
try {
execSync(`${command} --version`, { stdio: "ignore" });
return true;
} catch (e) {
return false;
}
}
try {
// Check if docker is installed
if (!checkCommandExists("docker")) {
throw new Error("Docker is not installed");
}
// Try docker compose first (new syntax)
try {
execSync("docker compose version", { stdio: "ignore" });
console.log("Starting containers with docker compose...");
execSync("docker compose up -d", { stdio: "inherit" });
} catch (e) {
// Fall back to docker-compose if the above fails
if (checkCommandExists("docker-compose")) {
console.log("Starting containers with docker-compose...");
execSync("docker-compose up -d", { stdio: "inherit" });
} else {
throw new Error("Neither 'docker compose' nor 'docker-compose' command is available");
}
}
} catch (error) {
console.error(`Error: ${error instanceof Error ? error.message : "Unknown error occurred"}`);
process.exit(1);
}
+2
View File
@@ -30,6 +30,8 @@ const loadConfig = (): AppConfig => {
stripe: {
apiKey: getEnv("STRIPE_API_KEY"),
webhookSecret: getEnv("STRIPE_WEBHOOK_SECRET"),
teamMonthlyPriceId: getEnv("STRIPE_TEAM_MONTHLY_PRICE_ID", "set-team-monthly-price-in-your-env"),
isTeamBillingEnabled: getEnv("IS_TEAM_BILLING_ENABLED", true),
},
app: {
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
+2
View File
@@ -21,6 +21,8 @@ export type AppConfig = {
stripe: {
apiKey: string;
webhookSecret: string;
teamMonthlyPriceId: string;
isTeamBillingEnabled: boolean;
};
app: {
baseUrl: string;
@@ -333,7 +333,7 @@ export class BookingsController_2024_04_15 {
}
private async getOAuthClientsParams(clientId: string): Promise<OAuthRequestParams> {
const res = DEFAULT_PLATFORM_PARAMS;
const res = { ...DEFAULT_PLATFORM_PARAMS };
try {
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
// fetch oAuthClient from db and use data stored in db to set these values
@@ -1,3 +1,4 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Transform, Type } from "class-transformer";
import {
IsBoolean,
@@ -13,53 +14,66 @@ import {
class Location {
@IsString()
@ApiProperty()
optionValue!: string;
@IsString()
@ApiProperty()
value!: string;
}
class Response {
@IsString()
@ApiProperty()
name!: string;
@IsEmail()
@ApiProperty()
email!: string;
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: [String] })
guests!: string[];
@IsOptional()
@ValidateNested()
@Type(() => Location)
@ApiPropertyOptional({ type: Location })
location?: Location;
@IsOptional()
@IsString()
@ApiPropertyOptional()
notes?: string;
}
export class CreateBookingInput_2024_04_15 {
@IsString()
@IsOptional()
@ApiPropertyOptional()
end?: string;
@IsString()
@ApiProperty()
start!: string;
@IsNumber()
@ApiProperty()
eventTypeId!: number;
@IsString()
@IsOptional()
@ApiPropertyOptional()
eventTypeSlug?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
rescheduleUid?: string;
@IsTimeZone()
@ApiProperty()
timeZone!: string;
@Transform(({ value }: { value: string | string[] }) => {
@@ -68,38 +82,48 @@ export class CreateBookingInput_2024_04_15 {
@IsOptional()
@IsArray()
@IsString({ each: true })
@ApiPropertyOptional({ type: [String] })
user?: string[];
@IsString()
@ApiProperty()
language!: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
bookingUid?: string;
@IsObject()
@ApiProperty({ type: Object })
metadata!: Record<string, string>;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
hasHashedBookingLink?: boolean;
@IsString()
@IsOptional()
@ApiPropertyOptional()
hashedLink!: string | null;
@IsString()
@IsOptional()
@ApiPropertyOptional()
seatReferenceUid?: string;
@Type(() => Response)
@ApiProperty({ type: Response })
responses!: Response;
@IsString()
@IsOptional()
@ApiPropertyOptional()
orgSlug?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
locationUrl?: string;
}
@@ -1,4 +1,5 @@
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsBoolean, IsString, IsNumber, IsOptional } from "class-validator";
import type { AppsStatus } from "@calcom/platform-libraries";
@@ -6,23 +7,29 @@ import type { AppsStatus } from "@calcom/platform-libraries";
export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2024_04_15 {
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
noEmail?: boolean;
@IsOptional()
@IsNumber()
@ApiPropertyOptional()
recurringCount?: number;
@IsOptional()
@ApiPropertyOptional({ type: [Object] })
appsStatus?: AppsStatus[] | undefined;
@IsOptional()
@ApiPropertyOptional({ type: [Object] })
allRecurringDates?: Record<string, string>[];
@IsOptional()
@IsNumber()
@ApiPropertyOptional()
currentRecurringIndex?: number;
@IsString()
@IsOptional()
@ApiPropertyOptional()
recurringEventId?: string;
}
@@ -1,22 +1,27 @@
import { ApiPropertyOptional, ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsOptional, IsArray, IsEmail, IsBoolean, ValidateNested } from "class-validator";
class Attendee {
@IsEmail()
@ApiProperty()
email!: string;
@IsBoolean()
@ApiProperty()
noShow!: boolean;
}
export class MarkNoShowInput_2024_04_15 {
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
noShowHost?: boolean;
@ValidateNested()
@Type(() => Attendee)
@IsArray()
@IsOptional()
@ApiPropertyOptional({ type: [Attendee] })
attendees?: Attendee[];
}
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsString,
@@ -17,145 +17,185 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Metadata {
@IsUrl()
@ApiProperty()
videoCallUrl!: string;
}
class Location {
@IsString()
@ApiProperty()
optionValue!: string;
@IsString()
@ApiProperty()
value!: string;
}
class Response {
@IsString()
@ApiProperty()
name!: string;
@IsEmail()
@ApiProperty()
email!: string;
@IsString()
@ApiProperty()
notes!: string;
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: [String] })
guests!: string[];
@ValidateNested()
@Type(() => Location)
@ApiProperty({ type: Location })
location!: Location;
}
class User {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty({ type: String, nullable: true })
name!: string | null;
@IsEmail()
@ApiProperty()
email!: string;
@IsString()
@ApiProperty({ type: String, nullable: true })
username!: string | null;
@IsString()
@ApiProperty()
timeZone!: string;
}
class Attendee {
@IsString()
@ApiProperty()
name!: string;
@IsEmail()
@ApiProperty()
email!: string;
@IsString()
@ApiProperty()
timeZone!: string;
}
class EventType {
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
eventName!: string | null;
@IsString()
@ApiProperty()
slug!: string;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
timeZone!: string | null;
}
class GetBookingData_2024_04_15 {
@IsString()
@ApiProperty()
title!: string;
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty()
uid!: string;
@IsString()
@ApiProperty({ type: String, nullable: true })
description!: string | null;
@IsObject()
@ApiProperty({ type: Object })
customInputs!: any;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
smsReminderNumber!: string | null;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
recurringEventId!: string | null;
@IsDateString()
@ApiProperty()
startTime!: Date;
@IsDateString()
@ApiProperty()
endTime!: Date;
@IsUrl()
@ApiProperty({ type: String, nullable: true })
location!: string | null;
@IsString()
@ApiProperty()
status!: string;
@Type(() => Metadata)
@ApiProperty({ type: Metadata })
metadata!: Metadata | any;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
cancellationReason!: string | null;
@ValidateNested()
@Type(() => Response)
@ApiProperty({ type: Response })
responses!: Response | any;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
rejectionReason!: string | null;
@IsString()
@IsEmail()
@ApiProperty({ type: String, nullable: true })
userPrimaryEmail!: string | null;
@ValidateNested()
@Type(() => User)
@ApiProperty({ type: User, nullable: true })
user!: User | null;
@ValidateNested()
@Type(() => Attendee)
@IsArray()
@ApiProperty({ type: [Attendee] })
attendees!: Attendee[];
@IsInt()
@ApiProperty({ type: Number, nullable: true })
eventTypeId!: number | null;
@ValidateNested()
@Type(() => EventType)
@ApiProperty({ type: EventType, nullable: true })
eventType!: EventType | null;
}
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsString,
@@ -26,179 +26,230 @@ enum Status {
class Attendee {
@IsInt()
@ApiProperty()
id!: number;
@IsEmail()
@ApiProperty()
email!: string;
@IsString()
@ApiProperty()
name!: string;
@IsString()
@ApiProperty()
timeZone!: string;
@IsString()
@ApiProperty({ type: String, nullable: true })
locale!: string | null;
@IsInt()
@ApiProperty({ type: Number, nullable: true })
bookingId!: number | null;
}
class EventType {
@IsString()
@IsOptional()
@IsString()
@ApiPropertyOptional()
slug?: string;
@IsOptional()
@IsInt()
@ApiPropertyOptional()
id?: number;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
eventName?: string | null;
@IsInt()
@ApiProperty()
price!: number;
@IsOptional()
@ApiPropertyOptional()
recurringEvent?: any;
@IsString()
@ApiProperty()
currency!: string;
@IsObject()
@ApiProperty()
metadata!: any;
@IsBoolean()
@IsOptional()
seatsShowAttendees?: boolean | undefined | null;
@ApiPropertyOptional({ type: Boolean, nullable: true })
seatsShowAttendees?: boolean | null;
@IsBoolean()
@IsOptional()
seatsShowAvailabilityCount?: boolean | undefined | null;
@ApiPropertyOptional({ type: Boolean, nullable: true })
seatsShowAvailabilityCount?: boolean | null;
@IsOptional()
@ApiPropertyOptional({ nullable: true })
team?: any | null;
}
class Reference {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty()
type!: string;
@IsString()
@ApiProperty()
uid!: string;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
meetingId?: string | null;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
thirdPartyRecurringEventId?: string | null;
@IsString()
@ApiProperty({ type: String, nullable: true })
meetingPassword!: string | null;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
meetingUrl?: string | null;
@IsInt()
@ApiProperty({ type: Number, nullable: true })
bookingId!: number | null;
@IsEmail()
@ApiProperty({ type: String, nullable: true })
externalCalendarId!: string | null;
@IsOptional()
@ApiPropertyOptional()
deleted?: any;
@IsInt()
@ApiProperty({ type: Number, nullable: true })
credentialId!: number | null;
}
class User {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty({ type: String, nullable: true })
name!: string | null;
@IsEmail()
@ApiProperty()
email!: string;
}
class GetBookingsDataEntry {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty()
title!: string;
@IsOptional()
@IsEmail()
@ApiProperty({ type: String, nullable: true })
userPrimaryEmail?: string | null;
@IsString()
@ApiProperty({ type: String, nullable: true })
description!: string | null;
@IsObject()
@ApiProperty({ type: Object })
customInputs!: object | any;
@IsDateString()
@ApiProperty()
startTime!: string;
@IsDateString()
@ApiProperty()
endTime!: string;
@ValidateNested({ each: true })
@Type(() => Attendee)
@IsArray()
@ApiProperty({ type: [Attendee] })
attendees!: Attendee[];
@ApiProperty()
metadata!: any;
@IsString()
@ApiProperty()
uid!: string;
@IsOptional()
@IsString()
@ApiProperty({ type: String, nullable: true })
recurringEventId!: string | null;
@IsUrl()
@ApiProperty({ type: String, nullable: true })
location!: string | null;
@ValidateNested()
@Type(() => EventType)
@ApiProperty({ type: EventType })
eventType!: EventType;
@IsEnum(Status)
status!: "CANCELLED" | "REJECTED" | "ACCEPTED" | "PENDING" | "AWAITING_HOST";
@ApiProperty({ enum: Status, type: String })
status!: Status;
@IsBoolean()
@ApiProperty({ type: Boolean })
paid!: boolean;
@IsArray()
@ApiProperty()
payment!: any[];
@ValidateNested()
@Type(() => Reference)
@IsArray()
@ApiProperty({ type: [Reference] })
references!: Reference[];
@IsBoolean()
@ApiProperty({ type: Boolean })
isRecorded!: boolean;
@IsArray()
@ApiProperty()
seatsReferences!: any[];
@ValidateNested()
@Type(() => User)
@ApiProperty({ type: User })
user!: User | null;
@IsOptional()
@ApiPropertyOptional()
rescheduled?: any;
}
@@ -206,17 +257,20 @@ class GetBookingsData_2024_04_15 {
@ValidateNested()
@Type(() => GetBookingsDataEntry)
@IsArray()
@ApiProperty({ type: [GetBookingsDataEntry] })
bookings!: GetBookingsDataEntry[];
@IsArray()
@ApiProperty()
recurringInfo!: any[];
@IsInt()
@ApiProperty({ type: Number, nullable: true })
nextCursor!: number | null;
}
export class GetBookingsOutput_2024_04_15 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS], type: String })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsString, IsEnum, IsOptional, ValidateNested, IsArray, IsEmail, IsBoolean } from "class-validator";
@@ -6,24 +6,29 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Attendee {
@IsEmail()
@ApiProperty()
email!: string;
@IsBoolean()
@ApiProperty()
noShow!: boolean;
}
class HandleMarkNoShowData_2024_04_15 {
@IsString()
@ApiProperty()
message!: string;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
noShowHost?: boolean;
@ValidateNested()
@Type(() => Attendee)
@IsArray()
@IsOptional()
@ApiPropertyOptional({ type: [Attendee] })
attendees?: Attendee[];
}
@@ -1,160 +0,0 @@
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 { 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 { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
describe("With api key", () => {
let app: INestApplication;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let apiKeyString: string;
const userEmail = "bookings-controller-e2e@api.com";
let user: User;
let eventTypeId: number;
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);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
user = await userRepositoryFixture.create({
email: userEmail,
});
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = keyString;
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
user.id
);
eventTypeId = event.id;
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
});
describe("create bookings", () => {
it("should create a booking with api key", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId,
attendee: {
name: "Mr Key",
email: "mr_key@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
meetingUrl: "https://meet.google.com/abc-def-ghi",
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.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);
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[0].id).toEqual(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString());
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(eventTypeId);
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(data.meetingUrl).toEqual(body.meetingUrl);
expect(data.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
await app.close();
});
});
});
@@ -0,0 +1,292 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
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 { 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";
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 { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
AttendeeScheduledEmail,
OrganizerScheduledEmail,
AttendeeRescheduledEmail,
OrganizerRescheduledEmail,
AttendeeCancelledEmail,
OrganizerCancelledEmail,
} from "@calcom/platform-libraries";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
CancelBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
jest.spyOn(AttendeeScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest.spyOn(OrganizerScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest
.spyOn(AttendeeRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
describe("Bookings Endpoints 2024-08-13", () => {
describe("With api key", () => {
let app: INestApplication;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let apiKeyString: string;
const userEmail = "bookings-controller-e2e@api.com";
let user: User;
let eventTypeId: number;
let createdBooking: BookingOutput_2024_08_13;
let rescheduledBooking: BookingOutput_2024_08_13;
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);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
user = await userRepositoryFixture.create({
email: userEmail,
});
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = keyString;
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
user.id
);
eventTypeId = event.id;
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
});
it("should create a booking with api key", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId,
attendee: {
name: "Mr Key",
email: "mr_key@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
meetingUrl: "https://meet.google.com/abc-def-ghi",
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.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);
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[0].id).toEqual(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString());
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(eventTypeId);
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(data.meetingUrl).toEqual(body.meetingUrl);
expect(data.absentHost).toEqual(false);
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
createdBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should reschedule a booking with api key", 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/${createdBooking.uid}/reschedule`)
.send(body)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.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)) {
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(2035, 0, 8, 15, 0, 0)).toISOString());
expect(data.rescheduledFromUid).toEqual(createdBooking.uid);
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual(createdBooking.status);
expect(data.duration).toEqual(createdBooking.duration);
expect(data.eventTypeId).toEqual(createdBooking.eventTypeId);
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
expect(data.location).toEqual(createdBooking.location);
expect(data.absentHost).toEqual(createdBooking.absentHost);
expect(data.metadata).toEqual(createdBooking.metadata);
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
rescheduledBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should cancel booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
const booking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid);
expect(booking).toBeDefined();
expect(booking?.status).toEqual("ACCEPTED");
return request(app.getHttpServer())
.post(`/v2/bookings/${rescheduledBooking.uid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_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.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("cancelled");
expect(data.cancellationReason).toEqual(body.cancellationReason);
expect(data.start).toEqual(rescheduledBooking.start);
expect(data.end).toEqual(rescheduledBooking.end);
expect(data.duration).toEqual(rescheduledBooking.duration);
expect(data.eventTypeId).toEqual(rescheduledBooking.eventTypeId);
expect(data.attendees[0]).toEqual(rescheduledBooking.attendees[0]);
expect(data.location).toEqual(rescheduledBooking.location);
expect(data.absentHost).toEqual(rescheduledBooking.absentHost);
const cancelledBooking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid);
expect(cancelledBooking).toBeDefined();
expect(cancelledBooking?.status).toEqual("CANCELLED");
expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
});
});
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
await app.close();
});
});
});
@@ -0,0 +1,526 @@
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 { 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 * 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 { randomNumber } from "test/utils/randomNumber";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
CreateBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { BookingOutput_2024_08_13 } from "@calcom/platform-types";
import { Booking, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
const suffix = randomNumber();
describe("Bookings Endpoints 2024-08-13", () => {
describe("Booking fields", () => {
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 = `alice-${suffix}@api.com`;
let user: User;
let bookingWithSplitName: Booking;
const splitName = {
firstName: "Oldie",
lastName: "Goldie",
};
let seatedBookingWithSplitName: Booking;
let eventTypeWithBookingFieldsId: number;
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: `booking fields ${suffix}` });
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: `normal-booking-${randomNumber()}`, length: 60 },
user.id
);
bookingWithSplitName = 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)),
title: "peer coding lets goo",
uid: "booking",
eventType: {
connect: {
id: event.id,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: splitName,
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie Goldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const seatedEvent = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: `seated-${randomNumber()}`, length: 60, seatsPerTimeSlot: 3 },
user.id
);
seatedBookingWithSplitName = await bookingsRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 15, 0, 0)),
title: "peer coding lets goo",
uid: "seated-booking",
eventType: {
connect: {
id: seatedEvent.id,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: splitName,
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie Goldie",
locale: "lv",
timeZone: "Europe/Rome",
bookingSeat: {
create: {
referenceUid: "unique-seat-uid",
data: {
responses: {
email: "oldie@gmail.com",
name: {
firstName: "Oldie",
lastName: "Goldie",
},
},
},
metadata: {},
booking: {
connect: {
uid: "seated-booking",
},
},
},
},
},
},
});
const eventTypeWithBookingFields = await eventTypesRepositoryFixture.create(
{
title: "peer coding with booking fields",
slug: `with-custom-booking-fields-${randomNumber()}`,
length: 60,
bookingFields: [
{
name: "name",
type: "name",
label: "",
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
variant: "fullName",
editable: "system",
required: true,
placeholder: "",
defaultLabel: "your_name",
variantsConfig: {
variants: {
fullName: {
fields: [
{
name: "fullName",
type: "text",
label: "your_name",
required: true,
placeholder: "",
},
],
},
firstAndLastName: {
fields: [
{
name: "firstName",
type: "text",
label: "name",
required: true,
placeholder: "lauris",
},
{
name: "lastName",
type: "text",
label: "surname",
required: true,
placeholder: "skraucis",
},
],
},
},
},
disableOnPrefill: false,
},
{
name: "email",
type: "email",
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system",
required: true,
defaultLabel: "email_address",
},
{
name: "location",
type: "radioInput",
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system",
required: false,
defaultLabel: "location",
getOptionsAt: "locations",
optionsInputs: {
phone: {
type: "phone",
required: true,
placeholder: "",
},
somewhereElse: {
type: "text",
required: true,
placeholder: "",
},
attendeeInPerson: {
type: "address",
required: true,
placeholder: "",
},
},
hideWhenJustOneOption: true,
},
{
name: "title",
type: "text",
hidden: true,
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system-but-optional",
required: true,
defaultLabel: "what_is_this_meeting_about",
defaultPlaceholder: "",
},
{
name: "notes",
type: "textarea",
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
{
name: "guests",
type: "multiemail",
hidden: false,
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_guests",
defaultPlaceholder: "email",
},
{
name: "rescheduleReason",
type: "textarea",
views: [
{
id: "reschedule",
label: "Reschedule View",
},
],
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system-but-optional",
required: false,
defaultLabel: "reason_for_reschedule",
defaultPlaceholder: "reschedule_placeholder",
},
{
name: "favorite-movie",
type: "text",
label: "favorite movie",
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: true,
placeholder: "matrix",
disableOnPrefill: false,
},
],
},
user.id
);
eventTypeWithBookingFieldsId = eventTypeWithBookingFields.id;
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;
}
describe("get individual booking", () => {
it("should 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)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_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.attendees[0].name).toEqual(`${splitName.firstName} ${splitName.lastName}`);
expect(data.bookingFieldsResponses.name).toEqual(splitName);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should 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)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_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)) {
// eslint-disable-next-line
// @ts-ignore
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
console.log("asap data", JSON.stringify(data, null, 2));
expect(data.attendees[0].name).toEqual(`${splitName.firstName} ${splitName.lastName}`);
expect(data.attendees[0].bookingFieldsResponses.name).toEqual(splitName);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
describe("make booking", () => {
it("should not be able to book an event type with custom required booking fields if they are missing in bookingFieldsResponses", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithBookingFieldsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
};
return request(app.getHttpServer())
.post(`/v2/bookings`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
it("should be able to book an event type with custom required booking fields", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithBookingFieldsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
"favorite-movie": "lord of the rings",
},
};
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);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.bookingFieldsResponses["favorite-movie"]).toEqual("lord of the rings");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
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();
});
});
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
});
@@ -1,6 +1,5 @@
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 { 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";
@@ -20,26 +19,9 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
AttendeeScheduledEmail,
OrganizerScheduledEmail,
AttendeeDeclinedEmail,
} from "@calcom/platform-libraries";
import { BookingOutput_2024_08_13, GetBookingOutput_2024_08_13 } from "@calcom/platform-types";
import { Booking, PlatformOAuthClient, Team } from "@calcom/prisma/client";
jest.spyOn(AttendeeScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest.spyOn(OrganizerScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest.spyOn(AttendeeDeclinedEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
describe("Bookings Endpoints 2024-08-13", () => {
describe("Bookings confirmation", () => {
let app: INestApplication;
@@ -87,6 +69,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -0,0 +1,474 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
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 { 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";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
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 { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
OrganizerScheduledEmail,
AttendeeScheduledEmail,
OrganizerRescheduledEmail,
AttendeeRescheduledEmail,
OrganizerCancelledEmail,
AttendeeCancelledEmail,
AttendeeRequestEmail,
OrganizerRequestEmail,
AttendeeDeclinedEmail,
} from "@calcom/platform-libraries";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
jest
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeRequestEmail.prototype, "getHtmlRequestEmail")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerRequestEmail.prototype, "getHtmlRequestEmail")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeDeclinedEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
type EmailSetup = {
user: User;
eventTypeId: number;
createdBookingUid: string;
rescheduledBookingUid: string;
};
describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
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 teamRepositoryFixture: TeamRepositoryFixture;
let emailsEnabledSetup: EmailSetup;
let emailsDisabledSetup: EmailSetup;
const authEmail = `admin-${Math.floor(Math.random() * 1000)}@example.com`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
authEmail,
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: "organization bookings" });
await setupEnabledEmails();
await setupDisabledEmails();
await userRepositoryFixture.create({
email: authEmail,
organization: {
connect: {
id: organization.id,
},
},
role: "ADMIN",
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function setupEnabledEmails() {
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
const user = await userRepositoryFixture.create({
email: `alice-${Math.floor(Math.random() * 1000)}@gmail.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60, requiresConfirmation: true },
user.id
);
emailsEnabledSetup = {
user,
eventTypeId: event.id,
createdBookingUid: "",
rescheduledBookingUid: "",
};
}
async function setupDisabledEmails() {
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
const user = await userRepositoryFixture.create({
email: `bob-${Math.floor(Math.random() * 1000)}@gmail.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60, requiresConfirmation: true },
user.id
);
emailsDisabledSetup = {
user,
eventTypeId: event.id,
createdBookingUid: "",
rescheduledBookingUid: "",
};
}
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
areEmailsEnabled: emailsEnabled,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
describe("OAuth client managed user bookings - emails disabled", () => {
beforeEach(async () => {
jest.clearAllMocks();
});
it("should not 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: emailsDisabledSetup.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(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should not send an email when confirming a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.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).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
it("should not 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: emailsDisabledSetup.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).not.toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should not send an email when declining a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.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).not.toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
});
describe("OAuth client managed user bookings - emails enabled", () => {
beforeEach(async () => {
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",
},
};
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();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
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"
);
}
});
});
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;
});
});
});
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.user.email);
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.user.email);
await userRepositoryFixture.deleteByEmail(authEmail);
await bookingsRepositoryFixture.deleteAllBookings(
emailsEnabledSetup.user.id,
emailsEnabledSetup.user.email
);
await bookingsRepositoryFixture.deleteAllBookings(
emailsDisabledSetup.user.id,
emailsDisabledSetup.user.email
);
await app.close();
});
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
});
@@ -0,0 +1,986 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
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 { 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";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { createParamDecorator, 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 { HostsRepositoryFixture } from "test/fixtures/repository/hosts.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 { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
OrganizerScheduledEmail,
AttendeeScheduledEmail,
OrganizerRescheduledEmail,
AttendeeRescheduledEmail,
OrganizerCancelledEmail,
AttendeeCancelledEmail,
OrganizerReassignedEmail,
AttendeeUpdatedEmail,
} from "@calcom/platform-libraries";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
jest
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerReassignedEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeUpdatedEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
type EmailSetup = {
team: Team;
member1: User;
member2: User;
collectiveEventType: {
id: number;
createdBookingUid: string;
rescheduledBookingUid: string;
};
roundRobinEventType: {
id: number;
createdBookingUid: string;
rescheduledBookingUid: string;
currentHostId: number;
};
};
describe("Bookings Endpoints 2024-08-13 team emails", () => {
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 teamRepositoryFixture: TeamRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let hostsRepositoryFixture: HostsRepositoryFixture;
let emailsEnabledSetup: EmailSetup;
let emailsDisabledSetup: EmailSetup;
const authEmail = "admin@example.com";
beforeAll(async () => {
const moduleRef = await withApiAuth(
authEmail,
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);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
await setupEnabledEmails();
await setupDisabledEmails();
await userRepositoryFixture.create({
email: authEmail,
organization: {
connect: {
id: organization.id,
},
},
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function setupEnabledEmails() {
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
const team = await teamRepositoryFixture.create({
name: "team 1",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClientEmailsEnabled.id,
},
},
});
const member1 = await userRepositoryFixture.create({
email: "alice@gmail.com",
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
},
},
});
const member2 = await userRepositoryFixture.create({
email: "bob@gmail.com",
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(member1.id, userSchedule);
await schedulesService.createUserSchedule(member2.id, userSchedule);
await profileRepositoryFixture.create({
uid: `usr-${member1.id}`,
username: member1.email,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: member1.id,
},
},
});
await profileRepositoryFixture.create({
uid: `usr-${member2.id}`,
username: member2.email,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: member2.id,
},
},
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: member1.id } },
team: { connect: { id: team.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: member2.id } },
team: { connect: { id: team.id } },
accepted: true,
});
const collectiveEvent = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team.id },
},
title: "Collective Event Type",
slug: "collective-event-type",
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
});
await hostsRepositoryFixture.create({
isFixed: true,
user: {
connect: {
id: member1.id,
},
},
eventType: {
connect: {
id: collectiveEvent.id,
},
},
});
await hostsRepositoryFixture.create({
isFixed: true,
user: {
connect: {
id: member2.id,
},
},
eventType: {
connect: {
id: collectiveEvent.id,
},
},
});
const roundRobinEvent = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "ROUND_ROBIN",
team: {
connect: { id: team.id },
},
title: "Round Robin Event Type",
slug: "round-robin-event-type",
length: 60,
assignAllTeamMembers: false,
bookingFields: [],
locations: [],
});
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: member1.id,
},
},
eventType: {
connect: {
id: roundRobinEvent.id,
},
},
});
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: member2.id,
},
},
eventType: {
connect: {
id: roundRobinEvent.id,
},
},
});
emailsEnabledSetup = {
team,
member1: member1,
member2: member2,
collectiveEventType: {
id: collectiveEvent.id,
createdBookingUid: "",
rescheduledBookingUid: "",
},
roundRobinEventType: {
id: roundRobinEvent.id,
createdBookingUid: "",
rescheduledBookingUid: "",
currentHostId: 0,
},
};
}
async function setupDisabledEmails() {
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
const team = await teamRepositoryFixture.create({
name: "team 2",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClientEmailsDisabled.id,
},
},
});
const member1 = await userRepositoryFixture.create({
email: "charlie@gmail.com",
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
},
},
});
const member2 = await userRepositoryFixture.create({
email: "dean@gmail.com",
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(member1.id, userSchedule);
await schedulesService.createUserSchedule(member2.id, userSchedule);
await profileRepositoryFixture.create({
uid: `usr-${member1.id}`,
username: member1.email,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: member1.id,
},
},
});
await profileRepositoryFixture.create({
uid: `usr-${member2.id}`,
username: member2.email,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: member2.id,
},
},
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: member1.id } },
team: { connect: { id: team.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: member2.id } },
team: { connect: { id: team.id } },
accepted: true,
});
const collectiveEvent = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team.id },
},
title: "Collective Event Type",
slug: "collective-event-type",
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
});
await hostsRepositoryFixture.create({
isFixed: true,
user: {
connect: {
id: member1.id,
},
},
eventType: {
connect: {
id: collectiveEvent.id,
},
},
});
await hostsRepositoryFixture.create({
isFixed: true,
user: {
connect: {
id: member2.id,
},
},
eventType: {
connect: {
id: collectiveEvent.id,
},
},
});
const roundRobinEvent = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "ROUND_ROBIN",
team: {
connect: { id: team.id },
},
title: "Round Robin Event Type",
slug: "round-robin-event-type",
length: 60,
assignAllTeamMembers: false,
bookingFields: [],
locations: [],
});
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: member1.id,
},
},
eventType: {
connect: {
id: roundRobinEvent.id,
},
},
});
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: member2.id,
},
},
eventType: {
connect: {
id: roundRobinEvent.id,
},
},
});
emailsDisabledSetup = {
team,
member1: member1,
member2: member2,
collectiveEventType: {
id: collectiveEvent.id,
createdBookingUid: "",
rescheduledBookingUid: "",
},
roundRobinEventType: {
id: roundRobinEvent.id,
createdBookingUid: "",
rescheduledBookingUid: "",
currentHostId: 0,
},
};
}
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
areEmailsEnabled: emailsEnabled,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
beforeEach(async () => {
jest.clearAllMocks();
});
describe("OAuth client team bookings - emails disabled", () => {
describe("book", () => {
it("should not send an email when booking collective event", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
eventTypeId: emailsDisabledSetup.collectiveEventType.id,
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(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.collectiveEventType.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should not send an email when booking round robin event", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
eventTypeId: emailsDisabledSetup.roundRobinEventType.id,
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(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.roundRobinEventType.createdBookingUid = responseBody.data.uid;
emailsDisabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
describe("reschedule", () => {
it("should not send an email when rescheduling collective booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 11, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.collectiveEventType.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);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.collectiveEventType.rescheduledBookingUid = responseBody.data.uid;
});
});
it("should not send an email when rescheduling round robin booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 12, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.roundRobinEventType.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);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.roundRobinEventType.rescheduledBookingUid = responseBody.data.uid;
emailsDisabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
});
});
});
describe("reassign", () => {
it("should not send an email when manually reassigning round robin booking", async () => {
const reassignToId =
emailsDisabledSetup.roundRobinEventType.currentHostId === emailsDisabledSetup.member1.id
? emailsDisabledSetup.member2.id
: emailsDisabledSetup.member1.id;
return request(app.getHttpServer())
.post(
`/v2/bookings/${emailsDisabledSetup.roundRobinEventType.rescheduledBookingUid}/reassign/${reassignToId}`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.roundRobinEventType.currentHostId = reassignToId;
});
});
it("should not send an email when automatically reassigning round robin booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.roundRobinEventType.rescheduledBookingUid}/reassign`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerReassignedEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled();
});
});
});
describe("cancel", () => {
it("should not send an email when cancelling a collective booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.collectiveEventType.rescheduledBookingUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
});
});
});
it("should not send an email when cancelling a round robin booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.roundRobinEventType.rescheduledBookingUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
});
});
});
describe("OAuth client team bookings - emails enabled", () => {
beforeEach(async () => {
jest.clearAllMocks();
});
describe("book", () => {
it("should send an email when booking collective event", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.collectiveEventType.id,
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(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
emailsEnabledSetup.collectiveEventType.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 booking round robin event", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.roundRobinEventType.id,
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(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
emailsEnabledSetup.roundRobinEventType.createdBookingUid = responseBody.data.uid;
emailsEnabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
describe("reschedule", () => {
it("should send an email when rescheduling collective booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 11, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.collectiveEventType.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);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
emailsEnabledSetup.collectiveEventType.rescheduledBookingUid = responseBody.data.uid;
});
});
it("should send an email when rescheduling round robin booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 12, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.roundRobinEventType.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);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid = responseBody.data.uid;
emailsEnabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
});
});
});
describe("reassign", () => {
it("should send an email when manually reassigning round robin booking", async () => {
const reassignToId =
emailsEnabledSetup.roundRobinEventType.currentHostId === emailsEnabledSetup.member1.id
? emailsEnabledSetup.member2.id
: emailsEnabledSetup.member1.id;
return request(app.getHttpServer())
.post(
`/v2/bookings/${emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid}/reassign/${reassignToId}`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(AttendeeUpdatedEmail.prototype.getHtml).toHaveBeenCalled();
emailsDisabledSetup.roundRobinEventType.currentHostId = reassignToId;
});
});
it("should send an email when automatically reassigning round robin booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid}/reassign`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerReassignedEmail.prototype.getHtml).toHaveBeenCalled();
expect(AttendeeUpdatedEmail.prototype.getHtml).toHaveBeenCalled();
});
});
});
describe("cancel", () => {
it("should send an email when cancelling a collective booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.collectiveEventType.rescheduledBookingUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
});
});
it("should send an email when cancelling round robin booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
});
});
});
});
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.member1.email);
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.member1.email);
await bookingsRepositoryFixture.deleteAllBookings(
emailsEnabledSetup.member1.id,
emailsEnabledSetup.member1.email
);
await bookingsRepositoryFixture.deleteAllBookings(
emailsDisabledSetup.member1.id,
emailsDisabledSetup.member1.email
);
await app.close();
});
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
});
@@ -0,0 +1,482 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
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 { 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";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
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 { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
OrganizerScheduledEmail,
AttendeeScheduledEmail,
OrganizerRescheduledEmail,
AttendeeRescheduledEmail,
OrganizerCancelledEmail,
AttendeeCancelledEmail,
} from "@calcom/platform-libraries";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
jest
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerRescheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
type EmailSetup = {
user: User;
eventTypeId: number;
recurringEventTypeId: number;
createdBookingUid: string;
rescheduledBookingUid: string;
};
describe("Bookings Endpoints 2024-08-13 user emails", () => {
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 teamRepositoryFixture: TeamRepositoryFixture;
let emailsEnabledSetup: EmailSetup;
let emailsDisabledSetup: EmailSetup;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.overrideGuard(ApiAuthGuard)
.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: "organization bookings" });
await setupEnabledEmails();
await setupDisabledEmails();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function setupEnabledEmails() {
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
const user = await userRepositoryFixture.create({
email: `alice-${Math.floor(Math.random() * 1000)}@gmail.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
user.id
);
const recurringEvent = await eventTypesRepositoryFixture.create(
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
{
title: "peer coding recurring",
slug: "peer-coding-recurring",
length: 60,
recurringEvent: { freq: 2, count: 3, interval: 1 },
},
user.id
);
emailsEnabledSetup = {
user,
eventTypeId: event.id,
recurringEventTypeId: recurringEvent.id,
createdBookingUid: "",
rescheduledBookingUid: "",
};
}
async function setupDisabledEmails() {
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
const user = await userRepositoryFixture.create({
email: `bob-${Math.floor(Math.random() * 1000)}@gmail.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
user.id
);
const recurringEvent = await eventTypesRepositoryFixture.create(
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
{
title: "peer coding recurring",
slug: "peer-coding-recurring",
length: 60,
recurringEvent: { freq: 2, count: 3, interval: 1 },
},
user.id
);
emailsDisabledSetup = {
user,
eventTypeId: event.id,
recurringEventTypeId: recurringEvent.id,
createdBookingUid: "",
rescheduledBookingUid: "",
};
}
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
areEmailsEnabled: emailsEnabled,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
beforeEach(async () => {
jest.clearAllMocks();
});
describe("OAuth client managed user bookings - emails disabled", () => {
it("should not send an email when creating a booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: emailsDisabledSetup.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(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should not send an email when creating a recurring booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
eventTypeId: emailsDisabledSetup.recurringEventTypeId,
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 (responseDataIsRecurringBooking(responseBody.data)) {
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
} else {
throw new Error("Invalid response data - expected booking array but received single booking");
}
});
});
it("should not send an email when rescheduling a 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/${emailsDisabledSetup.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);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
it("should not send an email when cancelling a booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.rescheduledBookingUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
});
});
});
describe("OAuth client managed user bookings - emails enabled", () => {
it("should send an email when creating a booking", 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)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).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 creating a recurring booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.recurringEventTypeId,
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 (responseDataIsRecurringBooking(responseBody.data)) {
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
} else {
throw new Error("Invalid response data - expected booking array but received single booking");
}
});
});
it("should send an email when rescheduling a 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/${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);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
it("should send an email when cancelling a booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "Going on a vacation",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.rescheduledBookingUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
});
});
});
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.user.email);
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.user.email);
await bookingsRepositoryFixture.deleteAllBookings(
emailsEnabledSetup.user.id,
emailsEnabledSetup.user.email
);
await bookingsRepositoryFixture.deleteAllBookings(
emailsDisabledSetup.user.id,
emailsDisabledSetup.user.email
);
await app.close();
});
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
function responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] {
return Array.isArray(data);
}
});
@@ -1,6 +1,5 @@
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 { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-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";
@@ -80,14 +79,19 @@ describe("Bookings Endpoints 2024-08-13", () => {
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" });
oAuthClient = await createOAuthClient(organization.id);
team = await teamRepositoryFixture.create({
name: "team 1",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClient.id,
},
},
});
oAuthClient = await createOAuthClient(organization.id);
teamUser1 = await userRepositoryFixture.create({
email: teamUserEmail,
locale: "it",
@@ -72,6 +72,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -176,6 +181,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -196,6 +202,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.recurringBookingUid).toBeDefined();
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -251,6 +258,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -272,6 +280,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.recurringBookingUid).toBeDefined();
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -291,6 +300,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(thirdBooking.recurringBookingUid).toBeDefined();
expect(thirdBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -375,6 +385,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -577,6 +592,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -93,6 +93,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -163,6 +168,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
metadata: {
userId: "100",
},
};
return request(app.getHttpServer())
@@ -197,6 +205,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -205,6 +214,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: body.attendee.name,
...body.bookingFieldsResponses,
},
metadata: body.metadata,
});
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
@@ -230,6 +240,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
codingLanguage: "Rust",
},
metadata: {
userId: "200",
},
};
return request(app.getHttpServer())
@@ -266,6 +279,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
const firstAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeOne);
expect(firstAttendee).toEqual({
name: createdSeatedBooking.attendees[0].name,
email: createdSeatedBooking.attendees[0].email,
timeZone: createdSeatedBooking.attendees[0].timeZone,
language: createdSeatedBooking.attendees[0].language,
absent: false,
@@ -274,10 +288,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: createdSeatedBooking.attendees[0].name,
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
},
metadata: createdSeatedBooking.attendees[0].metadata,
});
const secondAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeTwo);
expect(secondAttendee).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -286,6 +302,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: body.attendee.name,
...body.bookingFieldsResponses,
},
metadata: body.metadata,
});
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
@@ -314,6 +331,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
codingLanguage: "TypeScript",
},
recurrenceCount,
metadata: {
userId: "300",
},
};
return request(app.getHttpServer())
@@ -351,6 +371,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.attendees.length).toEqual(1);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -359,6 +380,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: body.attendee.name,
...body.bookingFieldsResponses,
},
metadata: body.metadata,
});
expect(firstBooking.location).toEqual(body.location);
expect(firstBooking.absentHost).toEqual(false);
@@ -384,6 +406,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.attendees.length).toEqual(1);
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -392,6 +415,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: body.attendee.name,
...body.bookingFieldsResponses,
},
metadata: body.metadata,
});
expect(secondBooking.location).toEqual(body.location);
expect(secondBooking.absentHost).toEqual(false);
@@ -557,6 +581,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
const attendee = createdSeatedBooking.attendees.find((a) => a.seatUid === body.seatUid);
expect(data.attendees[0]).toEqual({
name: attendee?.name,
email: attendee?.email,
timeZone: attendee?.timeZone,
language: attendee?.language,
absent: false,
@@ -565,6 +590,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: attendee?.name,
...attendee?.bookingFieldsResponses,
},
metadata: attendee?.metadata,
});
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
@@ -697,6 +723,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -769,6 +800,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
metadata: {
userId: "1000",
},
recurrenceCount,
};
@@ -807,6 +841,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.attendees.length).toEqual(1);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -815,6 +850,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: body.attendee.name,
...body.bookingFieldsResponses,
},
metadata: body.metadata,
});
expect(firstBooking.location).toEqual(body.location);
expect(firstBooking.absentHost).toEqual(false);
@@ -840,6 +876,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.attendees.length).toEqual(1);
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -848,6 +885,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: body.attendee.name,
...body.bookingFieldsResponses,
},
metadata: body.metadata,
});
expect(secondBooking.location).toEqual(body.location);
expect(secondBooking.absentHost).toEqual(false);
@@ -875,6 +913,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
metadata: {
userId: "2000",
},
recurrenceCount,
};
@@ -913,6 +954,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.attendees.length).toEqual(2);
expect(firstBooking.attendees[0]).toEqual({
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -921,9 +963,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: nameAttendeeOne,
...body.bookingFieldsResponses,
},
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
});
expect(firstBooking.attendees[1]).toEqual({
name: nameAttendeeTwo,
email: emailAttendeeTwo,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -932,6 +976,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: nameAttendeeTwo,
...body.bookingFieldsResponses,
},
metadata: body.metadata,
});
expect(firstBooking.location).toEqual(body.location);
expect(firstBooking.absentHost).toEqual(false);
@@ -957,6 +1002,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.attendees.length).toEqual(2);
expect(secondBooking.attendees[0]).toEqual({
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -965,9 +1011,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: nameAttendeeOne,
...body.bookingFieldsResponses,
},
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
});
expect(secondBooking.attendees[1]).toEqual({
name: nameAttendeeTwo,
email: emailAttendeeTwo,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -976,6 +1024,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: nameAttendeeTwo,
...body.bookingFieldsResponses,
},
metadata: body.metadata,
});
expect(secondBooking.location).toEqual(body.location);
expect(secondBooking.absentHost).toEqual(false);
@@ -85,30 +85,50 @@ describe("Bookings Endpoints 2024-08-13", () => {
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" });
oAuthClient = await createOAuthClient(organization.id);
team1 = await teamRepositoryFixture.create({
name: "team 1",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClient.id,
},
},
});
team2 = await teamRepositoryFixture.create({
name: "team 2",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClient.id,
},
},
});
oAuthClient = await createOAuthClient(organization.id);
teamUser = await userRepositoryFixture.create({
email: teamUserEmail,
locale: "it",
name: "orgUser1team1",
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
teamUser2 = await userRepositoryFixture.create({
email: teamUserEmail2,
locale: "es",
name: "orgUser2team1",
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -287,6 +307,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -339,12 +360,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees.length).toEqual(2);
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(data.attendees[1]).toEqual({
name: teamUser2.name,
email: teamUser2.email,
timeZone: teamUser2.timeZone,
language: teamUser2.locale,
absent: false,
@@ -14,6 +14,7 @@ import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import { advanceTo, clear } from "jest-date-mock";
import * as request from "supertest";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
@@ -95,6 +96,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
username: `bob-${Math.floor(Math.random() * 1000)}@gmail.com`,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -178,6 +185,136 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
describe("create bookings", () => {
describe("invalid metadata", () => {
it("should not be able to create a booking with metadata with more than 50 keys", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
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: {
key1: "1",
key2: "2",
key3: "3",
key4: "4",
key5: "5",
key6: "6",
key7: "7",
key8: "8",
key9: "9",
key10: "10",
key11: "11",
key12: "12",
key13: "13",
key14: "14",
key15: "15",
key16: "16",
key17: "17",
key18: "18",
key19: "19",
key20: "20",
key21: "21",
key22: "22",
key23: "23",
key24: "24",
key25: "25",
key26: "26",
key27: "27",
key28: "28",
key29: "29",
key30: "30",
key31: "31",
key32: "32",
key33: "33",
key34: "34",
key35: "35",
key36: "36",
key37: "37",
key38: "38",
key39: "39",
key40: "40",
key41: "41",
key42: "42",
key43: "43",
key44: "44",
key45: "45",
key46: "46",
key47: "47",
key48: "48",
key49: "49",
key50: "50",
key51: "51",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
it("should not be able to create a booking with metadata with a key longer than 40 characters", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
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: {
aaaaaaaaaabbbbbbbbbbccccccccccdddddddddde: "1",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
it("should not be able to create a booking with metadata with a value longer than 500 characters", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
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: {
key: `${"a".repeat(501)}`,
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
});
it("should create a booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
@@ -192,14 +329,19 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
const beforeCreate = new Date();
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const afterCreate = new Date();
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
@@ -210,6 +352,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
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.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString());
@@ -221,6 +364,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -233,6 +377,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
email: body.attendee.email,
...body.bookingFieldsResponses,
});
// Check createdAt date is between the time of the request and after the request
const createdAtDate = new Date(data.createdAt);
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
expect(data.metadata).toEqual(body.metadata);
createdBooking = data;
} else {
throw new Error(
@@ -253,6 +404,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
metadata: {
userId: "1000",
},
};
return request(app.getHttpServer())
@@ -281,6 +435,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -288,6 +443,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.location).toEqual(body.location);
expect(firstBooking.recurringBookingUid).toBeDefined();
expect(firstBooking.absentHost).toEqual(false);
expect(firstBooking.metadata).toEqual(body.metadata);
const secondBooking = data[1];
expect(secondBooking.id).toBeDefined();
@@ -301,12 +457,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.recurringBookingUid).toBeDefined();
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(secondBooking.location).toEqual(body.location);
expect(secondBooking.absentHost).toEqual(false);
expect(secondBooking.metadata).toEqual(body.metadata);
const thirdBooking = data[2];
expect(thirdBooking.id).toBeDefined();
@@ -320,12 +478,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(thirdBooking.recurringBookingUid).toBeDefined();
expect(thirdBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(thirdBooking.location).toEqual(body.location);
expect(thirdBooking.absentHost).toEqual(false);
expect(thirdBooking.metadata).toEqual(body.metadata);
createdRecurringBooking = data;
} else {
@@ -354,6 +514,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.id).toEqual(createdBooking.id);
expect(data.uid).toEqual(createdBooking.uid);
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.status).toEqual(createdBooking.status);
expect(data.start).toEqual(createdBooking.start);
expect(data.end).toEqual(createdBooking.end);
@@ -362,6 +523,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
expect(data.location).toEqual(createdBooking.location);
expect(data.absentHost).toEqual(createdBooking.absentHost);
expect(data.createdAt).toEqual(createdBooking.createdAt);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
@@ -835,12 +997,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
reschedulingReason: "Flying to mars that day",
};
const beforeCreate = new Date();
return request(app.getHttpServer())
.post(`/v2/bookings/${createdBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const afterCreate = new Date();
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
@@ -860,6 +1024,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
expect(data.location).toEqual(createdBooking.location);
expect(data.absentHost).toEqual(createdBooking.absentHost);
expect(data.metadata).toEqual(createdBooking.metadata);
// When a booking is rescheduled, a new booking is created and the old booking is cancelled.
// We want to make sure the createdAt date of the new booking is between the beforeCreate and afterCreate dates.
const createdAtDate = new Date(data.createdAt);
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
rescheduledBooking = data;
});
@@ -913,6 +1084,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
expect(data.location).toEqual(createdRecurringBooking[0].location);
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
expect(data.metadata).toEqual(createdRecurringBooking[0].metadata);
const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid);
expect(oldBooking).toBeDefined();
@@ -952,6 +1124,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
describe("mark absent", () => {
beforeAll(() => {
advanceTo(new Date(2035, 0, 9, 15, 0, 0));
});
afterAll(() => {
clear();
});
it("should mark host absent", async () => {
const body: MarkAbsentBookingInput_2024_08_13 = {
host: true,
@@ -1018,6 +1198,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
});
describe("cancel bookings", () => {
it("should cancel booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
@@ -1,9 +1,6 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
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";
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";
@@ -71,6 +68,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
@@ -10,7 +10,12 @@ import {
RecurringBookingOutput_2024_08_13,
} from "@calcom/platform-types";
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
@ApiExtraModels(
BookingOutput_2024_08_13,
RecurringBookingOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
GetRecurringSeatedBookingOutput_2024_08_13
)
export class CancelBookingOutput_2024_08_13 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
@@ -10,7 +10,12 @@ import {
CreateRecurringSeatedBookingOutput_2024_08_13,
} from "@calcom/platform-types";
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
@ApiExtraModels(
BookingOutput_2024_08_13,
RecurringBookingOutput_2024_08_13,
CreateSeatedBookingOutput_2024_08_13,
CreateRecurringSeatedBookingOutput_2024_08_13
)
export class CreateBookingOutput_2024_08_13 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
@@ -21,6 +26,8 @@ export class CreateBookingOutput_2024_08_13 {
oneOf: [
{ $ref: getSchemaPath(BookingOutput_2024_08_13) },
{ type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } },
{ $ref: getSchemaPath(CreateSeatedBookingOutput_2024_08_13) },
{ type: "array", items: { $ref: getSchemaPath(CreateRecurringSeatedBookingOutput_2024_08_13) } },
],
description:
"Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects",
@@ -12,7 +12,12 @@ import {
RecurringBookingOutput_2024_08_13,
} from "@calcom/platform-types";
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
@ApiExtraModels(
BookingOutput_2024_08_13,
RecurringBookingOutput_2024_08_13,
CreateSeatedBookingOutput_2024_08_13,
CreateRecurringSeatedBookingOutput_2024_08_13
)
export class RescheduleBookingOutput_2024_08_13 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
@@ -10,6 +10,7 @@ import { UsersRepository, UserWithProfile } from "@/modules/users/users.reposito
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { BadRequestException } from "@nestjs/common";
import { Request } from "express";
import { z } from "zod";
import {
handleNewBooking,
@@ -38,6 +39,7 @@ import {
CancelBookingInput,
} from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
import { EventType } from "@calcom/prisma/client";
type CreatedBooking = {
hosts: { id: number }[];
@@ -45,6 +47,14 @@ type CreatedBooking = {
start: string;
};
const eventTypeBookingFieldSchema = z.object({
name: z.string(),
required: z.boolean(),
editable: z.string(),
});
const eventTypeBookingFieldsSchema = z.array(eventTypeBookingFieldSchema);
@Injectable()
export class BookingsService_2024_08_13 {
private readonly logger = new Logger("BookingsService");
@@ -70,6 +80,8 @@ export class BookingsService_2024_08_13 {
const isRecurring = !!eventType?.recurringEvent;
const isSeated = !!eventType?.seatsPerTimeSlot;
await this.hasRequiredBookingFieldsResponses(body, eventType);
if (isRecurring && isSeated) {
return await this.createRecurringSeatedBooking(request, body);
}
@@ -91,6 +103,28 @@ export class BookingsService_2024_08_13 {
}
}
async hasRequiredBookingFieldsResponses(body: CreateBookingInput, eventType: EventType | null) {
const bookingFields = body.bookingFieldsResponses;
if (!bookingFields || !eventType || !eventType.bookingFields) {
return true;
}
// note(Lauris): we filter out system fields, because some of them are set by default and name and email are passed in the body.attendee
const eventTypeBookingFields = eventTypeBookingFieldsSchema
.parse(eventType.bookingFields)
.filter((field) => !field.editable.startsWith("system"));
for (const field of eventTypeBookingFields) {
if (field.required && !(field.name in bookingFields)) {
throw new BadRequestException(`
Missing required booking field response: ${field.name} - it is required by the event type booking fields, but missing in the bookingFieldsResponses.
You can fetch the event type with ID ${eventType.id} to see the required fields.`);
}
}
return true;
}
async createInstantBooking(request: Request, body: CreateInstantBookingInput_2024_08_13) {
const bookingRequest = await this.inputService.createBookingRequest(request, body);
const booking = await handleInstantMeeting(bookingRequest);
@@ -329,12 +363,17 @@ export class BookingsService_2024_08_13 {
async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_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)
: undefined;
await handleMarkNoShow({
bookingUid,
attendees: bodyTransformed.attendees,
noShowHost: bodyTransformed.noShowHost,
userId: bookingOwnerId,
platformClientParams,
});
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
@@ -389,11 +428,19 @@ export class BookingsService_2024_08_13 {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
const profile = this.usersService.getUserMainProfile(requestUser);
await roundRobinReassignment({
bookingId: booking.id,
orgId: profile?.organizationId || null,
emailsEnabled,
platformClientParams,
});
const reassigned = await this.bookingsRepository.getByUidWithUser(bookingUid);
@@ -420,6 +467,12 @@ export class BookingsService_2024_08_13 {
throw new NotFoundException(`User with id=${newUserId} was not found in the database`);
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
const profile = this.usersService.getUserMainProfile(user);
const reassigned = await roundRobinManualReassignment({
@@ -428,6 +481,8 @@ export class BookingsService_2024_08_13 {
orgId: profile?.organizationId || null,
reassignReason: body.reason,
reassignedById,
emailsEnabled,
platformClientParams,
});
return this.outputService.getOutputReassignedBooking(reassigned);
@@ -439,6 +494,12 @@ export class BookingsService_2024_08_13 {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
await confirmBookingHandler({
ctx: {
user: requestUser,
@@ -447,6 +508,8 @@ export class BookingsService_2024_08_13 {
bookingId: booking.id,
confirmed: true,
recurringEventId: booking.recurringEventId,
emailsEnabled,
platformClientParams,
},
});
@@ -459,6 +522,12 @@ export class BookingsService_2024_08_13 {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
await confirmBookingHandler({
ctx: {
user: requestUser,
@@ -468,6 +537,8 @@ export class BookingsService_2024_08_13 {
confirmed: false,
recurringEventId: booking.recurringEventId,
reason,
emailsEnabled,
platformClientParams,
},
});
@@ -1,7 +1,7 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import {
bookingResponsesSchema,
seatedBookingResponsesSchema,
seatedBookingDataSchema,
} from "@/ee/bookings/2024-08-13/services/output.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
@@ -18,7 +18,6 @@ import { NextApiRequest } from "next/types";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries";
import {
CancelBookingInput,
@@ -33,19 +32,10 @@ import {
RescheduleBookingInput_2024_08_13,
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
import { EventType, PlatformOAuthClient } from "@calcom/prisma/client";
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
const DEFAULT_PLATFORM_PARAMS = {
platformClientId: "",
platformCancelUrl: "",
platformRescheduleUrl: "",
platformBookingUrl: "",
arePlatformEmailsEnabled: false,
platformBookingLocation: undefined,
};
type OAuthRequestParams = {
platformClientId: string;
platformRescheduleUrl: string;
@@ -92,30 +82,53 @@ export class InputBookingsService_2024_08_13 {
body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13
): Promise<BookingRequest> {
const bodyTransformed = await this.transformInputCreateBooking(body);
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
const requestId = request.get("X-Request-Id");
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const oAuthParams = oAuthClientId
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
const location = request.body.location || request.body.meetingUrl;
this.logger.log(`createBookingRequest_2024_04_15`, {
requestId,
requestId: request.get("X-Request-Id"),
ownerId: userId,
location,
oAuthClientId,
...oAuthParams,
oAuthClientParams,
});
Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled };
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: false };
}
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) {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
inputBooking.eventTypeId
@@ -139,9 +152,7 @@ export class InputBookingsService_2024_08_13 {
eventTypeId: inputBooking.eventTypeId,
timeZone: inputBooking.attendee.timeZone,
language: inputBooking.attendee.language || "en",
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
// metadata: inputBooking.metadata || {},
metadata: {},
metadata: inputBooking.metadata || {},
hasHashedBookingLink: false,
guests: inputBooking.guests,
// note(Lauris): responses with name and email are required by the handleNewBooking
@@ -180,21 +191,23 @@ export class InputBookingsService_2024_08_13 {
): Promise<BookingRequest> {
// note(Lauris): update to this.transformInputCreate when rescheduling is implemented
const bodyTransformed = await this.transformInputCreateRecurringBooking(body);
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const oAuthParams = oAuthClientId
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
const location = request.body.location || request.body.meetingUrl;
Object.assign(newRequest, {
userId,
...oAuthParams,
platformBookingLocation: location,
noEmail: !oAuthParams.arePlatformEmailsEnabled,
});
if (oAuthClientParams) {
Object.assign(newRequest, {
userId,
...oAuthClientParams,
platformBookingLocation: location,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
});
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
}
newRequest.body = bodyTransformed.map((event) => ({
...event,
@@ -243,9 +256,7 @@ export class InputBookingsService_2024_08_13 {
recurringEventId,
timeZone: inputBooking.attendee.timeZone,
language: inputBooking.attendee.language || "en",
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
// metadata: inputBooking.metadata || {},
metadata: {},
metadata: inputBooking.metadata || {},
hasHashedBookingLink: false,
guests: inputBooking.guests,
// note(Lauris): responses with name and email are required by the handleNewBooking
@@ -285,18 +296,20 @@ export class InputBookingsService_2024_08_13 {
const bodyTransformed = this.isRescheduleSeatedBody(body)
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
: await this.transformInputRescheduleBooking(bookingUid, body);
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
const oAuthClientParams = await this.getOAuthClientParams(bodyTransformed.eventTypeId);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const oAuthParams = oAuthClientId
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
const location = await this.getRescheduleBookingLocation(bookingUid);
Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled };
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: false };
}
return newRequest as unknown as BookingRequest;
}
@@ -327,7 +340,7 @@ export class InputBookingsService_2024_08_13 {
throw new NotFoundException(`Seat with uid=${inputBooking.seatUid} does not exist.`);
}
const { responses: bookingResponses } = seatedBookingResponsesSchema.parse(seat.data);
const { responses: bookingResponses } = seatedBookingDataSchema.parse(seat.data);
const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
if (!attendee) {
@@ -345,9 +358,7 @@ export class InputBookingsService_2024_08_13 {
eventTypeId: eventType.id,
timeZone: attendee.timeZone,
language: attendee.locale,
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
// metadata: booking.metadata || {},
metadata: {},
metadata: seat.metadata || {},
hasHashedBookingLink: false,
guests: [],
responses: { ...bookingResponses },
@@ -386,9 +397,7 @@ export class InputBookingsService_2024_08_13 {
eventTypeId: eventType.id,
timeZone: attendee.timeZone,
language: attendee.locale,
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
// metadata: booking.metadata || {},
metadata: {},
metadata: booking.metadata || {},
hasHashedBookingLink: false,
guests: bookingResponses.guests,
responses: { ...bookingResponses, rescheduledReason: inputBooking.reschedulingReason },
@@ -423,24 +432,6 @@ export class InputBookingsService_2024_08_13 {
}
}
private async createBookingRequestOAuthClientParams(clientId: string) {
const params = DEFAULT_PLATFORM_PARAMS;
try {
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
if (client) {
params.platformClientId = clientId;
params.platformCancelUrl = client.bookingCancelRedirectUri ?? "";
params.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
params.platformBookingUrl = client.bookingRedirectUri ?? "";
params.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
}
return params;
} catch (err) {
this.logger.error(err);
return params;
}
}
transformGetBookingsFilters(queryParams: GetBookingsInput_2024_08_13) {
return {
attendeeEmail: queryParams.attendeeEmail,
@@ -473,17 +464,26 @@ export class InputBookingsService_2024_08_13 {
const bodyTransformed = this.isCancelSeatedBody(body)
? await this.transformInputCancelSeatedBooking(bookingUid, body)
: await this.transformInputCancelBooking(bookingUid, body);
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
const booking = await this.bookingsRepository.getByUid(bodyTransformed.uid);
if (!booking) {
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
}
const oAuthClientParams = booking.eventTypeId
? await this.getOAuthClientParams(booking.eventTypeId)
: undefined;
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const oAuthParams = oAuthClientId
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
Object.assign(newRequest, { userId, ...oAuthParams });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled };
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
} else {
Object.assign(newRequest, { userId });
newRequest.body = { ...bodyTransformed, noEmail: false };
}
return newRequest as unknown as BookingRequest;
}
@@ -1,9 +1,17 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import {
defaultBookingMetadata,
defaultBookingResponses,
defaultSeatedBookingData,
defaultSeatedBookingMetadata,
} from "@/lib/safe-parse/default-responses-booking";
import { safeParse } from "@/lib/safe-parse/safe-parse";
import { Injectable } from "@nestjs/common";
import { plainToClass } from "class-transformer";
import { DateTime } from "luxon";
import { z } from "zod";
import { bookingMetadataSchema } from "@calcom/platform-libraries";
import {
BookingOutput_2024_08_13,
CreateRecurringSeatedBookingOutput_2024_08_13,
@@ -19,22 +27,40 @@ import { Booking, BookingSeat } from "@calcom/prisma/client";
export const bookingResponsesSchema = z
.object({
email: z.string(),
name: z.string(),
name: z.union([
z.string(),
z.object({
firstName: z.string(),
lastName: z.string(),
}),
]),
guests: z.array(z.string()).optional(),
rescheduleReason: z.string().optional(),
})
.passthrough();
.passthrough()
.describe("BookingResponses");
export const seatedBookingResponsesSchema = z
export const seatedBookingDataSchema = z
.object({
responses: z
.object({
email: z.string(),
name: z.string(),
name: z.union([
z.string(),
z.object({
firstName: z.string(),
lastName: z.string(),
}),
]),
})
.passthrough(),
})
.passthrough();
.passthrough()
.describe("SeatedBookingData");
const seatedBookingMetadataSchema = z.object({}).catchall(z.string()).describe("SeatedBookingMetadata");
type DatabaseUser = { id: number; name: string | null; email: string; username: string | null };
type DatabaseBooking = Booking & {
eventType: {
@@ -49,9 +75,13 @@ type DatabaseBooking = Booking & {
noShow: boolean | null;
bookingSeat?: BookingSeat | null;
}[];
} & { user: { id: number; name: string | null; email: string } | null };
user: DatabaseUser | null;
createdAt: Date;
};
type BookingWithUser = Booking & { user: { id: number; name: string | null; email: string } | null };
type BookingWithUser = Booking & { user: DatabaseUser | null };
type DatabaseMetadata = z.infer<typeof bookingMetadataSchema>;
@Injectable()
export class OutputBookingsService_2024_08_13 {
@@ -61,15 +91,20 @@ export class OutputBookingsService_2024_08_13 {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
const duration = dateEnd.diff(dateStart, "minutes").minutes;
const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses);
const bookingResponses = safeParse(
bookingResponsesSchema,
databaseBooking.responses,
defaultBookingResponses
);
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const booking = {
id: databaseBooking.id,
uid: databaseBooking.uid,
title: databaseBooking.title,
description: databaseBooking.description,
hosts: [databaseBooking.user],
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
reschedulingReason: bookingResponses?.rescheduledReason,
@@ -88,18 +123,44 @@ export class OutputBookingsService_2024_08_13 {
absent: !!attendee.noShow,
})),
guests: bookingResponses.guests,
location: databaseBooking.location,
location,
// note(Lauris): meetingUrl is deprecated
meetingUrl: databaseBooking.location,
meetingUrl: location,
absentHost: !!databaseBooking.noShowHost,
createdAt: databaseBooking.createdAt,
};
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
// note(Lauris): I don't know why plainToClass erases bookings responses so attaching manually
// note(Lauris): I don't know why plainToClass erases bookings responses and metadata so attaching manually
bookingTransformed.bookingFieldsResponses = bookingResponses;
bookingTransformed.metadata = this.getUserDefinedMetadata(metadata);
return bookingTransformed;
}
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
if (databaseMetadata === null) return {};
const { videoCallUrl, ...userDefinedMetadata } = databaseMetadata;
return userDefinedMetadata;
}
getHost(user: DatabaseUser | null) {
if (!user) {
return {
id: "unknown",
name: "unknown",
email: "unknown",
username: "unknown",
};
}
return {
...user,
username: user.username || "unknown",
};
}
async getOutputRecurringBookings(bookingsIds: number[]) {
const transformed = [];
@@ -119,15 +180,20 @@ export class OutputBookingsService_2024_08_13 {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
const duration = dateEnd.diff(dateStart, "minutes").minutes;
const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses);
const bookingResponses = safeParse(
bookingResponsesSchema,
databaseBooking.responses,
defaultBookingResponses
);
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const booking = {
id: databaseBooking.id,
uid: databaseBooking.uid,
title: databaseBooking.title,
description: databaseBooking.description,
hosts: [databaseBooking.user],
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
reschedulingReason: bookingResponses?.rescheduledReason,
@@ -146,15 +212,22 @@ export class OutputBookingsService_2024_08_13 {
absent: !!attendee.noShow,
})),
guests: bookingResponses.guests,
location: databaseBooking.location,
location,
// note(Lauris): meetingUrl is deprecated
meetingUrl: databaseBooking.location,
meetingUrl: location,
recurringBookingUid: databaseBooking.recurringEventId,
absentHost: !!databaseBooking.noShowHost,
bookingFieldsResponses: databaseBooking.responses,
createdAt: databaseBooking.createdAt,
};
return plainToClass(RecurringBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
const bookingTransformed = plainToClass(RecurringBookingOutput_2024_08_13, booking, {
strategy: "excludeAll",
});
// note(Lauris): I don't know why plainToClass erases bookings responses and metadata so attaching manually
bookingTransformed.bookingFieldsResponses = bookingResponses;
bookingTransformed.metadata = this.getUserDefinedMetadata(metadata);
return bookingTransformed;
}
getOutputCreateSeatedBooking(
@@ -169,13 +242,15 @@ export class OutputBookingsService_2024_08_13 {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
const duration = dateEnd.diff(dateStart, "minutes").minutes;
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const booking = {
id: databaseBooking.id,
uid: databaseBooking.uid,
title: databaseBooking.title,
description: databaseBooking.description,
hosts: [databaseBooking.user],
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
@@ -185,17 +260,22 @@ export class OutputBookingsService_2024_08_13 {
// note(Lauris): eventTypeId is deprecated
eventTypeId: databaseBooking.eventTypeId,
attendees: [],
location: databaseBooking.location,
location,
// note(Lauris): meetingUrl is deprecated
meetingUrl: databaseBooking.location,
meetingUrl: location,
absentHost: !!databaseBooking.noShowHost,
createdAt: databaseBooking.createdAt,
};
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
parsed.attendees = databaseBooking.attendees.map((attendee) => {
const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data);
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData
);
const attendeeData = {
name: attendee.name,
@@ -208,6 +288,11 @@ export class OutputBookingsService_2024_08_13 {
};
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
attendeeParsed.bookingFieldsResponses = responses || {};
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
@@ -260,13 +345,15 @@ export class OutputBookingsService_2024_08_13 {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
const duration = dateEnd.diff(dateStart, "minutes").minutes;
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const booking = {
id: databaseBooking.id,
uid: databaseBooking.uid,
title: databaseBooking.title,
description: databaseBooking.description,
hosts: [databaseBooking.user],
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
@@ -277,11 +364,12 @@ export class OutputBookingsService_2024_08_13 {
// note(Lauris): eventTypeId is deprecated
eventTypeId: databaseBooking.eventTypeId,
attendees: [],
location: databaseBooking.location,
location,
// note(Lauris): meetingUrl is deprecated
meetingUrl: databaseBooking.location,
meetingUrl: location,
recurringBookingUid: databaseBooking.recurringEventId,
absentHost: !!databaseBooking.noShowHost,
createdAt: databaseBooking.createdAt,
};
const parsed = plainToClass(GetRecurringSeatedBookingOutput_2024_08_13, booking, {
@@ -290,7 +378,11 @@ export class OutputBookingsService_2024_08_13 {
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
parsed.attendees = databaseBooking.attendees.map((attendee) => {
const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data);
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData
);
const attendeeData = {
name: attendee.name,
@@ -303,9 +395,13 @@ export class OutputBookingsService_2024_08_13 {
};
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
attendeeParsed.bookingFieldsResponses = responses || {};
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
return attendeeParsed;
});
@@ -113,7 +113,7 @@ describe("Platform Calendars Endpoints", () => {
});
// TODO: Uncomment this once CI is ready to run proper Office365 tests
// it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/connect: it should redirect to auth-url for office 365 calendar oauth with valid access token `, async () => {
// it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/connect: it should redirect to auth-url for office 365 calendar OAuth with valid access token `, async () => {
// const response = await request(app.getHttpServer())
// .get(`/v2/calendars/${OFFICE_365_CALENDAR}/connect`)
// .set("Authorization", `Bearer ${accessTokenSecret}`)
@@ -123,7 +123,7 @@ describe("Platform Calendars Endpoints", () => {
// expect(data.authUrl).toBeDefined();
// });
it(`/GET/v2/calendars/${GOOGLE_CALENDAR}/connect: it should redirect to auth-url for google calendar oauth with valid access token `, async () => {
it(`/GET/v2/calendars/${GOOGLE_CALENDAR}/connect: it should redirect to auth-url for google calendar OAuth with valid access token `, async () => {
const response = await request(app.getHttpServer())
.get(`/v2/calendars/${GOOGLE_CALENDAR}/connect`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import {
ArrayNotEmpty,
IsBoolean,
@@ -50,11 +50,10 @@ export class CreateIcsFeedInputDto {
urls!: string[];
@IsBoolean()
@ApiProperty({
@ApiPropertyOptional({
example: false,
description: "Whether to allowing writing to the calendar or not",
type: "boolean",
required: false,
default: true,
})
@IsOptional()
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsDate, IsEnum, IsOptional, IsString, ValidateNested } from "class-validator";
@@ -6,13 +6,16 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class BusyTimesOutput {
@IsDate()
@ApiProperty({ type: Date })
start!: Date;
@IsDate()
@ApiProperty({ type: Date })
end!: Date;
@IsOptional()
@IsString()
@ApiPropertyOptional({ type: String, nullable: true })
source?: string | null;
}
@@ -24,5 +27,6 @@ export class GetBusyTimesOutput {
@ValidateNested()
@Type(() => BusyTimesOutput)
@IsArray()
@ApiProperty({ type: [BusyTimesOutput] })
data!: BusyTimesOutput[];
}
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsArray,
@@ -18,195 +18,246 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Integration {
@IsOptional()
@IsObject()
@ApiPropertyOptional({ type: Object, nullable: true })
appData?: object | null;
@IsOptional()
@IsString()
@ApiPropertyOptional()
dirName?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
__template?: string;
@IsString()
@ApiProperty()
name!: string;
@IsString()
@ApiProperty()
description!: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
installed?: boolean;
@IsString()
@ApiProperty()
type!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
title?: string;
@IsString()
@ApiProperty()
variant!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
category?: string;
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: [String] })
categories!: string[];
@IsString()
@ApiProperty()
logo!: string;
@IsString()
@ApiProperty()
publisher!: string;
@IsString()
@ApiProperty()
slug!: string;
@IsUrl()
@ApiProperty()
url!: string;
@IsEmail()
@ApiProperty()
email!: string;
@IsOptional()
@IsObject()
@ApiProperty({ type: Object, nullable: true })
locationOption!: object | null;
}
class Primary {
@IsEmail()
@ApiProperty()
externalId!: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
integration?: string;
@IsOptional()
@IsEmail()
@ApiPropertyOptional()
name?: string;
@IsBoolean()
@ApiProperty({ type: Boolean, nullable: true })
primary!: boolean | null;
@IsBoolean()
@ApiProperty()
readOnly!: boolean;
@IsEmail()
@IsOptional()
@ApiPropertyOptional()
email?: string;
@IsBoolean()
@ApiProperty()
isSelected!: boolean;
@IsInt()
@ApiProperty()
credentialId!: number;
}
export class Calendar {
@IsEmail()
@ApiProperty()
externalId!: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
integration?: string;
@IsEmail()
@IsOptional()
@ApiPropertyOptional()
name?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ type: Boolean, nullable: true })
primary?: boolean | null;
@IsBoolean()
@ApiProperty()
readOnly!: boolean;
@IsEmail()
@IsOptional()
@ApiPropertyOptional()
email?: string;
@IsBoolean()
@ApiProperty()
isSelected!: boolean;
@IsInt()
@ApiProperty()
credentialId!: number;
}
export class ConnectedCalendar {
@ValidateNested()
@IsObject()
@ApiProperty({ type: Integration })
integration!: Integration;
@IsInt()
@ApiProperty()
credentialId!: number;
@ValidateNested()
@IsObject()
@IsOptional()
@ApiPropertyOptional({ type: Primary })
primary?: Primary;
@ValidateNested({ each: true })
@IsArray()
@IsOptional()
@ApiPropertyOptional({ type: [Calendar] })
calendars?: Calendar[];
}
class DestinationCalendar {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty()
integration!: string;
@IsEmail()
@ApiProperty()
externalId!: string;
@IsEmail()
@ApiProperty({ type: String, nullable: true })
primaryEmail!: string | null;
@IsInt()
@ApiProperty({ nullable: true })
userId!: number | null;
@IsOptional()
@IsInt()
@ApiProperty({ type: Number, nullable: true })
eventTypeId!: number | null;
@IsInt()
@ApiProperty({ type: Number, nullable: true })
credentialId!: number | null;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
name?: string | null;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
primary?: boolean;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
readOnly?: boolean;
@IsEmail()
@IsOptional()
@ApiPropertyOptional()
email?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
integrationTitle?: string;
}
export class ConnectedCalendarsData {
@ValidateNested({ each: true })
@IsArray()
@ApiProperty({ type: [ConnectedCalendar] })
connectedCalendars!: ConnectedCalendar[];
@ValidateNested()
@IsObject()
@ApiProperty({ type: DestinationCalendar })
destinationCalendar!: DestinationCalendar;
}
export class ConnectedCalendarsOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
@@ -214,5 +265,6 @@ export class ConnectedCalendarsOutput {
@ValidateNested()
@Type(() => ConnectedCalendarsData)
@ApiProperty({ type: ConnectedCalendarsData })
data!: ConnectedCalendarsData;
}
@@ -4,13 +4,14 @@ import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { calendar_v3 } from "@googleapis/calendar";
import { Logger, NotFoundException } from "@nestjs/common";
import { BadRequestException, UnauthorizedException } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Prisma } from "@prisma/client";
import { Request } from "express";
import { google } from "googleapis";
import { OAuth2Client } from "googleapis-common";
import { z } from "zod";
import { SUCCESS_STATUS, GOOGLE_CALENDAR_TYPE } from "@calcom/platform-constants";
@@ -78,7 +79,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirectUri);
return oAuth2Client;
}
@@ -90,7 +91,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
}
if (gcalCredentials.invalid) {
throw new BadRequestException("Invalid google oauth credentials.");
throw new BadRequestException("Invalid google OAuth credentials.");
}
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
@@ -115,7 +116,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
) {
// User chose not to authorize your app or didn't authorize your app
// redirect directly without oauth code
if (!code) {
if (!code || code === "undefined") {
return { url: redir || origin };
}
@@ -134,8 +135,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
oAuth2Client.setCredentials(key);
const calendar = google.calendar({
version: "v3",
const calendar = new calendar_v3.Calendar({
auth: oAuth2Client,
});
@@ -151,7 +151,7 @@ export class OutlookService implements OAuthCalendarApp {
redir?: string
) {
// if code is not defined, user denied to authorize office 365 app, just redirect straight away
if (!code) {
if (!code || code === "undefined") {
return { url: redir || origin };
}
@@ -78,4 +78,11 @@ export class EventTypesRepository_2024_04_15 {
async deleteEventType(eventTypeId: number) {
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
}
async getEventTypeWithDuration(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
select: { length: true },
});
}
}
@@ -1,5 +1,5 @@
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
import { ApiProperty as DocsProperty, ApiPropertyOptional, ApiHideProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsString,
@@ -36,7 +36,10 @@ export class CreateEventTypeInput_2024_04_15 {
@IsOptional()
@IsString()
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
@ApiPropertyOptional({
type: String,
example: CREATE_EVENT_DESCRIPTION_EXAMPLE,
})
description?: string;
@IsOptional()
@@ -48,30 +51,38 @@ export class CreateEventTypeInput_2024_04_15 {
@ValidateNested({ each: true })
@Type(() => EventTypeLocation_2024_04_15)
@IsArray()
@ApiPropertyOptional({
type: [EventTypeLocation_2024_04_15],
})
locations?: EventTypeLocation_2024_04_15[];
@IsBoolean()
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
disableGuests?: boolean;
@IsOptional()
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
slotInterval?: number;
@IsOptional()
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
minimumBookingNotice?: number;
@IsOptional()
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
beforeEventBuffer?: number;
@IsOptional()
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
afterEventBuffer?: number;
// @ApiHideProperty()
@@ -1,4 +1,4 @@
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
import { ApiProperty, ApiHideProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validator";
// note(Lauris): We will gradually expose more properties if any customer needs them.
@@ -6,7 +6,7 @@ import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validato
export class EventTypeLocation_2024_04_15 {
@IsString()
@DocsProperty({ example: "link" })
@ApiProperty({ example: "link" })
type!: string;
@IsOptional()
@@ -16,7 +16,7 @@ export class EventTypeLocation_2024_04_15 {
@IsOptional()
@IsUrl()
@DocsProperty({ example: "https://masterchief.com/argentina/flan/video/9129412" })
@ApiPropertyOptional({ example: "https://masterchief.com/argentina/flan/video/9129412" })
link?: string;
@IsOptional()
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsBoolean, IsOptional, IsString } from "class-validator";
@@ -6,11 +6,11 @@ export class GetPublicEventTypeQueryParams_2024_04_15 {
@Transform(({ value }: { value: string }) => value === "true")
@IsBoolean()
@IsOptional()
@ApiProperty({ required: false })
@ApiPropertyOptional()
isTeamEvent?: boolean;
@ApiProperty({ required: false })
@IsString()
@IsOptional()
@IsString()
@ApiPropertyOptional({ type: String, nullable: true })
org?: string | null;
}
@@ -2,6 +2,7 @@ import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/e
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
import { Frequency } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/frequency";
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsString,
@@ -21,206 +22,259 @@ import {
class Option {
@IsString()
@ApiProperty()
value!: string;
@IsString()
@ApiProperty()
label!: string;
}
class Source {
@IsString()
@ApiProperty()
id!: string;
@IsString()
@ApiProperty()
type!: string;
@IsString()
@ApiProperty()
label!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
editUrl?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
fieldRequired?: boolean;
}
class View {
@IsString()
@ApiProperty()
id!: string;
@IsString()
@ApiProperty()
label!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
description?: string;
}
class OptionsInput {
@IsString()
@ApiProperty({
description: 'Type of the field, can be one of "address", "text", or "phone".',
enum: ["address", "text", "phone"],
example: "text",
})
type!: "address" | "text" | "phone";
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
required?: boolean;
@IsOptional()
@IsString()
@ApiPropertyOptional()
placeholder?: string;
}
class VariantField {
@IsString()
@ApiProperty()
type!: BaseField;
@IsString()
@ApiProperty()
name!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
label?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
labelAsSafeHtml?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
placeholder?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
required?: boolean;
}
class Variant {
@ValidateNested({ each: true })
@Type(() => VariantField)
@ApiProperty({ type: [VariantField] })
fields!: VariantField[];
}
class VariantsConfig {
@ApiProperty({ type: Object })
variants!: Record<string, Variant>;
}
export class BookingField_2024_04_15 {
@IsEnum(BaseField)
@ApiProperty({ enum: BaseField })
type!: BaseField;
@IsString()
@ApiProperty()
name!: string;
@IsOptional()
@ValidateNested({ each: true })
@Type(() => Option)
@ApiPropertyOptional({ type: [Option] })
options?: Option[];
@IsOptional()
@IsString()
@ApiPropertyOptional()
label?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
labelAsSafeHtml?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
defaultLabel?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
placeholder?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
required?: boolean;
@IsOptional()
@IsString()
@ApiPropertyOptional()
getOptionsAt?: string;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => OptionsInput)
@ApiPropertyOptional()
optionsInputs?: Record<string, OptionsInput>;
@IsOptional()
@IsString()
@ApiPropertyOptional()
variant?: string;
@IsOptional()
@ValidateNested()
@Type(() => VariantsConfig)
@ApiPropertyOptional({ type: VariantsConfig })
variantsConfig?: VariantsConfig;
@IsOptional()
@ValidateNested({ each: true })
@Type(() => View)
@ApiPropertyOptional({ type: [View] })
views?: View[];
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
hideWhenJustOneOption?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
hidden?: boolean;
@IsOptional()
@IsEnum(Editable)
@ApiPropertyOptional()
editable?: Editable;
@IsOptional()
@ValidateNested({ each: true })
@Type(() => Source)
@ApiPropertyOptional({ type: [Source] })
sources?: Source[];
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
disableOnPrefill?: boolean;
}
export class RecurringEvent_2024_04_15 {
@IsDate()
@IsOptional()
@ApiPropertyOptional({ type: Date })
dtstart?: Date;
@IsInt()
@ApiProperty()
interval!: number;
@IsInt()
@ApiProperty()
count!: number;
@IsEnum(Frequency)
@ApiProperty({ enum: Frequency })
freq!: Frequency;
@IsDate()
@IsOptional()
@ApiPropertyOptional({ type: Date })
until?: Date;
@IsString()
@IsOptional()
@ApiPropertyOptional()
tzid?: string;
}
export class IntervalLimits_2024_04_15 {
@IsNumber()
@IsOptional()
@ApiPropertyOptional()
PER_DAY?: number;
@IsNumber()
@IsOptional()
@ApiPropertyOptional()
PER_WEEK?: number;
@IsNumber()
@IsOptional()
@ApiPropertyOptional()
PER_MONTH?: number;
@IsNumber()
@IsOptional()
@ApiPropertyOptional()
PER_YEAR?: number;
}
@@ -228,27 +282,33 @@ export class UpdateEventTypeInput_2024_04_15 {
@IsInt()
@Min(1)
@IsOptional()
@ApiPropertyOptional()
length?: number;
@IsString()
@IsOptional()
@ApiPropertyOptional()
slug?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
title?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
description?: string;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
hidden?: boolean;
@ValidateNested({ each: true })
@Type(() => EventTypeLocation_2024_04_15)
@IsOptional()
@ApiPropertyOptional({ type: [EventTypeLocation_2024_04_15] })
locations?: EventTypeLocation_2024_04_15[];
// @IsInt()
@@ -283,6 +343,7 @@ export class UpdateEventTypeInput_2024_04_15 {
@IsArray()
@ValidateNested({ each: true })
@Type(() => BookingField_2024_04_15)
@ApiPropertyOptional({ type: [BookingField_2024_04_15] })
bookingFields?: BookingField_2024_04_15[];
// @IsString()
@@ -328,6 +389,7 @@ export class UpdateEventTypeInput_2024_04_15 {
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
disableGuests?: boolean;
// @IsBoolean()
@@ -337,16 +399,19 @@ export class UpdateEventTypeInput_2024_04_15 {
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
minimumBookingNotice?: number;
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
beforeEventBuffer?: number;
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
afterEventBuffer?: number;
// @IsInt()
@@ -384,6 +449,7 @@ export class UpdateEventTypeInput_2024_04_15 {
@IsInt()
@Min(0)
@IsOptional()
@ApiPropertyOptional()
slotInterval?: number;
// @IsString()
@@ -12,7 +12,7 @@ import {
IntervalLimits_2024_04_15,
RecurringEvent_2024_04_15,
} from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
import { ApiHideProperty, ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsArray,
@@ -29,23 +29,23 @@ import {
export class EventTypeOutput {
@IsInt()
@DocsProperty({ example: 1 })
@ApiProperty({ example: 1 })
id!: number;
@IsInt()
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
@ApiProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
length!: number;
@IsString()
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
@ApiProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
slug!: string;
@IsString()
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
@ApiProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
title!: string;
@IsString()
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
@ApiProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE, nullable: true })
description!: string | null;
@IsBoolean()
@@ -55,6 +55,7 @@ export class EventTypeOutput {
@ValidateNested({ each: true })
@Type(() => EventTypeLocation_2024_04_15)
@IsArray()
@ApiProperty({ type: [EventTypeLocation_2024_04_15], nullable: true })
locations!: EventTypeLocation_2024_04_15[] | null;
@IsInt()
@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsBoolean,
@@ -17,330 +17,421 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Location {
@IsString()
@ApiProperty()
type!: string;
}
class Source {
@IsString()
@ApiProperty()
id!: string;
@IsString()
@ApiProperty()
type!: string;
@IsString()
@ApiProperty()
label!: string;
}
class OptionInput {
@IsString()
@ApiProperty()
type!: string;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
required?: boolean;
@IsString()
@IsOptional()
@ApiPropertyOptional()
placeholder?: string;
}
class BookingField {
@IsString()
@ApiProperty()
name!: string;
@IsString()
@ApiProperty()
type!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
defaultLabel?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
label?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
placeholder?: string;
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
required?: boolean;
@IsOptional()
@ApiPropertyOptional()
getOptionsAt?: string;
@IsObject()
@IsOptional()
@ApiPropertyOptional()
optionsInputs?: { [key: string]: OptionInput };
@IsBoolean()
@IsOptional()
@ApiPropertyOptional()
hideWhenJustOneOption?: boolean;
@IsString()
@IsOptional()
@ApiPropertyOptional()
editable?: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => Source)
@IsOptional()
@ApiPropertyOptional({ type: [Source] })
sources?: Source[];
@IsOptional()
@IsBoolean()
@ApiPropertyOptional()
disableOnPrefill?: boolean;
}
class Organization {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
slug?: string | null;
@IsString()
@ApiProperty()
name!: string;
@IsOptional()
metadata!: Record<string, any>;
@ApiPropertyOptional({ type: Object })
metadata?: Record<string, any>;
}
class Profile {
@IsString()
@ApiProperty({ type: String, nullable: true })
username!: string | null;
@IsInt()
@ApiProperty({ type: Number, nullable: true })
id!: number | null;
@IsInt()
@IsOptional()
@ApiPropertyOptional()
userId?: number;
@IsString()
@IsOptional()
@ApiPropertyOptional()
uid?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional()
name?: string;
@IsInt()
@ApiProperty({ type: Number, nullable: true })
organizationId!: number | null;
@ValidateNested()
@Type(() => Organization)
@ApiPropertyOptional({ type: Organization, nullable: true })
organization?: Organization | null;
@IsString()
@ApiProperty()
upId!: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
image?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
brandColor?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
darkBrandColor?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
theme?: string;
@IsOptional()
@ApiPropertyOptional()
bookerLayouts?: any;
}
class Owner {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
avatarUrl?: string | null;
@IsString()
@ApiProperty({ type: String, nullable: true })
username!: string | null;
@IsString()
@ApiProperty({ type: String, nullable: true })
name!: string | null;
@IsString()
@ApiProperty()
weekStart!: string;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
brandColor?: string | null;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
darkBrandColor?: string | null;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
theme?: string | null;
@IsOptional()
metadata!: any;
@ApiPropertyOptional()
metadata?: any;
@IsInt()
@IsOptional()
@ApiPropertyOptional({ type: Number, nullable: true })
defaultScheduleId?: number | null;
@IsString()
@ApiProperty({ type: String, nullable: true })
nonProfileUsername!: string | null;
@ValidateNested()
@Type(() => Profile)
@ApiProperty({ type: Profile })
profile!: Profile;
}
class User {
@IsString()
@ApiProperty({ type: String, nullable: true })
username!: string | null;
@IsString()
@ApiProperty({ type: String, nullable: true })
name!: string | null;
@IsString()
@ApiProperty()
weekStart!: string;
@IsInt()
@IsOptional()
@ApiPropertyOptional()
organizationId?: number;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
avatarUrl?: string | null;
@ValidateNested()
@ApiProperty({ type: Profile })
profile!: Profile;
@IsString()
@ApiProperty()
bookerUrl!: string;
}
class Schedule {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty({ type: String, nullable: true })
timeZone!: string | null;
}
class PublicEventTypeOutput {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty()
title!: string;
@IsString()
@ApiProperty()
description!: string;
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
eventName?: string | null;
@IsString()
@ApiProperty()
slug!: string;
@IsBoolean()
@ApiProperty()
isInstantEvent!: boolean;
@IsOptional()
@ApiPropertyOptional()
aiPhoneCallConfig?: any;
@IsOptional()
@ApiPropertyOptional()
schedulingType?: any;
@IsInt()
@ApiProperty()
length!: number;
@IsArray()
@ValidateNested({ each: true })
@Type(() => Location)
@ApiProperty({ type: [Location] })
locations!: Location[];
@IsArray()
@ApiProperty({ type: [Object] })
customInputs!: any[];
@IsBoolean()
@ApiProperty()
disableGuests!: boolean;
@IsObject()
@ApiProperty({ type: Object, nullable: true })
metadata!: object | null;
@IsBoolean()
@ApiProperty()
lockTimeZoneToggleOnBookingPage!: boolean;
@IsBoolean()
@ApiProperty()
requiresConfirmation!: boolean;
@IsBoolean()
@ApiProperty()
requiresBookerEmailVerification!: boolean;
@IsOptional()
@ApiPropertyOptional()
recurringEvent?: any;
@IsNumber()
@ApiProperty()
price!: number;
@IsString()
@ApiProperty()
currency!: string;
@IsOptional()
@ApiPropertyOptional({ type: Number, nullable: true })
seatsPerTimeSlot?: number | null;
@IsBoolean()
@ApiProperty({ type: Boolean, nullable: true })
seatsShowAvailabilityCount!: boolean | null;
@IsArray()
@ValidateNested({ each: true })
@Type(() => BookingField)
@ApiProperty({ type: [BookingField] })
bookingFields!: BookingField[];
@IsOptional()
@ApiPropertyOptional()
team?: any;
@IsOptional()
@IsUrl()
@ApiPropertyOptional({ type: String, nullable: true })
successRedirectUrl?: string | null;
@IsArray()
@ApiProperty()
workflows!: any[];
@IsArray()
@ApiProperty()
hosts!: any[];
@ValidateNested()
@Type(() => Owner)
@ApiProperty({ type: Owner, nullable: true })
owner!: Owner | null;
@ValidateNested()
@Type(() => Schedule)
@ApiProperty({ type: Schedule, nullable: true })
schedule!: Schedule | null;
@IsBoolean()
@ApiProperty()
hidden!: boolean;
@IsBoolean()
@ApiProperty()
assignAllTeamMembers!: boolean;
@IsOptional()
@ApiPropertyOptional()
bookerLayouts?: any;
@IsArray()
@ValidateNested({ each: true })
@Type(() => User)
@ApiProperty({ type: [User] })
users!: User[];
@IsObject()
@ApiProperty({ type: Object })
entity!: object;
@IsBoolean()
@@ -10,10 +10,12 @@ 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, Schedule } from "@prisma/client";
import { PlatformOAuthClient, Team, User, Schedule, EventType } 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";
@@ -75,18 +77,22 @@ describe("Event types Endpoints", () => {
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let schedulesRepostoryFixture: SchedulesRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
const userEmail = "event-types-test-e2e@api.com";
const falseTestEmail = "false-event-types@api.com";
const name = "bob-the-builder";
const username = name;
let eventType: EventTypeOutput_2024_06_14;
let user: User;
let orgUser: User;
let falseTestUser: User;
let firstSchedule: Schedule;
let secondSchedule: Schedule;
let falseTestSchedule: Schedule;
let orgUserEventType1: EventType;
let orgUserEventType2: EventType;
let orgUserEventType3: EventType;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
@@ -109,8 +115,12 @@ describe("Event types Endpoints", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
schedulesRepostoryFixture = new SchedulesRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({
name: "organization",
slug: "event-type-2024-06-14-org-slug",
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
@@ -118,6 +128,49 @@ describe("Event types Endpoints", () => {
username,
});
orgUser = await userRepositoryFixture.create({
email: "event-types-2024-06-14-org-user@example.com",
name: "event-types-2024-06-14-org-user",
username: "event-types-2024-06-14-org-user",
});
profileRepositoryFixture.create({
uid: `usr-${orgUser.id}`,
username: orgUser.username as string,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: orgUser.id,
},
},
});
orgUserEventType1 = await eventTypesRepositoryFixture.create(
{ title: "orgUserEventType1", slug: "org-event-type-1", length: 60, locations: [] },
orgUser.id
);
orgUserEventType2 = await eventTypesRepositoryFixture.create(
{ title: "orgUserEventType2", slug: "org-event-type-2", length: 60, locations: [] },
orgUser.id
);
orgUserEventType3 = await eventTypesRepositoryFixture.create(
{ title: "orgUserEventType3", slug: "org-event-type-3", length: 60, locations: [] },
orgUser.id
);
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: orgUser.id } },
team: { connect: { id: organization.id } },
accepted: true,
});
falseTestUser = await userRepositoryFixture.create({
email: falseTestEmail,
name: "false-test",
@@ -342,7 +395,7 @@ describe("Event types Endpoints", () => {
});
});
it(`/GET/even-types by username`, async () => {
it(`/GET/event-types by username`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${username}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
@@ -386,6 +439,61 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType.color).toEqual(eventType.color);
});
it(`/GET/event-types by username and orgSlug`, async () => {
console.log(organization);
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(3);
expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined();
expect(responseBody.data?.find((e) => e.id === orgUserEventType2.id)?.id).toBeDefined();
expect(responseBody.data?.find((e) => e.id === orgUserEventType3.id)?.id).toBeDefined();
});
it(`/GET/event-types by username and orgSlug and eventSlug`, async () => {
const response = await request(app.getHttpServer())
.get(
`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}&eventSlug=${orgUserEventType1.slug}`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(1);
expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined();
});
it(`/GET/event-types by username and orgId`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${orgUser.username}&orgId=${organization.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(3);
expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined();
expect(responseBody.data?.find((e) => e.id === orgUserEventType2.id)?.id).toBeDefined();
expect(responseBody.data?.find((e) => e.id === orgUserEventType3.id)?.id).toBeDefined();
});
it("should return an error when creating an event type with seats enabled and multiple locations", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class 2",
@@ -919,6 +1027,12 @@ describe("Event types Endpoints", () => {
} catch (e) {
// User might have been deleted by the test
}
try {
await userRepositoryFixture.delete(orgUser.id);
} catch (e) {
// User might have been deleted by the test
}
await app.close();
});
});
@@ -1321,6 +1435,73 @@ describe("Event types Endpoints", () => {
});
});
it("should return event type with default bookingFields if they are not defined", async () => {
const eventTypeInput = {
title: "undefined booking fields",
description: "undefined booking fields",
length: 40,
hidden: false,
slug: "undefined-booking-fields",
locations: [],
schedulingType: SchedulingType.ROUND_ROBIN,
};
const eventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
return request(app.getHttpServer())
.get(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
{
isDefault: true,
type: "name",
slug: "name",
required: true,
},
{
isDefault: true,
type: "email",
slug: "email",
required: true,
},
{
isDefault: true,
type: "radioInput",
slug: "location",
required: false,
},
{
isDefault: true,
type: "text",
slug: "title",
required: true,
},
{
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
},
{
isDefault: true,
type: "multiemail",
slug: "guests",
required: false,
},
{
isDefault: true,
type: "textarea",
slug: "rescheduleReason",
required: false,
},
]);
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -1,4 +1,4 @@
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { ApiProperty as DocsProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsString, IsBoolean, IsOptional, IsEnum, Matches } from "class-validator";
@@ -38,34 +38,34 @@ export class CreatePhoneCallInput {
@IsOptional()
@IsString()
@DocsProperty({ description: "Scheduler name" })
@ApiPropertyOptional({ description: "Scheduler name" })
schedulerName?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => (value ? value : undefined))
@DocsProperty({ description: "Guest name" })
@ApiPropertyOptional({ description: "Guest name" })
guestName?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => (value ? value : undefined))
@DocsProperty({ description: "Guest email" })
@ApiPropertyOptional({ description: "Guest email" })
guestEmail?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => (value ? value : undefined))
@DocsProperty({ description: "Guest company" })
@ApiPropertyOptional({ description: "Guest company" })
guestCompany?: string;
@IsOptional()
@IsString()
@DocsProperty({ description: "Begin message" })
@ApiPropertyOptional({ description: "Begin message" })
beginMessage?: string;
@IsOptional()
@IsString()
@DocsProperty({ description: "General prompt" })
@ApiPropertyOptional({ description: "General prompt" })
generalPrompt?: string;
}
@@ -17,7 +17,7 @@ export class EventTypeResponseTransformPipe implements PipeTransform {
private transformEventType(eventType: EventTypeResponse): EventTypeOutput_2024_06_14 {
return plainToClass(
EventTypeOutput_2024_06_14,
this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType),
this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType, false),
{ strategy: "exposeAll" }
);
}
@@ -74,8 +74,13 @@ export class EventTypesService_2024_06_14 {
await this.checkUserOwnsSchedule(userId, body.scheduleId);
}
async getEventTypeByUsernameAndSlug(username: string, eventTypeSlug: string) {
const user = await this.usersRepository.findByUsername(username);
async getEventTypeByUsernameAndSlug(
username: string,
eventTypeSlug: string,
orgSlug?: string,
orgId?: number
) {
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
if (!user) {
return null;
}
@@ -92,8 +97,8 @@ export class EventTypesService_2024_06_14 {
};
}
async getEventTypesByUsername(username: string) {
const user = await this.usersRepository.findByUsername(username);
async getEventTypesByUsername(username: string, orgSlug?: string, orgId?: number) {
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
if (!user) {
return [];
}
@@ -152,27 +157,26 @@ export class EventTypesService_2024_06_14 {
}
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14) {
const { username, eventSlug, usernames } = queryParams;
const { username, eventSlug, usernames, orgSlug, orgId } = queryParams;
if (username && eventSlug) {
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug);
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug, orgSlug, orgId);
return eventType ? [eventType] : [];
}
if (username) {
return await this.getEventTypesByUsername(username);
return await this.getEventTypesByUsername(username, orgSlug, orgId);
}
if (usernames) {
const dynamicEventType = await this.getDynamicEventType(usernames);
const dynamicEventType = await this.getDynamicEventType(usernames, orgSlug, orgId);
return [dynamicEventType];
}
return [];
}
async getDynamicEventType(usernames: string[]) {
const users = await this.usersService.getByUsernames(usernames);
async getDynamicEventType(usernames: string[], orgSlug?: string, orgId?: number) {
const users = await this.usersService.getByUsernames(usernames, orgSlug, orgId);
const usersFiltered: UserWithProfile[] = [];
for (const user of users) {
if (user) {
@@ -21,6 +21,7 @@ import {
transformSeatsInternalToApi,
InternalLocation,
BookingFieldSchema,
getBookingFieldsWithSystemFields,
} from "@calcom/platform-libraries";
import {
TransformFutureBookingsLimitSchema_2024_06_14,
@@ -88,7 +89,11 @@ type Input = Pick<
@Injectable()
export class OutputEventTypesService_2024_06_14 {
getResponseEventType(ownerId: number, databaseEventType: Input): EventTypeOutput_2024_06_14 {
getResponseEventType(
ownerId: number,
databaseEventType: Input,
isOrgTeamEvent: boolean
): EventTypeOutput_2024_06_14 {
const {
id,
length,
@@ -122,7 +127,7 @@ export class OutputEventTypesService_2024_06_14 {
const customName = databaseEventType?.eventName ?? undefined;
const bookingFields = databaseEventType.bookingFields
? this.transformBookingFields(databaseEventType.bookingFields)
: [];
: this.getDefaultBookingFields(isOrgTeamEvent);
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
const users = this.transformUsers(databaseEventType.users || []);
@@ -240,6 +245,18 @@ export class OutputEventTypesService_2024_06_14 {
return [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
}
getDefaultBookingFields(isOrgTeamEvent: boolean) {
const defaultBookingFields = getBookingFieldsWithSystemFields({
disableGuests: false,
bookingFields: null,
customInputs: [],
metadata: null,
workflows: [],
isOrgTeamEvent,
});
return this.transformBookingFields(defaultBookingFields);
}
transformRecurringEvent(recurringEvent: any) {
if (!recurringEvent) return null;
const recurringEventParsed = parseRecurringEvent(recurringEvent);
@@ -95,7 +95,7 @@ describe("Platform Gcal Endpoints", () => {
.expect(401);
});
it(`/GET/gcal/oauth/auth-url: it should auth-url to google oauth with valid access token `, async () => {
it(`/GET/gcal/oauth/auth-url: it should auth-url to google OAuth with valid access token `, async () => {
const response = await request(app.getHttpServer())
.get(`/v2/gcal/oauth/auth-url`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
@@ -105,7 +105,7 @@ describe("Platform Gcal Endpoints", () => {
expect(data.authUrl).toBeDefined();
});
it(`/GET/gcal/oauth/save: without oauth code`, async () => {
it(`/GET/gcal/oauth/save: without OAuth code`, async () => {
await request(app.getHttpServer())
.get(
`/v2/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
+1 -4
View File
@@ -27,10 +27,7 @@ import {
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { Prisma } from "@prisma/client";
import { Request } from "express";
import { google } from "googleapis";
import { z } from "zod";
import { APPS_READ, GOOGLE_CALENDAR_TYPE, SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -103,7 +100,7 @@ export class GcalController {
}
if (gcalCredentials.invalid) {
throw new BadRequestException("Invalid google oauth credentials.");
throw new BadRequestException("Invalid google OAuth credentials.");
}
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
+14 -2
View File
@@ -1,39 +1,51 @@
import { ApiProperty as DocsProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsInt, IsEmail, IsOptional, IsString, ValidateNested } from "class-validator";
import { IsString, IsOptional, IsInt, IsEmail, ValidateNested } from "class-validator";
export class MeOrgOutput {
@DocsProperty()
isPlatform!: boolean;
@DocsProperty()
id!: number;
}
export class MeOutput {
@IsInt()
@DocsProperty()
id!: number;
@IsString()
@DocsProperty()
username!: string;
@IsEmail()
@DocsProperty()
email!: string;
@IsInt()
@DocsProperty()
timeFormat!: number;
@IsInt()
@IsOptional()
@DocsProperty({ type: Number, nullable: true })
defaultScheduleId!: number | null;
@IsString()
@DocsProperty()
weekStart!: string;
@IsString()
@DocsProperty()
timeZone!: string;
@IsInt()
@DocsProperty({ type: Number, nullable: true })
organizationId!: number | null;
@IsOptional()
@ValidateNested()
@Type(() => MeOrgOutput)
@ApiPropertyOptional({ type: MeOrgOutput })
organization?: MeOrgOutput;
}
@@ -9,6 +9,8 @@ import { ProviderModule } from "@/ee/provider/provider.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { SlotsModule } from "@/modules/slots/slots.module";
import { TeamsModule } from "@/modules/teams/teams/teams.module";
import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { Module } from "@nestjs/common";
@@ -24,7 +26,9 @@ import { Module } from "@nestjs/common";
CalendarsModule,
BookingsModule_2024_04_15,
BookingsModule_2024_08_13,
TeamsMembershipsModule,
SlotsModule,
TeamsModule,
],
})
export class PlatformEndpointsModule implements NestModule {
@@ -1,20 +1,25 @@
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsBoolean, IsTimeZone, IsOptional, IsString, ValidateNested } from "class-validator";
export class CreateScheduleInput_2024_04_15 {
@IsString()
@ApiProperty()
name!: string;
@IsTimeZone()
@ApiProperty()
timeZone!: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateAvailabilityInput_2024_04_15)
@IsOptional()
@ApiPropertyOptional({ type: [CreateAvailabilityInput_2024_04_15] })
availabilities?: CreateAvailabilityInput_2024_04_15[];
@IsBoolean()
@ApiProperty()
isDefault!: boolean;
}
@@ -1,95 +1,117 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsBoolean, IsInt, IsOptional, IsString, ValidateNested, IsArray } from "class-validator";
class EventTypeModel_2024_04_15 {
@IsInt()
@ApiProperty()
id!: number;
@IsOptional()
@IsString()
@ApiPropertyOptional({ type: String, nullable: true })
eventName?: string | null;
}
class AvailabilityModel_2024_04_15 {
@IsInt()
@ApiProperty()
id!: number;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
userId?: number | null;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
scheduleId?: number | null;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
eventTypeId?: number | null;
@IsArray()
@IsInt({ each: true })
@ApiProperty({ type: [Number] })
days!: number[];
@IsOptional()
@Type(() => Date)
@IsString() // Assuming date is serialized/deserialized appropriately
@IsString()
@ApiPropertyOptional()
startTime?: Date;
@IsOptional()
@Type(() => Date)
@IsString() // Assuming date is serialized/deserialized appropriately
@IsString()
@ApiPropertyOptional()
endTime?: Date;
@IsOptional()
@Type(() => Date)
@IsString() // Assuming date is serialized/deserialized appropriately
@IsString()
@ApiPropertyOptional({ type: String, nullable: true })
date?: Date | null;
}
class ScheduleModel_2024_04_15 {
@IsInt()
@ApiProperty()
id!: number;
@IsInt()
@ApiProperty()
userId!: number;
@IsString()
@ApiProperty()
name!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({ type: String, nullable: true })
timeZone?: string | null;
@IsOptional()
@ValidateNested({ each: true })
@Type(() => EventTypeModel_2024_04_15)
@IsArray()
@ApiPropertyOptional({ type: [EventTypeModel_2024_04_15] })
eventType?: EventTypeModel_2024_04_15[];
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AvailabilityModel_2024_04_15)
@IsArray()
@ApiPropertyOptional({ type: [AvailabilityModel_2024_04_15] })
availability?: AvailabilityModel_2024_04_15[];
}
export class UpdatedScheduleOutput_2024_04_15 {
@ValidateNested()
@Type(() => ScheduleModel_2024_04_15)
@ApiProperty({ type: ScheduleModel_2024_04_15 })
schedule!: ScheduleModel_2024_04_15;
@IsBoolean()
@ApiProperty()
isDefault!: boolean;
@IsOptional()
@IsString()
@ApiPropertyOptional()
timeZone?: string;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
prevDefaultId?: number | null;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
currentDefaultId?: number | null;
}
@@ -1,105 +1,132 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsDate, IsOptional, IsArray, IsBoolean, IsInt, IsString, ValidateNested } from "class-validator";
class AvailabilityModel {
@IsInt()
@ApiProperty()
id!: number;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
userId?: number | null;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
eventTypeId?: number | null;
@IsArray()
@IsInt({ each: true })
@ApiProperty({ type: [Number] })
days!: number[];
@IsDate()
@Type(() => Date)
@ApiProperty({ type: Date })
startTime!: Date;
@IsDate()
@Type(() => Date)
@ApiProperty({ type: Date })
endTime!: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
@ApiPropertyOptional({ type: Date, nullable: true })
date?: Date | null;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
scheduleId?: number | null;
}
class WorkingHours {
@IsArray()
@IsInt({ each: true })
@ApiProperty({ type: [Number] })
days!: number[];
@IsInt()
@ApiProperty()
startTime!: number;
@IsInt()
@ApiProperty()
endTime!: number;
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
userId?: number | null;
}
class TimeRange {
@IsOptional()
@IsInt()
@ApiPropertyOptional({ type: Number, nullable: true })
userId?: number | null;
@IsDate()
@ApiProperty({ type: Date })
start!: Date;
@IsDate()
@ApiProperty({ type: Date })
end!: Date;
}
export class ScheduleOutput {
@IsInt()
@ApiProperty()
id!: number;
@IsString()
@ApiProperty()
name!: string;
@IsBoolean()
@ApiProperty()
isManaged!: boolean;
@ValidateNested({ each: true })
@Type(() => WorkingHours)
@ApiProperty({ type: [WorkingHours] })
workingHours!: WorkingHours[];
@ValidateNested({ each: true })
@Type(() => AvailabilityModel)
@IsArray()
@ApiProperty({ type: [AvailabilityModel] })
schedule!: AvailabilityModel[];
@ApiProperty({ type: [[TimeRange]] })
availability!: TimeRange[][];
@IsString()
@ApiProperty()
timeZone!: string;
@ValidateNested({ each: true })
@IsArray()
@ApiPropertyOptional({ type: [Object] })
// note(Lauris) it should be
// dateOverrides!: { ranges: TimeRange[] }[];
// but docs aren't generating correctly it results in array of strings
dateOverrides!: unknown[];
@IsBoolean()
@ApiProperty()
isDefault!: boolean;
@IsBoolean()
@ApiProperty()
isLastSchedule!: boolean;
@IsBoolean()
@ApiProperty()
readOnly!: boolean;
}

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