Compare commits

..
428 Commits
Author SHA1 Message Date
GitHub Actions b0b93f612e chore: release v4.8.9
Post release / release (push) Canceled after 0s
2024-12-27 11:17:18 +00:00
Udit Takkar 558c8be740 perf: getAvailableSlots improvements (#18382) 2024-12-27 11:09:49 +00:00
Alex van Andel 1aee1742d5 chore: rely less on autoinfer, be able to use initial data types in consumers (#18359) 2024-12-27 16:23:49 +05:30
Hariom BalharaandUdit Takkar ab942ed4cd Fix routing form field input loosing focus on typing (#18381)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-27 14:30:10 +05:30
Hariom Balhara b5bec79354 perf: /router endpoint perf improvement (#18366) 2024-12-27 08:56:52 +00:00
Nizzy b48d0e1f48 remove yarnlock (#18377) 2024-12-27 00:44:21 -05:00
GitHub Actions 4a38b4dc89 chore: release v4.8.8
Post release / release (push) Canceled after 0s
2024-12-26 19:31:22 +00:00
Syed Ali Shahbaz ff12e387b3 fix: update contact email for CalDAV (#18351) 2024-12-26 19:24:11 +00:00
Kartik Saini 6b5e144042 feat: add name and email url params when rescheduling past bookings (#18318)
* feat: add name and email as url params when rescheduling past bookings

* remove logs

* pass booking seat name and email as url params
2024-12-26 19:23:28 +00:00
Anwar Sadath cd7b93a51e fix: Last used label in button shown in 2-factor code and backup screen (#18341) 2024-12-26 19:20:11 +00:00
Anik Dhabal Babu a76366cbcb fix: Recurring meeting creates increasing number of Zoom events (#18360) 2024-12-26 19:19:24 +00:00
Anwar Sadath 860c04bbda fix: discrepancy in description hyperlinks and calendar invite (#18365) 2024-12-26 19:18:54 +00:00
+6 225313e391 feat: Plain chat (#18284)
* plain custom desin

* No display on card

* dynamic plain chat component and hmac hash

* re-route users to plain.com chat instead of intercom chat when going to /support

* provider errors

* yarn lock fix

* plain chat removed unneeded hmac

* fix ts error

* error handling improved

* remove intercome provider from app-dir

* Create getting-started.mdx (#18342)

* Delete help directory (#18343)

* chore: moved docs/help to /help (#18345)

* chore: Delete unused guides directory (#18346)

* feat: booking filters (#18303)

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>

* chore: Remove `HeadSeo` components where no longer needed + improve app router metadata logic (#18348)

* remove HeadSeo for already migrated pages and refactor prepareMetadata

* create _generateMetadataWithoutImage and refactor _generateMetadata

* chore: app router - /bookings status page (#18183)

* chore: app router - /bookings page

* remove env vars

* fix

* Update middleware.ts

* revert unneeded change

* refactor for the better

* fix

* cache i18n instances (#18309)

* feat: virtual queues tab in insights (#18260)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>

* feat: update translations via @replexica (#18361)

Co-authored-by: Replexica <support@replexica.com>

* update OOO e2e tests to remove flakiness (#18367)

* fix: metadata is overwritten for child managed eventType when updating parent (#18059)

* fix: metadata is overwirten

* Update

* type error

* Update

* fix test

* fix type error

* chore: added routing support link (#18369)

* Added routing form support link

* small change

* Type fix

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>

* chore: refactor handling logic for embeds in app router (#18362)

* refactor handling logic for embeds in app router

* fix type checks

* add test for withEmbedSsrAppDir

* fix

* review changes

* yarn lock

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Calcom Bot <109866826+calcom-bot@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Replexica <support@replexica.com>
Co-authored-by: Vijay <vijayraghav22@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-26 19:07:08 +00:00
Benny Joo 3994929a51 chore: refactor handling logic for embeds in app router (#18362)
* refactor handling logic for embeds in app router

* fix type checks

* add test for withEmbedSsrAppDir

* fix
2024-12-26 10:39:07 +01:00
Peer RichelsenandJoe Au-Yeung ae38027d76 chore: added routing support link (#18369)
* Added routing form support link

* small change

* Type fix

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2024-12-24 17:19:37 +00:00
Anik Dhabal Babu 9036080fcf fix: metadata is overwritten for child managed eventType when updating parent (#18059)
* fix: metadata is overwirten

* Update

* type error

* Update

* fix test

* fix type error
2024-12-24 21:21:43 +05:30
Vijay e6ef4e1988 update OOO e2e tests to remove flakiness (#18367) 2024-12-24 13:49:49 +00:00
Calcom BotandReplexica 3c2619a7a3 feat: update translations via @replexica (#18361)
Co-authored-by: Replexica <support@replexica.com>
2024-12-23 22:42:02 +00:00
cfb09a1b21 feat: virtual queues tab in insights (#18260)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2024-12-23 19:16:38 +00:00
Benny Joo 5fe4e09975 cache i18n instances (#18309) 2024-12-23 17:14:52 +00:00
Benny Joo 542799e2cf chore: app router - /bookings status page (#18183)
* chore: app router - /bookings page

* remove env vars

* fix

* Update middleware.ts

* revert unneeded change

* refactor for the better

* fix
2024-12-23 10:12:22 +01:00
Benny Joo 3eda7858c0 chore: Remove HeadSeo components where no longer needed + improve app router metadata logic (#18348)
* remove HeadSeo for already migrated pages and refactor prepareMetadata

* create _generateMetadataWithoutImage and refactor _generateMetadata
2024-12-23 10:02:59 +01:00
Udit TakkarandSyed Ali Shahbaz b59bf7d1ee feat: booking filters (#18303)
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2024-12-23 14:06:29 +05:30
Peer Richelsen 0dadeac352 chore: Delete unused guides directory (#18346) 2024-12-22 21:44:43 +01:00
Peer Richelsen 5eb7a4d56e chore: moved docs/help to /help (#18345) 2024-12-22 21:43:53 +01:00
Calcom Bot 43fa944e2f Delete help directory (#18343) 2024-12-22 21:36:22 +01:00
Peer Richelsen 1866b2845c Create getting-started.mdx (#18342) 2024-12-22 21:35:30 +01:00
Anik Dhabal Babu 2cfc63e6e3 fix: Reschedule email for RR event shows wrong timezone to attendee (#18262)
Post release / release (push) Canceled after 0s
2024-12-20 14:02:03 -06:00
GitHub Actions 4f66f3d2c6 chore: release v4.8.7 2024-12-20 20:01:15 +00:00
Morgan ca3e7bd97d fix: hotfix hide hidden booking fields in event-types GET / (#18306) 2024-12-20 19:37:32 +00:00
Benny Joo cafa3f9cf3 fix: make sure to call refresh for pages rendered by app router (#18304) 2024-12-20 18:44:35 +00:00
Benny Joo e04faffcbb chore: add cache to static pages in app router (#18278)
* add cache

* fix

* refactor
2024-12-20 13:37:20 -05:00
Peer Richelsen 6f1556a2fd feat: added chatbase app (#18302) 2024-12-20 17:50:19 +00:00
mintlify[bot] d72a954896 Mintlify Docs Edits (#18301)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2024-12-20 17:10:39 +00:00
amrit abeb20373c fix portugese (pt-BR) password validation from a required eight to seven. (#18273) 2024-12-20 17:33:46 +05:30
MorganandRajiv Sahal 422ef2f5b8 chore: add cal.com logo to platform booker embed (#18269)
* chore: add cal.com logo to platform booker embed

* fixup! chore: add cal.com logo to platform booker embed

---------

Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2024-12-20 12:55:25 +02:00
Omar López ec920eb2a8 fix: prevents cron to be clogged with calendars with missing credentialId (#18281) 2024-12-19 23:53:51 +00:00
GitHub Actions 795eecf300 chore: release v4.8.6
Post release / release (push) Canceled after 0s
2024-12-19 22:42:27 +00:00
Calcom BotandReplexica 8ae3d6d55b feat: update translations via @replexica (#18279)
Co-authored-by: Replexica <support@replexica.com>
2024-12-19 20:51:57 +00:00
85070574da feat: Support event level conflict checking (#18052)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Omar López <zomars@me.com>
2024-12-19 19:34:24 +00:00
Morgan d1c2872f6e fix: platform booker timezone select (#18277) 2024-12-19 17:08:34 +00:00
Morgan 42d78f8099 fix: platform booker icons and styling (#18276) 2024-12-19 16:45:05 +00:00
Lauris Skraucis f0d7a757d4 fix: Booker atom default booking fields (#18275)
* refactor: rename transformBookingFieldsApiToInternal to transformBookingFieldsApiRequestToInternal

* refactor: default booking fields can required or not required

* fix: render system fields in booker

* fix: hidden property of booking fields

* fix: default field labels and placeholders

* chore: bump platform libraries

* fix: tests

* fix: e2e tests
2024-12-19 16:30:14 +00:00
GitHub Actions 0b2f09ab7d chore: release v4.8.5
Post release / release (push) Canceled after 0s
2024-12-19 15:10:13 +00:00
Benny Joo 355aaa7010 fix: include app directory in rewrites generation logic + migrate /reschedule pages to App Router (#18261)
* chore: app router - /reschedule pages (#18150)

* app router - /reschedule pages

* add to config.matcher

* generateMetdata is not needed

* include app directory in rewrites generation logic

* make it more readable

* make code more elegant

* fix

* fix

* fix

* fix

* test and refactor
2024-12-19 06:22:19 -07:00
Rajiv Sahal c27b73aa9a fix: event type atom availability (#18263) 2024-12-19 16:38:05 +05:30
Somay Chauhan a622c7b624 fix: seats option not working for booker atom (#18265) 2024-12-19 08:07:40 +00:00
GitHub Actions 19bd3e5849 chore: release v4.8.4
Post release / release (push) Canceled after 0s
2024-12-18 23:21:28 +00:00
Benny Joo 7827028e0a Revert "chore: app router - /reschedule pages (#18150)" (#18258)
This reverts commit a7f24e777b.
2024-12-18 22:48:03 +00:00
Joe Au-Yeung ebc3ced687 chore: CRM logging, add booking uid (#18256)
* Add more logging

* Add conditional
2024-12-18 21:23:04 +01:00
Nizzy b56824e626 removed posthog identifier (#18253) 2024-12-18 15:03:17 -05:00
Benny Joo 460e511459 chore: app router - /booking pages (#18182)
* chore: app router - /booking pages

* move pages to /app

* add to config matcher
2024-12-18 14:09:34 -05:00
Benny Joo b4fd6f3442 chore: bring back removed onboarding steps (#18254)
* add connected-calendar and connected-video steps back

* adapt test

* fix name
2024-12-18 20:05:17 +01:00
Benny Joo 0a7a0e3687 chore: app router - /teams single page (#18184)
* chore: app router - /teams single page

* fix type check
2024-12-18 17:51:01 +00:00
Calcom BotandReplexica 595fffbea0 feat: update translations via @replexica (#18249)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2024-12-18 17:44:50 +00:00
GitHub Actions 218c9152da chore: release v4.8.3
Post release / release (push) Canceled after 0s
2024-12-18 17:22:22 +00:00
Omar López 77fe23da49 fix: skip failed cache subscriptions from retrying (#18252) 2024-12-18 17:16:59 +00:00
sean-brydon a583db3fd4 refactor and fix bulk attribute handling (#18231) 2024-12-18 16:31:03 +00:00
cb1f803d0a feat: ooo calibration (#17864)
* first version of ooo calibration

* decrease precision when checking for full day event

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

* improve variable naming

* don't include hosts created after OOO

* fix failing tests

* fix all tests

* add test for ooo calibration

* first implementation to fix full day busy event reaching the current month

* allow mutiple day events

* make getAvailabilityWithTimeZones optional

* fix type error

* fix type errors

* fix tests

* fix type error

* fix test

* code clean up

* remove time adjustment from map

* code clean up

* fix tests

* getAllCalendars -> this.getAllCalendars

* fix typo

* extract mergeOverlappingRanges

* code improvements from feedback

* fix getCalIdsWithTimeZone

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2024-12-18 16:26:59 +00:00
Peer Richelsen 479d6b5961 Update i18n.json 2024-12-18 16:28:22 +01:00
Alex van Andel 4ab3ec73c0 chore: Set the stage for a primary key with value uuid() (#18245)
* chore: Set the stage for a primary key  with value uuid()

* fix: Test and ensure it validates the id value is set and defined

* Add unique index manually, prisma doesn't support this

* revert unique
2024-12-18 15:14:04 +00:00
Joe Au-Yeung 8dda418127 feat: Salesforce skip ownership check if attendee email is a free domain (#17916)
* Add email domain array

* Create numbered email domain object

* Check email domain

* Rename function

* Add tests

* Frontend enable skip ownership check if free email domain

* Backend ignore adding ownership to return records if free email domain check is enabled

* feat: Only require confirmation for free email domains (#17917)

* Add requiresConfirmationForFreeEmail to db

* Add option to event type settings

* Get requiresConfirmationForFreeEmail for event type page

* Include requiresConfirmationForFreeEmail in fetching event type

* Pass bookerEmail to `getRequiresConfirmationFlags`

* Add free email domain check to `determineRequiresConfirmation`

* Add `requiresConfirmationForFreeEmail` to types

* Add severity to Watchlist table

* Add migration for watchlist severity

* Add `getEmailDomainInWatchlist` method to watchlist repository

* Use watchlist repository to check for free email domain

* Mock watchlist repository in test

* Update test

* Rename method

* Add severity to blocked list

* Move check free email domain to async

* Type checks

* Adjust for promise returned

* Fix tests

* Fix

* Fix tests
2024-12-18 15:02:42 +00:00
Benny Joo a7f24e777b chore: app router - /reschedule pages (#18150)
* app router - /reschedule pages

* add to config.matcher

* generateMetdata is not needed
2024-12-18 14:54:15 +01:00
Benny Joo f78fca594a chore: app router - /getting-started page (#18185)
* remove env vars

* move pages to /app

* remove Head component

* remove pages router

* add to config matcher

* fix
2024-12-18 14:41:03 +01:00
Lauris Skraucis e6a7ae7da3 fix: platform decrease usage when cancelling booking (#18241)
* fix: decrement billing upon cancelled booking

* docs: explain billing in FAQ

* update docs
2024-12-18 14:50:31 +02:00
Lauris Skraucis ad5d92dff0 refactor: v2 calendars busy-times (#18244)
* refactor: make dateFrom and dateTo required

* refactor: make dateFrom and dateTo required

* refactor: useCalendarsBusyTimes enabled only if dateFrom & dateTo passed
2024-12-18 13:36:26 +01:00
Anik Dhabal Babu 8bf46ab8b6 fix: missing n8n app screenshot and add margin to the back button (#18240)
* fix: n8n app screenshot missing

* remove log
2024-12-18 12:35:26 +00:00
sean-brydon 34b0753f8d feat: match edit user sheet designs (#18243)
* edit sheet design changes

* remove attribute header
2024-12-18 11:50:19 +00:00
Calcom BotandReplexica 0bd7227150 feat: update translations via @replexica (#18234)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2024-12-17 23:22:10 -05:00
Omar López 4d192ee328 fix: single rejection caused cache cron to fail (#18239) 2024-12-17 23:04:08 -05:00
63013d402e feat: Plain integration (#18130)
* revert: "fix: Request permissions to allow events to be created on shared Office365/Outlook calendars (#17760)"

This reverts commit 1680cba5a5.

* plain.com card

* detiled error handling for get customer

* working email and id

* hmac

* Revert "fix: correct line-breaks in calendar event description (#18077)"

This reverts commit 06494a6999.

* pr changes requet

* remove pan for now

* add-new-implementation-for-early-review

* Pushing fix for createHmac stringify

* added validation and user repository for email check

* add apiRouteMiddleware which handles the error handling

* HMAC_SECRET_KEY -> PLAIN_HMAC_SECRET_KEY

* Use the right error

* Convey right error to consumer

* Fixup apiRouteMiddleware to handle handler

* Don't export handler, only export POST

* changed to app directory

* working unkown user card

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-12-17 20:24:07 -05:00
Lauris Skraucis 6b3ccbc61a fix: v2 isSystemAdmin (#18162)
* refactor: ApiAuthStrategy return isSystemAdmin and define returned type

* refactor: use ApiAuthGuardUser instead of GetUserReturnType

* regenerate docs
2024-12-17 19:32:01 +00:00
Lauris SkraucisandSomay Chauhan 2af5cba0d7 feat: v2 teams event types (#18085)
* feat: v2 teams event-types endpoints

* refactor: rely on teams event types service and repository

* finish mergin main into this branch

* test: teams event types

* refactor: move teams event types output from org to teams folder

* refactor: move teams event types output from org to teams folder

* chore: finish merge main

* refactor: logger name and unused import

* chore: finish main merge

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-12-17 20:25:42 +01:00
Omar LópezandKeith Williams ae7bbaf414 fix: calendar cache date ranges (#18237)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-12-17 19:20:19 +00:00
Hariom Balhara 4df4383e1e fixes:CAL-4905 (#18222) 2024-12-17 22:51:42 +05:30
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
Omar López 32aaa26bf1 chore: v4.7.4 (#17728)
release.yml / release (push) Canceled after 0s
2024-11-18 13:12:53 -07:00
Omar López 7c0ee9f186 revert: "feat: Populate gCal calendar cache via webhooks (#11928)"" (#17682) 2024-11-18 19:48:33 +00:00
Benny Joo 50df22928b fix: correct domain for link column in org member table (#17726) 2024-11-18 18:40:24 +00:00
Calcom BotandReplexica fbda8a38cb feat: update translations via @replexica (#17722)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2024-11-18 14:56:01 +00:00
8194ffcd07 feat: Autofill feature (#15437)
* feat: Autofill feature

* Revert "feat: Autofill feature"

This reverts commit ac94257cbb516e140ed550ae804e725ca5d036ca.

* feat: store prev response in localStorage

* review fixes

* add tests

* remove unused import

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
2024-11-18 20:04:02 +05:30
VijayandAnik Dhabal Babu 7b94822c8d fix: add default schedule while creating org member (#16855)
* add default schedule while creating org user

* naming as per review comments

* added e2e test

* move bookTeamEvent and expectPageNotFound to testUtils

* Update EventTeamAssignmentTab.tsx

* Update AddMembersWithSwitch.tsx

* Update utils.ts

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-11-18 20:03:24 +05:30
Syed Ali Shahbaz 9aa8f8a30c Update oauth.mdx (#17671) 2024-11-18 14:29:33 +00:00
Rajiv Sahal e206ead611 fix: calendar settings atom modifications (#17579)
* update selected calendars display when no calendar connected

* remove no calendar connected message
2024-11-18 11:05:34 -03:00
Udit Takkar ecac331fd1 perf: get event types (#17689)
* refactor: get event types

* chore

* test: add team and managed team event types

* perf: use Promise.all

* fix: type err
2024-11-18 11:05:12 -03:00
sean-brydon fb4cd7f0e4 feat: Add column visibility filters to routing insights table (#17715)
* add table as a child callback to we can pass it to columnVisibility

* add back failed bookings component
2024-11-18 11:03:58 -03:00
Rajiv Sahal f743db9eaa feat: enable all users to create platform team (#17707)
* add extra checks for platform to make sure user is genuine

* make slug unique for every team
2024-11-18 11:03:23 -03:00
Udit Takkar 0f3773d724 feat: add instant meeting parameter (#17647)
* feat: add instant meeting parameter

* feat: show modal only when parameter present

* chore: remove logs

* fix: type err

* fix: UI

* fix: remove useFieldArray
2024-11-18 19:29:10 +05:30
Eunjae Lee e6633050ba fix: provide explicit column width for member list (#17718) 2024-11-18 13:43:14 +00:00
Replexica Bot f1e972a213 feat: update translations via @replexica (#17712) 2024-11-18 13:01:07 +00:00
Morgan 12e78a7350 chore: add default reading direction to atoms wrapper (#17710) 2024-11-18 12:25:11 +00:00
Amit Sharmaandsean-brydon 277b3e0cf5 feat: DataTable component should use a "key" of data as the selection Key. (#17714)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-11-18 10:25:52 +00:00
Lauris Skraucis 2b1041460c feat: v2 subsequent recurring booking cancellation (#17645)
* refactor: cancelling all remaining seated recurring bookings not possible

* chore: document cancel booking endpoint

* chore: remove unused imports

* feat: cancel subsequent bookigns

* fix: flaky test

* refactor: getCanceledSubsequentBookings

* refactor: after cancellation return whole recurrence sequence
2024-11-18 08:17:48 +00:00
Keith Williams ce0d685849 chore: Remove faulty languages for Replexica (#17705) 2024-11-18 02:58:16 +00:00
cc5ce4e598 feat: update translations via @replexica (#17704)
* chore: sync with main

* feat: update translations via @replexica

* Update common.json

---------

Co-authored-by: Replexica <support@replexica.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-11-18 02:21:52 +00:00
Keith Williams 4123152bd5 feat: Added more supported languages (#17702) 2024-11-18 01:09:35 +00:00
Keith Williams 1b4469f6e5 chore: v4.7.3 (#17701)
release.yml / release (push) Canceled after 0s
2024-11-17 21:58:45 -03:00
Benny JooandKeith Williams 5401bcca4e feat: AI description - DB model + frontend + backend (fetch only) (#17651)
* feat: AI description - DB model + frontend + backend (fetch only)

* fix types and add validation to backend

* improve log

* improve

* import type

* fix replexica error

* fix

* fix test

* update replexica type

* Renamed descriptionTranslations to fieldTranslations

* Moved the eventTypeId column to 2nd

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-11-18 00:16:56 +00:00
Benny Joo f9afca2939 feat: Add link column to CSV file for org members (#17695)
* improve spacing

* feat: Add link column to CSV file for org members

* update tests
2024-11-17 10:53:43 +00:00
Alex van Andel fc1d553b74 chore: Release v4.7.2 (#17692)
release.yml / release (push) Canceled after 0s
2024-11-16 18:36:43 +00:00
Calcom BotandReplexica b8c2fdac44 feat: update translations via @replexica (#17691)
Co-authored-by: Replexica <support@replexica.com>
2024-11-16 18:32:52 +00:00
Abul Hassan SathuliandPrashant Varma e24c40f518 fix: how-to-setup-api-in-a-local-instance.mdx in docs with correction after api-v2 (#17667)
* Updating how-to-setup-api-in-a-local-instance.mdx with correction after api v2

After the implementation of api v2, some of the documents are not yet updated and could you make developers spend too much time on finding these things.

* fix: typo

---------

Co-authored-by: Prashant Varma <prashantvarma5083@gmail.com>
2024-11-16 18:27:02 +00:00
Alex van Andel c5cefc3e61 chore: Found a critical index missing on profileId (#17690) 2024-11-16 18:26:50 +00:00
Omar López 2c57ad0288 perf: optimize organization admin check (#17683) 2024-11-16 18:26:23 +00:00
sean-brydon af9bf14d9e fix: routing column filters (#17688)
* remove column filters

* remove column filters

* use a loading state while loading columns

* fix
2024-11-16 18:05:36 +00:00
sean-brydon b1a0cdc8e2 fix: large org routing insights UI issues (#17687)
* fix overflow colour in darkmode

* truncate/show overflow scroll indicator when overflowing on response values

* remove total created count

* overflow Y on failed response container

* fix darkmode styles on barlist

* radix hovercards to properly allow overflow in container

* column visible state

* remove constrained width on dropdowns to w-fit

* export type
2024-11-16 11:01:26 -03:00
Alex van AndelandPeer Richelsen 7fef3e7b85 chore: Release v4.7.1 (#17686)
release.yml / release (push) Canceled after 0s
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-11-16 10:00:16 +00:00
Hariom Balhara 384deab0ce test: findTeamMemberEmailFromCrm (#17685)
* We need to choose contact owner when crm app slug is not present

* Add unit tests for getTeamMemberEmailFromCrm module
2024-11-16 09:32:33 +00:00
Hariom Balhara 22da681f8c We need to choose contact owner when crm app slug is not present (#17684) 2024-11-16 09:02:24 +00:00
Omar López f4f4afad39 perf: optimize team admin check queries (#17681)
## What does this PR do?

Optimizes team and organization admin checks by:
- Removing unnecessary member data fetching in profile repository
- Simplifying team admin verification query to directly check membership
- Refactoring organization admin check to use the optimized team verification method

## Mandatory Tasks

- [x] I have self-reviewed the code
- [x] I have updated the developer docs in /docs
- [ ] I confirm automated tests are in place

## How should this be tested?

1. Log in as a team admin/owner
2. Verify team admin privileges still work correctly
3. Log in as an organization admin
4. Confirm organization admin permissions are maintained
5. Check that admin-only features remain accessible

Expected behavior:
- Team admins/owners should retain all their existing permissions
- Organization admins should maintain their administrative capabilities
- No degradation in admin functionality while improving query performance

## Checklist

- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my changes generate no new warnings
2024-11-15 19:00:18 -07:00
Keith Williams 1c05952077 chore: Remove playwright install for API v2 E2E (#17677) 2024-11-15 23:47:26 +00:00
1379 changed files with 76472 additions and 17790 deletions
+23 -16
View File
@@ -107,6 +107,14 @@ NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=
# plain.com config
PLAIN_API_KEY=
PLAIN_API_URL=https://api.plain.com/v1
PLAIN_HMAC_SECRET_KEY=
PLAIN_CHAT_ID=
PLAIN_CHAT_HMAC_SECRET_KEY=
# Zendesk Config
NEXT_PUBLIC_ZENDESK_KEY=
@@ -119,7 +127,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
@@ -129,6 +137,10 @@ GOOGLE_LOGIN_ENABLED=false
# Needed to enable Google Calendar integration and Login with Google
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
GOOGLE_API_CREDENTIALS=
# Token to verify incoming webhooks from Google Calendar
GOOGLE_WEBHOOK_TOKEN=
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
GOOGLE_WEBHOOK_URL=
# Inbox to send user feedback
SEND_FEEDBACK_EMAIL=
@@ -143,6 +155,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=
@@ -233,7 +247,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=
@@ -341,8 +356,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
@@ -350,16 +363,8 @@ APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
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
@@ -368,10 +373,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
@@ -408,3 +409,9 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL=
# NEXT_PUBLIC_LOGGER_LEVEL=3 sets to log info, warn, error and fatal logs.
# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal]
NEXT_PUBLIC_LOGGER_LEVEL=
# Used to use Replexica SDK, a tool for real-time AI-powered localization
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 -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 }}
@@ -65,7 +66,6 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- uses: ./.github/actions/cache-db
- name: Run Tests
working-directory: apps/api/v2
+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
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"
+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
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@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"
+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"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- calcom-monorepo
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
View File
+2
View File
@@ -0,0 +1,2 @@
undecided:
- calcom-monorepo
+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
@@ -61,6 +61,8 @@ describe("rateLimitApiKey middleware", () => {
reset: Date.now(),
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
onRateLimiterResponse(rateLimiterResponse);
});
+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 { checkIfEmailIsBlockedInWatchlistController } 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 checkIfEmailIsBlockedInWatchlistController(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,
});
@@ -9,11 +9,25 @@ export const schemaSelectedCalendarBaseBodyParams = SelectedCalendar;
export const schemaSelectedCalendarPublic = SelectedCalendar.omit({});
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams.partial({
userId: true,
});
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams
.partial({
userId: true,
})
.omit({
// id will be set by the database
id: true,
// No eventTypeId support in API v1
eventTypeId: true,
});
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams.partial();
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams
.omit({
// id is decided by DB
id: true,
// No eventTypeId support in API v1
eventTypeId: true,
})
.partial();
export const selectedCalendarIdSchema = schemaQueryIdAsString.transform((v, ctx) => {
/** We can assume the first part is the userId since it's an integer */
+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[] }>;
+2 -1
View File
@@ -14,7 +14,8 @@
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"start": "PORT=3003 next start",
"docker-start-api": "PORT=80 next start",
"type-check": "tsc --pretty --noEmit"
"type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit"
},
"devDependencies": {
"@calcom/tsconfig": "*",
@@ -1,9 +1,10 @@
import type { NextApiRequest } from "next";
import type { UserWithCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
import { getConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
import { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/lib/getConnectedDestinationCalendars";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { UserRepository } from "@calcom/lib/server/repository/user";
import prisma from "@calcom/prisma";
import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery";
@@ -104,12 +105,8 @@ async function getHandler(req: NextApiRequest) {
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
const usersWithCalendars = await prisma.user.findMany({
where: { id: { in: userIds } },
include: {
selectedCalendars: true,
destinationCalendar: true,
},
const usersWithCalendars = await UserRepository.findManyByIdsIncludeDestinationAndSelectedCalendars({
ids: userIds,
});
return await getConnectedCalendars(usersWithCalendars);
@@ -117,11 +114,12 @@ async function getHandler(req: NextApiRequest) {
async function getConnectedCalendars(users: UserWithCalendars[]) {
const connectedDestinationCalendarsPromises = users.map((user) =>
getConnectedDestinationCalendars(user, false, prisma).then((connectedCalendarsResult) =>
connectedCalendarsResult.connectedCalendars.map((calendar) => ({
userId: user.id,
...calendar,
}))
getConnectedDestinationCalendarsAndEnsureDefaultsInDb({ user, onboarding: false, prisma }).then(
(connectedCalendarsResult) =>
connectedCalendarsResult.connectedCalendars.map((calendar) => ({
userId: user.id,
...calendar,
}))
)
);
const connectedDestinationCalendars = await Promise.all(connectedDestinationCalendarsPromises);
@@ -1,7 +1,7 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
@@ -49,7 +49,9 @@ import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
export async function deleteHandler(req: NextApiRequest) {
const { query } = req;
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
await prisma.selectedCalendar.delete({ where: { userId_integration_externalId } });
await SelectedCalendarRepository.deleteUserLevel({
where: userId_integration_externalId,
});
return { message: `Selected Calendar with id: ${query.id} deleted successfully` };
}
@@ -1,7 +1,7 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import { schemaSelectedCalendarPublic, selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
@@ -49,8 +49,8 @@ import { schemaSelectedCalendarPublic, selectedCalendarIdSchema } from "~/lib/va
export async function getHandler(req: NextApiRequest) {
const { query } = req;
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
const data = await prisma.selectedCalendar.findUniqueOrThrow({
where: { userId_integration_externalId },
const data = await SelectedCalendarRepository.findUserLevelUniqueOrThrow({
where: userId_integration_externalId,
});
return { selected_calendar: schemaSelectedCalendarPublic.parse(data) };
}
@@ -3,6 +3,8 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import type { UpdateArguments } from "@calcom/lib/server/repository/selectedCalendar";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import prisma from "@calcom/prisma";
import {
@@ -56,7 +58,7 @@ export async function patchHandler(req: NextApiRequest) {
const { query, isSystemWideAdmin } = req;
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
const { userId: bodyUserId, ...data } = schemaSelectedCalendarUpdateBodyParams.parse(req.body);
const args: Prisma.SelectedCalendarUpdateArgs = { where: { userId_integration_externalId }, data };
const args: UpdateArguments = { where: { ...userId_integration_externalId }, data };
if (!isSystemWideAdmin && bodyUserId)
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
@@ -67,7 +69,7 @@ export async function patchHandler(req: NextApiRequest) {
args.data.userId = bodyUserId;
}
const result = await prisma.selectedCalendar.update(args);
const result = await SelectedCalendarRepository.updateUserLevel(args);
return { selected_calendar: schemaSelectedCalendarPublic.parse(result) };
}
@@ -1,9 +1,9 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import type { FindManyArgs } from "@calcom/lib/server/repository/selectedCalendar";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import { schemaSelectedCalendarPublic } from "~/lib/validations/selected-calendar";
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
@@ -34,7 +34,7 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
async function getHandler(req: NextApiRequest) {
const { userId, isSystemWideAdmin } = req;
/* Admin gets all selected calendar by default, otherwise only the user's ones */
const args: Prisma.SelectedCalendarFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
const args: FindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
/** Only admins can query other users */
if (!isSystemWideAdmin && req.query.userId)
@@ -46,7 +46,7 @@ async function getHandler(req: NextApiRequest) {
if (Array.isArray(query.userId)) args.orderBy = { userId: "asc" };
}
const data = await prisma.selectedCalendar.findMany(args);
const data = await SelectedCalendarRepository.findManyUserLevel(args);
return { selected_calendars: data.map((v) => schemaSelectedCalendarPublic.parse(v)) };
}
@@ -3,6 +3,7 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import prisma from "@calcom/prisma";
import {
@@ -52,7 +53,11 @@ import {
async function postHandler(req: NextApiRequest) {
const { userId, isSystemWideAdmin } = req;
const { userId: bodyUserId, ...body } = schemaSelectedCalendarBodyParams.parse(req.body);
const args: Prisma.SelectedCalendarCreateArgs = { data: { ...body, userId } };
const args: {
data: Prisma.SelectedCalendarUncheckedCreateInput;
} = {
data: { ...body, userId },
};
if (!isSystemWideAdmin && bodyUserId)
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
@@ -63,7 +68,7 @@ async function postHandler(req: NextApiRequest) {
args.data.userId = bodyUserId;
}
const data = await prisma.selectedCalendar.create(args);
const data = await SelectedCalendarRepository.create(args.data);
return {
selected_calendar: schemaSelectedCalendarPublic.parse(data),
+4 -1
View File
@@ -52,6 +52,8 @@ describe("POST /api/attendees", () => {
bookingId: 1,
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
prismaMock.attendee.create.mockResolvedValue(attendeeData);
req.isSystemWideAdmin = true;
req.userId = 123;
@@ -84,7 +86,8 @@ describe("POST /api/attendees", () => {
timeZone: "UTC",
bookingId: 1,
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
prismaMock.attendee.create.mockResolvedValue(attendeeData);
req.userId = 123;
+1 -1
View File
@@ -11,7 +11,7 @@ import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
import prisma from "@calcom/prisma";
import type { Booking } from "@calcom/prisma";
import type { Booking } from "@calcom/prisma/client";
import handler from "../../../pages/api/bookings/_post";
@@ -21,6 +21,8 @@ describe("DELETE /api/event-types/[id]", () => {
beforeEach(() => {
vi.resetAllMocks();
// Mocking membership.findFirst
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
prismaMock.membership.findFirst.mockImplementation(({ where }) => {
const { userId, teamId, accepted, role } = where;
const mockData = [
@@ -72,6 +72,7 @@ describe("POST /api/event-types", () => {
},
});
req.userId = memberUserId;
// @ts-expect-error - Return type is wrong
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false);
await handler(req, res);
@@ -114,6 +115,7 @@ describe("POST /api/event-types", () => {
req.isSystemWideAdmin = true;
req.userId = adminUserId;
// @ts-expect-error - Return type is wrong
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false);
await handler(req, res);
@@ -138,6 +140,7 @@ describe("POST /api/event-types", () => {
req.isSystemWideAdmin = true;
req.userId = adminUserId;
// @ts-expect-error - Return type is wrong
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => true);
vi.mocked(checkParentEventOwnership).mockImplementationOnce(async () => undefined);
vi.mocked(checkUserMembership).mockImplementationOnce(async () => undefined);
@@ -1,17 +1,18 @@
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 type { ILicenseKeyService } from "@calcom/ee/common/server/LicenseKeyService";
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,13 +21,8 @@ afterEach(() => {
vi.resetAllMocks();
});
vi.mock("@calcom/prisma");
vi.mock("~/lib/utils/isAdmin", () => ({
isAdminGuard: vi.fn(),
}));
describe("Verify API key", () => {
let service: LicenseKeyService;
let service: ILicenseKeyService;
beforeEach(async () => {
service = await LicenseKeyService.create();
@@ -45,7 +41,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 +64,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 +85,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 +104,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 +126,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 +163,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 +175,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();
});
});
@@ -79,6 +79,15 @@ describe("POST /api/selected-calendars", () => {
integration: "google",
externalId: "ext123",
userId: 444444,
id: "xxx-xxx",
eventTypeId: null,
domainWideDelegationCredentialId: null,
googleChannelId: null,
googleChannelKind: null,
googleChannelResourceId: null,
googleChannelResourceUri: null,
googleChannelExpiration: null,
error: null,
});
await handler(req, res);
@@ -104,10 +113,19 @@ describe("POST /api/selected-calendars", () => {
req.userId = 333333;
prismaMock.selectedCalendar.create.mockResolvedValue({
id: "xxx-xxx",
credentialId: 1,
integration: "google",
externalId: "ext123",
userId: 333333,
googleChannelId: null,
googleChannelKind: null,
googleChannelResourceId: null,
googleChannelResourceUri: null,
googleChannelExpiration: null,
domainWideDelegationCredentialId: null,
eventTypeId: null,
error: null,
});
await handler(req, res);
@@ -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,91 @@
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it, beforeEach } from "vitest";
import { WatchlistSeverity } from "@calcom/prisma/enums";
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",
severity: WatchlistSeverity.CRITICAL,
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.60",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.78",
"@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();
});
});
});
@@ -191,7 +191,12 @@ export class BookingsController_2024_08_13 {
@Post("/:bookingUid/cancel")
@UseGuards(BookingUidGuard)
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Cancel a booking" })
@ApiOperation({
summary: "Cancel a booking",
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
})
async cancelBooking(
@Req() request: Request,
@Param("bookingUid") bookingUid: string,
@@ -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";
@@ -25,13 +24,7 @@ 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 {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RecurringBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
@@ -86,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",
@@ -0,0 +1,711 @@
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 { 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 { AttendeeCancelledEmail, OrganizerCancelledEmail } from "@calcom/platform-libraries";
import {
CreateRecurringBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Creating recurring bookings", () => {
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 = "bookings-controller-e2e@api.com";
let user: User;
const maxRecurrenceCount = 3;
let recurringEventTypeId: 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: "organization bookings" });
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 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: maxRecurrenceCount, interval: 1 },
},
user.id
);
recurringEventTypeId = recurringEvent.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;
}
it("should not create recurring booking with recurrenceCount larger than event type recurrence count", async () => {
const recurrenceCount = 1000;
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper Recurring",
email: "mr_proper_recurring@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
recurrenceCount,
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
it("should create a recurring booking with recurrenceCount smaller than event type recurrence count", async () => {
const recurrenceCount = maxRecurrenceCount - 1;
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper Recurring",
email: "mr_proper_recurring@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
recurrenceCount,
};
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(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(maxRecurrenceCount - 1);
const firstBooking = data[0];
expect(firstBooking.id).toBeDefined();
expect(firstBooking.uid).toBeDefined();
expect(firstBooking.hosts[0].id).toEqual(user.id);
expect(firstBooking.status).toEqual("accepted");
expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString());
expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString());
expect(firstBooking.duration).toEqual(60);
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,
});
expect(firstBooking.location).toEqual(body.location);
expect(firstBooking.recurringBookingUid).toBeDefined();
expect(firstBooking.absentHost).toEqual(false);
const secondBooking = data[1];
expect(secondBooking.id).toBeDefined();
expect(secondBooking.uid).toBeDefined();
expect(secondBooking.hosts[0].id).toEqual(user.id);
expect(secondBooking.status).toEqual("accepted");
expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString());
expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString());
expect(secondBooking.duration).toEqual(60);
expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId);
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);
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
it("should create a recurring booking with recurrenceCount equal to event type recurrence count", async () => {
const recurrenceCount = maxRecurrenceCount;
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper Recurring",
email: "mr_proper_recurring@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
recurrenceCount,
};
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(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(maxRecurrenceCount);
const firstBooking = data[0];
expect(firstBooking.id).toBeDefined();
expect(firstBooking.uid).toBeDefined();
expect(firstBooking.hosts[0].id).toEqual(user.id);
expect(firstBooking.status).toEqual("accepted");
expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString());
expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString());
expect(firstBooking.duration).toEqual(60);
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,
});
expect(firstBooking.location).toEqual(body.location);
expect(firstBooking.meetingUrl).toEqual(body.location);
expect(firstBooking.recurringBookingUid).toBeDefined();
expect(firstBooking.absentHost).toEqual(false);
const secondBooking = data[1];
expect(secondBooking.id).toBeDefined();
expect(secondBooking.uid).toBeDefined();
expect(secondBooking.hosts[0].id).toEqual(user.id);
expect(secondBooking.status).toEqual("accepted");
expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString());
expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString());
expect(secondBooking.duration).toEqual(60);
expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId);
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);
const thirdBooking = data[2];
expect(thirdBooking.id).toBeDefined();
expect(thirdBooking.uid).toBeDefined();
expect(thirdBooking.hosts[0].id).toEqual(user.id);
expect(thirdBooking.status).toEqual("accepted");
expect(thirdBooking.start).toEqual(new Date(Date.UTC(2030, 1, 18, 13, 0, 0)).toISOString());
expect(thirdBooking.end).toEqual(new Date(Date.UTC(2030, 1, 18, 14, 0, 0)).toISOString());
expect(thirdBooking.duration).toEqual(60);
expect(thirdBooking.eventTypeId).toEqual(recurringEventTypeId);
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);
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
afterEach(async () => {
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
});
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 responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] {
return Array.isArray(data);
}
describe("Recurring bookings cancel all subsequent bookings", () => {
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 = "bookings-controller-e2e@api.com";
let user: User;
const maxRecurrenceCount = 4;
let recurringEventTypeId: number;
let recurringBooking: RecurringBookingOutput_2024_08_13[];
beforeAll(async () => {
jest.spyOn(AttendeeCancelledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest.spyOn(OrganizerCancelledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
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: "organization bookings" });
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 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: maxRecurrenceCount, interval: 1 },
},
user.id
);
recurringEventTypeId = recurringEvent.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;
}
it("should create a recurring booking", async () => {
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper Recurring",
email: "mr_proper_recurring@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(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(maxRecurrenceCount);
recurringBooking = data;
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
it("should cancel all recurrences after a specific recurrence", async () => {
const recurringBookingUid = recurringBooking[2].recurringBookingUid;
const bookings = await bookingsRepositoryFixture.getByRecurringBookingUid(recurringBookingUid);
for (const booking of bookings) {
expect(booking.status).toEqual("ACCEPTED");
}
// note(Lauris): cancel all recurrences after and including third recurrence in the series of 4 recurrences
// aka cancel 3rd and 4th recurrences.
const thirdRecurrenceUid = recurringBooking[2].uid;
const fourthRecurringUid = recurringBooking[3].uid;
const body: CancelBookingInput_2024_08_13 = {
cancelSubsequentBookings: true,
};
return request(app.getHttpServer())
.post(`/v2/bookings/${thirdRecurrenceUid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(4);
const firstRecurrence = data.find((booking) => booking.uid === recurringBooking[0].uid);
expect(firstRecurrence).toBeDefined();
expect(firstRecurrence?.status).toEqual("accepted");
const secondRecurrence = data.find((booking) => booking.uid === recurringBooking[1].uid);
expect(secondRecurrence).toBeDefined();
expect(secondRecurrence?.status).toEqual("accepted");
const thirdRecurrence = data.find((booking) => booking.uid === thirdRecurrenceUid);
expect(thirdRecurrence).toBeDefined();
expect(thirdRecurrence?.status).toEqual("cancelled");
const fourthRecurrence = data.find((booking) => booking.uid === fourthRecurringUid);
expect(fourthRecurrence).toBeDefined();
expect(fourthRecurrence?.status).toEqual("cancelled");
const bookings = await bookingsRepositoryFixture.getByRecurringBookingUid(recurringBookingUid);
const bookingFirst = bookings.find((booking) => booking.uid === recurringBooking[0].uid);
expect(bookingFirst?.status).toEqual("ACCEPTED");
const bookingSecond = bookings.find((booking) => booking.uid === recurringBooking[1].uid);
expect(bookingSecond?.status).toEqual("ACCEPTED");
const bookingThird = bookings.find((booking) => booking.uid === recurringBooking[2].uid);
expect(bookingThird?.status).toEqual("CANCELLED");
const bookingFourth = bookings.find((booking) => booking.uid === recurringBooking[3].uid);
expect(bookingFourth?.status).toEqual("CANCELLED");
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
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();
});
});
describe("Recurring bookings cancel all remaining bookings (bookings with start time greater than this moment)", () => {
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 = "bookings-controller-e2e@api.com";
let user: User;
const maxRecurrenceCount = 4;
let recurringEventTypeId: number;
let recurringBooking: RecurringBookingOutput_2024_08_13[];
beforeAll(async () => {
jest.spyOn(AttendeeCancelledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
jest.spyOn(OrganizerCancelledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
});
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: "organization bookings" });
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 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: maxRecurrenceCount, interval: 1 },
},
user.id
);
recurringEventTypeId = recurringEvent.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;
}
it("should create a recurring booking", async () => {
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 1, 3, 13, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper Recurring",
email: "mr_proper_recurring@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(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(maxRecurrenceCount);
recurringBooking = data;
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
it("should cancel all remaning recurrences", async () => {
const recurringBookingUid = recurringBooking[0].recurringBookingUid;
return request(app.getHttpServer())
.post(`/v2/bookings/${recurringBookingUid}/cancel`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(4);
for (const booking of data) {
expect(booking.status).toEqual("cancelled");
}
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
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();
});
});
});
@@ -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 = {
@@ -1127,293 +1308,4 @@ describe("Bookings Endpoints 2024-08-13", () => {
function responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] {
return Array.isArray(data);
}
describe("Recurring bookings", () => {
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 = "bookings-controller-e2e@api.com";
let user: User;
const maxRecurrenceCount = 3;
let recurringEventTypeId: 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: "organization bookings" });
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
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: maxRecurrenceCount, interval: 1 },
},
user.id
);
recurringEventTypeId = recurringEvent.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;
}
it("should not create recurring booking with recurrenceCount larger than event type recurrence count", async () => {
const recurrenceCount = 1000;
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper Recurring",
email: "mr_proper_recurring@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
recurrenceCount,
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
it("should create a recurring booking with recurrenceCount smaller than event type recurrence count", async () => {
const recurrenceCount = maxRecurrenceCount - 1;
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper Recurring",
email: "mr_proper_recurring@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
recurrenceCount,
};
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(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(maxRecurrenceCount - 1);
const firstBooking = data[0];
expect(firstBooking.id).toBeDefined();
expect(firstBooking.uid).toBeDefined();
expect(firstBooking.hosts[0].id).toEqual(user.id);
expect(firstBooking.status).toEqual("accepted");
expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString());
expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString());
expect(firstBooking.duration).toEqual(60);
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(firstBooking.location).toEqual(body.location);
expect(firstBooking.recurringBookingUid).toBeDefined();
expect(firstBooking.absentHost).toEqual(false);
const secondBooking = data[1];
expect(secondBooking.id).toBeDefined();
expect(secondBooking.uid).toBeDefined();
expect(secondBooking.hosts[0].id).toEqual(user.id);
expect(secondBooking.status).toEqual("accepted");
expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString());
expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString());
expect(secondBooking.duration).toEqual(60);
expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(secondBooking.recurringBookingUid).toBeDefined();
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(secondBooking.location).toEqual(body.location);
expect(secondBooking.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
it("should create a recurring booking with recurrenceCount equal to event type recurrence count", async () => {
const recurrenceCount = maxRecurrenceCount;
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper Recurring",
email: "mr_proper_recurring@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
recurrenceCount,
};
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(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(maxRecurrenceCount);
const firstBooking = data[0];
expect(firstBooking.id).toBeDefined();
expect(firstBooking.uid).toBeDefined();
expect(firstBooking.hosts[0].id).toEqual(user.id);
expect(firstBooking.status).toEqual("accepted");
expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString());
expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString());
expect(firstBooking.duration).toEqual(60);
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(firstBooking.location).toEqual(body.location);
expect(firstBooking.meetingUrl).toEqual(body.location);
expect(firstBooking.recurringBookingUid).toBeDefined();
expect(firstBooking.absentHost).toEqual(false);
const secondBooking = data[1];
expect(secondBooking.id).toBeDefined();
expect(secondBooking.uid).toBeDefined();
expect(secondBooking.hosts[0].id).toEqual(user.id);
expect(secondBooking.status).toEqual("accepted");
expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString());
expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString());
expect(secondBooking.duration).toEqual(60);
expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(secondBooking.recurringBookingUid).toBeDefined();
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(secondBooking.location).toEqual(body.location);
expect(secondBooking.absentHost).toEqual(false);
const thirdBooking = data[2];
expect(thirdBooking.id).toBeDefined();
expect(thirdBooking.uid).toBeDefined();
expect(thirdBooking.hosts[0].id).toEqual(user.id);
expect(thirdBooking.status).toEqual("accepted");
expect(thirdBooking.start).toEqual(new Date(Date.UTC(2030, 1, 18, 13, 0, 0)).toISOString());
expect(thirdBooking.end).toEqual(new Date(Date.UTC(2030, 1, 18, 14, 0, 0)).toISOString());
expect(thirdBooking.duration).toEqual(60);
expect(thirdBooking.eventTypeId).toEqual(recurringEventTypeId);
expect(thirdBooking.recurringBookingUid).toBeDefined();
expect(thirdBooking.attendees[0]).toEqual({
name: body.attendee.name,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(thirdBooking.location).toEqual(body.location);
expect(thirdBooking.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
afterEach(async () => {
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
});
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();
});
});
});
@@ -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);
@@ -306,18 +340,43 @@ export class BookingsService_2024_08_13 {
}
const bookingRequest = await this.inputService.createCancelBookingRequest(request, bookingUid, body);
await handleCancelBooking(bookingRequest);
const res = await handleCancelBooking(bookingRequest);
if (!res.onlyRemovedAttendee) {
await this.billingService.cancelUsageByBookingUid(res.bookingUid);
}
if ("cancelSubsequentBookings" in body && body.cancelSubsequentBookings) {
return this.getAllRecurringBookingsByIndividualUid(bookingUid);
}
return this.getBooking(bookingUid);
}
private async getAllRecurringBookingsByIndividualUid(bookingUid: string) {
const booking = await this.bookingsRepository.getByUid(bookingUid);
const recurringBookingUid = booking?.recurringEventId;
if (!recurringBookingUid) {
throw new BadRequestException(
`Booking with bookingUid=${bookingUid} is not part of a recurring booking.`
);
}
return await this.getBooking(recurringBookingUid);
}
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);
@@ -372,11 +431,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);
@@ -403,6 +470,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({
@@ -411,6 +484,8 @@ export class BookingsService_2024_08_13 {
orgId: profile?.organizationId || null,
reassignReason: body.reason,
reassignedById,
emailsEnabled,
platformClientParams,
});
return this.outputService.getOutputReassignedBooking(reassigned);
@@ -422,6 +497,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,
@@ -430,6 +511,8 @@ export class BookingsService_2024_08_13 {
bookingId: booking.id,
confirmed: true,
recurringEventId: booking.recurringEventId,
emailsEnabled,
platformClientParams,
},
});
@@ -442,6 +525,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,
@@ -451,6 +540,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;
}
@@ -493,23 +493,38 @@ export class InputBookingsService_2024_08_13 {
}
async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) {
let allRemainingBookings = false;
let uid = bookingUid;
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUserAndEvent(
bookingUid
);
const recurringBooking = await this.bookingsRepository.getRecurringByUid(bookingUid);
// note(Lauris): isRecurring means that recurringEventId was passed as uid. isRecurring does not refer to the uid of 1 individual booking within a recurring booking consisting of many bookings.
// That is what recurringEventId refers to.
const isRecurringUid = !!recurringBooking.length;
if (recurringBooking.length) {
// note(Lauirs): this means that bookingUid is equal to recurringEventId on individual bookings of recurring one aka main recurring event
allRemainingBookings = true;
// note(Lauirs): we need to set uid as one of the individual recurring ids, not the main recurring event id
uid = recurringBooking[0].uid;
if (isRecurringUid && inputBooking.cancelSubsequentBookings) {
throw new BadRequestException(
"Cannot cancel subsequent bookings for recurring event - you have to provide uid of one of the individual bookings of a recurring booking."
);
}
if (isRecurringUid) {
return {
// note(Lauris): set uid as one of the oldest individual recurring ids
uid: recurringBooking[0].uid,
cancellationReason: inputBooking.cancellationReason,
allRemainingBookings: true,
};
}
if (inputBooking.cancelSubsequentBookings) {
return {
uid: bookingUid,
cancellationReason: inputBooking.cancellationReason,
cancelSubsequentBookings: true,
};
}
return {
uid,
uid: bookingUid,
cancellationReason: inputBooking.cancellationReason,
allRemainingBookings,
allRemainingBookings: false,
};
}
@@ -517,23 +532,12 @@ export class InputBookingsService_2024_08_13 {
bookingUid: string,
inputBooking: CancelSeatedBookingInput_2024_08_13
) {
let allRemainingBookings = false;
let uid = bookingUid;
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUserAndEvent(
bookingUid
);
if (recurringBooking.length) {
// note(Lauirs): this means that bookingUid is equal to recurringEventId on individual bookings of recurring one aka main recurring event
allRemainingBookings = true;
// note(Lauirs): we need to set uid as one of the individual recurring ids, not the main recurring event id
uid = recurringBooking[0].uid;
}
// note(Lauris): for recurring seated booking it is not possible to cancel all remaining bookings
// for an individual person, so api users need to call booking by booking using uid + seatUid to cancel it.
return {
uid,
uid: bookingUid,
cancellationReason: "",
allRemainingBookings,
allRemainingBookings: false,
seatReferenceUid: inputBooking.seatUid,
};
}
@@ -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}`)
@@ -86,18 +86,16 @@ export class CalendarsController {
@UseGuards(ApiAuthGuard)
@Get("/busy-times")
@ApiOperation({ summary: "Get busy times" })
@ApiOperation({
summary: "Get busy times",
description:
"Get busy times from a calendar. Example request URL is `https://api.cal.com/v2/calendars/busy-times?loggedInUsersTz=Europe%2FMadrid&dateFrom=2024-12-18&dateTo=2024-12-18&calendarsToLoad[0][credentialId]=135&calendarsToLoad[0][externalId]=skrauciz%40gmail.com`",
})
async getBusyTimes(
@Query() queryParams: CalendarBusyTimesInput,
@GetUser() user: UserWithProfile
): Promise<GetBusyTimesOutput> {
const { loggedInUsersTz, dateFrom, dateTo, calendarsToLoad } = queryParams;
if (!dateFrom || !dateTo) {
return {
status: SUCCESS_STATUS,
data: [],
};
}
const busyTimes = await this.calendarsService.getBusyTimes(
calendarsToLoad,
@@ -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()

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