Compare commits

..
328 Commits
Author SHA1 Message Date
GitHub Actions 089941553e chore: release v5.1.16
Post release / release (push) Canceled after 0s
2025-04-04 23:02:43 +00:00
Benny Joo 2dc83a1c55 chore: remove unused dep—react-intl (#20531)
* remove react-intl

* yarn lock
2025-04-04 22:49:29 +00:00
Joe Au-Yeung 5bfb6fc148 refactor: SCIM - handleGroupEvents (#20537)
* Add logging to `handleGroupEvents`

* `createUsersAndConnectOrg` skipDuplicates when creating new users

* `ProfileRepository.createMany` to get username from `orgAutoAcceptEmail`

* Refactor `createUsersAndConnectToOrg` to use `createMany` instead of individual db calls

* Add error logging to `sendSignupToOrganizationEmail`

* Move `sendSignupToOrganizationEmail` to a `Promise.allSettled`

* Add `createManyForExistingUsers` and `createManyPromise`

* Move creating existing users profiles to `ProfileRepository.createManyForExistingUsers`

* Move `sendExistingUserTeamInviteEmails` to a `Promise.allSettled`

* Refactor `createUsersAndConnectToOrg` to use `ProfileRepository.createManyPromise`

* Type fix

* Create `generateNewChildEventTypeDataForDB`

* Create `addNewMembersToEventTypes`

* Type fix

* Type fix

* Type fix

* `createMany` new workflowsOnEventTypes

* Introduce `DATABASE_CHUNK_SIZE`

* Batch connect children event types

* Batch connect profiles to existing users
2025-04-04 17:36:26 -04:00
Max Prilutskiy 31ccdba203 feat: update translations via @lingodotdev (#20559) 2025-04-04 20:58:54 +00:00
sean-brydonandSean Brydon 82fb5272c8 add border to footer (#20543)
Co-authored-by: Sean Brydon <sean@Seans-Mac-mini.local>
2025-04-04 20:40:46 +01:00
e86ad03cde perf: Slim down logged in round 2 (#20535)
* perf: Slim down loggedInViewer some more

* Move locationOptions to apps router

* Moved calVideo related handlers to separate router

* Moved calendar related handlers to separate router

* Rename

* Moved deleteCredential to new router

* Moved updateProfile to me router

* fix unit test

* fix e2e test

* tweak

* missing

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
2025-04-04 15:33:14 -04:00
Morgan 1953402f81 fix: platform dialog portal issue (#20553) 2025-04-04 15:39:36 +00:00
Lauris Skraucis d6a086c9c7 docs: platform refresh tokens and team event type atoms (#20524)
* docs: managing refresh token expiring

* docs: team event type creation and settings
2025-04-04 15:39:11 +00:00
sean-brydonandSean Brydon 9a517a89b6 fix: toggle group segment design (#20549)
* add toggle state correctly

* fix height

---------

Co-authored-by: Sean Brydon <sean@Seans-Mac-mini.local>
2025-04-04 13:47:05 +00:00
Keith Williams 19f2197eef Vital and libphonenumber-js (#20547) 2025-04-04 13:35:12 +00:00
Keith Williams 3d583678cf perf: Remove dynamic imports of Sentry (#20545) 2025-04-04 13:22:27 +00:00
Christoph HeikeandTushar Bhatt ddde762f9b feat: Implements Greetmate.ai app into Cal.com (#19812)
* Implements Greetmate.ai app into Cal.com

* Update GreetmateButton.tsx

* Greetmate app

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-04-04 10:07:13 -03:00
sean-brydonandSean Brydon 9332eba5d7 fix: badge dot sizes to be 8x8 (#20544)
* fix badge sizes to be 8x8

* add data-testid to dot

---------

Co-authored-by: Sean Brydon <sean@Seans-Mac-mini.local>
2025-04-04 09:49:00 -03:00
sean-brydon 46d1823ccc feat: add new UI component for multi option inputs (#20533)
* add multi option input support

* fix import

* add multi-key-field-support
2025-04-04 10:27:04 +00:00
Lauris Skraucis 277da187d0 feat: v2 admin GET bookings, user information (#20221)
* feat: membership user return bio and metadata

* feat: input and output bio and metadata for users and manager users

* test: managed event type appears when fetching user event types

* test: managed event type appears when fetching user event types

* test: managed event type appears when fetching user event types

* test: managed event type appears when fetching non-org team user event types

* feat: organizations users bookings endpoint

* feat: book using eventSlug & username & organizationSlug

* chore: generate latest docs
2025-04-04 10:25:10 +00:00
Carina WollendorferandCarinaWolli 7c6b055cd1 add include timezone param (#20542)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-04-04 10:05:51 +00:00
Benny Joo 97308ebf1c perf: /event-types/[type] — use server-fetched data to skip client-side trpc fetch (#20497) 2025-04-04 09:43:04 +00:00
Pranav Patil c2cf55f10c fix: Updated DateRangePicker in limit future booking (#20483)
* fix: Updated DateRangePicker in limit future booking

* removed unnecessary comments

* minor changes in end date selection

* final changes in end date selection

* minor changes in end date selection

* fixed issue of logs

* added allowPastDates props for out of office data selection
2025-04-04 13:42:18 +05:30
Joe Au-Yeung 3867ff6874 chore: Salesforce - Handle datetime and textarea field types (#20539)
* Add datetime and textarea field types

* Add logic for datetime and textarea field types
2025-04-04 09:32:30 +02:00
Pratheek Ravikumar 0e1c4e53d8 fix: Fixing the overlapping timezone dropdown menu (#20487)
* Fixing the overlapping timezone dropdown menu

* making the timezone menu to display 5 timezones at a time
2025-04-04 12:54:15 +05:30
Vincent Lamandamrit 3834e69100 Added new table under Insights page for most cancelled bookings (#19858)
- Added new Table componnent
- Added trpc call
- Added locale title

fixed rebase changes

Co-authored-by: amrit <iamamrit27@gmail.com>
2025-04-04 11:49:49 +05:30
Pranav Rajveer a05935cdf6 fix: fixed language translations (#20488)
* fix: fixed language translations

* fix: removed type-error

* Update DisableAllEmailsSetting.tsx
2025-04-04 05:36:21 +00:00
Benny Joo 19a0f968ca perf: Speed up User Type booking page (#20516)
* refactor [user]/[type] server logic

* refactor useSlot

* refactor

* fix

* fix OG images

* fix
2025-04-03 22:13:53 -03:00
GitHub Actions 3b945ba439 chore: release v5.1.15
Post release / release (push) Canceled after 0s
2025-04-03 22:23:50 +00:00
Keith Williams 7a47eacb34 chore: Update favicon for docs (#20532) 2025-04-03 21:48:15 +00:00
MorganandEunjae Lee da853958f7 feat: get org bookings endpoint api-v2 (#20462)
* feat: get org bookings endpoint api-v2

* fix userId filter transform

* fix: update getBookings to take filter.userIds in account

* refactor: getBooking where clause

* fixup! refactor: getBooking where clause

* feat: enable org owners to see org member bookings as attendees

* update flaky e2e tests

* fixup! feat: enable org owners to see org member bookings as attendees

* fixup! fixup! feat: enable org owners to see org member bookings as attendees

* chore: differentiate between org/team owner/admin

* chore: throw error with invalid userIds filter

* chore: clearly ensure non team orgs when orgId is not specified

* bump libraries and add comments

* bump libraries

* chore: give main profile orgId to getAllUserBookings

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-04-03 17:36:10 -04:00
Udit Takkar d63d15b7b2 fix: don't display user event types with no owner (#20525) 2025-04-03 21:08:19 +00:00
6bc040b7bb feat: Update @dub/analytics to support domainsConfig.refer (#20495)
* Update `@dub/analytics` to support domainsConfig.refer

* add reverse proxy

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-04-03 16:32:58 -04:00
0465c639e6 feat: options to disable cancelling and/or rescheduling (#20489)
* feat:disable-rescheduling

* feat:disable-cancelling

* enhancements

* enhancements-2

* enhancements-3

* revert CancelBooking.tsx

* add-test

* final-changes

* minor-change

* fix: Revert yarn.lock accidental commit

* minor-change

* fix:type checks

* fix:type check

* fix:type check

* add-test

* add-tests

* add-test

* fix:type checks

* fix:type check

* fix:clean-code

* fix:type check

* fix:type check

* fix:minor-change

* fix:tests

* fix:BookingListItem

* fix:type-check

* fix:type-check

* adding check for platform

* revert-change

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-04-03 20:04:02 +00:00
Tushar Bhatt 8d59c2fc16 feat: max-seats-per-time-slot (#20213)
* feat: max-seats-per-time-slot

* fix : test

* enhancements

* enhancements

* fix:test

* fix:test
2025-04-03 15:29:57 -04:00
Lauris Skraucis cb365117b3 feat: v2 return booking icsUid (#20527)
* feat: v2 return booking icsUid

* docs: regenerate
2025-04-03 14:37:40 +00:00
Benny Joo 7c9aae7eaf selectedDateString could be null (#20517) 2025-04-03 11:40:28 +00:00
Eunjae Leeandsean-brydon 1e7453edcd fix: keep search term on the url (DataTableToolbar.SearchBar) (#20521)
* fix: keep search term on the url (DataTableToolbar.SearchBar)

* update comment

* Update packages/features/data-table/components/DataTableToolbar.tsx

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-04-03 12:59:36 +02:00
Morgan f353fdf252 chore: bridge logs api v2 <-> libraries (#20440)
* chore: bridge logs api v2 <-> libraries

* chore: replace tslog directly

* chore: add settings handling

* chore: use minLevel to stop debug logs

* handle log levels

* bump libraries
2025-04-03 08:27:12 +00:00
Lauris Skraucis feff588603 fix: v2 allow creating team events without hosts (#20520)
* fix: v2 allow creating team events without hosts

* chore: remove log
2025-04-03 07:53:58 +00:00
8ae5d32fe5 fixed firstname lastname identifiers (#20480)
Co-authored-by: romit <romitgabani1@gmail.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-04-03 04:21:03 +00:00
Anik Dhabal Babu 9600ab6085 fix: Add to calendar feature not working properly (#20515) 2025-04-03 02:41:36 +00:00
417a612aa3 refactor: extract NextRequest from handle new booking (#20082)
* extract NextRequest

* update api and tests

* booking limits tests

* fix more tests to use new approach

* update more tests with new format

* extract getOrgDomainConfig to not use req

* extract req from loadNewUsers and pass in hostname/forcedSlug

* fix instant meeting types and hostname fixes

* fix handleNewBookingReq

* fix type errors in tests

* make hostName and forcedSlug optional

* fix type err

* Revert "fix type err"

This reverts commit 9d5de9019d9dafe348c97b876baaa1d0675967e5.

* wip fix e2e

* fix: add missing headers

* migrate handle recurring event and also create tests specific to fn

* platform recurringbooking

* fix type

* hard code types on request object

* bump libraries

* fixup! bump libraries

* fix: accessing host if headers not passed

* fix: v2 recurring booking

* fix: accessing host if headers not passed

* chore: bump platform libraries

* fix tests

* push

* chore: bump libraries

* push lock changes

* bump libraries

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
2025-04-02 17:57:58 -04:00
Eunjae Lee 4db3c592e5 fix: add oauth client id to table identifier on platform managed user list (#20513) 2025-04-02 21:40:38 +02:00
Lauris Skraucis d99db25dca docs: CalProvider (#20508) 2025-04-03 00:34:32 +05:30
Alex van Andel ba423e1061 Revert "fix: Correct import of markdownToSafeHtml on the client (#18195)"
This reverts commit f5750f3933.
2025-04-02 18:25:06 +01:00
GitHub Actions d5ca11b27b chore: release v5.1.14
Post release / release (push) Canceled after 0s
2025-04-02 16:08:32 +00:00
Benny Joo 25534f721a chore: refactor to use createRouterCaller util (#20494)
* create createRouterCaller and use it

* fix
2025-04-02 16:46:17 +01:00
fad5497363 feat: allow conferencing apps to be installed on teams (#19784)
* feat: added zoom, office365-video as valid event-type locations

* added docs

* Update stripe.service.ts

* enable conferencing apps for teams initial commit

* fix: make conferencing atom work with teams

* update team events in bulk update default location

* feat: Separate endpoints for team conferencing apps instead of query params

* Display all team events in 'Bulk update existing event types' modal when conferencing atom is enabled for Teams

* fix: bulk update default location for team events

* removed the organizations endpoints

* Refactor: Separate functions for team events to follow SRP

* undo extra changes

* undo extra changes

* fix: platform libraries build breaking

* feat: only allow integrations which are installed in create/update team/user event-type endpoints

* fix: merge conflicts

* remove redundant imports

* Update ConferencingAppsViewPlatformWrapper.tsx

* Update useBanners.ts

* fix: axios error response interceptor

* feat: update :app param docs in `organizations-conferencing.controller`

* Revert "fix: axios error response interceptor"

This reverts commit 729eac3eee9f81c19b5368bbae4b1666884221fd.

* Update useAtomsGetInstalledConferencingApps.ts

* upgrade platform libraries

* Update apps.ts

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-04-02 11:32:11 -04:00
Lauris Skraucis 563b31b7d0 fix: v2 bookings attendeeEmail filter (#20506)
* fix: GET bookings if attendeeEmail belong to managed user

* fix: GET bookings if attendeeEmail belong to managed user but does not include oAuthId
2025-04-02 15:21:27 +00:00
Joe Au-Yeung 8c9eb18463 chore: Add spam checking for workflow bodies (#18822)
* Add akismet package to tasker

* Create scanWorkflowBody task

* Schedule workflow body scan

* Add AKISMET_API_KEY .env

* Auto lock user if spam is detected

* Uncommit key

* Add safe param to workflow step

* Migration for safe field

* Do not process workflow steps is `safe` is false

* Update migration to set previous records to true

* Address comments

* Refactor `scheduleWorkflowNotifications` to accept an object

* If new steps or editing old ones send to tasker

* Call `scheduleWorkflowNotifications` in task

* Fix `IS_SELF_HOSTED`

* Remove unused function

* Make `safe` optional in schema

* Type fix

* Revert "Make `safe` optional in schema"

This reverts commit d0964702affa87c35562300301473d25635c565b.

* Revert "Type fix"

This reverts commit d9a031303269a2994ae46f576ab2a3d31e4d977b.

* Type fixes

* Type fixes

* Address comments

* Fix tests

* Add tests

* Update tests

* Typo fix

* Update `safe` to `verifiedAt`

* feat: Compare workflow reminder bodies to default template (#19060)

* Add `getTemplateForAction` function

* Use `getTemplateForAction` when creating a new step

* Use `getTemplateForAction` when action changes

* Have `emailReminderTemplate` accept an object as a param

* Rename `getTemplateForAction` to  `getTemplateBodyForAction`

* Simplify changing body when changing templates

* Create `compareReminderBodyToTemplate`

* In task, compare if reminderBody is a template

* Linting

* Add tests

* refactor: `emailReminderTemplate` to accept object as param (#19288)

* Add `getTemplateForAction` function

* Use `getTemplateForAction` when creating a new step

* Use `getTemplateForAction` when action changes

* Have `emailReminderTemplate` accept an object as a param

* Rename `getTemplateForAction` to  `getTemplateBodyForAction`

* Simplify changing body when changing templates

* Create `compareReminderBodyToTemplate`

* In task, compare if reminderBody is a template

* Linting

* Add tests

* Refactor `scheduleEmailReminders`

* Refactor `create.handler` for new workflows

* Refactor `emailReminderManager`

* Refactor `getEmailTemplateText`

* Fix typo

* Type fix - whatsapp plain text template imports

* Type fix - no template found

* Type fix - add `isBrandingDisabled` to `emailReminderTemplate`

* Add workflow and user to prisma mock

* Fix imports for akismet dependencies

* Record user lock reason

* Undo linting changes

* Fix tests

* New workflow, at verify created step

* Handle if `SCANNING_WORKFLOW_STEPS` is toggled

* Move `verifiedAt` checks to specific schedule functions
- `scheduleWhatsappReminder`
- `scheduleEmailReminder`
- `scheduleSMSReminder`

* Update logic

* Do not fallback verifiedAt

* Add comment to next.config.js
2025-04-02 08:16:26 -07:00
Lauris Skraucis 0965fad72a fix: v2 filter out ooo days out of slots (#20303)
* refactor: slots input service rely on eventTypeId and set start hours if none set

* fix: filter out ooo slots

* test: ooo days filtered out of slots

* revert changes
2025-04-02 11:53:52 -03:00
Lauris Skraucis e76bb37f78 refactor: v2 access and refresh tokens (#20486)
* feat: add refreshTokenExpiresAt to /force-refresh, /exchange/, /refresh

* feat: encode ownerId, expiresAt in tokens & return refresh expires at when creating managed user

* driveby:fix: pass language when creating managed user

* refactor: remove void notation and await access token propagation

* refactor: centralize createOAuthTokens and refreshOAuthTokens logic

* docs: tokens expires at and payload

* chore: comment selectedDate in examples app booker
2025-04-02 17:27:00 +03:00
sean-brydon b0a85a5637 removes additional space on top and bottom (#20503) 2025-04-02 13:13:59 +00:00
Morgan 82f374d84c chore: set x-request-id in response header for exeptions apiv2 (#20505) 2025-04-02 11:56:26 +00:00
Eunjae Lee 9ac037c524 fix: switch saveMode for SaveFilterSegmentButton (#20502) 2025-04-02 09:58:18 +00:00
Anik Dhabal Babu f5750f3933 fix: Correct import of markdownToSafeHtml on the client (#18195)
* chore: markdowntosafeHtml refactor

* update

* Update
2025-04-02 14:41:33 +05:30
sean-brydon 8f2cb0522d fix: google signin getting started redirect (#20501)
* fix redirect conditions

* remove addiiotnal query call
2025-04-02 08:59:18 +00:00
Eunjae Lee 4cd8dbbab4 feat: support Filter Segment in other pages (#20461)
* feat: support Filter Segment in other pages

* minor changes to the behaviors

* remove comment

* fix type error

* fix e2e test
2025-04-02 10:36:42 +02:00
1fb778ae78 feat: v3/UI docs (#20454)
* ui playground

* add types to ui

* push avatar ignore ts and eslint errors

* feat: V3 UI avatar  (#18997)

* avatar playground

* add overflow-hidden back for loading indicator

* update lock

* feat: v3/badge  (#18999)

* badge intro

* fix text on small badge

* add dark mode warning

* feat: v3/button (#19014)

* TOC

* wip buttons

* shadows

* secondary

* semantic colours

* minimal buttons

* update disabled states

* checkbox

* dropdown v3

* load icons

* fix design sections

* fix icon gap

* fix

* remove client component from rsc

* use peer as avatar

* gif

* fix button gap - dropdown iocn

* support start icon in dropdowns

* add alert demo + v3 component + darken semantic text

* base inputs and text field

* wip input

* input field addon/suffix

* toast page

* wip select

* match designs for input and select rounded

* fix

* fix select isloading transform

* fix multi icons transition rotation

* fix group heading

* range slider demo + range slider

* range slider popover design

* v3 dialog

* fix slider thumb size

* wrap in suspense for useSearchParam difference in next14+

* base of tabs + horizontal tabs

* fix select sizing to match input 36px

* fix dark mode hover on button

* target domState class for light/dark so we work on portal elements

* fix default bg on popover

* lightmode toasts + eleveation token

* empty screen

* intro to split button concept

* mass colour updates

* intro to themes

* remove i18n from label with name

* bouncy buttons

* feat: small select

* nav item dumb component

* full demo

* settings demo

* fix tabs

* fix mt

* fix dropdown feedback from loom

* alert fixes mentioned in loom

* push font-cal

* move to fumadocs

* fix avatar

* colors table

* add typeography

* icon + spacing guide

* add border radius and shadow tables

* fix colours and border radius

* remove prose undline via fumas fork

* fix icon buttons sizes

* button group component

* fix horizontal tabs

* tab fixes

* switches

* pagination component

* show toasts components raw

* sync with airtable

* add use client

* 10px on input again

* 10px in select also

* fix input height

* fix files merge

* fix loading state for icon buttons

* fix button fab

* Enable addon to now overflow to next line in DS

* use cal font for dialog text

* add avatar square variant

* fix select padding and menu size

* fix type errors

* fix type errors

* add switch small size and fix type errors

* fix ref types on input

* update web styles

* remove addonFilled to fix type errors

* push dialog fixes

* fix checkbox display

* remove prose from alert examples

* remove prose from all our component examples

* badge rounded-lg on lg size

* update colors

* force w/h on icon buttons instead of inheriting

* remove border-focus

* remove border-focus from tailwind config

* update tokens to match new colours

* add new border subtle tokens and add to toasts

* fix lg button padding

* fix and drastically improve rtl rests

* multi select use badges

* use badge design for select pills

* fix avatar alignment

* fix tests and remove redudant ones

* fix tests and remove redudant ones

* update atoms globals css

* extract tokens.css

* extract app.css

* force flex for atoms

* add @calcom/ui/css

* revert ui playground styles abstraction

* fix input focus states

* fix focus states for buttons

* feat: v3/app work (#19233)

* globals.css for app + event types listing search

* few app wide NITS

* fix select foucs within styling

* fix formbuilder switch styles

* fix disabled state of nested inputs

* workflow fixes

* webhook form fixes

* i18n on tabs info

* booking filters + tab

* fix filters spacing

* make first item so we get correct border radius if not default

* round radio group in create event types + make button small

* profile page

* update editor br

* button nits in existing places in the app

* fix timezone select and multilist in guest form

* fix text area on public booking page

* size small event types create

* margin on routing filter

* filters improvments

* routing insights fixes

* fix date filter range

* ooo fixes

* fix teams/members bulk actions

* create team UI

* i18n in useTabs instead of in tab component now

* Add event type setup border to availability

* fix bulk actions on org members table

* onboarding fixes

* fix input types with ref stripe comparision

* i18n fixes for platform

* update globals of atoms

* fix form builder firstname/lastname switch + correct footer useage

* fix select

* Revert "add @calcom/ui/css"

This reverts commit 0dd8f9012907a278cfede4a98f289cb0744c9244.

* revert tokens css

* Revert "extract app.css"

This reverts commit 10b998604816ad3310c24b36a02a638dabc5be24.

* linear nits

* workflow dropdown

* fix page theme heading sizes to match cards

* fix stepper for team creation

* adjust margin to FilterSearchField

* fix ooo icon

* fix booker and dark mode due to darkgray 50

* more dark mode fixes

* add size prop

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Morgan Vernay <morgan@cal.com>

* update package name

* update package json lost in merge

* update package json lost in merge

* re-genrate source

* fix imports

* imports

* fix merge issues and imports

* restore globals css

* add source to git ignore

* remove old source files

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-04-02 09:10:07 +01:00
Udit TakkarandOmar López bc9d76e7da chore: do not route to meeting-ended page for daily link used for testing (#20371)
* chore: remove 14 day buffer for QA

* chore: check uid

---------

Co-authored-by: Omar López <zomars@me.com>
2025-04-02 00:00:28 +00:00
Joe Au-Yeung af51ce1890 chore: Salesforce - handle checkbox type fields (#20490)
* Add checkbox field type

* Handle accepting booleans under value field when saving writing to field settings

* UI saving settings to write to checkbox field

* Prepare update body to pass boolean value to checkbox field

* Type fixes

* Type fix
2025-04-02 00:00:02 +00:00
Benny Joo 75c79f76c8 perf: Remove ssrInit completely (#20439)
* remove ssrInit completely

* remove instances of dehydratedState

* fix

* refactor

* fix type checks

* fix linting

* Refactor

* Refactor

* remove log
2025-04-01 19:14:46 -04:00
Benny Joo b5b4134b75 feat: implement booking page error boundary (#20492)
* implement booking page error boundary

* use it
2025-04-01 22:13:18 +00:00
GitHub Actions 01ca2ca3de chore: release v5.1.13
Post release / release (push) Canceled after 0s
2025-04-01 15:19:27 +00:00
f94e80558a feat: refactor handleCancelBooking to not have next request (#20241)
* refactor handleCancelBooking to not have next request

* fix platform v2 function calls

* dont fall back to "" if no seat refrence

* add missing type from merge

* chore: bump platform libraries

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
2025-04-01 14:52:24 +00:00
Alex van Andel a5c15e6517 chore: Allow tracing orphaned event types (added id) (#20479) 2025-04-01 13:56:32 +00:00
Udit Takkar aaa39cccf8 chore: update status code (#20443)
* chore: update status code

* chore: update status code

* fix: error unhandled
2025-04-01 10:08:14 -03:00
amrit d24a93962f fix: trigger lingo.dev by removing duplicate value (#20475) 2025-04-01 15:38:24 +05:30
Eunjae Lee 1d9d52ecf8 fix: add missing DataTableProvider (#20473)
* fix: add missing DataTableProvider

* add e2e test
2025-04-01 10:57:36 +01:00
sean-brydon f1a832921a fix form builder dark mode (#20474) 2025-04-01 09:30:07 +00:00
amrit af1633db28 fix: trigger Lingo.dev for a translation not picked up (#20472) 2025-04-01 13:30:07 +05:30
Benny Joo 7353b6a604 fix: trigger Lingo.dev for a translation not picked up (#20468) 2025-04-01 02:39:58 -04:00
Anik Dhabal Babu dab0b4a4d8 fix: user fields response not pupulated (#20458) 2025-04-01 06:30:00 +00:00
GitHub Actions 3711e01a77 chore: release v5.1.12
Post release / release (push) Canceled after 0s
2025-03-31 22:17:14 +00:00
Anik Dhabal Babu 63c9cb6b92 test: embed e2e tests for mobile viewport and hideEventTypeDetails=true (#20445)
* test: embed e2e test

* update

* tweak

* Update fixtures.ts

* update

* something new

* Update playwright.config.ts
2025-03-31 23:05:53 +01:00
Eunjae Lee f039e116d7 fix: eventTypeId filter on Download at /insights (#20463)
* fix: eventTypeId filter on Download at /insights

* show toast for error
2025-03-31 22:00:15 +00:00
Joe Au-Yeung 93621baea5 Add users to event types when provisioned (#20465) 2025-03-31 21:49:08 +00:00
Tushar BhattandEunjae Lee eaa17fa48d fix: booking filtering queryParams (#20447)
* fix: bookng-filtering-queryParams

* adding comments

* feedback changes

* fix: typo

* minor-change

* revert Pagination.tsx

* minor-change

* minor-changes

* Update packages/features/data-table/DataTableProvider.tsx

* fix: type-check

* fix: test

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-03-31 17:19:28 +01:00
Tushar Bhatt 9ce94fbbfc fix:theme-flakiness (#20460) 2025-03-31 21:18:06 +05:30
Lauris Skraucis 69548e6ed0 docs: v2 authorization headers (#20453)
* refactor: client secret header for oauth users and webhooks

* fix: platform introduction link to api reference

* regenerate docs for commits above

* refactor: specify request headers

* refactor: specify request headers
2025-03-31 14:28:05 +03:00
sean-brydon 5cd2ba1362 perf: move stripe to billing service out of app store (#20376)
* add more methods to billingService

* update profile handler

* stripe customer handler

* verify email

* move imports + move generic methods to billing service

* move to strip ebilling service class

* push changes to mocks

* fix enum

* fix typo

* correctly update customer metadata

* fix userId type to number

* fix types on billing services

* fix type errors due to return methods differing

* fix return types
2025-03-31 09:20:27 +00:00
Eunjae Lee f15d347804 feat: implement filter segment (#20066)
* feat: add filter segment

* add tests

* add ui

* provide tableIdentifier from context

* fix components

* update save button

* try to fix router

* add proper implementation

* update icon

* remove segmentId if invalid

* fix tests

* fix type errors

* fix type errors

* fix unit test

* clean up

* minor fixes

* clean up types

* avoid barrel imports

* fix imports

* minor improvements

* fix type error

* fix radio import

* fix little issues

* menu permission

* update icon snapshot

* fix: remember last used filter segment (#20261)

* fix: remember last used filter segment

* extract segments logic as a separate file

* remove unnecessary query

* apply feedback

* deep comparison
2025-03-31 08:53:48 +00:00
Morgan c95118715a chore: don't log the whole delegated credential (#20452) 2025-03-31 08:25:35 +00:00
Benny Joo 21e7ac3cbe fix: middleware config matcher (#20450)
* fix: middleware config matcher - ending slash

* improve comment
2025-03-30 23:02:20 -03:00
Amit SharmaandKeith Williams 81de95bd28 feat: allow plain ask a question for paid users only (#20390)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-30 04:17:12 +00:00
Omar López d80ed07093 fix: calendar cache date mismtach (#20446)
* fix: calendar cache date mismtach

* Update tests
2025-03-29 17:00:26 -07:00
Anik Dhabal Babu b2ac86c759 fix: send email to is not being displayed in routing forms (#20444)
* feat: ability to set locked timezone

* fix: send email to is not being displayed in routing forms

* update

* Update EventAdvancedTab.tsx
2025-03-29 14:49:05 +00:00
Vachmara 295cf40c6e fix(intercom app): team / org url validation (#20431) 2025-03-29 10:56:07 +05:30
Omar López 92827b2c9d fix: atom builds (#20437) 2025-03-28 23:18:56 +00:00
Omar López e1076690c1 chore: bump platform (#20436) 2025-03-28 21:10:46 +00:00
Calcom BotandLingo.dev 70dd9b9c2c feat: update translations via @lingodotdev (#20432)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-28 20:20:38 +00:00
GitHub Actions 71bfd17f81 chore: release v5.1.11
Post release / release (push) Canceled after 0s
2025-03-28 19:29:33 +00:00
Eunjae Lee 5b08b9f11f fix: add isAdminOrOwner and useIsOrgAdminOrOwner (#20429)
* fix: add isAdminOrOwner and useIsOrgAdminOrOwner

* make useIsOrgAdminOrOwner to accept session

* revert change

* remove useIsOrgAdminOrOwner and rename isAdminOrOwner to checkAdminOrOwner

* fix type

* fix type

* fix import
2025-03-28 17:01:06 +00:00
Keith Williams 3456477012 chore: Upgrade lingo.dev (#20430) 2025-03-28 13:39:58 -03:00
Amit Sharma c654795a3e fix: Follow Up for dub referrral embed (#20427)
* fix: Follow Up for dub referrral embed

* fix meta title and description
2025-03-28 21:57:01 +05:30
Eunjae Lee a7b1981cf1 fix: wrong dates on OOO due to summer time (#20408)
* fix: wrong dates on OOO due to summer time

* update styles

* fix test

* fix edit button
2025-03-28 21:00:56 +05:30
Joe Au-YeungandHariom Balhara 48016e0f3e chore: Move CRM owner calls handleResponse (#19316)
* Handle normal ownership routing

* Skip contact owner check if true

* Fix typos

* Call getting contact owner in promise.all in handleResponse

* Pass crm contact owner data to params

* Add crm contact params

* Only get CRM owner calls if params don't exist

* Handle if properties don't exist

* Fix typo

* Pass CRM specific variables to getServerSideProps

* Remove logs

* Type fixes

* Use `wrapGetServerSIdePropsWIthSentry` in router getServerSideProps

* Add sentry monitoring

* Type fixes

* Type fix

* Type fixes

* Update imports for sentryWrapper

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-03-28 10:42:28 -04:00
30367cd5f3 [Snyk] Security upgrade next from 13.5.7 to 13.5.9 (#20412)
* fix: apps/api/v1/package.json to reduce vulnerabilities

The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-NEXT-9508709

* updated yarn.lock

---------

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-28 14:36:45 +00:00
Joe Au-YeungandUdit Takkar 47d822a092 fix: Dsync group table formatting (#20422)
* Fix column to take up entire table

* Fix formatting of group names

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-28 13:24:26 +00:00
Joe Au-Yeung b597dad450 feat: Salesforce - Fallback to Lead if Contact Isn't Found For Routing (#20423)
* Add fallback to lead to schema

* FE option to fallback to lead owner

* Logic to query for contacts and leads for RR skip
2025-03-28 16:54:03 +05:30
Lauris Skraucis e67e073f04 refactor: v2 team event type assignAllTeamMembers (#20400)
* fix: platform seed data

* fix: assignAllTeamMembers: true excludes team owners in platform

* refactor: platform plan guard explicit error message

* refactor: require either hosts or assignAllTeamMembers=true

* refactor: require either hosts or assignAllTeamMembers=true

* fix: before creating or updating team event type check if doesnt exist with that slug

* chore: regenerate docs

* test: can update round robin that had assign all team members true

* fix: platform guard spec test

* fix: seed platform membership role

* fix: test ts error
2025-03-28 05:56:26 -03:00
Hariom Balhara 17f493a488 fix: 500 error on missing response id. Should be 400 (#20420)
* fix: 500 error on missing response id. Should be 400

* fix failing test
2025-03-28 07:53:32 +00:00
Hariom Balhara 96fcf7beec fix: Flaky organization.spec.ts test for i.cal.com (#20421)
* Fix flakiness in checjly

* Fix isEmbed check
2025-03-28 04:50:09 +00:00
Joe Au-Yeung c0aa97178c feat: Prevent watchlisted entities from creating bookings (#19107)
* Add `searchForAllBlockedRecords` to watchlist repository

* Create `checkIfUsersAreBlocked`

* Call `checkIfUsersAreBlocked` in `loadAndValidateUsers`

* Draft test

* Add tests

* Type fix

* Type fix
2025-03-28 01:11:10 +00:00
Peer Richelsen fc8ff2d4ae nit changes to referral embed (#20419) 2025-03-27 23:41:16 +00:00
Anik Dhabal Babu c1a6ccc260 fix: system booking field is not tranlate in calndar event (#20410) 2025-03-27 19:27:31 +00:00
Morgan b17f47236e fix: atoms build inferred types (#20409)
* fix: atoms build inferred types

* type button cva

* chore: refactor react imports

* chore: refactor react imports toast
2025-03-27 19:00:45 +00:00
GitHub Actions 236fd967ad chore: release v5.1.10
Post release / release (push) Canceled after 0s
2025-03-27 18:51:29 +00:00
Joe Au-Yeung 7f5534b340 fix: Salesforce ownership query (#20405)
* Fix query

* Add test
2025-03-27 16:08:09 +00:00
Alex van Andel 50bacf6ea2 Add 'await' to searchParams, parse failed (#20407) 2025-03-27 16:07:52 +00:00
Hariom Balhara 0d1adcca58 Ensure upId in token for Google Signin which is used by getServerSession (#20399) 2025-03-27 15:42:09 +00:00
Somay ChauhanandMorgan 81962efb2e fix: Restore event name field in Advanced Event Settings for Atom (#20394)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-03-27 17:22:12 +02:00
sean-brydonandKeith Williams da47168941 fix: Stop throwing 202 for stripe handler (#20397)
* dont throw - log error

* invoice paid

* update to warn

* Update packages/features/ee/billing/api/webhook/_invoice.paid.ts

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-27 15:06:15 +00:00
Hariom Balharaandamrit 2aa583bb6d feat: Toggle to hide Team Profile link on Booker (#20369)
* hide team profile link on booker

* fix ts errors

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
2025-03-27 15:00:35 +00:00
Eunjae Lee ea250febbc fix: style DataTableSelectionBar (#20403) 2025-03-27 14:29:45 +00:00
Keith Williams f062014952 chore: Use fixed mintlify version for builds (#20402) 2025-03-27 14:11:50 +00:00
Eunjae LeeandAlex van Andel 88961a1c67 fix: attribute filters on organization member list (#20380)
* fix: attribute filters on organization member list

* fix total count

* add tests

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-03-27 14:36:47 +01:00
MorganandhbjORbj cd952dc93c chore: decouple ui dialog from AppRouter and Atoms (#20088)
* chore: decouple ui dialog from AppRouter and Atoms

* fix type e2e test

* fix: import dialog from @calcom/ui/components/dialog

* fix: remove log

* fix unit test

* fix

* fix and move data-override-list test to features package

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-27 09:24:42 +02:00
Hariom Balhara b0ed1fd5e6 dont install the scripts on booking pages (#20373) 2025-03-27 11:12:28 +05:30
Keith WilliamsandhbjORbj be883aac02 perf: Remove ssrInit from layout and event-types (#20368)
* perf: Remove ssrInit from layout and event-types

* remove ssg file since not used

* refator trpc usage in app router

* fix

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-26 22:04:52 -03:00
Alex van Andel 2ab4e8b40a fix: support Stripe subscription items (#20389)
* fix: Implement backwards compatible stripe change to customer.subscription.deleted

* Further handling, including price -> plan change
2025-03-26 23:24:38 +00:00
sean-brydonandPeer Richelsen 6bd11009a8 allow promotion codes (#20387)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-03-26 22:51:10 +00:00
099c534daa feat: dub refer embed (#20258)
* feat: dub refer embed

* review fix

* review fix

* @dub/embed-core seems not needed

* @dub/embed-core can stay in web

* move refer page out of main-nav

* update yarn lock

* use loading.tsx

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-26 21:06:21 +00:00
Udit Takkar 7aa1d8035c fix: update error handling in validateBookingTimeIsNotOutOfBounds function to use HttpError (#20386) 2025-03-26 17:24:33 +00:00
Udit Takkar 596e9b4e5a fix: circular dependencies (#20378)
* fix: circular dependencies

* fix: type err
2025-03-26 12:42:40 -04:00
Lauris Skraucis d04f10e7d1 refactor: platform - remove host and attendee automatic no show webhook trigger (#20385)
* docs: connect calendar redir and auto accept team creator

* refactor: platform hide automatic no show webhook triggers
2025-03-26 21:55:49 +05:30
0fec74a7fe fix: #11378 (#20384)
* fix:#11378

* fix: #11378

* final-changes

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Tushar <tusharbhatt0135@gmail.com>
2025-03-26 14:51:39 +00:00
Eunjae Lee d927662dd7 fix: relocate action buttons on org member list (#20336)
* fix: relocate filter segment buttons on org member list

* implement toolbarContainerRef

* adjust location

* reorganize the hierarchy of DataTableProvider

* rename toolbar container to cta container

* move clear button to toolbar right
2025-03-26 14:18:39 +00:00
sean-brydon e1c001baff adds scroll indicators and sticks to one line (#20383) 2025-03-26 14:15:54 +00:00
Joe Au-Yeung 122af4538d fix: Add assignment reason for round robin reassignments (#19199)
* Add `RR_REASSIGNED` to schema

* Handle recording reassigned manual vs rr

* Pass manual assignment prop to `AssignmentReasonRecorder`

* Add log to determine who is doing the reassignment

* RR reassign - add initializing log

* Record assignment reason when reassigning via rr

* Add `reassignedById` in trpc endpoint

* Add `reassignedById` param in api v2 endpoint

* Migration to add `RR_REASSIGNED`
2025-03-26 10:39:16 -03:00
GitHub Actions e215786afb chore: release v5.1.9
Post release / release (push) Canceled after 0s
2025-03-26 11:49:54 +00:00
Eunjae Lee f699fd1732 fix: add UID filter to /insights/routing (#20356) 2025-03-26 09:52:40 +00:00
Hariom Balhara a9ddea9184 fix crash in vercel domain creation flow (#20281) 2025-03-26 13:05:55 +05:30
Gaurav VermaandTushar Bhatt d4edd30a07 fix: layout of dialog (edit location) on bookings page (#20268)
* refactor: streamline DialogFooter structure in EditLocationDialog component

* refactor: enhance form handling and structure in EditLocationDialog component

* refactor: update icon styling and layout in EditLocationDialog component

* revert: size to h,w

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-26 09:35:05 +05:30
Calcom BotandLingo.dev f98a3c9b69 feat: update translations via @lingodotdev (#20367)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-26 02:34:39 +00:00
Benny Joo dcc079773b fix: type error in payment page (#20366) 2025-03-26 01:48:27 +00:00
Omar López 2a2e2871a8 ci: add QA status badge (#20363) 2025-03-25 21:28:42 +00:00
GitHub Actions 0befc6431f chore: release v5.1.8
Post release / release (push) Canceled after 0s
2025-03-25 21:10:18 +00:00
Benny Joo 35c363f75c fix: better translations for korean (#20362) 2025-03-25 19:13:49 +00:00
Benny Joo ad6d5a01f8 fix: some translations got wrong closing tags (#20360) 2025-03-25 14:59:51 -04:00
0xIvan 11ae957d97 Fix quickstart link (#20355) 2025-03-25 18:27:24 +00:00
Alex van Andel c21094ab0a fix: Buffers when combined with seated events (#20233) 2025-03-25 18:17:25 +00:00
Benny Joo 9b952fd922 chore: Set daily video testing meeting link to env var (#20359) 2025-03-25 18:17:00 +00:00
Ayush Agarwal ecd85cd15a feat: enhance onboarding steps with loading state and button component (#20350)
- Added loading state to ConnectedCalendars and ConnectedVideoStep components.
- Replaced native button elements with a custom Button component for consistency.
- Updated SetupAvailability to use async mutation methods.
- Integrated loading state into the onboarding view for smoother transitions between steps.
2025-03-25 18:13:09 +00:00
Alex van Andel 0772e1d168 fix: skip when browsing month has no available slots (#19626)
* fix: skip when browsing month has no available slots

* Fix zustand error + add wip tests

* Fix issue with using brows
ingDate too early

* Use slots instead of schedule return types to remove hard coupling to tRPC

* Some await fixes + use type definition from extended core feature

* Move useNextMonth to custom hook

* Add unit tests to cover the next month scenario

* Use changed interface, schedule -> slots,isLoading

* Return no-op until ready
2025-03-25 17:28:50 +00:00
Benny Joo d131d58b19 feat: upgrade @daily libs (#20348)
* upgrade @daily libs

* install peer dep

* add code for local testing
2025-03-25 17:27:00 +00:00
Lauris SkraucisandRajiv Sahal 8a18e167e9 fix: remove oAuth clientId from email when rescheduling platform booking (#20354)
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2025-03-25 13:18:29 +00:00
Benny Joo c7c2c03f6a chore: move ServerTrans to packages/lib (#20353)
* chore: move ServerTrans to packages/lib

* remove file not meant for this PR
2025-03-25 10:23:42 +00:00
Morgan a183b31269 refactor: atoms dependency on web through AdditionalCalendarSelector (#20352) 2025-03-25 09:09:57 +00:00
Benny Joo bec8bf2eaa chore: improve e2e test suite for i18n routing (#20349) 2025-03-25 13:55:29 +05:30
Anik Dhabal Babu 1789aef7d3 fix: organizer req email v2 issue (#20314) 2025-03-25 05:10:42 +00:00
Anik Dhabal Babu 7b9dabfa17 fix: disable guest is not updating onchange (#20340) 2025-03-25 04:51:13 +00:00
Anik Dhabal Babu 3ecc9319c7 fix: Introduce a new prop in the session to extract the profile username when inside an org (#18979)
* fix: flaky test organization.spec

* fix: use profile.username

* update

* update

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

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

* Add picklist value to Salesforce settings

* Add `getPicklistFieldValue` method

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

* Added webapp

* Update layout.tsx

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

* add back localeDetection

* rewrite /en/* paths to /

* add e2e test

* add comment

* add test

---------

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

* styling tweaks (#20215)

* minor-changes

---------

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

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

* Moved test file

* push fix for import path in tests

* fix trpc response url that we wait for

---------

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

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

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

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

* add padding

* padding

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

* update layoutHOC

* wip

* remove app router related code no longer used

* remove more

* await cookies, headers, params, serachparams

* update yarn.lock

* await cookies, headers, params, serachparams more

* update yarn lock again

* downgrade next-auth to 4.22.1

* update yarn lock

* fix

* update yarn lock

* fix type checks

* update yarn lock

* await headers and cookies

* restore pages folder

* restore yarn.lock

* update yarn.lock

* await headers and cookies

* remove

* await params in API routes

* updates

* restore next.config.js

* remove i18n from next.config.js

* Fixed tests

* Fixed types

* Removed duplicate favicon.ico

* Fixing more types

* ImageResponse moved to next/og

* Fixed prisma import issues

* dynamic import for @ewsjs/xhr

* remove deasync dep

* dynamic import for @tryvital/vital-node

* fix type checks

* add back turbopack command

* Type fix

* Removed unneeded file

* fix turbopack relative path errors

* add comments

* remove unneeded code

* Fixed build errors

* await apis

* use Promise<Params> type in defaultResponderForAppDir util

* refactor scim api route

* fix type checks

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

* wip

* refactor routing forms components

* revert unneeded changes for easier review

* fix

* fix

* use CustomTrans

* fix type

* fix unit tests

* fix type error

* fix build error

* fix build error

* fix build error

* fix warnings

* fix warnings

* upgrade @tremor/react and tailwindcss

* numCols -> numItems

* fix forgot-password e2e test

* fix 1 more e2e test

* fix login e2e test

* fix e2e tests

* fix e2e tests

* clean up

* remove error

* use tremor/react 2.11.0

* fix

* rename CustomTrans to ServerTrans

* address comment

* fix test

* fix ServerTrans

* fix

* fix type

* fix ServerTrans usages 1

* fix translations

* fix type checks

* fix type checks

* link styling

* fix

---------

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

* update error messages

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

* fix: types

* fix: types

* fix: types

---------

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

* fix merge conflicts

* update typings

* add props values to test

* fixup: PR feedback

* PR feedback

* fix wrong prop name

* fix merge conflicts

* fixup

* update docs

* update docs examples

* fix props naming

* fix timezone component logic and typo

* extract typings of timeZones

* update typings for Timezone

* update docs

* resolve merge conflicts

* fix typings

---------

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

* minor tweak in ooo tests

* chore

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

* fix test

---------

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

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

* Button and ButtonGroup

* UserAvatar

* AvatarGroup

* Avatar

* WizardLayout

* Dialogs

* EmptyScreen

* showToast and TextField

* Editor

* Skeleton

* Skeleton

* TopBanner and showToast

* Button again

* more

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

* more

* Fixing types

* Icon

* Fixed casing

* dropdown

* more

* Select

* more

* Badge

* List

* more

* Divider

* more

* fix

* fix type check

* refactor

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix type check

* fix

* fix

* fix

* fix

* more

* more

* more

* more

* add index file to components/command

* fix

* fix

* fix

* fix imports

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix build errors

* fix build errors

* fix

---------

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

* chore: submit auto generated docs

* fix: slots event type metadata parsing

* fix: booking responses without email with attendee phone

* refactor

* refactor: test

* remove unused import

* refactor

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

* Added animate spin class

---------

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

* feat: confirmStep.confirmButton & backButton booker styles

* refactor: rename submitButtonDisabled to confirmButtonDisabled

* feat: implement this feature for skip confirm step

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

* fix: module imports

---------

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

* fixes trpc session cirlce dep

* fix relative imports

* fix more imports to use types and not trpc

* fix exports

* Fixed types

---------

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

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

* Fixing types

* Fixed casing

* fix type check

* refactor

* fix

* fix

* fix

* fix

* fix

* fix

---------

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

* Add log when no contacts are created

* Replace console.log with logger

* Add logging around `buildRecordUpdatePayload`

* Add details if field isn't being caught

* Revert change

* Type fixes

---------

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

* update

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

* Improvements

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

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

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

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

* Support for skip confirmation form flow

* feat: Enhance slot availability and reservation mechanism

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

* refactor: Improve slot availability quick check mechanism

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

* Add tests booker

* Support minimu booking check too

* change data-testid

* Rneame

* Fix race condition with uid cookie not set

* fix tests

* Remove unsded variable

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

* Fix the unavailability bug when only seconds differ

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

* Fi ts and unit tests

---------

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

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

* feat: toggle if email field is hidden

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

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

* chore: ensure users cant have phone only event type

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

* chore: bump libraries

* docs

* refactor: confirm failing test response message

* fix: unit test

* chore: default organizer booking location docs

---------

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

* Add tests

* Type fix

* Type fixes

* Type fix

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

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

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

* Fixed trpc client calls for new routes

* Moved bookingUnconfirmedCount

* Moved getUserTopBanners to me router

* Moved shouldVerifyEmail to me router

* Moved i18n from public to its own router

* fix ssrInit

* fix ssrInit usage

* fix type check

* better naming

* fix

* fix

* Fix types

* Removed used of importHandler

* perf: Move apps procedures off of loggedInViewer

* Moved 2 more apps-related procedures

---------

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

* refactor: centralize useSchedules hook with isInit

* refactor: useConnectedCalendars enabled if isInit

* chore: some type fixes

* chore: type fix

* fix: import paths

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

* Fixed trpc client calls for new routes

* Moved bookingUnconfirmedCount

* Moved getUserTopBanners to me router

* Moved shouldVerifyEmail to me router

* Moved i18n from public to its own router

* fix ssrInit

* fix ssrInit usage

* fix type check

* better naming

* fix

* fix

* Fix types

* Removed used of importHandler

---------

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

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

* enhancements

---------

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

* Got it working just for server

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

* save tracking to booking

* return undefined if no utm params

* add test for handleNewBooking

* fix type error in test

* refactor code

* code clean up

* code clean up

* add missing import

* fix type import

* add utm params to insights (#20063)

* add utm params to insights

* remove duplicates

* remove duplicate columns

---------

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

---------

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

* feat: afterCreatedAt and beforeCreatedAt v2 bookings filters

* wip

* finish

* chore: bump libraries

* fix: tests

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

* ignore node_modules in platform tailwind preset

---------

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

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

* update style

* fix type error

* use relative import

---------

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

* Make translations optional

* Fix mocking implementation of Button (never worked)

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

* remove console

* Update booking.tsx

* fix: type check failing

* undo changes

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

* add `onBookerStateChange` docs

* Update store.ts

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

* minor change

---------

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

* fix: type error

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

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

* fix: only https urls

---------

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

* fix: get orginial scheduled booking

* fix: create logger

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

* rename params

* fix type error

---------

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

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

* migrate about/new pages

* pages refactor + use persistant zustand store

* invited members view + confirm button

* add check to org slug

* check reserved subdomain

* remove quotes from subdomain env var

* intro to creation + billing

* create price

* fix types

* createWithPaymentIntent + permission check on teams

* open stripe link in popup

* intro to tests

* organization price and seat override tests

* move permissions to a new permission service

* update env and permission check

* intro to paid invoice trigger

* dont use subId

* wip

* Get e2e working with migration of teams and members

* fix ts errors

* Get flow working again

* Fix various issues and refactor

* Some more fixes

* Fix wrong page route link

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

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

* Fix tests, found a bug

* Get custom price flow working

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

* Fix unit test

* Admin onboarding handover

* fix admin onboarding tests

* fix ts error

* Get updateQuantity working

* More fixes

* fix test

* fix schema name

* Add tests

* Add missing file

* More tests

* Add more tests

* fix mt-2 moving down input into overflow

* fix handover layout removing HOC

* Update PR_TODO.md

* fix ts error due to merge conflict

---------

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

* fix type error

* test update

---------

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

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

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

* nit

* fixed tests, admin chk reqd for forMembers only

* check reqd in case of teamOOO only

* datefilter with default and improved search filter

* fix preset options

* fix ooo tests, due to default date filter

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

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

* added test for non-admin team ooo

* show disabled icons instead of notshowing

* tests for dateRangeFilters

* some more daterangefilter tests

* nit

* include only fetch prev records in this pr

* nit

* chore

* chore

* review comments

* chore

* update to fix tests

* nit

* nit

* update pagination mode

* removed unrelated changes

* remove unrelated changes

---------

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

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

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

* regenerate docs

* remove describe only

* update docs

* tests

* tests

* remove as any

* fix tests

* log

* remove unused dependency

* fix: bookings module dependency

* fix test

* fix: allow all integrations as booking location

* refactor: add organizersDefaultApp

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

* fix test TTL time

---------

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

* use from new file

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

* fix parseRequestData

* add logging

* add fallback

* use ??

* updates

* use parseUrlFormData in saml/callback

* update tests

* fix

* remove log

* update

* any -> unknown

* use HttpErrors

* addressed

* unknown -> any

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

* fix

* fix unit test

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

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

* cleanup: remove functions not being used by schedules

* fix tsconfig ]

* bump platform libs

* bump platform libs

---------

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

* fix: new bookings controller

* fix: BookerPlatformWrapper pass rescheduledBy to BookerComponent

* rescheduleBy email

* fix test

* remove log

* centralize getOAuthUserEmail, examples app rescheduledBy

* revert

* finalize

* fix: old bookings rescheduling

* unify email validation

* update input description

* chore: move booker platform wrapper types

---------

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

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

* fix type error

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

* refactor: Team metadata

* refactor: Managed orgs metadata

* refactor: Team metadata

* test: organizations teams create team with metadata

---------

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

* docs

* fix: offset query param not showing up in docs

* finish merge main

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

* Tackle other places that use parseBookingLimit/parseDurationLimit

* More type fixups that were hidden by previous fails

* Fixed up booking-limits file

* Remove server-only

## What does this PR do?

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

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

## Visual Demo (For contributors especially)

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

#### Video Demo (if applicable):

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

#### Image Demo (if applicable):

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

## Mandatory Tasks (DO NOT REMOVE)

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

## How should this be tested?

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

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

## Checklist

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

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

* Add unit tests and e2e

* fix ts errors

* Add comment

* bump platform libs

* fix: yarn-lock

* fix yarn.lock

* doc update

* Add isString

---------

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

* fix parseRequestData

* add logging

* add fallback

* use ??

* updates

* use parseUrlFormData in saml/callback

* update tests

* fix

* remove log

* update

* any -> unknown

* use HttpErrors

* addressed

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

* update hooks page

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

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

* fix: add missing configs

* add sendSignupToOrganizationEmail to barrel email file

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

* fix type error

* i18n for pagination

* add paginationMode

* apply the change to PlatformManagedUsersTable

* replace useInfiniteQuery with useQuery

* fix type error

* fix type error

* fix type error

* fix type error

* update comment

* remove optimistic update

* minor changes

* update usage on nuqs

---------

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

* extracted DWD to app-store barrel file

* removed getCalendar as not used in api/v2

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

* fix: type error

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

* refactor: add load audio

* chore: remove

* fix: type error

* fix: notification

* fix: prevent multiple initialization
=

* feat: create notification page

* chore: update the mp3 file
2025-03-11 11:49:51 +05:30
Tushar Bhatt 4daad2ac08 fix/view (#19928) 2025-03-11 03:04:20 +00:00
Alex van Andel 9d28f5574d fix: Remove hardcoded Node version from engines 2025-03-10 23:35:00 +00:00
2115 changed files with 66677 additions and 20100 deletions
+30 -11
View File
@@ -17,6 +17,7 @@
#
# To enable enterprise-only features please add your environment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
CALCOM_LICENSE_KEY=
# Signature token for the Cal.com License API (used for self-hosted integrations)
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
@@ -116,6 +117,11 @@ NEXT_PUBLIC_PLAIN_CHAT_ID=
PLAIN_CHAT_HMAC_SECRET_KEY=
NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS=
# Dub Config
DUB_API_KEY=
NEXT_PUBLIC_DUB_PROGRAM_ID=
# Zendesk Config
NEXT_PUBLIC_ZENDESK_KEY=
@@ -192,7 +198,10 @@ NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
STRIPE_TEAM_MONTHLY_PRICE_ID=
STRIPE_TEAM_PRODUCT_ID=
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
STRIPE_ORG_MONTHLY_PRICE_ID=
STRIPE_ORG_PRODUCT_ID=
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET_APPS=
STRIPE_PRIVATE_KEY=
@@ -285,7 +294,7 @@ NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL=
# use organizations
ORGANIZATIONS_ENABLED=
NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS=30
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE=3700 # $37.00 per seat
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW=37 # $37.00 per seat
# This variable should only be set to 1 or true if you want to autolink external provider sign-ups with
# existing organizations based on email domain address
@@ -353,13 +362,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
# ***********************************************************************************************************
# provide a value between 0 and 100 to ensure the percentage of traffic
# redirected from the legacy to the future pages
AB_TEST_BUCKET_PROBABILITY=50
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
APP_ROUTER_APPS_ENABLED=0
APP_ROUTER_TEAM_ENABLED=0
# disable setry server source maps
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
@@ -402,8 +404,17 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL=
# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal]
NEXT_PUBLIC_LOGGER_LEVEL=
# Used to use Replexica SDK, a tool for real-time AI-powered localization
REPLEXICA_API_KEY=
# For storing Daily Video recordings on S3 Bucket
CAL_VIDEO_BUCKET_NAME=
CAL_VIDEO_BUCKET_REGION=
CAL_VIDEO_ASSUME_ROLE_ARN=
# For local testing of Daily Video
# Set this to the link of a working Daily Video meeting link
CAL_VIDEO_MEETING_LINK_FOR_TESTING=
# Used to use lingo.dev SDK, a tool for real-time AI-powered localization
LINGO_DOT_DEV_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=
@@ -413,6 +424,8 @@ DIRECTORY_IDS_TO_LOG=
# Read more about it in the README.md
NEXT_PUBLIC_SINGLE_ORG_SLUG=
AKISMET_API_KEY=
## Env variables related to avoiding booking failures
# Request for checking reservation would be attempted to send every these seconds if the request is stale at that time
NEXT_PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS=
@@ -423,4 +436,10 @@ NEXT_PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS=
# Used to invalidate available slots when navigating to booking form
NEXT_PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM=0
# Used to enable quick availability checks for x% of all visitors
NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT=10
NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT=10
# Deprecated. Use NEXT_PUBLIC_BODY_SCRIPTS instead. NEXT_PUBLIC_HEAD_SCRIPTS are now injected into the body only.
NEXT_PUBLIC_HEAD_SCRIPTS=
NEXT_PUBLIC_BODY_SCRIPTS=
DATABASE_CHUNK_SIZE=
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
working-directory: docs
run: |
export NODE_OPTIONS="--max_old_space_size=8192"
npm install -g mintlify
npm install -g mintlify@4.0.436
mintlify dev &
sleep 5 # Let it run for 5 seconds
kill $!
+1 -1
View File
@@ -17,7 +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 }}
LINGO_DOT_DEV_API_KEY: ${{ secrets.CI_LINGO_DOT_DEV_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 }}
+1 -1
View File
@@ -21,5 +21,5 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
with:
api-key: ${{ secrets.CI_REPLEXICA_API_KEY }}
api-key: ${{ secrets.CI_LINGO_DOT_DEV_API_KEY }}
pull-request: true
+1 -1
View File
@@ -2,7 +2,7 @@ name: Unit
on:
workflow_call:
env:
REPLEXICA_API_KEY: ${{ secrets.CI_REPLEXICA_API_KEY }}
LINGO_DOT_DEV_API_KEY: ${{ secrets.CI_LINGO_DOT_DEV_API_KEY }}
permissions:
contents: read
jobs:
+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
+6
View File
@@ -0,0 +1,6 @@
undecided:
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
View File
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+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"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@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"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
View File
View File
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+1
View File
@@ -24,6 +24,7 @@
<p align="center">
<a href="https://www.producthunt.com/products/cal-com"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
<img src="https://api.checklyhq.com/v1/badges/groups/1120718?style=flat&theme=default" alt="Checkly QA">
<a href="https://status.cal.com"><img height="20px" src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
<a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
+14 -14
View File
@@ -1,4 +1,3 @@
import type { Page } from "@playwright/test";
import { test, expect } from "@playwright/test";
test.describe("Org", () => {
@@ -8,14 +7,16 @@ test.describe("Org", () => {
const response = await page.goto("https://i.cal.com/embed");
expect(response?.status()).toBe(200);
await page.screenshot({ path: "screenshot.jpg" });
await expectPageToBeRenderedWithEmbedSsr(page);
const body = await response?.text();
await expectPageToBeRenderedWithEmbedSsr(body);
});
test("Org User(Rick) Page should be embeddable", async ({ page }) => {
const response = await page.goto("https://i.cal.com/team-rick/embed");
expect(response?.status()).toBe(200);
await expect(page.locator("text=Used by Checkly")).toBeVisible();
await expectPageToBeRenderedWithEmbedSsr(page);
const body = await response?.text();
await expectPageToBeRenderedWithEmbedSsr(body);
});
test("Org User Event(/team-rick/test-event) Page should be embeddable", async ({ page }) => {
@@ -23,14 +24,16 @@ test.describe("Org", () => {
expect(response?.status()).toBe(200);
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
await expectPageToBeRenderedWithEmbedSsr(page);
const body = await response?.text();
await expectPageToBeRenderedWithEmbedSsr(body);
});
test("Org Team Profile(/sales) page should be embeddable", async ({ page }) => {
const response = await page.goto("https://i.cal.com/sales/embed");
expect(response?.status()).toBe(200);
await expect(page.locator("text=Cal.com Sales")).toBeVisible();
await expectPageToBeRenderedWithEmbedSsr(page);
const body = await response?.text();
await expectPageToBeRenderedWithEmbedSsr(body);
});
test("Org Team Event page(/sales/hippa) should be embeddable", async ({ page }) => {
@@ -38,7 +41,8 @@ test.describe("Org", () => {
expect(response?.status()).toBe(200);
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
await expectPageToBeRenderedWithEmbedSsr(page);
const body = await response?.text();
await expectPageToBeRenderedWithEmbedSsr(body);
});
});
@@ -68,7 +72,8 @@ test.describe("Org", () => {
test("Organization Homepage - Has Engineering and Marketing Teams", async ({ page }) => {
const response = await page.goto("https://i.cal.com");
expect(response?.status()).toBe(200);
await expect(page.locator("text=Cal.com")).toBeVisible();
// Somehow there are two Cal.com text momentarily, but shouldn't be the concern of this check
await expect(page.locator("text=Cal.com").first()).toBeVisible();
await expect(page.locator("text=Engineering")).toBeVisible();
await expect(page.locator("text=Marketing")).toBeVisible();
});
@@ -95,11 +100,6 @@ test.describe("Org", () => {
});
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
async function expectPageToBeRenderedWithEmbedSsr(page: Page) {
expect(
await page.evaluate(() => {
//@ts-expect-error - __NEXT_DATA__ is a global variable defined by Next.js
return window.__NEXT_DATA__.props.pageProps.isEmbed;
})
).toBe(true);
async function expectPageToBeRenderedWithEmbedSsr(responseText: string | undefined) {
expect(responseText).toContain('\\"isEmbed\\":true');
}
+8
View File
@@ -0,0 +1,8 @@
import { setUser as SentrySetUser } from "@sentry/nextjs";
import type { NextMiddleware } from "next-api-middleware";
export const captureUserId: NextMiddleware = async (req, res, next) => {
if (req.userId) SentrySetUser({ id: req.userId });
await next();
};
+7 -4
View File
@@ -2,15 +2,16 @@ import { label } from "next-api-middleware";
import { addRequestId } from "./addRequestid";
import { captureErrors } from "./captureErrors";
import { captureUserId } from "./captureUserId";
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
import { extendRequest } from "./extendRequest";
import {
HTTP_POST,
HTTP_DELETE,
HTTP_PATCH,
HTTP_GET,
HTTP_GET_OR_POST,
HTTP_GET_DELETE_PATCH,
HTTP_GET_OR_POST,
HTTP_PATCH,
HTTP_POST,
} from "./httpMethods";
import { rateLimitApiKey } from "./rateLimitApiKey";
import { verifyApiKey } from "./verifyApiKey";
@@ -31,6 +32,7 @@ const middleware = {
extendRequest,
pagination: withPagination,
captureErrors,
captureUserId,
verifyCredentialSyncEnabled,
};
@@ -44,8 +46,9 @@ const middlewareOrder = [
"verifyApiKey",
"rateLimitApiKey",
"addRequestId",
"captureUserId",
] as Middleware[]; // <-- Provide a list of middleware to call automatically
const withMiddleware = label(middleware, middlewareOrder);
export { withMiddleware, middleware, middlewareOrder };
export { middleware, middlewareOrder, withMiddleware };
+2 -1
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import { MAX_SEATS_PER_TIME_SLOT } from "@calcom/lib/constants";
import slugify from "@calcom/lib/slugify";
import { _EventTypeModel as EventType, _HostModel } from "@calcom/prisma/zod";
import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
@@ -103,7 +104,7 @@ const schemaEventTypeEditParams = z
.transform((s) => slugify(s))
.optional(),
length: z.number().int().optional(),
seatsPerTimeSlot: z.number().optional(),
seatsPerTimeSlot: z.number().min(1).max(MAX_SEATS_PER_TIME_SLOT).nullable().optional(),
seatsShowAttendees: z.boolean().optional(),
seatsShowAvailabilityCount: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(),
+1 -1
View File
@@ -33,7 +33,7 @@
"@sentry/nextjs": "^8.8.0",
"bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0",
"next": "^13.5.6",
"next": "^13.5.9",
"next-api-middleware": "^1.0.1",
"next-axiom": "^0.17.0",
"next-swagger-doc": "^0.3.6",
@@ -79,9 +79,10 @@ async function handler(req: NextApiRequest) {
allRemainingBookings: req.query.allRemainingBookings === "true",
});
// Normalizing for universal handler
req.body = { id, allRemainingBookings, cancellationReason, cancelledBy };
return await handleCancelBooking(req);
return await handleCancelBooking({
bookingData: { id, allRemainingBookings, cancellationReason, cancelledBy },
userId: req.userId,
});
}
export default defaultResponder(handler);
+14 -4
View File
@@ -213,12 +213,14 @@ import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
* description: Authorization information is missing or invalid.
*/
async function handler(req: NextApiRequest) {
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
const { isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
let userId = req.userId;
req.body = {
...req.body,
creationSource: CreationSource.API_V1,
};
if (isSystemWideAdmin) req.userId = req.body.userId || userId;
if (isSystemWideAdmin) userId = req.body.userId || userId;
if (isOrganizationOwnerOrAdmin) {
const accessibleUsersIds = await getAccessibleUsers({
@@ -226,11 +228,19 @@ async function handler(req: NextApiRequest) {
memberUserIds: [req.body.userId || userId],
});
const [requestedUserId] = accessibleUsersIds;
req.userId = requestedUserId || userId;
userId = requestedUserId || userId;
}
try {
return await handleNewBooking(req, getBookingDataSchemaForApi);
return await handleNewBooking(
{
bookingData: req.body,
userId,
hostname: req.headers.host || "",
forcedSlug: req.headers["x-cal-force-slug"] as string | undefined,
},
getBookingDataSchemaForApi
);
} catch (error: unknown) {
const knownError = error as Error;
if (knownError?.message === ErrorCode.NoAvailableUsersFound) {
+1 -1
View File
@@ -1,8 +1,8 @@
import type { NextApiRequest } from "next";
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
import stripe from "@calcom/app-store/stripepayment/lib/server";
import { getDubCustomer } from "@calcom/features/auth/lib/dub";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
+6
View File
@@ -32,3 +32,9 @@ DOCS_URL=
# Axiom logging
AXIOM_DATASET=
AXIOM_TOKEN=
# DEBUG: 0
# INFO: 1
# WARN: 2
# ERROR: 3
LOGGER_BRIDGE_LOG_LEVEL="1"
+30 -1
View File
@@ -54,9 +54,29 @@ CALCOM_LICENSE_KEY=c4234812-12ab-42s6-a1e3-55bedd4a5bb
## Running the app
# Development
```bash
# development
$ yarn run start
```
OR if you don't want to use docker, you can run following command.
```bash
$ yarn dev:no-docker
```
Additionally you can run following command(in different terminal) to ensure that any change in any of the dependencies is rebuilt and detected. It watches platform-libraries, platform-constants, platform-enums, platform-utils, platform-types.
```bash
$ yarn run dev:build:watch
```
If you are making changes in packages/platform/libraries, you should run the following command too that would connect your local packages/platform/libraries to the api/v2
```bash
$ yarn local
```
# watch mode
$ yarn run start:dev
@@ -65,6 +85,9 @@ $ yarn run start:dev
$ yarn run start:prod
```
## Test
```bash
@@ -74,6 +97,12 @@ $ yarn run test
# e2e tests
$ yarn run test:e2e
# e2e tests in watch mode
$ yarn test:e2e:watch
# run specific e2e test file in watch mode
$ yarn test:e2e:watch --testPathPattern=filePath
# test coverage
$ yarn run test:cov
```
+10 -3
View File
@@ -9,8 +9,14 @@
"build": "yarn dev:build && nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch",
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build ",
"dev:build:watch": "concurrently --names \"libraries,lru-fix,constants,enums,utils,types\" \"yarn _dev:build:watch:libraries\" \"yarn _dev:build:watch:libraries:lru-fix\" \"yarn _dev:build:watch:constants\" \"yarn _dev:build:watch:enums\" \"yarn _dev:build:watch:utils\" \"yarn _dev:build:watch:types\"",
"_dev:build:watch:libraries": "yarn workspace @calcom/platform-libraries build:watch",
"_dev:build:watch:libraries:lru-fix": "yarn workspace @calcom/platform-libraries watch-lru-fix",
"_dev:build:watch:constants": "yarn workspace @calcom/platform-constants build:watch",
"_dev:build:watch:enums": "yarn workspace @calcom/platform-enums build:watch",
"_dev:build:watch:utils": "yarn workspace @calcom/platform-utils build:watch",
"_dev:build:watch:types": "yarn workspace @calcom/platform-types build:watch",
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build",
"dev": "yarn dev:build && ts-node scripts/docker-start.ts && yarn copy-swagger-module && yarn start --watch",
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
"start:debug": "nest start --debug --watch",
@@ -32,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.142",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.171",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -57,6 +63,7 @@
"bull": "^4.12.4",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"concurrently": "^9.1.2",
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
"fs-extra": "^11.2.0",
+2
View File
@@ -21,6 +21,7 @@ import {
X_CAL_PLATFORM_EMBED,
} from "@calcom/platform-constants";
import { CalendarServiceExceptionFilter } from "./filters/calendar-service-exception.filter";
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
@@ -76,6 +77,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
app.useGlobalFilters(new ZodExceptionFilter());
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalFilters(new TRPCExceptionFilter());
app.useGlobalFilters(new CalendarServiceExceptionFilter());
app.use(cookieParser());
@@ -1,15 +1,20 @@
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { BillingModule } from "@/modules/billing/billing.module";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UsersModule } from "@/modules/users/users.module";
@@ -33,6 +38,11 @@ import { Module } from "@nestjs/common";
ApiKeysRepository,
OAuthClientUsersService,
PlatformBookingsService,
CalendarsService,
CredentialsRepository,
AppsRepository,
CalendarsRepository,
SelectedCalendarsRepository,
],
controllers: [BookingsController_2024_04_15],
})
@@ -351,6 +351,64 @@ describe("Bookings Endpoints 2024-04-15", () => {
});
});
it("should create a booking with split name", async () => {
const bookingStart = "2040-05-21T11:30:00.000Z";
const bookingEnd = "2040-05-21T12:30:00.000Z";
const bookingEventTypeId = eventTypeId;
const bookingTimeZone = "Europe/London";
const bookingLanguage = "en";
const bookingHashedLink = "";
const bookingMetadata = {
timeFormat: "12",
meetingType: "organizer-phone",
};
const bookingResponses = {
name: { firstName: "John", lastName: "Doe" },
email: "tester@example.com",
location: {
value: "link",
optionValue: "",
},
notes: "test",
};
const body: CreateBookingInput_2024_04_15 = {
start: bookingStart,
end: bookingEnd,
eventTypeId: bookingEventTypeId,
timeZone: bookingTimeZone,
language: bookingLanguage,
metadata: bookingMetadata,
hashedLink: bookingHashedLink,
responses: bookingResponses,
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.userPrimaryEmail).toBeDefined();
expect(responseBody.data.userPrimaryEmail).toEqual(userEmail);
expect(responseBody.data.id).toBeDefined();
expect(responseBody.data.uid).toBeDefined();
expect(responseBody.data.startTime).toEqual(bookingStart);
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
expect(responseBody.data.metadata).toEqual(bookingMetadata);
expect(responseBody.data.responses).toEqual({
...bookingResponses,
name: `${bookingResponses.name.firstName} ${bookingResponses.name.lastName}`,
});
createdBooking = responseBody.data;
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
@@ -14,8 +14,11 @@ import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { BillingService } from "@/modules/billing/services/billing.service";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
Post,
@@ -34,7 +37,6 @@ import {
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { CreationSource } from "@prisma/client";
import { Request } from "express";
import { NextApiRequest } from "next/types";
@@ -101,7 +103,9 @@ export class BookingsController_2024_04_15 {
private readonly billingService: BillingService,
private readonly config: ConfigService,
private readonly apiKeyRepository: ApiKeysRepository,
private readonly platformBookingsService: PlatformBookingsService
private readonly platformBookingsService: PlatformBookingsService,
private readonly usersRepository: UsersRepository,
private readonly usersService: UsersService
) {}
@Get("/")
@@ -111,18 +115,19 @@ export class BookingsController_2024_04_15 {
@ApiQuery({ name: "limit", type: "number", required: false })
@ApiQuery({ name: "cursor", type: "number", required: false })
async getBookings(
@GetUser() user: User,
@GetUser() user: UserWithProfile,
@Query() queryParams: GetBookingsInput_2024_04_15
): Promise<GetBookingsOutput_2024_04_15> {
const { filters, cursor, limit } = queryParams;
const bookingListingByStatus = filters?.status ?? Status_2024_04_15["upcoming"];
const profile = this.usersService.getUserMainProfile(user);
const bookings = await getAllUserBookings({
bookingListingByStatus: [bookingListingByStatus],
skip: cursor ?? 0,
take: limit ?? 10,
filters,
ctx: {
user: { email: user.email, id: user.id },
user: { email: user.email, id: user.id, orgId: profile?.organizationId },
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
},
});
@@ -171,11 +176,19 @@ export class BookingsController_2024_04_15 {
const oAuthClientId =
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
const { orgSlug, locationUrl } = body;
req.headers["x-cal-force-slug"] = orgSlug;
try {
const booking = await handleNewBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed)
);
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed);
const booking = await handleNewBooking({
bookingData: bookingRequest.body,
userId: bookingRequest.userId,
hostname: bookingRequest.headers?.host || "",
forcedSlug: orgSlug,
platformClientId: bookingRequest.platformClientId,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
});
if (booking.userId && booking.uid && booking.startTime) {
void (await this.billingService.increaseUsageByUserId(booking.userId, {
uid: booking.uid,
@@ -211,9 +224,16 @@ export class BookingsController_2024_04_15 {
if (bookingUid) {
try {
req.body.uid = bookingUid;
const res = await handleCancelBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed)
);
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
const res = await handleCancelBooking({
bookingData: bookingRequest.body,
userId: bookingRequest.userId,
arePlatformEmailsEnabled: bookingRequest.arePlatformEmailsEnabled,
platformClientId: bookingRequest.platformClientId,
platformCancelUrl: bookingRequest.platformCancelUrl,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
});
if (!res.onlyRemovedAttendee) {
void (await this.billingService.cancelUsageByBookingUid(res.bookingUid));
}
@@ -274,9 +294,18 @@ export class BookingsController_2024_04_15 {
}
}
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
await this.createNextApiRecurringBookingRequest(req, oAuthClientId, undefined, isEmbed)
);
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
const createdBookings: BookingResponse[] = await handleNewRecurringBooking({
bookingData: bookingRequest.body,
userId: bookingRequest.userId,
hostname: bookingRequest.headers?.host || "",
platformClientId: bookingRequest.platformClientId,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
});
createdBookings.forEach(async (booking) => {
if (booking.userId && booking.uid && booking.startTime) {
@@ -343,7 +372,8 @@ export class BookingsController_2024_04_15 {
return keyData?.userId;
} else {
// Access Token
return this.oAuthFlowService.getOwnerId(bearerToken);
const ownerId = await this.oAuthFlowService.getOwnerId(bearerToken);
return ownerId;
}
}
} catch (err) {
@@ -351,6 +381,30 @@ export class BookingsController_2024_04_15 {
}
}
private async getOwnerIdRescheduledBooking(
request: Request,
platformClientId?: string
): Promise<number | undefined> {
if (
platformClientId &&
request.body.rescheduledBy &&
!request.body.rescheduledBy.includes(platformClientId)
) {
request.body.rescheduledBy = OAuthClientUsersService.getOAuthUserEmail(
platformClientId,
request.body.rescheduledBy
);
}
if (request.body.rescheduledBy) {
if (request.body.rescheduledBy !== request.body.responses.email) {
return (await this.usersRepository.findByEmail(request.body.rescheduledBy))?.id;
}
}
return undefined;
}
private async getOAuthClientIdFromEventType(eventTypeId: number): Promise<string | undefined> {
if (!eventTypeId) {
return undefined;
@@ -395,7 +449,9 @@ export class BookingsController_2024_04_15 {
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
const requestId = req.get("X-Request-Id");
const clone = { ...req };
const userId = (await this.getOwnerId(req)) ?? -1;
const userId = clone.body.rescheduleUid
? await this.getOwnerIdRescheduledBooking(req, oAuthClientId)
: await this.getOwnerId(req);
const oAuthParams = oAuthClientId
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
: DEFAULT_PLATFORM_PARAMS;
@@ -12,6 +12,7 @@ import {
CreateManagedUserData,
CreateManagedUserOutput,
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
@@ -140,7 +141,9 @@ describe("Managed user bookings 2024-04-15", () => {
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
@@ -193,7 +196,9 @@ describe("Managed user bookings 2024-04-15", () => {
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
@@ -220,7 +225,9 @@ describe("Managed user bookings 2024-04-15", () => {
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
@@ -444,11 +451,6 @@ describe("Managed user bookings 2024-04-15", () => {
});
});
function getOAuthUserEmail(oAuthClientId: string, userEmail: string) {
const [username, emailDomain] = userEmail.split("@");
return `${username}+${oAuthClientId}@${emailDomain}`;
}
afterAll(async () => {
await userRepositoryFixture.delete(firstManagedUser.user.id);
await userRepositoryFixture.delete(secondManagedUser.user.id);
@@ -12,6 +12,41 @@ import {
isEmail,
Validate,
} from "class-validator";
import { ValidationOptions, registerDecorator } from "class-validator";
import { RESCHEDULED_BY_DOCS } from "@calcom/platform-types";
type BookingName = { firstName: string; lastName: string };
function ValidateBookingName(validationOptions?: ValidationOptions) {
return function (target: object, propertyName: string) {
registerDecorator({
name: "validateBookingName",
target: target.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any) {
if (typeof value === "string") {
return value.trim().length > 0;
}
if (typeof value === "object" && value !== null) {
return (
typeof value.firstName === "string" &&
typeof value.lastName === "string" &&
value.firstName.trim().length > 0 &&
value.lastName.trim().length > 0
);
}
return false;
},
defaultMessage(): string {
return "Name must be either a non-empty string or an object with non-empty firstName and lastName";
},
},
});
};
}
class Location {
@IsString()
@@ -24,9 +59,14 @@ class Location {
}
class Response {
@IsString()
@ApiProperty()
name!: string;
@ApiProperty({
oneOf: [
{ type: "string" },
{ type: "object", properties: { firstName: { type: "string" }, lastName: { type: "string" } } },
],
})
@ValidateBookingName()
name!: string | BookingName;
@Validate((value: string) => !value || isEmail(value), {
message: "Invalid response email",
@@ -76,6 +116,13 @@ export class CreateBookingInput_2024_04_15 {
@ApiPropertyOptional()
rescheduleUid?: string;
@IsOptional()
@ApiPropertyOptional({ description: RESCHEDULED_BY_DOCS })
@Validate((value: string) => !value || isEmail(value), {
message: "Invalid rescheduledBy email format",
})
rescheduledBy?: string;
@IsTimeZone()
@ApiProperty()
timeZone!: string;
@@ -2,7 +2,7 @@ import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/c
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsBoolean, IsString, IsNumber, IsOptional } from "class-validator";
import type { AppsStatus } from "@calcom/platform-libraries";
import type { AppsStatus } from "@calcom/platform-libraries/app-store";
export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2024_04_15 {
@IsBoolean()
@@ -4,19 +4,24 @@ import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bo
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { BillingModule } from "@/modules/billing/billing.module";
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UsersModule } from "@/modules/users/users.module";
@@ -47,6 +52,11 @@ import { Module } from "@nestjs/common";
BookingSeatRepository,
ApiKeysRepository,
PlatformBookingsService,
CalendarsService,
CredentialsRepository,
AppsRepository,
CalendarsRepository,
SelectedCalendarsRepository,
],
controllers: [BookingsController_2024_08_13],
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
@@ -1,6 +1,7 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import type { Prisma } from "@prisma/client";
@Injectable()
export class BookingsRepository_2024_08_13 {
@@ -98,7 +99,7 @@ export class BookingsRepository_2024_08_13 {
}
async getByUidWithAttendeesAndUserAndEvent(uid: string) {
return this.dbRead.prisma.booking.findUnique({
const booking = await this.dbRead.prisma.booking.findUnique({
where: {
uid,
},
@@ -108,6 +109,15 @@ export class BookingsRepository_2024_08_13 {
eventType: true,
},
});
if (!booking) {
return null;
}
return {
...booking,
responses: booking.responses as Prisma.JsonObject,
metadata: booking.metadata as Prisma.JsonObject | null,
};
}
async getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid: string) {
@@ -1,4 +1,5 @@
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
import { CalendarLinksOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
@@ -6,10 +7,12 @@ import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outpu
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
@@ -32,17 +35,20 @@ import {
ApiBody,
ApiExtraModels,
} from "@nestjs/swagger";
import { User } from "@prisma/client";
import { Request } from "express";
import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
CancelBookingInput,
CancelBookingInput_2024_08_13,
CancelBookingInputPipe,
CancelSeatedBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
RescheduleBookingInput,
RescheduleBookingInput_2024_08_13,
RescheduleBookingInputPipe,
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import {
CreateBookingInputPipe,
@@ -64,14 +70,20 @@ import {
@DocsTags("Bookings")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-08-13\``,
description: `Must be set to ${VERSION_2024_08_13}`,
example: VERSION_2024_08_13,
required: true,
schema: {
default: VERSION_2024_08_13,
},
})
export class BookingsController_2024_08_13 {
private readonly logger = new Logger("BookingsController_2024_08_13");
constructor(private readonly bookingsService: BookingsService_2024_08_13) {}
constructor(
private readonly bookingsService: BookingsService_2024_08_13,
private readonly usersService: UsersService
) {}
@Post("/")
@ApiOperation({
@@ -87,6 +99,10 @@ export class BookingsController_2024_08_13 {
For team event types it is possible to create instant meeting. To do that just pass \`"instant": true\` to the request body.
The start needs to be in UTC aka if the timezone is GMT+2 in Rome and meeting should start at 11, then UTC time should have hours 09:00 aka without time zone.
Finally, there are 2 ways to book an event type:
1. Provide \`eventTypeId\` in the request body.
2. Provide \`eventTypeSlug\` and \`username\` and optionally \`organizationSlug\` if the user with the username is within an organization.
`,
})
@ApiBody({
@@ -98,7 +114,7 @@ export class BookingsController_2024_08_13 {
],
},
description:
"Accepts different types of booking input: CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, or CreateRecurringBookingInput_2024_08_13",
"Accepts different types of booking input: Create Booking (Option 1), Create Instant Booking (Option 2), or Create Recurring Booking (Option 3)",
})
@ApiExtraModels(
CreateBookingInput_2024_08_13,
@@ -147,19 +163,20 @@ export class BookingsController_2024_08_13 {
@Get("/")
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Permissions([BOOKING_READ])
@ApiOperation({ summary: "Get all bookings" })
async getBookings(
@Query() queryParams: GetBookingsInput_2024_08_13,
@GetUser() user: User
@GetUser() user: UserWithProfile
): Promise<GetBookingsOutput_2024_08_13> {
const bookings = await this.bookingsService.getBookings(queryParams, user);
const profile = this.usersService.getUserMainProfile(user);
const bookings = await this.bookingsService.getBookings(queryParams, {
email: user.email,
id: user.id,
orgId: profile?.organizationId,
});
return {
status: SUCCESS_STATUS,
@@ -171,9 +188,19 @@ export class BookingsController_2024_08_13 {
@UseGuards(BookingUidGuard)
@ApiOperation({
summary: "Reschedule a booking",
description:
"Reschedule a booking by passing `:bookingUid` of the booking that should be rescheduled and pass request body with a new start time to create a new booking.",
description: "Reschedule a booking or seated booking",
})
@ApiBody({
schema: {
oneOf: [
{ $ref: getSchemaPath(RescheduleBookingInput_2024_08_13) },
{ $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) },
],
},
description:
"Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2)",
})
@ApiExtraModels(RescheduleBookingInput_2024_08_13, RescheduleSeatedBookingInput_2024_08_13)
async rescheduleBooking(
@Param("bookingUid") bookingUid: string,
@Body(new RescheduleBookingInputPipe())
@@ -198,6 +225,17 @@ export class BookingsController_2024_08_13 {
For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
})
@ApiBody({
schema: {
oneOf: [
{ $ref: getSchemaPath(CancelBookingInput_2024_08_13) },
{ $ref: getSchemaPath(CancelSeatedBookingInput_2024_08_13) },
],
},
description:
"Accepts different types of cancel booking input: Cancel Booking (Option 1) or Cancel Seated Booking (Option 2)",
})
@ApiExtraModels(CancelBookingInput_2024_08_13, CancelSeatedBookingInput_2024_08_13)
async cancelBooking(
@Req() request: Request,
@Param("bookingUid") bookingUid: string,
@@ -216,13 +254,10 @@ export class BookingsController_2024_08_13 {
@HttpCode(HttpStatus.OK)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Mark a booking absence - provided authorization header refers to owner of the booking.",
})
@ApiOperation({ summary: "Mark a booking absence" })
async markNoShow(
@Param("bookingUid") bookingUid: string,
@Body() body: MarkAbsentBookingInput_2024_08_13,
@@ -240,13 +275,11 @@ export class BookingsController_2024_08_13 {
@HttpCode(HttpStatus.OK)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary:
"Automatically reassign booking to a new host automatically - provided authorization header refers to person who reassigned the booking.",
})
@ApiOperation({ summary: "Automatically reassign booking to a new host" })
async reassignBooking(
@Param("bookingUid") bookingUid: string,
@GetUser() user: UserWithProfile
@@ -263,13 +296,11 @@ export class BookingsController_2024_08_13 {
@HttpCode(HttpStatus.OK)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary:
"Reassign a booking to a specific user specified by the :userId - provided authorization header refers to person who reassigned the booking.",
})
@ApiOperation({ summary: "Reassign a booking to a specific user" })
async reassignBookingToUser(
@Param("bookingUid") bookingUid: string,
@Param("userId") userId: number,
@@ -293,13 +324,11 @@ export class BookingsController_2024_08_13 {
@HttpCode(HttpStatus.OK)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary:
"Confirm booking that requires a confirmation - provided authorization header refers to owner of the booking.",
})
@ApiOperation({ summary: "Confirm booking that requires a confirmation" })
async confirmBooking(
@Param("bookingUid") bookingUid: string,
@GetUser() user: UserWithProfile
@@ -316,13 +345,11 @@ export class BookingsController_2024_08_13 {
@HttpCode(HttpStatus.OK)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary:
"Decline booking that requires a confirmation - provided authorization header refers to owner of the booking.",
})
@ApiOperation({ summary: "Decline booking that requires a confirmation" })
async declineBooking(
@Param("bookingUid") bookingUid: string,
@Body() body: DeclineBookingInput_2024_08_13,
@@ -335,4 +362,23 @@ export class BookingsController_2024_08_13 {
data: booking,
};
}
@Get("/:bookingUid/calendar-links")
@UseGuards(ApiAuthGuard, BookingUidGuard)
@Permissions([BOOKING_READ])
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Get 'Add to Calendar' links for a booking",
description:
"Retrieve calendar links for a booking that can be used to add the event to various calendar services. Returns links for Google Calendar, Microsoft Office, Microsoft Outlook, and a downloadable ICS file.",
})
@HttpCode(HttpStatus.OK)
async getCalendarLinks(@Param("bookingUid") bookingUid: string): Promise<CalendarLinksOutput_2024_08_13> {
const calendarLinks = await this.bookingsService.getCalendarLinks(bookingUid);
return {
status: SUCCESS_STATUS,
data: calendarLinks,
};
}
}
@@ -30,7 +30,7 @@ import {
OrganizerRescheduledEmail,
AttendeeCancelledEmail,
OrganizerCancelledEmail,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/emails";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
@@ -434,7 +434,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
}
describe("get individual booking", () => {
it("should should get a seated booking with split name responses", async () => {
it("should get a seated booking with split name responses", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${bookingWithSplitName.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -457,7 +457,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get a seated booking with split name responses", async () => {
it("should get a seated booking with split name responses", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${seatedBookingWithSplitName.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -0,0 +1,185 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import * as request from "supertest";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import { Booking, PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Calendar Links", () => {
let app: INestApplication;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
const userEmail = `calendar-links-2024-08-13-user-${randomString()}@api.com`;
let user: User;
let booking: Booking;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
)
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({
name: `confirm-bookings-2024-08-13-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: `calendar-links-2024-08-13-${randomString()}-schedule`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{
title: `calendar-links-2024-08-13-${randomString()}-event-type`,
slug: `calendar-links-2024-08-13-${randomString()}-event-type-slug`,
length: 60,
},
user.id
);
booking = await bookingsRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2020, 0, 8, 12, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
uid: `calendar-links-2024-08-13-booking-${randomString()}`,
title: "Test Booking",
eventType: {
connect: {
id: event.id,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
attendees: {
create: {
email: "johndoe@example.com",
name: "John Doe",
locale: "en",
timeZone: "America/New_York",
},
},
responses: {
create: {
name: "John Doe",
email: "johndoe@example.com",
},
},
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it("should get calendar links for a booking", async () => {
const response = await request(app.getHttpServer())
.get(`/v2/bookings/${booking.uid}/calendar-links`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200);
expect(response.body.status).toEqual(SUCCESS_STATUS);
expect(response.body.data).toBeDefined();
const googleCalendarLink = response.body.data.find(
(item: { id: string }) => item.id === "googleCalendar"
).link;
const microsoftOfficeLink = response.body.data.find(
(item: { id: string }) => item.id === "microsoftOffice"
).link;
const microsoftOutlookLink = response.body.data.find(
(item: { id: string }) => item.id === "microsoftOutlook"
).link;
const icsLink = response.body.data.find((item: { id: string }) => item.id === "ics").link;
expect(googleCalendarLink).toMatch(/^https:\/\/calendar\.google\.com\//);
expect(microsoftOfficeLink).toMatch(/^https:\/\/outlook\.office\.com\//);
expect(microsoftOutlookLink).toMatch(/^https:\/\/outlook\.live\.com\//);
expect(icsLink).toMatch(/^data:text\/calendar,/);
});
it("should return 404 for non-existent booking", async () => {
await request(app.getHttpServer())
.get(`/v2/bookings/non-existent-uid/calendar-links`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(404);
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
await app.close();
});
});
});
@@ -34,7 +34,7 @@ import {
AttendeeRequestEmail,
OrganizerRequestEmail,
AttendeeDeclinedEmail,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/emails";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
@@ -94,6 +94,8 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
let emailsDisabledSetup: EmailSetup;
const authEmail = `confirm-emails-2024-08-13-admin-${randomString()}@api.com`;
let userEmailsEnabled = "";
const userEmailsDisabled = `confirm-emails-2024-08-13-user-${randomString()}@api.com`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -141,8 +143,12 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
async function setupEnabledEmails() {
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
userEmailsEnabled = `confirm-emails-2024-08-13-user-${randomString()}+${
oAuthClientEmailsEnabled.id
}@api.com`;
const user = await userRepositoryFixture.create({
email: `confirm-emails-2024-08-13-user-${randomString()}@api.com`,
email: userEmailsEnabled,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
@@ -179,7 +185,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
const user = await userRepositoryFixture.create({
email: `confirm-emails-2024-08-13-user-${randomString()}@api.com`,
email: userEmailsDisabled,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
@@ -282,9 +288,6 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
@@ -338,10 +341,6 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeDeclinedEmail.prototype.getHtml).not.toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
});
@@ -351,117 +350,176 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
jest.clearAllMocks();
});
it("should send an email when creating a booking that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
describe("confirming booking that requires confirmation and rescheduling it", () => {
it("should send an email when creating a booking that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should send an email when confirming a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/confirm`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
});
});
describe("rescheduling emails", () => {
it("should send confirmation emails when organizer reschedules a booking that requires confirmation", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 14, 30, 0)).toISOString(),
rescheduledBy: userEmailsEnabled,
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("accepted");
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should send requested rescheduling emails when attendee rescheduling a booking that requires confirmation", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.rescheduledBookingUid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
});
it("should send an email when confirming a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/confirm`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
describe("declining booking that requires confirmation", () => {
it("should send an email when creating a booking that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
it("should send an email when creating a booking that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
it("should send an email when declining a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/decline`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeDeclinedEmail.prototype.getHtml).toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
it("should send an email when declining a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/decline`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeDeclinedEmail.prototype.getHtml).toHaveBeenCalled();
});
});
});
});
@@ -36,7 +36,7 @@ import {
AttendeeCancelledEmail,
OrganizerReassignedEmail,
AttendeeUpdatedEmail,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/emails";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
@@ -30,7 +30,7 @@ import {
AttendeeRescheduledEmail,
OrganizerCancelledEmail,
AttendeeCancelledEmail,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/emails";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
@@ -7,6 +7,7 @@ import {
CreateManagedUserData,
CreateManagedUserOutput,
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
@@ -149,7 +150,9 @@ describe("Managed user bookings 2024-08-13", () => {
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
@@ -202,7 +205,9 @@ describe("Managed user bookings 2024-08-13", () => {
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
@@ -229,7 +234,9 @@ describe("Managed user bookings 2024-08-13", () => {
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
@@ -294,8 +301,8 @@ describe("Managed user bookings 2024-08-13", () => {
attendee: {
name: thirdManagedUser.user.name!,
email: thirdManagedUserEmail,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
timeZone: thirdManagedUser.user.timeZone,
language: thirdManagedUser.user.locale,
},
guests: [secondManagedUserEmail],
location: "https://meet.google.com/abc-def-ghi",
@@ -411,8 +418,8 @@ describe("Managed user bookings 2024-08-13", () => {
attendee: {
name: thirdManagedUser.user.name!,
email: thirdManagedUser.user.email,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
timeZone: thirdManagedUser.user.timeZone,
language: thirdManagedUser.user.locale,
},
guests: [secondManagedUser.user.email],
location: "https://meet.google.com/abc-def-ghi",
@@ -470,10 +477,71 @@ describe("Managed user bookings 2024-08-13", () => {
});
});
function getOAuthUserEmail(oAuthClientId: string, userEmail: string) {
const [username, emailDomain] = userEmail.split("@");
return `${username}+${oAuthClientId}@${emailDomain}`;
}
describe("fetch bookings by attendeeEmail", () => {
it("should return bookings for the original email when original email is attendee only", async () => {
const response = await request(app.getHttpServer())
.get(`/v2/bookings?attendeeEmail=${thirdManagedUserEmail}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const thirdManagedUserAttendeeBookings: BookingOutput_2024_08_13[] =
thirdManagedUserAttendeeBookingsResponseBody.data;
expect(thirdManagedUserBookingsCount).toEqual(2);
expect(thirdManagedUserAttendeeBookings.length).toEqual(thirdManagedUserBookingsCount);
});
it("should return bookings for the oAuth email when original oAuth email is attendee only", async () => {
const response = await request(app.getHttpServer())
.get(`/v2/bookings?attendeeEmail=${thirdManagedUser.user.email}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const thirdManagedUserAttendeeBookings: BookingOutput_2024_08_13[] =
thirdManagedUserAttendeeBookingsResponseBody.data;
expect(thirdManagedUserBookingsCount).toEqual(2);
expect(thirdManagedUserAttendeeBookings.length).toEqual(thirdManagedUserBookingsCount);
});
it("should return bookings for the original email when original email is attendee or guest", async () => {
const response = await request(app.getHttpServer())
.get(`/v2/bookings?attendeeEmail=${secondManagedUserEmail}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const secondManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserAttendeeBookings: BookingOutput_2024_08_13[] =
secondManagedUserAttendeeBookingsResponseBody.data;
expect(secondManagedUserBookingsCount).toEqual(4);
expect(secondManagedUserAttendeeBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("should return bookings for the oAuth email when original oAuth email is attendee or guest", async () => {
const response = await request(app.getHttpServer())
.get(`/v2/bookings?attendeeEmail=${secondManagedUser.user.email}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const secondManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserAttendeeBookings: BookingOutput_2024_08_13[] =
secondManagedUserAttendeeBookingsResponseBody.data;
expect(secondManagedUserBookingsCount).toEqual(4);
expect(secondManagedUserAttendeeBookings.length).toEqual(secondManagedUserBookingsCount);
});
});
afterAll(async () => {
await userRepositoryFixture.delete(firstManagedUser.user.id);
@@ -21,7 +21,7 @@ import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import { AttendeeCancelledEmail, OrganizerCancelledEmail } from "@calcom/platform-libraries";
import { AttendeeCancelledEmail, OrganizerCancelledEmail } from "@calcom/platform-libraries/emails";
import {
CreateRecurringBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
@@ -40,6 +40,8 @@ import {
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
describe("Seated bookings", () => {
let app: INestApplication;
let organization: Team;
@@ -330,7 +332,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
location: googleMeetUrl,
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
@@ -383,6 +385,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -418,6 +424,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -797,7 +807,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
location: googleMeetUrl,
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
@@ -850,6 +860,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -885,6 +899,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -910,7 +928,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
location: googleMeetUrl,
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
@@ -966,6 +984,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: nameAttendeeOne,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
});
@@ -982,6 +1004,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: nameAttendeeTwo,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -1017,6 +1043,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: nameAttendeeOne,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
});
@@ -1030,6 +1060,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: nameAttendeeTwo,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -1,6 +1,7 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
@@ -31,6 +32,7 @@ import {
RecurringBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
@@ -66,6 +68,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
const team2EventTypeSlug = `team-bookings-event-type-${randomString()}`;
const phoneOnlyEventTypeSlug = `team-bookings-event-type-${randomString()}`;
let phoneBasedBooking: BookingOutput_2024_08_13;
beforeAll(async () => {
const moduleRef = await withApiAuth(
teamUserEmail,
@@ -408,12 +412,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
it("should create a phone based booking", async () => {
const phoneNumber = "+919876543210";
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
eventTypeId: phoneOnlyEventTypeId,
attendee: {
name: "alice",
phoneNumber: "+919876543210",
phoneNumber,
timeZone: "Europe/Madrid",
language: "es",
},
@@ -453,6 +458,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
expect(data.meetingUrl).toEqual(body.meetingUrl);
expect(data.absentHost).toEqual(false);
expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(phoneNumber);
phoneBasedBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
@@ -523,7 +530,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
describe("get team bookings", () => {
it("should should get bookings by teamId", async () => {
it("should get bookings by teamId", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamId=${team1.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -542,7 +549,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by teamId", async () => {
it("should get bookings by teamId", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamId=${team2.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -584,21 +591,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamId=${team2.id}&eventTypeId=90909`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(0);
});
.expect(400);
});
it("should should get bookings by teamIds", async () => {
it("should get bookings by teamIds", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamIds=${team1.id},${team2.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -619,6 +615,59 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("reschedule", () => {
it("should reschedule phone based booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${phoneBasedBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts.length).toEqual(1);
expect(data.hosts[0].id).toEqual(teamUser.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 14, 15, 0)).toISOString());
expect(data.duration).toEqual(15);
expect(data.eventTypeId).toEqual(phoneOnlyEventTypeId);
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: phoneBasedBooking.attendees[0].name,
email: phoneBasedBooking.attendees[0].email,
phoneNumber: phoneBasedBooking.attendees[0].phoneNumber,
timeZone: phoneBasedBooking.attendees[0].timeZone,
language: phoneBasedBooking.attendees[0].language,
absent: false,
});
expect(data.meetingUrl).toEqual(phoneBasedBooking.meetingUrl);
expect(data.absentHost).toEqual(false);
expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(
phoneBasedBooking.bookingFieldsResponses.attendeePhoneNumber
);
phoneBasedBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
@@ -4,6 +4,7 @@ import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
@@ -13,7 +14,7 @@ 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 { EventType, 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";
@@ -27,10 +28,12 @@ import { withApiAuth } from "test/utils/withApiAuth";
import {
CAL_API_VERSION_HEADER,
SUCCESS_STATUS,
VERSION_2024_06_14,
VERSION_2024_08_13,
X_CAL_CLIENT_ID,
} from "@calcom/platform-constants";
import {
CreateEventTypeInput_2024_06_14,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
@@ -63,6 +66,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
let user: User;
let eventTypeId: number;
let eventType: EventType;
const eventTypeSlug = `user-bookings-event-type-${randomString()}`;
let recurringEventTypeId: number;
const recurringEventTypeSlug = `user-bookings-event-type-${randomString()}`;
@@ -95,6 +99,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
organization = await teamRepositoryFixture.create({
name: `user-bookings-organization-${randomString()}`,
isOrganization: true,
});
oAuthClient = await createOAuthClient(organization.id);
@@ -124,6 +129,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
user.id
);
eventTypeId = event.id;
eventType = event;
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
@@ -146,7 +152,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
startTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: "booking-in-the-past",
uid: `booking-in-the-past-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
@@ -326,6 +332,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
it("should create a booking", async () => {
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId,
@@ -335,7 +342,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
location: googleMeetUrl,
bookingFieldsResponses: {
customField: "customValue",
},
@@ -389,6 +396,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
email: body.attendee.email,
...body.bookingFieldsResponses,
guests: body.guests,
location: {
optionValue: googleMeetUrl,
value: "link",
},
});
expect(data.guests).toEqual(body.guests);
@@ -518,7 +529,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
describe("get individual booking", () => {
it("should should get a booking", async () => {
it("should get a booking", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${createdBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -553,7 +564,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get a booking with rating", async () => {
it("should get a booking with rating", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${bookingInThePast.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -577,7 +588,66 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get 1 recurrence of a recurring booking", async () => {
it("should get a booking with icsUid", async () => {
const mockBooking = await bookingsRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `booking-in-the-past-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
rating: 10,
iCalUID: "ics-uid",
});
return request(app.getHttpServer())
.get(`/v2/bookings/${mockBooking.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.id).toEqual(mockBooking.id);
expect(data.uid).toEqual(mockBooking.uid);
expect(data.icsUid).toEqual(mockBooking.iCalUID);
await bookingsRepositoryFixture.deleteById(mockBooking.id);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should get 1 recurrence of a recurring booking", async () => {
const recurrenceUid = createdRecurringBooking[0].uid;
return request(app.getHttpServer())
.get(`/v2/bookings/${recurrenceUid}`)
@@ -613,7 +683,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get all recurrences of the recurring bookings", async () => {
it("should get all recurrences of the recurring bookings", async () => {
const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid;
return request(app.getHttpServer())
.get(`/v2/bookings/${recurringBookingUid}`)
@@ -682,7 +752,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
describe("get bookings", () => {
it("should should get all bookings", async () => {
it("should get all bookings", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -700,7 +770,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should take bookings", async () => {
it("should take bookings", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?take=3`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -718,7 +788,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should skip bookings", async () => {
it("should skip bookings", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?skip=2`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -736,7 +806,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get upcoming bookings", async () => {
it("should get upcoming bookings", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?status=upcoming`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -754,7 +824,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get past bookings", async () => {
it("should get past bookings", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?status=past`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -772,7 +842,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get upcoming and past bookings", async () => {
it("should get upcoming and past bookings", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?status=upcoming,past`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -790,7 +860,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get recurring booking recurrences", async () => {
it("should get recurring booking recurrences", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?status=recurring`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -808,7 +878,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by attendee email", async () => {
it("should get bookings by attendee email", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?attendeeEmail=mr_proper@gmail.com`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -826,7 +896,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by attendee name", async () => {
it("should get bookings by attendee name", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?attendeeName=Mr Proper Recurring`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -844,7 +914,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by eventTypeId", async () => {
it("should get bookings by eventTypeId", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -862,7 +932,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by eventTypeIds", async () => {
it("should get bookings by eventTypeIds", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeIds=${eventTypeId},${recurringEventTypeId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -880,7 +950,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by after specified start time", async () => {
it("should get bookings by after specified start time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?afterStart=${createdRecurringBooking[1].start}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -898,7 +968,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by before specified end time", async () => {
it("should get bookings by before specified end time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?beforeEnd=${createdRecurringBooking[0].end}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -916,7 +986,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings after specified update time", async () => {
it("should get bookings after specified update time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?afterUpdatedAt=${createdRecurringBooking[1].updatedAt}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -934,7 +1004,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings before specified update time", async () => {
it("should get bookings before specified update time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?beforeUpdatedAt=${createdRecurringBooking[0].updatedAt}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -952,7 +1022,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings in update range", async () => {
it("should get bookings in update range", async () => {
return request(app.getHttpServer())
.get(
`/v2/bookings?afterUpdatedAt=${createdRecurringBooking[0].updatedAt}&beforeUpdatedAt=${createdRecurringBooking[2].updatedAt}`
@@ -972,7 +1042,65 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by start in descending order", async () => {
describe("createdAt filter", () => {
it("should get bookings after specified createdAt time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?afterCreatedAt=${createdRecurringBooking[1].createdAt}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(2);
});
});
it("should get bookings before specified createdAt time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?beforeCreatedAt=${createdRecurringBooking[0].createdAt}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(3);
});
});
it("should get bookings in createdAt range", async () => {
return request(app.getHttpServer())
.get(
`/v2/bookings?afterCreatedAt=${createdRecurringBooking[0].createdAt}&beforeCreatedAt=${createdRecurringBooking[1].createdAt}`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(2);
});
});
});
it("should sort bookings by start in descending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=desc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -992,7 +1120,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by start in ascending order", async () => {
it("should sort bookings by start in ascending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=asc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1012,7 +1140,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by end in descending order", async () => {
it("should sort bookings by end in descending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortEnd=desc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1032,7 +1160,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by end in ascending order", async () => {
it("should sort bookings by end in ascending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortEnd=asc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1052,7 +1180,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by created in descending order", async () => {
it("should sort bookings by created in descending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortCreated=desc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1072,7 +1200,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by created in ascending order", async () => {
it("should sort bookings by created in ascending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortCreated=asc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1092,7 +1220,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by updated in descending order", async () => {
it("should sort bookings by updated in descending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortUpdatedAt=desc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1112,7 +1240,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by updated in ascending order", async () => {
it("should sort bookings by updated in ascending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortUpdatedAt=asc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1134,7 +1262,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
describe("reschedule bookings", () => {
it("should should reschedule normal booking", async () => {
it("should reschedule normal booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
@@ -1537,6 +1665,569 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("book by username and event type slug", () => {
it("should not create a booking by if neither event type id nor username and event type slug provided", async () => {
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
const body = {
start: new Date(Date.UTC(2036, 0, 8, 15, 0, 0)).toISOString(),
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: googleMeetUrl,
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
guests: ["bob@gmail.com"],
};
const response = await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(
response.body.error.message.includes(
"Either eventTypeId OR (eventTypeSlug + username) must be provided"
)
).toBe(true);
});
it("should create a booking by username and event type slug", async () => {
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2036, 0, 8, 15, 0, 0)).toISOString(),
eventTypeSlug: eventType.slug,
username: user.username || "",
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: googleMeetUrl,
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
guests: ["bob@gmail.com"],
};
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();
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.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2036, 0, 8, 16, 0, 0)).toISOString());
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(eventTypeId);
expect(data.eventType).toEqual({
id: eventTypeId,
slug: eventTypeSlug,
});
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.location).toEqual(body.location);
expect(data.meetingUrl).toEqual(body.location);
expect(data.absentHost).toEqual(false);
expect(data.bookingFieldsResponses).toEqual({
name: body.attendee.name,
email: body.attendee.email,
...body.bookingFieldsResponses,
guests: body.guests,
location: {
optionValue: googleMeetUrl,
value: "link",
},
});
expect(data.guests).toEqual(body.guests);
// 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());
// Check updatedAt date is between the time of the request and after the request
expect(data.updatedAt).toBeDefined();
const updatedAtDate = data.updatedAt ? new Date(data.updatedAt) : null;
expect(updatedAtDate?.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(updatedAtDate?.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
expect(data.metadata).toEqual(body.metadata);
createdBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
});
describe("booking location", () => {
const address = "123 Main St";
const link = "https://cal.com/join/123456";
const phone = "+37121999999";
let eventTypeWithAllLocationsId: number;
it("can create event type with all locations except google meet", async () => {
const eventTypeBody: CreateEventTypeInput_2024_06_14 = {
title: "book using any location",
slug: "book-using-any-location",
lengthInMinutes: 15,
locations: [
{
type: "integration",
integration: "cal-video",
},
{
type: "address",
address,
public: true,
},
{
type: "link",
link,
public: true,
},
{
type: "phone",
phone,
public: true,
},
{
type: "attendeeAddress",
},
{
type: "attendeePhone",
},
{
type: "attendeeDefined",
},
],
};
const eventTypeResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(eventTypeBody)
.expect(201);
const eventTypeResponseBody: CreateEventTypeOutput_2024_06_14 = eventTypeResponse.body;
const createdEventType = eventTypeResponseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.locations).toHaveLength(7);
expect(createdEventType.locations).toEqual(createdEventType.locations);
eventTypeWithAllLocationsId = createdEventType.id;
});
it("can book with cal video location", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "integration",
integration: "cal-video",
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(
createdBooking.location?.startsWith("http") || createdBooking.location === "integrations:daily"
).toEqual(true);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with address location", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "address",
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(address);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with link location", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "link",
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(link);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with link location", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "phone",
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(phone);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with attendeeAddress location", async () => {
const attendeeAddress = "123 Example St, City, Valhalla";
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "attendeeAddress",
address: attendeeAddress,
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(attendeeAddress);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
xit("can book with attendeeAddress location", async () => {
const attendeePhone = "+37120993151";
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "attendeePhone",
phone: attendeePhone,
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(attendeePhone);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with attendeeDefined location", async () => {
const attendeeDefinedLocation = "namek 100";
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "attendeeDefined",
location: attendeeDefinedLocation,
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(attendeeDefinedLocation);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can't book with not location that is not in event type", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "integration",
integration: "google-meet",
},
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
it("can't book with invalid location type", async () => {
const bookingBody = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "blablabala",
},
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
});
describe("rescheduling booking with invalid booking responses", () => {
it("should reschedule", async () => {
const email = `user-bookings-attendee-${randomString(10)}@gmail.com`;
const username = `user-bookings-attendee-${randomString(10)}`;
const locale = "it";
const timeZone = "Europe/Rome";
const attendee = await userRepositoryFixture.create({
email,
username,
locale,
timeZone,
});
const booking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${randomString(10)}`,
title: "booking title",
startTime: "2060-09-05T11:00:00.000Z",
endTime: "2060-09-05T12:00:00.000Z",
eventType: {
connect: {
id: eventTypeId,
},
},
status: "ACCEPTED",
metadata: {},
responses: "null",
user: {
connect: {
id: user.id,
},
},
attendees: {
create: {
email: email,
name: username,
timeZone,
locale,
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2060, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${booking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2060, 0, 9, 15, 0, 0)).toISOString());
expect(data.rescheduledFromUid).toEqual(booking.uid);
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual("accepted");
expect(data.eventTypeId).toEqual(booking.eventTypeId);
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: attendee.username,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: false,
});
expect(data.bookingFieldsResponses).toEqual({
name: attendee.username,
email: attendee.email,
rescheduledReason: body.reschedulingReason,
guests: [],
});
await bookingsRepositoryFixture.deleteById(data.id);
await userRepositoryFixture.delete(attendee.id);
});
});
});
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
return (
!Array.isArray(data) &&
@@ -0,0 +1,33 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum, IsString } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class CalendarLink {
@IsString()
@ApiProperty({
description: "The label of the calendar link",
})
label!: string;
@IsString()
@ApiProperty({
description: "The link to the calendar",
})
link!: string;
}
export class CalendarLinksOutput_2024_08_13 {
@ApiProperty({
description: "The status of the request, always 'success' for successful responses",
example: SUCCESS_STATUS,
})
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
description: "Calendar links for the booking",
type: [CalendarLink],
})
data!: CalendarLink[];
}
@@ -1,10 +1,13 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { BillingService } from "@/modules/billing/services/billing.service";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
@@ -15,6 +18,7 @@ import { z } from "zod";
import {
handleNewRecurringBooking,
getTranslation,
getAllUserBookings,
handleInstantMeeting,
handleCancelBooking,
@@ -22,6 +26,7 @@ import {
roundRobinManualReassignment,
handleMarkNoShow,
confirmBookingHandler,
getCalendarLinks,
} from "@calcom/platform-libraries";
import { handleNewBooking } from "@calcom/platform-libraries";
import {
@@ -40,7 +45,7 @@ import {
CancelBookingInput,
} from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
import { EventType } from "@calcom/prisma/client";
import { EventType, User, Team } from "@calcom/prisma/client";
type CreatedBooking = {
hosts: { id: number }[];
@@ -56,6 +61,8 @@ const eventTypeBookingFieldSchema = z.object({
const eventTypeBookingFieldsSchema = z.array(eventTypeBookingFieldSchema);
export type EventTypeWithOwnerAndTeam = EventType & { owner: User | null; team: Team | null };
@Injectable()
export class BookingsService_2024_08_13 {
private readonly logger = new Logger("BookingsService");
@@ -69,32 +76,49 @@ export class BookingsService_2024_08_13 {
private readonly billingService: BillingService,
private readonly usersService: UsersService,
private readonly usersRepository: UsersRepository,
private readonly platformBookingsService: PlatformBookingsService
private readonly platformBookingsService: PlatformBookingsService,
private readonly oAuthClientRepository: OAuthClientRepository
) {}
async createBooking(request: Request, body: CreateBookingInput) {
try {
if ("instant" in body && body.instant) {
return await this.createInstantBooking(request, body);
const eventType = await this.getBookedEventType(body);
if (!eventType) {
if (body.username && body.eventTypeSlug && !body.organizationSlug) {
throw new NotFoundException(
`Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} not found.`
);
}
if (body.username && body.eventTypeSlug && body.organizationSlug) {
throw new NotFoundException(
`Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} within organization ${body.organizationSlug} not found.`
);
}
throw new NotFoundException(`Event type with id ${body.eventTypeId} not found.`);
}
body.eventTypeId = eventType.id;
if ("instant" in body && body.instant) {
return await this.createInstantBooking(request, body, eventType);
}
const eventType = await this.eventTypesRepository.getEventTypeById(body.eventTypeId);
const isRecurring = !!eventType?.recurringEvent;
const isSeated = !!eventType?.seatsPerTimeSlot;
await this.hasRequiredBookingFieldsResponses(body, eventType);
if (isRecurring && isSeated) {
return await this.createRecurringSeatedBooking(request, body);
return await this.createRecurringSeatedBooking(request, body, eventType);
}
if (isRecurring && !isSeated) {
return await this.createRecurringBooking(request, body);
return await this.createRecurringBooking(request, body, eventType);
}
if (isSeated) {
return await this.createSeatedBooking(request, body);
return await this.createSeatedBooking(request, body, eventType);
}
return await this.createRegularBooking(request, body);
return await this.createRegularBooking(request, body, eventType);
} catch (error) {
if (error instanceof Error) {
if (error.message === "no_available_users_found_error") {
@@ -105,6 +129,22 @@ export class BookingsService_2024_08_13 {
}
}
async getBookedEventType(body: CreateBookingInput) {
if (body.eventTypeId) {
return await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(body.eventTypeId);
} else if (body.username && body.eventTypeSlug) {
const user = await this.usersRepository.findByUsername(body.username, body.organizationSlug);
if (!user) {
throw new NotFoundException(`User with username ${body.username} not found`);
}
return await this.eventTypesRepository.getUserEventTypeBySlugWithOwnerAndTeam(
user.id,
body.eventTypeSlug
);
}
return null;
}
async hasRequiredBookingFieldsResponses(body: CreateBookingInput, eventType: EventType | null) {
const bookingFields = body.bookingFieldsResponses;
if (!bookingFields || !eventType || !eventType.bookingFields) {
@@ -127,8 +167,12 @@ export class BookingsService_2024_08_13 {
return true;
}
async createInstantBooking(request: Request, body: CreateInstantBookingInput_2024_08_13) {
const bookingRequest = await this.inputService.createBookingRequest(request, body);
async createInstantBooking(
request: Request,
body: CreateInstantBookingInput_2024_08_13,
eventType: EventTypeWithOwnerAndTeam
) {
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
const booking = await handleInstantMeeting(bookingRequest);
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(
@@ -141,24 +185,52 @@ export class BookingsService_2024_08_13 {
return this.outputService.getOutputBooking(databaseBooking);
}
async createRecurringBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) {
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body);
const bookings = await handleNewRecurringBooking(bookingRequest);
async createRecurringBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13, eventType: EventTypeWithOwnerAndTeam) {
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body, eventType);
const bookings = await handleNewRecurringBooking({
bookingData: bookingRequest.body,
userId: bookingRequest.userId,
hostname: bookingRequest.headers?.host || "",
platformClientId: bookingRequest.platformClientId,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
noEmail: bookingRequest.noEmail,
});
const ids = bookings.map((booking) => booking.id || 0);
return this.outputService.getOutputRecurringBookings(ids);
}
async createRecurringSeatedBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) {
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body);
const bookings = await handleNewRecurringBooking(bookingRequest);
async createRecurringSeatedBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13, eventType: EventTypeWithOwnerAndTeam) {
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body, eventType);
const bookings = await handleNewRecurringBooking({
bookingData: bookingRequest.body,
userId: bookingRequest.userId,
hostname: bookingRequest.headers?.host || "",
platformClientId: bookingRequest.platformClientId,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
});
return this.outputService.getOutputCreateRecurringSeatedBookings(
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" }))
);
}
async createRegularBooking(request: Request, body: CreateBookingInput_2024_08_13) {
const bookingRequest = await this.inputService.createBookingRequest(request, body);
const booking = await handleNewBooking(bookingRequest);
async createRegularBooking(request: Request, body: CreateBookingInput_2024_08_13, eventType: EventTypeWithOwnerAndTeam) {
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
const booking = await handleNewBooking({
bookingData: bookingRequest.body,
userId: bookingRequest.userId,
hostname: bookingRequest.headers?.host || "",
platformClientId: bookingRequest.platformClientId,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
});
if (!booking.uid) {
throw new Error("Booking missing uid");
@@ -172,9 +244,18 @@ export class BookingsService_2024_08_13 {
return this.outputService.getOutputBooking(databaseBooking);
}
async createSeatedBooking(request: Request, body: CreateBookingInput_2024_08_13) {
const bookingRequest = await this.inputService.createBookingRequest(request, body);
const booking = await handleNewBooking(bookingRequest);
async createSeatedBooking(request: Request, body: CreateBookingInput_2024_08_13, eventType: EventTypeWithOwnerAndTeam) {
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
const booking = await handleNewBooking({
bookingData: bookingRequest.body,
userId: bookingRequest.userId,
hostname: bookingRequest.headers?.host || "",
platformClientId: bookingRequest.platformClientId,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
});
if (!booking.uid) {
throw new Error("Booking missing uid");
@@ -222,13 +303,23 @@ export class BookingsService_2024_08_13 {
return this.outputService.getOutputRecurringBookings(ids);
}
async getBookings(queryParams: GetBookingsInput_2024_08_13, user: { email: string; id: number }) {
async getBookings(
queryParams: GetBookingsInput_2024_08_13,
user: { email: string; id: number; orgId?: number },
userIds?: number[]
) {
if (queryParams.attendeeEmail) {
queryParams.attendeeEmail = await this.getAttendeeEmail(queryParams.attendeeEmail, user);
}
const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({
bookingListingByStatus: queryParams.status || [],
skip: queryParams.skip ?? 0,
// note(Lauris): we substract -1 because getAllUSerBookings child function adds +1 for some reason
take: queryParams.take ? queryParams.take - 1 : 100,
filters: this.inputService.transformGetBookingsFilters(queryParams),
take: queryParams.take ?? 100,
filters: {
...this.inputService.transformGetBookingsFilters(queryParams),
...(userIds?.length ? { userIds } : {}),
},
ctx: {
user,
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
@@ -279,6 +370,35 @@ export class BookingsService_2024_08_13 {
return formattedBookings;
}
async getAttendeeEmail(queryParamsAttendeeEmail: string, user: { id: number }) {
// note(Lauris): this is to handle attendees that are managed users - in attendee table their email is one of managed users e.g
// urdasdqinm+clxyyy21o0003sbk7yw5z6tzg@example.com but if attendeeEmail is passed as urdasdqinm@example.com then we check if user whose
// access token is used is a managed user and if attendee with passed email has managed user email composed of passed email without oAuth client id +
// authenticated user oAuth client id.
const oAuthClient = await this.oAuthClientRepository.getByUserId(user.id);
if (!oAuthClient) {
return queryParamsAttendeeEmail;
}
// note(Lauris): query param already contains oAuth client id in the attendeeEmail
if (queryParamsAttendeeEmail.includes(oAuthClient.id)) {
return queryParamsAttendeeEmail;
}
const managedAttendeeEmail = OAuthClientUsersService.getOAuthUserEmail(
oAuthClient.id,
queryParamsAttendeeEmail
);
const [attendee, managedAttendee] = await Promise.all([
this.usersRepository.findByEmail(queryParamsAttendeeEmail),
this.usersRepository.findByEmail(managedAttendeeEmail),
]);
if (!attendee && managedAttendee) {
return managedAttendeeEmail;
}
return queryParamsAttendeeEmail;
}
async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput) {
try {
const bookingRequest = await this.inputService.createRescheduleBookingRequest(
@@ -286,7 +406,16 @@ export class BookingsService_2024_08_13 {
bookingUid,
body
);
const booking = await handleNewBooking(bookingRequest);
const booking = await handleNewBooking({
bookingData: bookingRequest.body,
userId: bookingRequest.userId,
hostname: bookingRequest.headers?.host || "",
platformClientId: bookingRequest.platformClientId,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
});
if (!booking.uid) {
throw new Error("Booking missing uid");
}
@@ -342,7 +471,16 @@ export class BookingsService_2024_08_13 {
}
const bookingRequest = await this.inputService.createCancelBookingRequest(request, bookingUid, body);
const res = await handleCancelBooking(bookingRequest);
const res = await handleCancelBooking({
bookingData: bookingRequest.body,
userId: bookingRequest.userId,
arePlatformEmailsEnabled: bookingRequest.arePlatformEmailsEnabled,
platformClientId: bookingRequest.platformClientId,
platformCancelUrl: bookingRequest.platformCancelUrl,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
});
if (!res.onlyRemovedAttendee) {
await this.billingService.cancelUsageByBookingUid(res.bookingUid);
}
@@ -446,6 +584,7 @@ export class BookingsService_2024_08_13 {
orgId: profile?.organizationId || null,
emailsEnabled,
platformClientParams,
reassignedById: requestUser.id,
});
const reassigned = await this.bookingsRepository.getByUidWithUser(bookingUid);
@@ -549,4 +688,34 @@ export class BookingsService_2024_08_13 {
return this.getBooking(bookingUid);
}
async getCalendarLinks(bookingUid: string): Promise<CalendarLink[]> {
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
}
if (!booking.eventTypeId) {
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
}
const eventType = await this.eventTypesRepository.getEventTypeByIdIncludeUsersAndTeam(
booking.eventTypeId
);
if (!eventType) {
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
}
// TODO: Maybe we should get locale from query params?
return getCalendarLinks({
booking,
eventType: {
...eventType,
// TODO: Support dynamic event bookings later. It would require a slug input it seems
isDynamic: false,
},
// It can be made customizable through the API endpoint later.
t: await getTranslation("en", "common"),
});
}
}
@@ -1,25 +1,35 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { EventTypeWithOwnerAndTeam } from "@/ee/bookings/2024-08-13/services/bookings.service";
import {
bookingResponsesSchema,
seatedBookingDataSchema,
} from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { defaultBookingResponses } from "@/lib/safe-parse/default-responses-booking";
import { safeParse } from "@/lib/safe-parse/safe-parse";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { CreationSource } from "@prisma/client";
import { isURL, isPhoneNumber } from "class-validator";
import { Request } from "express";
import { DateTime } from "luxon";
import { NextApiRequest } from "next/types";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
import {
EventTypeMetaDataSchema,
apiToInternalintegrationsMapping,
} from "@calcom/platform-libraries/event-types";
import {
CancelBookingInput,
CancelBookingInput_2024_08_13,
@@ -33,9 +43,13 @@ import {
RescheduleBookingInput_2024_08_13,
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { BookingInputLocation_2024_08_13 } from "@calcom/platform-types/bookings/2024-08-13/inputs/location.input";
import { EventType } from "@calcom/prisma/client";
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
type BookingRequest = NextApiRequest & {
userId: number | undefined;
noEmail: boolean | undefined;
} & OAuthRequestParams;
type OAuthRequestParams = {
platformClientId: string;
@@ -75,51 +89,52 @@ export class InputBookingsService_2024_08_13 {
private readonly config: ConfigService,
private readonly apiKeyRepository: ApiKeysRepository,
private readonly bookingSeatRepository: BookingSeatRepository,
private readonly platformBookingsService: PlatformBookingsService
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
private readonly platformBookingsService: PlatformBookingsService,
private readonly usersRepository: UsersRepository
) {}
async createBookingRequest(
request: Request,
body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13
body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13,
eventType: EventTypeWithOwnerAndTeam
): Promise<BookingRequest> {
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(body.eventTypeId);
const bodyTransformed = await this.transformInputCreateBooking(body, oAuthClientParams?.platformClientId);
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParamsForEventType(eventType);
const bodyTransformed = await this.transformInputCreateBooking(
body,
eventType,
oAuthClientParams?.platformClientId
);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const location = request.body.location || request.body.meetingUrl;
this.logger.log(`createBookingRequest_2024_04_15`, {
this.logger.log(`createBookingRequest_2024_08_13`, {
requestId: request.get("X-Request-Id"),
ownerId: userId,
location,
oAuthClientParams,
});
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
Object.assign(newRequest, { userId, ...oAuthClientParams });
newRequest.body = {
...bodyTransformed,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
Object.assign(newRequest, { userId });
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
}
return newRequest as unknown as BookingRequest;
}
async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13, platformClientId?: string) {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
inputBooking.eventTypeId
);
if (!eventType) {
throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} not found`);
}
async transformInputCreateBooking(
inputBooking: CreateBookingInput_2024_08_13,
eventType: EventTypeWithOwnerAndTeam,
platformClientId?: string
) {
this.validateBookingLengthInMinutes(inputBooking, eventType);
const lengthInMinutes = inputBooking.lengthInMinutes ?? eventType.length;
@@ -140,6 +155,10 @@ export class InputBookingsService_2024_08_13 {
)
: inputBooking.attendee.email;
const inputLocation = inputBooking.location || inputBooking.meetingUrl;
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
return {
start: startTime.toISO(),
end: endTime.toISO(),
@@ -156,6 +175,7 @@ export class InputBookingsService_2024_08_13 {
email: attendeeEmail ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
location,
},
};
}
@@ -181,29 +201,28 @@ export class InputBookingsService_2024_08_13 {
async createRecurringBookingRequest(
request: Request,
body: CreateRecurringBookingInput_2024_08_13
body: CreateRecurringBookingInput_2024_08_13,
eventType: EventTypeWithOwnerAndTeam
): Promise<BookingRequest> {
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(body.eventTypeId);
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParamsForEventType(eventType);
// note(Lauris): update to this.transformInputCreate when rescheduling is implemented
const bodyTransformed = await this.transformInputCreateRecurringBooking(
body,
eventType,
oAuthClientParams?.platformClientId
);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const location = request.body.location || request.body.meetingUrl;
if (oAuthClientParams) {
Object.assign(newRequest, {
userId,
...oAuthClientParams,
platformBookingLocation: location,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
});
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
Object.assign(newRequest, { userId });
}
newRequest.body = bodyTransformed.map((event) => ({
@@ -211,19 +230,158 @@ export class InputBookingsService_2024_08_13 {
creationSource: CreationSource.API_V2,
}));
return newRequest as unknown as BookingRequest;
return {
...newRequest,
headers: {
hostname: request.headers["host"] || "",
forcedSlug: request.headers["x-cal-force-slug"] as string | undefined,
},
} as unknown as BookingRequest;
}
transformLocation(location: string | BookingInputLocation_2024_08_13): {
value: string;
optionValue: string;
} {
if (typeof location === "string") {
// note(Lauris): this is for backwards compatibility because before switching to booking location objects
// we only received a string. If someone is complaining that their location is not displaying as a URL
// or whatever check that they are not providing a string for bookign location but one of the input objects.
if (isURL(location, { require_protocol: false }) || location.startsWith("www.")) {
return {
value: "link",
optionValue: location,
};
}
if (isPhoneNumber(location)) {
return {
value: "phone",
optionValue: location,
};
}
return {
value: "somewhereElse",
optionValue: location,
};
}
if (location.type === "integration") {
const integration = apiToInternalintegrationsMapping[location.integration];
if (!integration) {
throw new BadRequestException(`Invalid integration: ${location.integration}`);
}
return {
value: integration,
optionValue: "",
};
}
if (location.type === "address") {
return {
value: "inPerson",
optionValue: "",
};
}
if (location.type === "attendeeAddress") {
return {
value: "attendeeInPerson",
optionValue: location.address,
};
}
if (location.type === "link") {
return {
value: "link",
optionValue: "",
};
}
if (location.type === "phone") {
return {
value: "userPhone",
optionValue: "",
};
}
if (location.type === "organizersDefaultApp") {
return {
value: "conferencing",
optionValue: "",
};
}
if (location.type === "attendeePhone") {
return {
value: "phone",
optionValue: location.phone,
};
}
if (location.type === "attendeeDefined") {
return {
value: "somewhereElse",
optionValue: location.location,
};
}
throw new BadRequestException(
`Booking location with type ${(location as BookingInputLocation_2024_08_13).type} not valid.`
);
}
private isBookingLocationWithEventTypeLocations(
inputBookingLocation: undefined | string | BookingInputLocation_2024_08_13,
dbEventType: EventType
) {
if (!inputBookingLocation || typeof inputBookingLocation === "string") {
// note(Lauris): for backwards compatibility because we had string locations before so let them pass.
return true;
}
const eventTypeLocations = this.outputEventTypesService.transformLocations(dbEventType.locations);
const allowedLocationTypes = eventTypeLocations.map((location) => location.type);
const isAllowed = allowedLocationTypes.includes(inputBookingLocation.type);
if (!isAllowed) {
throw new BadRequestException(
`Booking location with type ${inputBookingLocation.type} not valid for event type with id=${
dbEventType.id
}. The event type has following location types: ${allowedLocationTypes.join(
", "
)}, and only these types are allowed for booking location.`
);
}
if (inputBookingLocation.type === "integration" && "integration" in inputBookingLocation) {
const allowedIntegrations = eventTypeLocations
.filter((location) => location.type === "integration")
.map((location) => location.integration);
const isAllowedIntegration = allowedIntegrations.includes(inputBookingLocation.integration);
if (!isAllowedIntegration) {
throw new BadRequestException(
`Booking location with integration ${
inputBookingLocation.integration
} not valid for event type with id=${
dbEventType.id
}. The event type has following integrations: ${allowedIntegrations.join(
", "
)}, and only these integrations are allowed for booking location.`
);
}
}
return true;
}
async transformInputCreateRecurringBooking(
inputBooking: CreateRecurringBookingInput_2024_08_13,
eventType: EventTypeWithOwnerAndTeam,
platformClientId?: string
) {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
inputBooking.eventTypeId
);
if (!eventType) {
throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} not found`);
}
if (!eventType.recurringEvent) {
throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`);
}
@@ -259,6 +417,10 @@ export class InputBookingsService_2024_08_13 {
)
: inputBooking.attendee.email;
const inputLocation = inputBooking.location || inputBooking.meetingUrl;
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
for (let i = 0; i < repeatsTimes; i++) {
const endTime = startTime.plus({ minutes: eventType.length });
@@ -277,7 +439,9 @@ export class InputBookingsService_2024_08_13 {
...(inputBooking.bookingFieldsResponses || {}),
name: inputBooking.attendee.name,
email: attendeeEmail,
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
location,
},
schedulingType: eventType.schedulingType,
});
@@ -314,7 +478,24 @@ export class InputBookingsService_2024_08_13 {
);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
let userId: number | undefined = undefined;
if (
oAuthClientParams &&
request.body.rescheduledBy &&
!request.body.rescheduledBy.includes(oAuthClientParams.platformClientId)
) {
request.body.rescheduledBy = OAuthClientUsersService.getOAuthUserEmail(
oAuthClientParams.platformClientId,
request.body.rescheduledBy
);
}
if (request.body.rescheduledBy) {
if (request.body.rescheduledBy !== bodyTransformed.responses.email) {
userId = (await this.usersRepository.findByEmail(request.body.rescheduledBy))?.id;
}
}
const location = await this.getRescheduleBookingLocation(bookingUid);
if (oAuthClientParams) {
@@ -397,8 +578,19 @@ export class InputBookingsService_2024_08_13 {
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
}
const bookingResponses = bookingResponsesSchema.parse(booking.responses);
const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
const bookingResponses = safeParse(
bookingResponsesSchema,
booking.responses,
defaultBookingResponses,
false
);
const bookingResponsesMissing =
bookingResponses.name === defaultBookingResponses.name &&
bookingResponses.email === defaultBookingResponses.email;
const attendee = bookingResponsesMissing
? booking.attendees[0]
: booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
if (!attendee) {
throw new NotFoundException(
@@ -406,6 +598,12 @@ export class InputBookingsService_2024_08_13 {
);
}
if (bookingResponsesMissing) {
bookingResponses.name = attendee.name;
bookingResponses.email = attendee.email;
bookingResponses.attendeePhoneNumber = attendee.phoneNumber || undefined;
}
const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone);
const endTime = startTime.plus({ minutes: eventType.length });
@@ -462,6 +660,8 @@ export class InputBookingsService_2024_08_13 {
queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined),
afterUpdatedDate: queryParams.afterUpdatedAt,
beforeUpdatedDate: queryParams.beforeUpdatedAt,
afterCreatedDate: queryParams.afterCreatedAt,
beforeCreatedDate: queryParams.beforeCreatedAt,
};
}
@@ -27,6 +27,7 @@ import { Booking, BookingSeat } from "@calcom/prisma/client";
export const bookingResponsesSchema = z
.object({
email: z.string(),
attendeePhoneNumber: z.string().optional(),
name: z.union([
z.string(),
z.object({
@@ -45,6 +46,7 @@ export const seatedBookingDataSchema = z
responses: z
.object({
email: z.string(),
attendeePhoneNumber: z.string().optional(),
name: z.union([
z.string(),
z.object({
@@ -134,6 +136,7 @@ export class OutputBookingsService_2024_08_13 {
createdAt: databaseBooking.createdAt,
updatedAt: databaseBooking.updatedAt,
rating: databaseBooking.rating,
icsUid: databaseBooking.iCalUID,
};
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
@@ -229,6 +232,7 @@ export class OutputBookingsService_2024_08_13 {
createdAt: databaseBooking.createdAt,
updatedAt: databaseBooking.updatedAt,
rating: databaseBooking.rating,
icsUid: databaseBooking.iCalUID,
};
const bookingTransformed = plainToClass(RecurringBookingOutput_2024_08_13, booking, {
@@ -277,6 +281,7 @@ export class OutputBookingsService_2024_08_13 {
createdAt: databaseBooking.createdAt,
updatedAt: databaseBooking.updatedAt,
rating: databaseBooking.rating,
icsUid: databaseBooking.iCalUID,
};
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
@@ -386,6 +391,7 @@ export class OutputBookingsService_2024_08_13 {
createdAt: databaseBooking.createdAt,
updatedAt: databaseBooking.updatedAt,
rating: databaseBooking.rating,
icsUid: databaseBooking.iCalUID,
};
const parsed = plainToClass(GetRecurringSeatedBookingOutput_2024_08_13, booking, {
@@ -9,7 +9,6 @@ import { PlatformOAuthClient } from "@calcom/prisma/client";
@Injectable()
export class PlatformBookingsService {
constructor(
private readonly oAuthClientsUsersService: OAuthClientUsersService,
private readonly usersRepository: UsersRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly oAuthClientRepository: OAuthClientRepository
@@ -25,7 +24,7 @@ export class PlatformBookingsService {
if (!attendeeEmail.includes(platformClientId)) {
// note(Lauris): we need to do this when managed user is booking another managed user and the managed user doing the booking entered their email without oAuth client id
// or if one of the guests added are managed users without oAuth client id in their email.
const oAuthUserEmail = this.oAuthClientsUsersService.getOAuthUserEmail(platformClientId, attendeeEmail);
const oAuthUserEmail = OAuthClientUsersService.getOAuthUserEmail(platformClientId, attendeeEmail);
const oAuthUser = await this.usersRepository.findByEmail(oAuthUserEmail);
if (oAuthUser) {
return oAuthUserEmail;
@@ -43,6 +42,34 @@ export class PlatformBookingsService {
} else if (eventType?.teamId) {
oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId);
}
// Last resort check the hosts of the event-type
if (!oAuthClient && eventType?.teamId) {
oAuthClient = await this.oAuthClientRepository.getByEventTypeHosts(eventTypeId);
}
if (oAuthClient) {
return {
platformClientId: oAuthClient.id,
platformCancelUrl: oAuthClient.bookingCancelRedirectUri,
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
platformBookingUrl: oAuthClient.bookingRedirectUri,
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
};
}
return undefined;
}
async getOAuthClientParamsForEventType(eventType: {
userId: number | null | undefined;
teamId: number | null | undefined;
}) {
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 {
@@ -31,7 +31,7 @@ import {
} from "@calcom/platform-constants";
import { OFFICE_365_CALENDAR_ID, OFFICE_365_CALENDAR_TYPE } from "@calcom/platform-constants";
import { ICS_CALENDAR } from "@calcom/platform-constants/apps";
import { IcsFeedCalendarService } from "@calcom/platform-libraries";
import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
const CLIENT_REDIRECT_URI = "http://localhost:5555";
@@ -14,6 +14,7 @@ import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service";
import { OutlookService } from "@/ee/calendars/services/outlook.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -34,7 +35,7 @@ import {
Post,
Body,
} from "@nestjs/common";
import { ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { plainToClass } from "class-transformer";
import { Request } from "express";
@@ -68,6 +69,7 @@ export class CalendarsController {
@Post("/ics-feed/save")
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Save an ICS feed" })
async createIcsFeed(
@GetUser("id") userId: number,
@@ -79,12 +81,14 @@ export class CalendarsController {
@Get("/ics-feed/check")
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Check an ICS feed" })
async checkIcsFeed(@GetUser("id") userId: number): Promise<ApiResponse> {
return await this.icsFeedService.check(userId);
}
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Get("/busy-times")
@ApiOperation({
summary: "Get busy times",
@@ -113,6 +117,7 @@ export class CalendarsController {
@Get("/")
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Get all calendars" })
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
const calendars = await this.calendarsService.getCalendars(userId);
@@ -129,9 +134,16 @@ export class CalendarsController {
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Get("/:calendar/connect")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Get oAuth connect URL" })
@ApiQuery({
name: "redir",
required: false,
type: String,
description: "Redirect URL after successful calendar authorization.",
})
async redirect(
@Req() req: Request,
@Headers("Authorization") authorization: string,
@@ -193,6 +205,7 @@ export class CalendarsController {
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Post("/:calendar/credentials")
@ApiOperation({ summary: "Sync credentials" })
async syncCredentials(
@@ -221,6 +234,7 @@ export class CalendarsController {
@Get("/:calendar/check")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard, PermissionsGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Permissions([APPS_READ])
@ApiOperation({ summary: "Check a calendar connection" })
async check(@GetUser("id") userId: number, @Param("calendar") calendar: string): Promise<ApiResponse> {
@@ -245,6 +259,7 @@ export class CalendarsController {
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Post("/:calendar/disconnect")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Disconnect a calendar" })
@@ -0,0 +1,33 @@
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { Process, Processor } from "@nestjs/bull";
import { Logger } from "@nestjs/common";
import { Job } from "bull";
export const DEFAULT_CALENDARS_JOB = "default_calendars_job";
export const CALENDARS_QUEUE = "calendars";
export type DefaultCalendarsJobDataType = {
userId: number;
};
@Processor(CALENDARS_QUEUE)
export class CalendarsProcessor {
private readonly logger = new Logger(CalendarsProcessor.name);
constructor(public readonly calendarsService: CalendarsService) {}
@Process(DEFAULT_CALENDARS_JOB)
async handleEnsureDefaultCalendars(job: Job<DefaultCalendarsJobDataType>) {
const { userId } = job.data;
try {
// getCalendars calls getConnectedDestinationCalendarsAndEnsureDefaultsInDb from platform libraries
// which gets the calendars from third party providers and ensure default calendars are set in DB
await this.calendarsService.getCalendars(userId);
} catch (err) {
this.logger.error(`Failed to load default calendars of user with id: ${userId}`, {
userId,
err,
});
}
return;
}
}
@@ -5,7 +5,8 @@ import { BadRequestException, UnauthorizedException } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
import { symmetricEncrypt, CalendarService, symmetricDecrypt } from "@calcom/platform-libraries";
import { symmetricEncrypt, symmetricDecrypt } from "@calcom/platform-libraries";
import { CalendarService } from "@calcom/platform-libraries/app-store";
import { Credential } from "@calcom/prisma/client";
@Injectable()
@@ -29,7 +30,7 @@ export class AppleCalendarService implements CredentialSyncCalendarApp {
}
async checkIfCalendarConnected(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
const appleCalendarCredentials = await this.credentialRepository.getByTypeAndUserId(
const appleCalendarCredentials = await this.credentialRepository.findCredentialByTypeAndUserId(
APPLE_CALENDAR_TYPE,
userId
);
@@ -93,7 +94,7 @@ export class AppleCalendarService implements CredentialSyncCalendarApp {
}
if (!!hasCalendarWithGivenCredentials && !hasMatchingUsernameAndPassword) {
await this.credentialRepository.upsertAppCredential(
await this.credentialRepository.upsertUserAppCredential(
APPLE_CALENDAR_TYPE,
symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY || ""),
userId,
@@ -125,7 +126,7 @@ export class AppleCalendarService implements CredentialSyncCalendarApp {
user: { email: userEmail },
});
await dav?.listCalendars();
await this.credentialRepository.upsertAppCredential(APPLE_CALENDAR_TYPE, data.key, userId);
await this.credentialRepository.upsertUserAppCredential(APPLE_CALENDAR_TYPE, data.key, userId);
} catch (reason) {
throw new BadRequestException(`Could not add this apple calendar account: ${reason}`);
}
@@ -172,7 +172,7 @@ export class CalendarsService {
calendarType: keyof typeof APPS_TYPE_ID_MAPPING,
credentialId?: number | null
) {
const credential = await this.credentialsRepository.upsertAppCredential(
const credential = await this.credentialsRepository.upsertUserAppCredential(
calendarType,
key,
userId,
@@ -84,7 +84,10 @@ export class GoogleCalendarService implements OAuthCalendarApp {
}
async checkIfCalendarConnected(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
const gcalCredentials = await this.credentialRepository.findCredentialByTypeAndUserId(
"google_calendar",
userId
);
if (!gcalCredentials) {
throw new BadRequestException("Credentials for google_calendar not found.");
@@ -6,7 +6,8 @@ import { BadRequestException, UnauthorizedException, Logger } from "@nestjs/comm
import { Injectable } from "@nestjs/common";
import { SUCCESS_STATUS, ICS_CALENDAR_TYPE, ICS_CALENDAR } from "@calcom/platform-constants";
import { symmetricEncrypt, IcsFeedCalendarService } from "@calcom/platform-libraries";
import { symmetricEncrypt } from "@calcom/platform-libraries";
import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
@Injectable()
export class IcsFeedService implements ICSFeedCalendarApp {
@@ -51,7 +52,7 @@ export class IcsFeedService implements ICSFeedCalendarApp {
);
}
const credential = await this.credentialRepository.upsertAppCredential(
const credential = await this.credentialRepository.upsertUserAppCredential(
ICS_CALENDAR_TYPE,
data.key,
userId
@@ -74,7 +75,10 @@ export class IcsFeedService implements ICSFeedCalendarApp {
}
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
const icsFeedCredentials = await this.credentialRepository.getByTypeAndUserId(ICS_CALENDAR_TYPE, userId);
const icsFeedCredentials = await this.credentialRepository.findCredentialByTypeAndUserId(
ICS_CALENDAR_TYPE,
userId
);
if (!icsFeedCredentials) {
throw new BadRequestException("Credentials for Ics Feed calendar not found.");
@@ -71,7 +71,7 @@ export class OutlookService implements OAuthCalendarApp {
}
async checkIfCalendarConnected(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
const office365CalendarCredentials = await this.credentialRepository.getByTypeAndUserId(
const office365CalendarCredentials = await this.credentialRepository.findCredentialByTypeAndUserId(
"office365_calendar",
userId
);
@@ -10,6 +10,7 @@ import {
IsOptional,
ValidateNested,
Min,
Max,
IsInt,
IsEnum,
IsArray,
@@ -17,6 +18,8 @@ import {
IsNumber,
} from "class-validator";
import { MAX_SEATS_PER_TIME_SLOT } from "@calcom/platform-constants";
// note(Lauris): We will gradually expose more properties if any customer needs them.
// Just uncomment any below when requested. Go to bottom of file to see UpdateEventTypeInput.
@@ -414,9 +417,12 @@ export class UpdateEventTypeInput_2024_04_15 {
@ApiPropertyOptional()
afterEventBuffer?: number;
// @IsInt()
// @IsOptional()
// seatsPerTimeSlot?: number;
@IsInt()
@Min(1)
@Max(MAX_SEATS_PER_TIME_SLOT)
@IsOptional()
@ApiPropertyOptional()
seatsPerTimeSlot?: number;
// @IsBoolean()
// @IsOptional()
@@ -116,6 +116,7 @@ describe("Event types Endpoints", () => {
slug: "email",
required: true,
disableOnPrefill: false,
hidden: false,
};
const defaultResponseBookingFieldLocation = {
@@ -336,6 +337,67 @@ describe("Event types Endpoints", () => {
.expect(404);
});
it("should not be able to create phone-only event type", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Phone coding consultation",
slug: "phone-coding-consultation",
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
bookingFields: [
{
type: "email",
required: false,
label: "Email",
hidden: true,
},
{
type: "phone",
slug: "attendeePhoneNumber",
required: true,
label: "Phone number",
hidden: false,
},
],
};
const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
expect(response.body.error.message).toBe(
"checkIsEmailUserAccessible - Email booking field must be required and visible"
);
});
it("should not allow creating an event type with integration not installed on user", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class",
slug: "coding-class",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "zoom",
},
],
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
});
it("should create an event type", async () => {
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
type: "name",
@@ -1049,6 +1111,23 @@ describe("Event types Endpoints", () => {
.expect(404);
});
it("should not allow to update event type with integration not installed on user", async () => {
const body: UpdateEventTypeInput_2024_06_14 = {
locations: [
{
type: "integration",
integration: "office365-video",
},
],
};
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
});
it(`/GET/:id`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${eventType.id}`)
@@ -1191,7 +1270,14 @@ describe("Event types Endpoints", () => {
const expectedReturnSystemFields = [
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{
isDefault: true,
required: true,
slug: "email",
type: "email",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "radioInput",
@@ -1644,6 +1730,7 @@ describe("Event types Endpoints", () => {
slug: "email",
required: true,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
@@ -7,6 +7,7 @@ import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_202
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -29,7 +30,12 @@ import {
} from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
EVENT_TYPE_READ,
EVENT_TYPE_WRITE,
SUCCESS_STATUS,
VERSION_2024_06_14,
} from "@calcom/platform-constants";
import {
UpdateEventTypeInput_2024_06_14,
GetEventTypesQuery_2024_06_14,
@@ -45,8 +51,12 @@ import {
@DocsTags("Event Types")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-06-14\``,
description: `Must be set to ${VERSION_2024_06_14}`,
example: VERSION_2024_06_14,
required: true,
schema: {
default: VERSION_2024_06_14,
},
})
export class EventTypesController_2024_06_14 {
constructor(
@@ -58,19 +68,14 @@ export class EventTypesController_2024_06_14 {
@Post("/")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Create an event type" })
async createEventType(
@Body() body: CreateEventTypeInput_2024_06_14,
@GetUser() user: UserWithProfile
): Promise<CreateEventTypeOutput_2024_06_14> {
const transformedBody = await this.inputEventTypesService.transformAndValidateCreateEventTypeInput(
user.id,
user,
body
);
@@ -85,12 +90,7 @@ export class EventTypesController_2024_06_14 {
@Get("/:eventTypeId")
@Permissions([EVENT_TYPE_READ])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Get an event type" })
async getEventTypeById(
@Param("eventTypeId") eventTypeId: string,
@@ -140,12 +140,7 @@ export class EventTypesController_2024_06_14 {
@Patch("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Update an event type" })
async updateEventType(
@@ -155,7 +150,7 @@ export class EventTypesController_2024_06_14 {
): Promise<UpdateEventTypeOutput_2024_06_14> {
const transformedBody = await this.inputEventTypesService.transformAndValidateUpdateEventTypeInput(
body,
user.id,
user,
eventTypeId
);
@@ -170,12 +165,7 @@ export class EventTypesController_2024_06_14 {
@Delete("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Delete an event type" })
async deleteEventType(
@Param("eventTypeId") eventTypeId: number,
@@ -8,6 +8,7 @@ import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { ConferencingModule } from "@/modules/conferencing/conferencing.module";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
@@ -18,7 +19,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule],
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule, ConferencingModule],
providers: [
EventTypesRepository_2024_06_14,
EventTypesService_2024_06_14,
@@ -1,7 +1,7 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UsersService } from "@/modules/users/services/users.service";
import { Injectable } from "@nestjs/common";
import type { Prisma } from "@prisma/client";
import { InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
@@ -76,6 +76,22 @@ export class EventTypesRepository_2024_06_14 {
});
}
async getEventTypeByIdIncludeUsersAndTeam(eventTypeId: number) {
const eventType = await this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { users: true, team: true },
});
if (!eventType) {
return null;
}
return {
...eventType,
recurringEvent: eventType.recurringEvent as Prisma.JsonObject | null | undefined,
};
}
async getEventTypeByIdWithOwnerAndTeam(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
@@ -95,6 +111,18 @@ export class EventTypesRepository_2024_06_14 {
});
}
async getUserEventTypeBySlugWithOwnerAndTeam(userId: number, slug: string) {
return this.dbRead.prisma.eventType.findUnique({
where: {
userId_slug: {
userId: userId,
slug: slug,
},
},
include: { owner: true, team: true },
});
}
async deleteEventType(eventTypeId: number) {
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
}
@@ -14,6 +14,8 @@ import {
updateEventType,
getEventTypesPublic,
EventTypesPublic,
SystemField,
CustomField,
} from "@calcom/platform-libraries/event-types";
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
@@ -31,6 +33,9 @@ export class EventTypesService_2024_06_14 {
) {}
async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) {
if (body.bookingFields) {
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
}
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
const { destinationCalendar: _destinationCalendar, ...rest } = body;
@@ -78,6 +83,16 @@ export class EventTypesService_2024_06_14 {
await this.checkUserOwnsSchedule(userId, body.scheduleId);
}
checkHasUserAccessibleEmailBookingField(bookingFields: (SystemField | CustomField)[]) {
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
if (!isEmailFieldRequiredAndVisible) {
throw new BadRequestException(
"checkIsEmailUserAccessible - Email booking field must be required and visible"
);
}
}
async getEventTypeByUsernameAndSlug(
username: string,
eventTypeSlug: string,
@@ -219,6 +234,9 @@ export class EventTypesService_2024_06_14 {
body: Partial<InputEventTransformed_2024_06_14>,
user: UserWithProfile
) {
if (body.bookingFields) {
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
}
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
@@ -1,6 +1,7 @@
import { ConnectedCalendarsData } from "@/ee/calendars/outputs/connected-calendars.output";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, BadRequestException } from "@nestjs/common";
@@ -37,8 +38,6 @@ import {
} from "@calcom/platform-types";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
interface ValidationContext {
eventTypeId?: number;
seatsPerTimeSlot?: number | null;
@@ -51,14 +50,15 @@ interface ValidationContext {
export class InputEventTypesService_2024_06_14 {
constructor(
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
private readonly calendarsService: CalendarsService
private readonly calendarsService: CalendarsService,
private readonly conferencingService: ConferencingService
) {}
async transformAndValidateCreateEventTypeInput(
userId: UserWithProfile["id"],
user: UserWithProfile,
inputEventType: CreateEventTypeInput_2024_06_14
) {
await this.validateInputLocations(user, inputEventType.locations);
const transformedBody = this.transformInputCreateEventType(inputEventType);
await this.validateEventTypeInputs({
@@ -69,19 +69,21 @@ export class InputEventTypesService_2024_06_14 {
});
transformedBody.destinationCalendar &&
(await this.validateInputDestinationCalendar(userId, transformedBody.destinationCalendar));
(await this.validateInputDestinationCalendar(user.id, transformedBody.destinationCalendar));
transformedBody.useEventTypeDestinationCalendarEmail &&
(await this.validateInputUseDestinationCalendarEmail(userId));
(await this.validateInputUseDestinationCalendarEmail(user.id));
return transformedBody;
}
async transformAndValidateUpdateEventTypeInput(
inputEventType: UpdateEventTypeInput_2024_06_14,
userId: UserWithProfile["id"],
user: UserWithProfile,
eventTypeId: number
) {
await this.validateInputLocations(user, inputEventType.locations);
const transformedBody = await this.transformInputUpdateEventType(inputEventType, eventTypeId);
await this.validateEventTypeInputs({
@@ -93,10 +95,10 @@ export class InputEventTypesService_2024_06_14 {
});
transformedBody.destinationCalendar &&
(await this.validateInputDestinationCalendar(userId, transformedBody.destinationCalendar));
(await this.validateInputDestinationCalendar(user.id, transformedBody.destinationCalendar));
transformedBody.useEventTypeDestinationCalendarEmail &&
(await this.validateInputUseDestinationCalendarEmail(userId));
(await this.validateInputUseDestinationCalendarEmail(user.id));
return transformedBody;
}
@@ -128,12 +130,11 @@ export class InputEventTypesService_2024_06_14 {
} = inputEventType;
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const hasMultipleLocations = (locations || defaultLocations).length > 1;
const eventType = {
...rest,
length: lengthInMinutes,
locations: this.transformInputLocations(locations || defaultLocations),
bookingFields: this.transformInputBookingFields(bookingFields, hasMultipleLocations),
bookingFields: this.transformInputBookingFields(bookingFields),
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
@@ -182,15 +183,12 @@ export class InputEventTypesService_2024_06_14 {
: {};
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const hasMultipleLocations = !!(locations && locations?.length > 1);
const eventType = {
...rest,
length: lengthInMinutes,
locations: locations ? this.transformInputLocations(locations) : undefined,
bookingFields: bookingFields
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
: undefined,
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
@@ -220,10 +218,7 @@ export class InputEventTypesService_2024_06_14 {
return transformLocationsApiToInternal(inputLocations);
}
transformInputBookingFields(
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
hasMultipleLocations: boolean
) {
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
const internalFields: (SystemField | CustomField)[] = inputBookingFields
? transformBookingFieldsApiToInternal(inputBookingFields)
: [];
@@ -252,7 +247,28 @@ export class InputEventTypesService_2024_06_14 {
systemCustomRescheduleReasonField || systemAfterFieldRescheduleReason,
];
return [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
const bookingFields = [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
if (!this.hasEmailOrPhoneOnlySetup(bookingFields)) {
throw new BadRequestException(
"Booking fields validation failed: visible and required email or visible and required attendee phone field is needed."
);
}
return bookingFields;
}
hasEmailOrPhoneOnlySetup(bookingFields: (SystemField | CustomField)[]) {
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
const attendeePhoneNumberField = bookingFields.find(
(field) => field.type === "phone" && field.name === "attendeePhoneNumber"
);
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
const isAttendeePhoneNumberFieldRequiredAndVisible =
attendeePhoneNumberField?.required && !attendeePhoneNumberField?.hidden;
return isEmailFieldRequiredAndVisible || isAttendeePhoneNumberFieldRequiredAndVisible;
}
isUserCustomField(field: SystemField | CustomField): field is CustomField {
@@ -437,4 +453,20 @@ export class InputEventTypesService_2024_06_14 {
return;
}
async validateInputLocations(
user: UserWithProfile,
inputLocations: CreateEventTypeInput_2024_06_14["locations"] | undefined
) {
await Promise.all(
inputLocations?.map(async (location) => {
if (location.type === "integration") {
// cal-video is global, so we can skip this check
if (location.integration !== "cal-video") {
await this.conferencingService.checkAppIsValidAndConnected(user, location.integration);
}
}
}) ?? []
);
}
}
+4 -1
View File
@@ -94,7 +94,10 @@ export class GcalController {
@Permissions([APPS_READ])
@ApiOperation({ summary: "Check a calendar connection status" })
async check(@GetUser("id") userId: number): Promise<GcalCheckOutput> {
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
const gcalCredentials = await this.credentialRepository.findCredentialByTypeAndUserId(
"google_calendar",
userId
);
if (!gcalCredentials) {
throw new BadRequestException("Credentials for google_calendar not found.");
+3 -1
View File
@@ -2,6 +2,7 @@ import { GetMeOutput } from "@/ee/me/outputs/get-me.output";
import { UpdateMeOutput } from "@/ee/me/outputs/update-me.output";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -10,7 +11,7 @@ import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-us
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
import { Controller, UseGuards, Get, Patch, Body } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { PROFILE_READ, PROFILE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { userSchemaResponse } from "@calcom/platform-types";
@@ -21,6 +22,7 @@ import { userSchemaResponse } from "@calcom/platform-types";
})
@UseGuards(ApiAuthGuard, PermissionsGuard)
@DocsTags("Me")
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
export class MeController {
constructor(
private readonly usersRepository: UsersRepository,
@@ -1,6 +1,7 @@
import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-access-token.output";
import { ProviderVerifyClientOutput } from "@/ee/provider/outputs/verify-client.output";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
@@ -16,7 +17,7 @@ import {
UnauthorizedException,
UseGuards,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiExcludeController, ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -24,6 +25,9 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
path: "/v2/provider",
version: API_VERSIONS_VALUES,
})
// note(Lauris): this controller is used by useOAuthClient and useOAuthFlow internal hooks and if customer
// wants to know if access token is expired then the flow is to make request and then receive message it expired.
@ApiExcludeController()
@DocsTags("Platform / Cal Provider")
export class CalProviderController {
constructor(private readonly oauthClientRepository: OAuthClientRepository) {}
@@ -53,6 +57,7 @@ export class CalProviderController {
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Verify an access token" })
@ApiHeader(ACCESS_TOKEN_HEADER)
async verifyAccessToken(
@Param("clientId") clientId: string,
@GetUser() user: UserWithProfile
@@ -1,5 +1,6 @@
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
import { VERSION_2024_06_14, VERSION_2024_06_11 } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -39,15 +40,14 @@ import {
@DocsTags("Schedules")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-06-11\``,
required: true,
})
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
description: `Must be set to ${VERSION_2024_06_11}`,
example: VERSION_2024_06_11,
required: true,
schema: {
default: VERSION_2024_06_11,
},
})
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
export class SchedulesController_2024_06_11 {
constructor(private readonly schedulesService: SchedulesService_2024_06_11) {}
@@ -1,6 +1,9 @@
import { Injectable } from "@nestjs/common";
import { transformApiScheduleOverrides, transformApiScheduleAvailability } from "@calcom/platform-libraries";
import {
transformApiScheduleOverrides,
transformApiScheduleAvailability,
} from "@calcom/platform-libraries/schedules";
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
@@ -0,0 +1,58 @@
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
import { Request } from "express";
import { ERROR_STATUS } from "@calcom/platform-constants";
import {
CalendarAppError,
CalendarAppDelegationCredentialInvalidGrantError,
CalendarAppDelegationCredentialError,
CalendarAppDelegationCredentialConfigurationError,
CalendarAppDelegationCredentialClientIdNotAuthorizedError,
CalendarAppDelegationCredentialNotSetupError,
} from "@calcom/platform-libraries/app-store";
import { Response } from "@calcom/platform-types";
type CalendarError =
| CalendarAppError
| CalendarAppDelegationCredentialInvalidGrantError
| CalendarAppDelegationCredentialError
| CalendarAppDelegationCredentialConfigurationError
| CalendarAppDelegationCredentialNotSetupError
| CalendarAppDelegationCredentialClientIdNotAuthorizedError;
@Catch(
CalendarAppError,
CalendarAppDelegationCredentialInvalidGrantError,
CalendarAppDelegationCredentialError,
CalendarAppDelegationCredentialConfigurationError,
CalendarAppDelegationCredentialClientIdNotAuthorizedError,
CalendarAppDelegationCredentialNotSetupError
)
export class CalendarServiceExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger("CalendarServiceExceptionFilter");
catch(exception: CalendarError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
response.setHeader("X-Request-Id", requestId.toString());
this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, {
exception,
body: request.body,
headers: filterReqHeaders(request.headers),
url: request.url,
method: request.method,
requestId,
});
response.status(400).json({
status: ERROR_STATUS,
timestamp: new Date().toISOString(),
path: request.url,
error: { code: exception.name, message: exception.message },
});
}
}
@@ -14,8 +14,8 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const statusCode = exception.getStatus();
const requestId = request.headers["X-Request-Id"];
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
response.setHeader("X-Request-Id", requestId.toString());
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
exception,
body: request.body,
@@ -40,7 +40,8 @@ export class PrismaExceptionFilter implements ExceptionFilter {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const requestId = request.headers["X-Request-Id"];
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
response.setHeader("X-Request-Id", requestId.toString());
this.logger.error(`PrismaError: ${error.message}`, {
error,
@@ -16,33 +16,79 @@ export class TRPCExceptionFilter implements ExceptionFilter {
const request = ctx.getRequest<Request>();
let statusCode = 500;
let errorMessage = exception.message;
switch (exception.code) {
case "UNAUTHORIZED":
statusCode = 401;
errorMessage = "You are not authorized to access this resource";
break;
case "FORBIDDEN":
statusCode = 403;
errorMessage = "You don't have necessary permissions to access this resource";
break;
case "NOT_FOUND":
statusCode = 404;
errorMessage = "The requested resource was not found";
break;
case "INTERNAL_SERVER_ERROR":
statusCode = 500;
errorMessage = "An unexpected error occurred on the server. Please try again later";
break;
case "BAD_REQUEST":
statusCode = 400;
errorMessage = "Bad request: Please check your request data and try again";
break;
case "CONFLICT":
statusCode = 409;
errorMessage = "Could not process the request due to a resource conflict with the current state";
break;
case "TOO_MANY_REQUESTS":
statusCode = 429;
errorMessage = "You have exceeded the allowed number of requests";
break;
case "PAYLOAD_TOO_LARGE":
statusCode = 413;
errorMessage = "The request payload is too large";
break;
case "CLIENT_CLOSED_REQUEST":
statusCode = 499;
errorMessage =
"The client closed the connection before the server could finish processing the request";
break;
case "METHOD_NOT_SUPPORTED":
statusCode = 405;
errorMessage = "The requested method is not supported for this resource";
case "NOT_IMPLEMENTED":
statusCode = 501;
errorMessage = "The requested method is not implemented on the server";
break;
case "PRECONDITION_FAILED":
statusCode = 412;
errorMessage =
"The server does not meet one of the preconditions that the requester put on the request";
break;
case "TIMEOUT":
statusCode = 408;
errorMessage = "The request took too long to complete. Please try again later";
break;
case "UNPROCESSABLE_CONTENT":
statusCode = 422;
errorMessage =
"The server was unable to process the request because the request payload is semantically incorrect or because the request is syntactically incorrect";
break;
case "PARSE_ERROR":
statusCode = 400;
errorMessage = "The request could not be parsed due to invalid syntax.";
break;
default:
statusCode = 500;
errorMessage = exception.message;
break;
}
const requestId = request.headers["X-Request-Id"];
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
response.setHeader("X-Request-Id", requestId.toString());
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
exception,
@@ -57,7 +103,7 @@ export class TRPCExceptionFilter implements ExceptionFilter {
status: ERROR_STATUS,
timestamp: new Date().toISOString(),
path: request.url,
error: { code: exception.name, message: exception.message },
error: { code: exception.name, message: errorMessage },
});
}
}
@@ -15,7 +15,8 @@ export class ZodExceptionFilter implements ExceptionFilter {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const requestId = request.headers["X-Request-Id"];
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
response.setHeader("X-Request-Id", requestId.toString());
this.logger.error(`ZodError: ${error.message}`, {
error,
+60
View File
@@ -0,0 +1,60 @@
import { ApiHeaderOptions } from "@nestjs/swagger";
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
export const OPTIONAL_X_CAL_CLIENT_ID_HEADER: ApiHeaderOptions = {
name: X_CAL_CLIENT_ID,
description: "For platform customers - OAuth client ID",
required: false,
};
export const OPTIONAL_X_CAL_SECRET_KEY_HEADER: ApiHeaderOptions = {
name: X_CAL_SECRET_KEY,
description: "For platform customers - OAuth client secret key",
required: false,
};
export const X_CAL_CLIENT_ID_HEADER: ApiHeaderOptions = {
name: X_CAL_CLIENT_ID,
description: "For platform customers - OAuth client ID",
required: false,
};
export const X_CAL_SECRET_KEY_HEADER: ApiHeaderOptions = {
name: X_CAL_SECRET_KEY,
description: "For platform customers - OAuth client secret key",
required: false,
};
export const OPTIONAL_API_KEY_HEADER: ApiHeaderOptions = {
name: "Authorization",
description:
"For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
required: false,
};
export const API_KEY_HEADER: ApiHeaderOptions = {
name: "Authorization",
description: "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
required: true,
};
export const API_KEY_OR_ACCESS_TOKEN_HEADER: ApiHeaderOptions = {
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
required: true,
};
export const OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER: ApiHeaderOptions = {
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
required: false,
};
export const ACCESS_TOKEN_HEADER: ApiHeaderOptions = {
name: "Authorization",
description: "value must be `Bearer <token>` where `<token>` is managed user access token",
required: true,
};
+10 -9
View File
@@ -2,19 +2,20 @@ import { IncomingHttpHeaders } from "http";
export function filterReqHeaders(headers: IncomingHttpHeaders): Partial<IncomingHttpHeaders> {
return {
"Content-Type": headers["content-type"],
"User-Agent": headers["user-agent"],
"Content-Type": headers["Content-Type"] ?? headers["content-type"],
Authorization: headers["Authorization"] ?? headers["authorization"],
"X-Cal-Client-Id": headers["X-Cal-Client-Id"] ?? headers["x-cal-client-id"],
"Cal-Api-Version": headers["Cal-Api-Version"] ?? headers["cal-api-version"],
"X-Request-Id": headers["X-Request-Id"] ?? headers["x-request-id"],
// IP-related headers
"User-Agent": headers["User-agent"] ?? headers["user-agent"],
"X-Forwarded-For": headers["x-forwarded-for"],
"X-Forwarded-Host": headers["x-forwarded-host"],
"CF-Connecting-IP": headers["cf-connecting-ip"],
"CloudFront-Viewer-Address": headers["cloudfront-viewer-address"],
// Vercel headers
"x-vercel-id": headers["x-vercel-id"],
"x-vercel-deployment-url": headers["x-vercel-deployment-url"],
"x-vercel-country": headers["x-vercel-country"],
"x-vercel-region": headers["x-vercel-region"],
Host: headers["host"],
"X-Vercel-Id": headers["x-vercel-id"],
"X-Vercel-Deployment-Url": headers["x-vercel-deployment-url"],
"X-Vercel-Country": headers["x-vercel-country"],
"X-Vercel-Region": headers["x-vercel-region"],
Host: headers["Host"] ?? headers["host"],
};
}
+152
View File
@@ -0,0 +1,152 @@
import { Injectable, Logger as NestLogger, Scope } from "@nestjs/common";
// 1. Define an interface for the settings
interface IMyLoggerSettings {
minLevel: number; // Example: 0=debug, 1=info, 2=warn, 3=error
displayTimestamp: boolean;
logFormat: "pretty" | "json" | "simple";
// Add any other settings you need, mimicking tslog or your own requirements
// e.g., name?: string; displayFunctionName?: boolean; etc.
}
// Define log level constants (optional but recommended for readability)
const LogLevel = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
};
/**
* This logger acts as a bridge between Nest.js Logger and log calls originating
* from platform libraries. It forwards logs to NestLogger, allowing centralization
* (e.g., sending to Axiom) and adds an optional prefix for context.
*/
@Injectable({ scope: Scope.TRANSIENT }) // TRANSIENT ensures getSubLogger provides truly independent instances if needed elsewhere
export class Logger {
// Use NestLogger for the actual logging output
private readonly nestLogger = new NestLogger("LoggerBridge");
// Prefix to add to messages for this instance
private prefix = "";
// Add a public `settings` property, typed with the interface
public settings: IMyLoggerSettings;
// Define default settings
private static readonly defaultSettings: IMyLoggerSettings = {
minLevel: process?.env?.LOGGER_BRIDGE_LOG_LEVEL
? Number(process.env.LOGGER_BRIDGE_LOG_LEVEL)
: LogLevel.INFO, // Default to INFO level
displayTimestamp: true,
logFormat: "pretty",
};
constructor(userSettings?: Partial<IMyLoggerSettings>) {
// Merge default settings with user-provided settings
// User settings override defaults
this.settings = {
...Logger.defaultSettings,
...userSettings,
};
}
/**
* Creates a new LoggerBridge instance with a specific prefix.
* Useful for providing context from different parts of a library.
* @param options - Configuration for the sub-logger.
* @param options.prefix - An array of strings to join as the log prefix.
* @returns A new LoggerBridge instance with the specified prefix.
*/
getSubLogger(options: { prefix?: string[] }): Logger {
const newLogger = new Logger();
// Set the prefix for the *new* instance
newLogger.prefix = options?.prefix?.join(" ") ?? "";
return newLogger;
}
// --- Public logging methods ---
info(...args: any[]) {
this.settings.minLevel <= 1 && this.logInternal("log", ...args);
}
warn(...args: any[]) {
this.settings.minLevel <= 2 && this.logInternal("warn", ...args);
}
error(...args: any[]) {
this.settings.minLevel <= 3 && this.logInternal("error", ...args);
}
debug(...args: any[]) {
this.settings.minLevel === 0 && this.logInternal("debug", ...args);
}
trace(...args: any[]) {
this.settings.minLevel === 0 && this.logInternal("verbose", ...args);
}
fatal(...args: any[]) {
// Prepend FATAL: to the message and log as error
const fatalMessage = `fatal: ${this.formatArgsAsString(args)}`;
this.settings.minLevel <= 3 && this.logInternal("error", fatalMessage);
}
silly(...args: any[]) {
const sillyMessage = `silly: ${this.formatArgsAsString(args)}`;
this.settings.minLevel === 0 && this.logInternal("verbose", sillyMessage);
}
// --- Internal logging implementation ---
/** Formats arguments into a single string */
private formatArgsAsString(args: any[]): string {
return args
.map((arg) => {
if (typeof arg === "string") {
return arg;
}
// Attempt to stringify non-string arguments
try {
return JSON.stringify(arg);
} catch (e) {
return "[Unserializable Object]";
}
})
.join(" "); // Use space as separator, adjust if needed
}
/** Central method to forward logs to NestLogger */
private logInternal(level: "log" | "warn" | "error" | "debug" | "verbose", ...args: any[]) {
try {
// Format message from potentially multiple arguments
const formattedMessage = this.formatArgsAsString(args);
// Prepend the prefix if it exists
const message = this.prefix ? `${this.prefix} ${formattedMessage}` : formattedMessage;
// Call the corresponding NestLogger method
switch (level) {
case "log":
this.nestLogger.log(message);
break;
case "warn":
this.nestLogger.warn(message);
break;
case "error":
this.nestLogger.error(message);
break;
case "debug":
this.nestLogger.debug(message);
break;
case "verbose":
this.nestLogger.verbose(message);
break;
}
} catch (err) {
this.nestLogger.error(
`Could not bridge log message. Error: ${err instanceof Error ? err.message : String(err)}`
);
}
}
}
@@ -2,7 +2,7 @@ export const defaultBookingResponses = {
name: "unknown",
email: "unknown",
guests: [],
rescheduleReason: "unknown",
rescheduledReason: "unknown",
};
export const defaultBookingMetadata = { videoCallUrl: "unknown" };
@@ -21,13 +21,23 @@ export class ResponseInterceptor implements NestInterceptor {
const { statusCode } = response;
const responseTime = Date.now() - startTime;
let jsonBodyString = "{}";
try {
if (data && typeof data === "object") {
jsonBodyString = JSON.stringify(data);
}
} catch (err) {
this.logger.error("Could not parse request body");
}
this.logger.log("Outgoing Response", {
requestId,
method,
url,
statusCode,
responseTime,
responseBody: data,
responseBody: jsonBodyString,
timestamp: new Date().toISOString(),
});
})
@@ -11,13 +11,22 @@ export class RequestIdMiddleware implements NestMiddleware {
const requestId = uuid();
req.headers["X-Request-Id"] = requestId;
const { method, headers, body: requestBody, baseUrl } = req;
let jsonBodyString = "{}";
try {
if (requestBody && typeof requestBody === "object") {
jsonBodyString = JSON.stringify(requestBody);
}
} catch (err) {
this.logger.error("Could not parse request body");
}
this.logger.log("Incoming Request", {
requestId,
method,
url: baseUrl,
headers: filterReqHeaders(headers),
requestBody,
requestBody: jsonBodyString,
timestamp: new Date().toISOString(),
});
@@ -1,4 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { RefreshApiKeyInput } from "@/modules/api-keys/inputs/refresh-api-key.input";
import { RefreshApiKeyOutput } from "@/modules/api-keys/outputs/refresh-api-key.output";
import { ApiKeysService } from "@/modules/api-keys/services/api-keys.service";
@@ -23,12 +24,7 @@ export class ApiKeysController {
summary: "Refresh API Key",
description: `Generate a new API key and delete the current one. Provide API key to refresh as a Bearer token in the Authorization header (e.g. "Authorization: Bearer <apiKey>").`,
})
@ApiHeader({
name: "Authorization",
description: "Bearer <apiKey>",
required: true,
schema: { type: "string" },
})
@ApiHeader(API_KEY_HEADER)
@Post("/refresh")
@HttpCode(HttpStatus.OK)
async refresh(
@@ -31,6 +31,15 @@ export class AppsRepository {
});
}
async deleteTeamAppCredentials(credentialIdsToDelete: number[], teamId: number) {
return this.dbWrite.prisma.credential.deleteMany({
where: {
id: { in: credentialIdsToDelete },
teamId,
},
});
}
async createTeamAppCredential(type: string, key: Prisma.InputJsonValue, teamId: number, appId: string) {
return this.dbWrite.prisma.credential.create({
data: {
@@ -8,6 +8,7 @@ import { CredentialsRepository } from "@/modules/credentials/credentials.reposit
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisService } from "@/modules/redis/redis.service";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
@@ -24,6 +25,7 @@ import { Module } from "@nestjs/common";
AtomsRepository,
UsersService,
SchedulesRepository_2024_06_11,
RedisService,
],
exports: [EventTypesAtomService],
controllers: [AtomsController],

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