Compare commits

...
324 Commits
Author SHA1 Message Date
GitHub Actions 76d5cefe78 chore: release v4.9.6
Post release / release (push) Canceled after 0s
2025-02-04 11:25:00 +00:00
Alex van Andel 252ce5dec8 fix: some modifications to logging event detail input (#19070)
* some modifications to logging event detail input

* add descriptive name
2025-02-04 11:03:43 +00:00
Hariom BalharaandHariom Balhara 98d9cd105b Revert "feat: show ooo forwarding and emoji when not available (#18054)" (#19073)
This reverts commit a0f6e50d5b.

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
2025-02-04 06:38:14 +00:00
GitHub Actions 240f27ef4c chore: release v4.9.5
Post release / release (push) Canceled after 0s
2025-02-03 22:32:55 +00:00
Eunjae Lee ab96707a0e fix: clean up fields columns on /insights/routing (#19062)
* fix: clean up fields columns on /insights/routing

* remove duplicates

* move implementation to the backend
2025-02-03 21:13:28 +00:00
Eunjae Lee 7f86f69528 fix: remove unused attribute columns for /insights/routing (#19059) 2025-02-03 14:22:56 +00:00
Eunjae Lee 93da9c547a fix: remove padding for custom data table header (#19058) 2025-02-03 13:52:07 +00:00
Joe Au-Yeung 4475509225 chore: Salesforce - Add logging (#19047) 2025-02-03 17:23:52 +05:30
Lauris Skraucis 1043085741 refactor: v2 api e2e performance (#19028)
* refactor: use unique names in e2e tests

* refactor: version number in tests

* refactor: use unique names in e2e tests

* refactor: use unique names in e2e tests

* refactor: test cleanup

* refactor: increase e2e maxWorkers to 8

* refactor: randomNumber -> randomString

* chore: add local e2e command
2025-02-03 11:40:37 +01:00
amrit 4fd5b3133d fix small alignment issue (#19049) 2025-02-03 07:55:55 +00:00
Benny Joo ca650babaf fix: username is visible in private booking link (#19054) 2025-02-02 22:36:24 -07:00
Benny Joo f0dc3bba17 chore: tech debt clearing - getServerSession has a dead arg: res (#19053)
* remove res from arg list in getServerSession

* remove res from all usages of getServerSession
2025-02-03 00:12:42 -05:00
Benny Joo 15d6839985 fix: semi-colon in /teams route UI (#19052) 2025-02-03 04:43:36 +00:00
Anik Dhabal Babu c20b3419d4 fix: Global e2e failure (#19055)
* fix: flaky test organization.spec

* fix: global e2e failure
2025-02-03 04:21:49 +00:00
Calcom BotandLingo.dev b280e78da8 feat: update translations via @lingodotdev (#19045)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-01-31 20:58:29 +00:00
Benny Joo 3ca44cff0c chore: migrate /apps/[slug]/setup (#19039)
* chore: remove unused files in /future dir

* chore: migrate /apps/[slug]/setup
2025-01-31 21:13:56 +01:00
Benny Joo 8dd05dda70 perf: move /teams route to (main-nav) route group (#19037) 2025-01-31 21:12:12 +01:00
Benny Joo dde76ed74d fix: add missing metadata to /auth/setup (#19035)
* fix: invalid iCal download link must not crash booking page

* fix: add missing metadata to /auth/setup
2025-01-31 21:11:56 +01:00
Benny Joo 564cc3a394 perf: remove WithLayout from more App Router routes (#19036) 2025-01-31 19:48:58 +01:00
Eunjae Lee 7bbacca32a chore: remove dead code regarding insights (#19022) 2025-01-31 19:23:40 +01:00
Udit Takkar b7df700771 chore: add logs for debugging (#19012) 2025-01-31 16:54:22 +00:00
Eunjae Lee fdc8e084ea fix: stats on /insights/routing (#18985)
* fix: replace filter implementation on /insights/routing

* remove unused functions

* fix type error

* refactor: extract insights parameters

* remove FiltersProvider
2025-01-31 17:16:53 +01:00
Eunjae LeeandPeer Richelsen 195e645f24 fix: update style of data table's column resizer (#19026)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-31 15:09:59 +00:00
Udit Takkar 82b1e681de fix: add check for cal video (#19020)
* fix: add check for cal video

* chore: pass location
2025-01-31 15:00:55 +00:00
Morgan 024d47cbc0 chore: add default form field support for booker embed routing url (#19023)
* chore: add default forn field support for booker embed routing url

* improvements
2025-01-31 10:30:26 +00:00
Somay Chauhan 56cd9cef8b perf: don't fetch all the hosts #18319 followup (#19021)
* perf: don't fetch all the hosts #18319 followup

* undo for user events

* undo for regular user events

* fix: limitHostsToThree -> hostsLimit

* accepting hostLimit prop in <Booker />

* Update booking.tsx

* hostLimit -> hostsLimit
2025-01-31 12:19:21 +02:00
8d2dc0bbe5 feat: setting to enable weights on attribute in route (#18592)
* add UI to Routes

* add attributeIdForWeights to route json

* fixes and clean up

* clean up onChangeAttributeIdForWeights

* fix type errors

* fix attribute weights in getLuckyUser

* adjust tests

* fixes for attribute rule changes

* fix type error

* Keep weighted attributes logic outside Route (#18974)

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>

* use findUnique

* fix test

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
2025-01-30 11:44:42 -05:00
a0f6e50d5b feat: show ooo forwarding and emoji when not available (#18054)
* feat: show ooo when not available

* fix: send correct username for toUser if user belongs to org

* enrich ooo user with profile in getUserAvailability

* fix: set correct ooo slots

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-01-30 16:36:27 +01:00
Eunjae LeeandPeer Richelsen 7ec928f5c2 feat: keep column visibility on query params (#19013)
* feat: keep column visibility on query params

* fix resizing logic

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-30 13:19:03 +00:00
Calcom BotandLingo.dev 06729cfc9a feat: update translations via @lingodotdev (#19015)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-01-30 13:12:49 +00:00
Anik Dhabal Babu cdc3c49b23 fix: few flaky e2e tests (#19010)
* fix: flaky test organization.spec

* fix: few flaky tests

* Update apps.ts

* Update apps.ts

* Update testUtils.ts
2025-01-30 17:08:36 +05:30
Anik Dhabal Babu 962f692e29 fix: Disable sms/whatsapp action for trialing teams (#18896)
* fix: flaky test organization.spec

* Workflow doesn't save when setting a WhatsApp workflow

* update
2025-01-30 08:55:13 +00:00
sean-brydonandPeer Richelsen 783f10ffb5 ui playground (#18995)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-29 18:53:38 +00:00
Alex van Andel b6a7a8f75a fix: modify error handling of incompleteBookingWriteToRecord (#18998)
* fix: modify error handling of incompleteBookingWriteToRecord

* fix: TS error?

* fix: TS error attempt #2?
2025-01-29 17:12:33 +00:00
1e610ea684 perf: don't fetch all the hosts (#18319)
* perf: don't fetch all the hosts

* fix: type err

* use appschema only when necessary

* Variable casing change and type fixes

* Another missing ts fix

* chore: update variable name

* chore: remove from personal event types

* chore: remove limitHostsToThree

* chore: make it a variable

* chore: move it below

* chore: change name to firstThreeHosts

* chore: update to first three users

* fix: type error

* chore: add firstThreeHosts

* fix: type error

* fix: type error

* fix: support users

* chore: update metadata

* fix: API v2 build error

* chore: change var name

* chore: remove cursorrules

* chore: undo API v2 changes

* chore: add type

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-01-29 19:05:54 +02:00
Eunjae Lee 5c98861f6f fix: re-design DateRangeFilter (#18924)
* fix: re-design DateRangeFilter

* update style

* adjust padding
2025-01-29 16:41:53 +00:00
Somay Chauhan be1b9d1a79 feat: skip confirm step in booker (#18773)
* feat: skip confirm step

* better naming

* disable on loading

* feat: added cloudflare turnstile captcha to booker

* Update Booker.tsx

* Update AvailableTimeSlots.tsx

* made optional to fix type errors

* Update Booker.tsx

* Update getBookingResponsesSchema.ts

* Update Booker.tsx

* fixed failing tests

* added tests

* fix: fixed failing embed tests
2025-01-29 12:56:23 -03:00
Hariom Balhara ac8b4a2861 fix: Wrong username with -{DOMAIN} in case of an autoAcceptEmail when synced through SCIM (#18384)
* Okta fixes

* Fix username and name in case of sync from Okta

* Fix username and name in case of sync from Okta

* Add test

* cleanup code and add tests
2025-01-29 12:05:27 -03:00
sean-brydon 33903f1a03 fix:clear cancellation reason on other select (#18982) 2025-01-29 14:12:19 +00:00
Syed Ali Shahbaz 92678dc042 fix: eventtype null cant be booked error (#18975)
* fix err msg and add more description

* improvements

* fix cause

* add warn logs for when failing to specific limit checks

* typefix

* safestringify log

* improve error

* test fix and log min booking notice

* adds out of bounds error code

* test fix

* fix
2025-01-29 13:00:51 +00:00
8615ccb1aa feat: routing forms integration for booker atom (#18726)
* update booker atom for routing form

* remove logs

* hide routing form properties from docs

* fixup

* add comments to explain why certain fields are not defined for routing forms in v2

* chore: routing for handler return team and org id

* bump libraries platform

* fixup! bump libraries platform

* chore: get routing form params to embed

* logs to remove

* fix import path

* chore: handle routing form params in booker embed

* wip need router api v2 endpoint

* resolve merge conflicts

* fixup

* fixup! Merge branch 'main' into remove-redirect-on-success-for-platform

* fixup! fixup! Merge branch 'main' into remove-redirect-on-success-for-platform

* router atom

* fix libraries version

* cleanup

* remove console logs

* bump libraries

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-01-29 13:51:58 +02:00
Benny Joo 9608669f98 chore: migrate /routing-forms to app router (#18976) 2025-01-29 07:36:06 +00:00
Alex van Andel f8fa8ff294 fix: Exclude .next from TS & fixup ImageResponse types (#18972) 2025-01-29 02:33:59 +00:00
Joe Au-YeungandOmar López 2a5e93b015 chore: Salesforce - Add additional logging (#18959)
Co-authored-by: Omar López <zomars@me.com>
2025-01-28 14:08:31 -08:00
Benny Joo e23ab02c23 chore: migrate /auth/forgot-password pages (#18945) 2025-01-28 21:51:14 +00:00
Benny Joo 8205431640 migrate /auth/sso pages (#18951) 2025-01-28 21:49:06 +00:00
Benny Joo 9cec970943 fix: invalid iCal download link must not crash booking page (#18966) 2025-01-28 18:16:32 -03:00
Benny Joo 296a09dc7b chore: move /auth/verify out of /future (#18952) 2025-01-28 21:00:07 +00:00
Benny Joo 377d4e5bfd migrate / index page (#18960) 2025-01-28 20:33:00 +01:00
Benny Joo ce4c9dfc9a chore: migrate /auth/saml-idp (#18943) 2025-01-28 20:32:12 +01:00
438a73b0cd feat: Setting for rescheduling past bookings (#18358)
* feat: Setting for rescheduling past bookings

* fix stuff

* rename column

* add e2e test

* fix column name

* fix typo

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-01-28 19:25:41 +00:00
Joe Au-Yeung 39891ee308 fix: Tasker CRM - parse full app metadata (#18961)
* Parse correct app data

* Add error catching
2025-01-28 15:57:55 -03:00
Syed Ali Shahbaz 2646f57da4 fix: API V1 cancellationReason fallback/default and doc update (#18946)
* adds fallback/default cancellationReason

* add doc update
2025-01-28 15:52:37 -03:00
Eunjae Lee f8a443557a fix: show sort state on column header (#18950) 2025-01-28 18:50:16 +01:00
Carina WollendorferandCarinaWolli fa82ecd2a5 fix: admin should see all routing forms (Router position) (#18931)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-01-28 17:29:44 +00:00
Eunjae Lee 8f8af6536c fix: remove stale cache for column visibility state (#18944) 2025-01-28 16:37:06 +00:00
Benny Joo 9ad45bb477 chore: migrate /auth/setup (#18941) 2025-01-28 11:34:52 -05:00
Benny Joo a1e4959fb7 chore: migrate /auth/verify-email-change (#18942) 2025-01-28 11:34:39 -05:00
sean-brydon e5946db179 allow nullable reason (#18939) 2025-01-28 14:45:44 +00:00
Benny Joo f2d12a4c21 fix: safe-parse query schema in signup page to avoid zod throw (#18927) 2025-01-28 10:15:19 +01:00
sean-brydon d8c9d2ad94 fix managed events (#18933) 2025-01-28 08:53:20 +00:00
sean-brydon cc94fb4a4b disable cancel if no internal note is selected (#18926) 2025-01-28 08:53:07 +00:00
Benny Joo cd413cf640 chore: remove unused getStaticProps file (#18928) 2025-01-27 22:36:46 +00:00
Joe Au-Yeung 7d4abea9b4 Add uid and event responses to tasker calendar event (#18930) 2025-01-27 22:29:48 +00:00
e0527587c1 feat: routing forms for api v2 (#18885)
* init router endpoint for v2

* fixup fixup

* resolve merge conflicts

* update libraries

* update router controller

* make router endpoint public

* chore: refactor router endpoint and add team/org id

* cleanup

* chore: Only import Sentry when needed (#18882)

* perf: prerender routes in main navigation (#18883)

* chore: migrate /apps/categories pages (#18871)

* migrate apps/categories pages

* remove pages dir

* chore: app router - /payment pages (#18152)

* remove Head from payment page comp

* remove route from pages router

* move out of future folder

* add to config.matcher

* remove unneeded code

* update middleware

* fix

* fix

* perf: replace `getServerSessionForAppDir` usages with our original `getServerSession` function (#18897)

* chore: migrate /apps/installation pages (#18870)

* migrate apps/installation pages

* fix test

* chore: migrate `/auth/logout` (#18901)

* migrate /auth/verify-email page

* update middleware

* migrate /auth/logout page

* expose router endpoint

* small refactor pageprops not necessary

* sentry is already tracking performance

* reuse logic

* fixup! reuse logic

* fixup! fixup! reuse logic

* fixup! fixup! fixup! reuse logic

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-01-27 19:02:11 +00:00
Benny Joo ae51c8b599 fix: make workflow page not static (#18925)
* fix: make workflow page not static

* fix

* fix
2025-01-27 18:05:15 +00:00
Calcom BotandLingo.dev 7fb60fe5f7 feat: update translations via @lingodotdev (#18922)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-01-27 18:04:15 +00:00
Joe Au-Yeung 4a93511f50 fix: Add Stripe past_due to active team subscription status (#18917) 2025-01-27 16:17:09 +00:00
Eunjae Lee 61a2a3ef5a chore: clean up ColumnFilterType (#18915) 2025-01-27 17:13:10 +01:00
Hariom BalharaandPeer Richelsen f06bac7129 fix: Missing salesforce options in Routing Form (#18884)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-27 14:49:13 +00:00
Anik Dhabal Babu 0ecc90ccb4 fix: Checkly flake (#18904)
* fix: flaky test organization.spec

* fix: checkly flake
2025-01-27 13:35:07 +00:00
Benny JooandPeer Richelsen ee31633e2b perf: layout persistence and reduced bundle size (#18889)
* create /(main-nav)/layout.tsx file that uses Shell

* Add ShellMainAppDir that clones ShellMain but with client/server separation

* event-types, availability, bookings

* update pagesAndRewritePaths logic

* fix

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-27 11:08:47 +00:00
Eunjae Lee 8be2ea8117 chore: split data-table/components/filters/index.tsx (#18679)
* chore: split data-table/components/filters/index.tsx

* fix type error

* fix type error
2025-01-27 10:35:51 +00:00
Benny Joo df4359e3e5 migrate /auth/login page (#18903) 2025-01-27 10:23:51 +00:00
Eunjae Lee d3ef807af5 feat: add command list to DataTable column header (#18867)
* feat: add command list to DataTable column header

* disable hiding and sorting for some columns

* disable hiding & sorting
2025-01-27 10:20:59 +00:00
Syed Ali ShahbazandMorgan Vernay 41f63582b2 feat: Add booking and user creation source (#18768)
* migration and init to accept creationSource for new bookings

* V1 create booking

* V1 user creation

* webapp booking + V1 user

* user creation in V1, V2 and webapp

* booking source V2 and fix for v1 user

* fit type

* --fix type

* add test -- WIP

* fix type

* fix type

* ^

* Need more sleep zzz

* -_-

* bump libraries platform

* adds for v2 recurring booking

* fix lint

* instant meetings

* fix: api v2 creation source

* fixup! fix: api v2 creation source

* bump libraries

* add user

* fix test

* fixup! fix test

* add more source

* more source...

* fix type & test --1

* fix type & test --2

* typefix

* fixup test

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-01-27 11:01:33 +01:00
Benny Joo 3f622fe277 chore: migrate /auth/verify (#18902)
* migrate /auth/verify page

* update middleware
2025-01-27 10:59:26 +01:00
Benny Joo ed3579c5c7 chore: migrate /auth/logout (#18901)
* migrate /auth/verify-email page

* update middleware

* migrate /auth/logout page
2025-01-27 14:22:33 +05:30
Benny Joo 2f9beed450 chore: migrate /apps/installation pages (#18870)
* migrate apps/installation pages

* fix test
2025-01-26 23:04:30 +01:00
Benny Joo 2b79ef0836 perf: replace getServerSessionForAppDir usages with our original getServerSession function (#18897) 2025-01-26 21:57:02 +00:00
Benny Joo 828940b4d6 chore: app router - /payment pages (#18152)
* remove Head from payment page comp

* remove route from pages router

* move out of future folder

* add to config.matcher

* remove unneeded code

* update middleware

* fix

* fix
2025-01-25 13:23:41 -05:00
Benny Joo 0dab3e1a6d chore: migrate /apps/categories pages (#18871)
* migrate apps/categories pages

* remove pages dir
2025-01-25 18:45:59 +01:00
Benny Joo c41396d8d8 perf: prerender routes in main navigation (#18883) 2025-01-25 18:30:07 +01:00
Alex van Andel c5d0d66a26 chore: Only import Sentry when needed (#18882) 2025-01-25 10:41:44 +00:00
Benny Joo 30e576377a migrate /auth/verify-email page (#18876) 2025-01-24 21:20:36 +00:00
Lauris Skraucis 4235a36887 refactor: v2 return booking host email (#18864) 2025-01-24 19:00:15 +00:00
Eunjae Lee b234b16ad0 fix: convert People filter on /insights/routing from single select to multi select (#18735) 2025-01-24 17:15:49 +00:00
Anik Dhabal BabuandUdit Takkar 3586046786 fix: Hide events in the event type assignment tab for teams does not save (#18868)
* fix: flaky test organization.spec

* fix: Hide events in the event type assignment tab for teams does not save

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-01-24 22:15:11 +05:30
Eunjae LeeandBenny Joo d5e633b481 fix: re-implement filter components in /insights/routing (#18655)
* fix: re-implement team filter & people filter components

* implement DateRangeFilter

* show selected filter values

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-01-24 17:38:48 +01:00
Benny Joo a069af715e chore: migrate /apps/installed pages (#18869) 2025-01-24 10:41:24 -05:00
Benny Joo beb542e7cb chore: migrate /apps/[slug] page (#18848)
* migrate apps/[slug] page

* remove future/apps/[slug]
2025-01-24 15:57:52 +01:00
Calcom BotandReplexica 84df8dc6eb feat: update translations via @replexica (#18860)
Co-authored-by: Replexica <support@replexica.com>
2025-01-24 09:55:37 +00:00
sean-brydonandUdit Takkar 38e7eb03f9 fix: attribute assignment hosts (#18831)
* load correct users in query state for attribute rr

* add segment loading state

* skeleton

* fix query value

* update tests

* fix type

* use query by testId so no error is thrown

* ensure queryValue children has valid values before we trigger api key

* chore: remove lg

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2025-01-24 09:41:13 +00:00
sean-brydonandEunjae Lee 656d6dc195 feat: Internal Notes for bookings (#18804)
* migration + migrate booking limit page to team settings

* new pages

* use settings toggle for displaying content

* get current team presets

* update mutation

* i18n for settings page

* remove redudant await

* cancelation UI for select

* handle other selection

* extract into a util

* inital cancelation reason flow

* fix i18n to be more readable

* add cancel reason logic

* nits

* improve copy

* ensure is teamMember and not isAdmin for getting internalNotes

* improvie spacing between elements

* fix types

* more type fixes

* correct types

* Update packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts

* use input component and remove mb-2 to allign with icon button

* Update packages/trpc/server/routers/viewer/teams/updateInternalNotesPresets.handler.ts

Co-authored-by: Eunjae Lee <hey@eunjae.dev>

* nits

* type fixs

* fix type

* fix type

* fix nullable option lost in merge

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-01-24 09:26:52 +00:00
Omar López 1fd4c3fd4c refactor: Specific error status codes (#18847) 2025-01-24 07:09:54 +00:00
Hariom Balhara b3f946dd0b feat: "cal.isBookingDryRun" to be used for Routing form as well (#18832) 2025-01-24 09:27:18 +05:30
Omar López 161aece3a1 fix: dual-sided incentives on webapp (#18846) 2025-01-23 21:30:36 +00:00
Calcom BotandReplexica fec8019775 feat: update translations via @replexica (#18845)
Co-authored-by: Replexica <support@replexica.com>
2025-01-23 20:06:42 +00:00
Benny Joo 91225c03f3 chore: migrate org domain /[user] page (#18715)
* chore: migrate org domain [user] page

* fix type checks
2025-01-23 19:48:49 +01:00
Anik Dhabal Babu 81daa6581f fix: checkly tests (#18842)
* fix: flaky test organization.spec

* fix: chckly tests
2025-01-23 18:38:47 +00:00
Eunjae Leeandsean-brydon b304467f15 fix: use columnPinning API (#18830)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-01-23 18:25:42 +00:00
Lauris Skraucis e748d3e252 fix: AvailabilitySettings and Booker atom time zones (#18833) 2025-01-23 17:38:06 +01:00
Max Prilutskiy f8630cebf4 chore: update naming (#18837) 2025-01-23 16:06:46 +00:00
Benny Joo 36d756e469 fix: flickering in /bookings/[status] page (#18836) 2025-01-23 16:44:29 +01:00
Benny Joo 8e191fed71 perf: Use one PageWrapper instance for every non-settings / non-bookings page in App Router (#18747)
* add a layoutfile to use-page-wrapper route group

* refactor and move bookings status page to route group

* improve logic in pagesAndRewritePaths

* fix

* add use client directive

* move /403 and /500

* move /auth/error

* move /connect-and-join

* move /enterprise

* move /maintenance

* move /more

* move /upgrade

* chore: remove WithLayout from 8 low-risk single pages (#18745)

* move /403 and /500

* move /auth/error

* move /connect-and-join

* move /enterprise

* move /maintenance

* move /more

* move /upgrade

* move /availability pages

* move /event-types pages

* move /insights pages

* move /apps to use-page-wrapper group

* move /getting-started to use-page-wrapper group

* move /signup to use-page-wrapper group

* move /workflows to use-page-wrapper group

* move /video pages to use-page-wrapper group

* pass dehydratedState in layout

* pass dehydratedState in layout

* fix test

* fix @test

* fix
2025-01-23 10:01:46 -05:00
Anik Dhabal Babu 2335aba843 fix: flaky test organization.spec (#18828) 2025-01-23 12:45:09 +00:00
1ffea493ec fix: use userId as ratelimit identifier (#18803)
* fix: use userId as ratelimit identifier

Using raw api keys as identifiers is problematic because they are logged
and are accessible via Unkey's UI.

My first idea was to use the api key's primary key, which would solve
this just fine. But after looking at the implementation it seemed easier
to use the user's id, which is what most people are interested in
limiting, not the actual keys. Also the userId was already passed in the
requests context, which made it easier. Sean confirmed that ratelimiting
the user, not their keys made more sense, so here we are.

* Update rateLimitApiKey.ts

* Update rateLimitApiKey.ts

* Fix tests

* Fix tests

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2025-01-22 21:37:30 -05:00
Omar López 3765c76c96 refactor: bookings failures should be caught faster (#18817)
* refactor: bookings failures should be caught faster

* fix for expected failures

* Update booking-pages.e2e.ts
2025-01-23 05:52:42 +05:30
Omar LópezandKeith Williams 1af3caa8cc refactor: better observability in api routes (#18810)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-01-22 20:52:09 +00:00
Anik Dhabal Babu 35565c8f7e fix: HubSpot is not working for team events (#18812)
* fix: hubspot is not working for team events

* update
2025-01-22 20:40:34 +00:00
Omar López 0f07ab2f7b fix: Load plain dynamically (#18571) 2025-01-22 20:10:11 +00:00
Benny Joo f0d1c92e99 perf: create (use-page-wrapper) route group and replace WithLayout from /bookings/[status] page + 8 low-risk single pages (#18737)
* add a layoutfile to use-page-wrapper route group

* refactor and move bookings status page to route group

* improve logic in pagesAndRewritePaths

* fix

* add use client directive

* chore: remove WithLayout from 8 low-risk single pages (#18745)

* move /403 and /500

* move /auth/error

* move /connect-and-join

* move /enterprise

* move /maintenance

* move /more

* move /upgrade

* pass dehydratedState in layout
2025-01-22 12:52:58 -05:00
Alex van Andel d1134c8560 chore: Fix sublogger/better logging (#18808) 2025-01-22 13:57:54 +00:00
Rajiv Sahal 586b3532b5 feat: add option to disable deleting calendar credentials for CalendarSettings atom (#18759) 2025-01-22 13:09:25 +00:00
Rajiv Sahal 15e7fc192e fix: requires confirmation for event type atom (#18729)
* redirect on success from event types

* fixup
2025-01-22 18:32:10 +05:30
GitHub Actions 95bf90402c chore: release v4.9.4
Post release / release (push) Canceled after 0s
2025-01-21 20:23:21 +00:00
Carina WollendorferandCarinaWolli f523ce7795 fix: custom templates on team plan (#18792)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-01-21 20:16:57 +00:00
Joe Au-Yeung 65b70d682d fix: Initialize Hubspot client in CRMService (#18789)
* Initialize Hubspot client in class

* Remove debugging artifact
2025-01-21 16:47:32 -03:00
Alex van Andel 2d518f49dd Double available memory on api/book/event (#18790) 2025-01-21 19:01:58 +00:00
7a9d291d1f fix: handle cancelation reason when user is host (#18681)
* handle cancelation reason when user is host

* add attendee reason warning

* add cancellation reason to tests when we are the host

* req.body and not req

* fix: isloggedIn logic

* use new isLoggedInUser from ssr instead of client

* update e2e tests to fill in reason when hosts

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-21 20:24:06 +05:30
Keith Williams 1a1a506324 chore: Remove Chromatic GitHub workflow (#18780) 2025-01-21 13:50:53 +00:00
Benny Joo 0f7ecb3345 chore: migrate low-risk 3 /auth pages to App Router (#18753)
* add i18n string

* Update config.matcher in middleware

* migrate auth/oauth2/authorize

* migrate auth/platform/authorize

* migrate auth/signin
2025-01-21 10:41:00 +01:00
Mehul 4f1d71e660 fix: routing form list overflow (#18686) 2025-01-21 10:40:44 +01:00
Udit Takkar 30eb7ce846 feat: add transcription as attachment (#18740)
* feat: add transcription as attachment

* fix: change style
2025-01-21 06:07:10 +00:00
3eb5490df9 feat: Use Cloudflare Turnstile in booker (#18755)
* feat: Use Cloudflare Turnstile in booker

* move files arround

* render turnstile widget and add token to store

* use token from body instead of headers

* watch cf token to re-render disabled state

* Update packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx

* Update packages/features/bookings/lib/create-booking.ts

* ensure bool

* add to recuring event

* fix recuring events

---------

Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-01-21 11:27:56 +05:30
GitHub Actions 235382213f chore: release v4.9.3
Post release / release (push) Canceled after 0s
2025-01-20 21:05:54 +00:00
Alex van Andel 01baf7d3c0 chore: Add more logging around getUserAvailability (#18769)
* chore: Add more logging around getUserAvailability

* Add initialData to logging
2025-01-20 17:04:43 +00:00
Calcom BotandReplexica 1a649158e1 feat: update translations via @replexica (#18764)
Co-authored-by: Replexica <support@replexica.com>
2025-01-20 12:41:25 -03:00
Joe Au-Yeung d6306dcd22 fix: Disable custom templates for trialing teams (#18748)
* Create `checkIfTeamHasActivePlan` method on `stripe-billing-service`

* Add `checkIfTeamPlanIsActive` to `InternalTeamBilling`

* Create `hasActiveTeamPlan` trpc endpoint

* Check for active team plan when updating workflows

* Create `useHasActiveTeamPlan` hook

* Use `useHasActiveTeamPlan` on workflow edit page

* Type fixes

* Type fix

* Add `accepted` to team query
2025-01-20 14:48:33 +00:00
Muhammad Aiman Sulaimanandkutsaniuk 7131a1f015 feat: HitPay Payment App (#17213)
* Build HitPay app

* Deleted a lint comment

* Add redirect for iframe

* Fix type check error

* Fixed issue of payment pending

* Update hitpay package.json

* fix: fix lint issue in KeyInput and update zod

* fix: fix issue of parsing string to number

* fix: add default hitpay API links

* fix: resolve atoms build error

---------

Co-authored-by: kutsaniuk <kutsaniuk@gmail.com>
2025-01-20 15:36:23 +01:00
Eunjae Lee 5357228f72 fix: nested scrolls on /bookings/upcoming (#18760) 2025-01-20 09:18:12 -05:00
Lauris Skraucis bfeafaa21f fix: ensure platform managed user has 1 schedule by default (#18719)
* fix: ensure managed user has 1 schedule

* chore: bump platform libraries
2025-01-20 09:11:26 +00:00
Syed Ali Shahbaz 02ef6dc934 Fix raycast bug (#18756) 2025-01-20 10:03:54 +01:00
Benny Joo 4424b2aea3 useIsThemeSupported hook (#18746) 2025-01-20 14:30:18 +05:30
Syed Ali Shahbaz 1f91fe888a bump libphonenumber-js version to 1.11.18 (#18757) 2025-01-20 07:14:01 +00:00
sean-brydonandAlex van Andel b4a51d3560 feat: auto lock (#18630)
* add delete to redis service

* auto lock + tests

* changes to autolock in api/book call

* remove log clutter

* add detection in api v1

* tpye changes

* throw error and add tests for API

* add response logic on locked + tests

* type response properly i hope?

* type fix

* add tests for counter not existing + redis errors

* remove IP - add sentry to track

* rename symbol

* fix type error

* fix tests

* remove sentry call spy

* fix type on sentry setUser call

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-18 16:21:12 -03:00
Benny Joo 4855b6b40f fix: small spacing fix in app router 404 (#18741) 2025-01-17 16:45:30 -07:00
Eunjae Lee 4b264a867b fix: adjust table height for a few rows on /bookings (#18728) 2025-01-17 23:07:39 +00:00
GitHub Actions 9caaa0af32 chore: release v4.9.2
Post release / release (push) Canceled after 0s
2025-01-17 22:03:02 +00:00
f411c6ed11 feat: Use dub.customer.list to issue dual-sided incentives (#18452)
* [FEAT] Use dub.customer to issue dual-sided incentives

* update dub implementation

* uncommit yarn.lock

* Update yarn.lock

* account for self-hosting

* remove comments

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-17 21:33:33 +00:00
Benny Joo 245196fbf3 chore: technical debt clearing - remove getServerSideProps for /event-types (#18733)
* chore: technical debt clearing - remove getServerSideProps for event-types

* refactor
2025-01-17 21:35:03 +01:00
Steven TeyandAlex van Andel 6be69b820c chore: Bump Dub version to fix createMany error (#18591)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-17 08:53:44 -07:00
Benny Joo b63c9791ec fix (#18718) 2025-01-17 10:48:31 -05:00
Anik Dhabal Babu 8cc61a1fe9 fix: Impersonation issue from userstable (#18608)
* fix: visible error in the userlist handler

* Uncommit yarn.lock

* update
2025-01-17 14:04:41 +00:00
Anik Dhabal Babu be2e1274ff fix: Getting an error with managed events when accessing through api/v1 (#18683)
* fix: getting error with managed events when get through api/v1

* update
2025-01-17 13:59:34 +00:00
Anik Dhabal Babu fa3b1c17ea fix: duplicating an event turns private link on (#18727) 2025-01-17 13:59:19 +00:00
Benny Joo 015edd3c9b chore: migrate /auth/new (#18732)
* move out of /future dir

* remove legacy pages
2025-01-17 06:46:59 -05:00
Benny Joo de05e8e989 chore: migrate /auth/error (#18730)
* migrate auth/error

* fix

* fix type check
2025-01-17 06:46:47 -05:00
Benny Joo 78a6963cd9 chore: clean up <HeadSeo> components where not needed (#18731)
* clean up headseo from d-type-view

* clean up headseo from bookings-single-view

* clean up headseo from team-view
2025-01-17 10:57:39 +00:00
Udit Takkar ea827ebbc6 fix: edit webhook time (#18549) 2025-01-17 08:56:50 +00:00
Joe Au-YeungandPeer Richelsen 08e58851e7 feat: Salesforce - write to field without validation (#18722)
* Allow user to select custom field type

* Get field value and write to field - ignoring field type

* Only validate field length if present

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-17 08:55:34 +00:00
Cao Hoang da1b8137c2 fix: filters ui breaking on mobile in routing insights page (#18713) 2025-01-17 09:40:16 +01:00
Anik Dhabal BabuandUdit Takkar d182e5c99a fix: Booking with the same email when using capital letters (#18694)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-01-17 09:37:10 +01:00
Nizzy 42259a113b fix: resize app-store images to use less bytes (#18675) 2025-01-17 09:35:01 +01:00
+8 6561b92f40 perf: Move CRM event creation to tasker (#18370)
* Upgrade jsforce to 3.6.2

* Refactor connecting to Salesforce

* Revert yarn.lock changes

* Add `TASKER_ENABLE_CRM_EVENT_CREATION` to .env

* Add createCRMEvent scheduler

* Schedule CRM event creating in `EventManager`

* Add calendar event builder for CRM tasks

* Do not write to person record if fields don't exist

* Change type to expect string from tasker

* Create CRM event

* Create booking references

* Type fixes

* Migrate callback endpoint

* Add jsforce node dependency

* Migrate add endpoint

* Import

* Import package into crmService

* Use new package types

* Type fix

* Update vite config

* Push updated lockfile

* Attempt to bump platform/libraries to unlock jsforce

* Also update lockfile, naturally

* bump platform libraries

* feat: salesforce to tasker improvements (#18419)

* feat: salesforce to tasker

* refactor: event manager

* tests: add unit tests for create CRM Event

* Update vite.config

* Add jsforce to vite config

* Revert mint.json changes

* Default to not enabling

* Revert yarn.lock changes

* Remove `TASKER_ENABLE_CRM_EVENT_CREATION` variable

* Revert yarn.lock changes

* feat: Round Robin weights future members toggle (#17782)

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

* detailed customer card (#18511)

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

* chore: app router - all sub-pages in `/apps` (#16976)

* chore: apps/[slug] remove pages router

* remove apps/[slug] pages from /future

* chore: apps/installed remove pages router

* chore: apps/installation remove pages router

* remove Head element

* fix metadata

* fix test

* fix another test

* chore: apps/categories remove pages router

* revert unneeded changes

* update middleware

* Remove <Head>

* remove unused import and code

* remove unused import and code again

* fix

* fix category page

* add split icon

* add /routing paths to middleware matcher

* wip

* remove HeadSeo from App.tsx

* clean up head-seo test

* add generateAppMetadata

* use generateAppMetadata in apps/[slug] page

* delete file

* remove log

* fix

* fix

* fix apps/installed pages

* fix cateogires pages

* fix

* fix imports

* wip

* fix

* fix

* fix metadata

* fix

* redirect /apps/routing-forms to /routing

* replace all usages of /apps/routing-forms to /routing

* better naming

* /routing -> /routing/forms

* fix

* fix

* fix

* fix

* remove backPath as it is irrelevant when withoutMain is true

* fix type checks

* fix type check in apps/[slug]

* refactors

* fix

* fix test

* fix

* fix

* fix

* Replace multiple leading slashes with a single slash

* migrate routing-forms too

* add re routing

* fix

* add redirection

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>

* chore: app router 404 page (#18597)

* wip

* wip

* fix not found page

* render middleware for /settings pages

* fix

* remove global-error page

* add metadata to not-found page

* make not-found page static

* remove 404

* adding not-found to middleware is not necessary

* add every routes to config.matcher

* fix test

* fix style

* use i18n string

* fix tests

* fix

* fix

* revert unneeded changes

* fix

* fix

* fix

* fix style

* fix

* remove 404

* remove log

* fix

* fix

* fix

* fix

* better naming

* parallel testing

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>

* feat: render custom error page for unexpected sever error + remove` pages/_error` (#18606)

* remove page/_error

* refactor app/error

* fix: app/not-found cannot be a static page (#18610)

* Async False (#18611)

* chore: redirect to /500 if pathname does not exist + better error handling (#18615)

* fix lint error

* fix booking page and better error handling

* chore: gracefully handle 404s from pages router's dynamic pages + tests (#18618)

* restore pages/_error

* set custom header in pages/_error

* handle it in middleware

* add test

* remove logs

* better test description

* chore: try using custom 404 in pages/_error (#18622)

* fix: parsing teamId (#18623)

* chore: restore error pages for pages router (#18625)

* disable emails to all guests (#18628)

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

* revert: "feat: bulk shorten links with dub.links.createMany (#18539)" (#18587)

This reverts commit 4902c6a0f8.

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

* chore: release v4.8.18

* fix: disable sending sms when email is present (#18632)

* fix: disable sending sms when email is present

* fix: update test

* fix: main lint errors (#18634)

* fix: error-page.tsx related lint errors

* fix: lint no continue-on-error

* Adding more lint fixes

* Bring back annotate code linting results

* Bring back linting continue-on-error

* Slimline lint

* feat: do not show automation webhooks in webhook list (#18607)

* fix: make ESLint work and fix lint errors that were undetected before (#18639)

* fix eslint config

* fix

* add it to dev dep

* fix

* sync eslint version

* force ts-node to compile our ESLint plugin's TS files into CommonJS (which ESLint requires)

* fix some lint errors

* fix lint errors

* remove duplicate classname

* make @typescript-eslint/ban-types a warn for packages/trpc files

* fix lint errors in trpc

* fix lint errors in trpc - 2

* fix

* fix

* fix lint warnings

* chore: clean up config.matcher in middleware (#18638)

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

* Allow lint to error but continue (unblock pipeline)

* fix: potential fix for flaky layout shift (#18651)

* potential fix: layout shift

* fix lint error

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

* chore: sync with main

* feat: update translations via @replexica

---------

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

* Update yarn.lock

* nit: let tasker handle payload stringification

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Nizzy <140507264+nizzyabi@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: GitHub Actions <github-actions@github.com>
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
Co-authored-by: Calcom Bot <109866826+calcom-bot@users.noreply.github.com>
Co-authored-by: Replexica <support@replexica.com>
2025-01-16 22:07:30 -05:00
Benny Joo 9e5665f2fb Revert "fix: revert app router error pages (#18696)" (#18714)
This reverts commit 3cc5d9c065.
2025-01-16 16:53:47 -05:00
GitHub Actions 07778d22ed chore: release v4.9.1
Post release / release (push) Canceled after 0s
2025-01-16 18:01:24 +00:00
Joe Au-Yeung 56df8bdf7c feat: Salesforce - map booking questions to event record (#18532) 2025-01-16 12:28:07 -05:00
GitHub Actions a518c7152a chore: release v4.9.1-rc1
Post release / release (push) Canceled after 0s
2025-01-16 15:53:45 +00:00
Benny Joo c37a8a587b fix: add decoding logic to app router params (#18712)
* decode app router params in buildLegacyCtx

* add helpful comments

* add comment

* address comment
2025-01-16 15:14:03 +00:00
Alex van Andel b7f7c33afc feature: Allow non-calcom domains to run team slug pages (#18711) 2025-01-16 09:44:14 -03:00
Lauris Skraucis 1d449ecc6b fix: v2 booking guests (#18690)
* fix: v2 booking guests

* test
2025-01-16 11:20:18 +02:00
Lauris Skraucis f582faed2b fix: Booking atom phone booking field and booking fields order (#18678)
* chore: examples app make managed user admin

* fix: phone field not appearing

* fix: booking fields order

* feat: local libraries setup script

* chore: add log when parsing booking field fails

* fix: backwards compatbile name booking field

* chore: libraries publish support scripts

* chore: bump libraries

* docs

* fix: phone default field return label and placeholder

* fix: e2e tests
2025-01-16 11:10:06 +02:00
GitHub Actions 79a8ab79f8 chore: release v4.9.0
Post release / release (push) Canceled after 0s
2025-01-16 01:26:26 +00:00
Benny Joo ce87de4fc7 fix: Revert - chore: app router - all sub-pages in /apps #16976 (#18702)
* revert

* add incomplete-booking
2025-01-15 21:12:18 -03:00
Calcom BotandReplexica b128579729 feat: update translations via @replexica (#18699)
Co-authored-by: Replexica <support@replexica.com>
2025-01-15 20:38:44 +00:00
Benny Joo 3cc5d9c065 fix: revert app router error pages (#18696) 2025-01-15 16:47:30 -03:00
Benny Joo 7897e14273 fix: add back global-error.tsx (#18692) 2025-01-15 16:55:43 +00:00
GitHub Actions c581456586 chore: release v4.8.19
Post release / release (push) Canceled after 0s
2025-01-15 15:30:52 +00:00
36dd3e9e6c fix: disable custom templates for free plan (#18635)
* add upgrade badge

* adjust update handler

* code clean up

* clean up template code

* only handle email templates

* fixes

* code clean up

* fix workflow template zod type

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-15 15:25:14 +00:00
Alex van Andel f137aee436 Remove export keyword @ route - only default allowed (#18682) 2025-01-15 14:33:02 +00:00
Eunjae Lee 4f494d5a32 refactor: use DataTable for /bookings (#18589)
* refactor: use DataTable for /bookings

* remove unnecessary animation (for better scroll performance)

* add consistent search box
2025-01-15 14:29:50 +00:00
Benny Joo cce6e83578 chore: migrate /org team pages and team type pages and user type page (#18668) 2025-01-15 09:21:21 +00:00
Lauris Skraucis c21ba636d2 fix: Booker atom name booking field (#18653)
* feat: split name booking field in v2

* fix: booker atom handle split name

* fix: after and before start dates bookings filter

* chore: bump platform libraries
2025-01-15 14:08:06 +05:30
Benny Joo 5ccf5e6be8 chore: migrate /team booking pages (#18656)
* wip

* remove pages/team/[slug]

* fix tests

* fix tests 2
2025-01-14 23:48:41 +00:00
Joe Au-YeungandAlex van Andel 66b3e735d8 feat: Routing form submitted but no booking - Salesforce actions (#18616)
* Add booking incomplete actions table

* Add incomplete booking page tab

* Add `getincompleteBookingSettings` trpc endpoints

* Add incomplete booking page

* Abstract enabled apps array

* Handle no enabled credentials and no actions

* Add enabled field to incomplete booking action db record

* Add new write record entries

* UI add separation between switch and inputs

* Fix typo

* clean up

* Add saveIncompleteBookingSettings endpoint

* Save incomplete booking settings

* Fix language around when to write to field

* Add `credentialId` to action record

* Choose which credential to assign to action

* Save credential to action

* Revert "Save credential to action"

This reverts commit ba6a1c808baed54f6d3d4c6155cf192c813d0696.

* Revert "Choose which credential to assign to action"

This reverts commit 968f6e5295d098c64abe95268afd1fa139a175ba.

* Revert "Add `credentialId` to action record"

This reverts commit 579f9ff4167b65aa987659e4e8f8c26f9e773317.

* Add credentialId to action record - rewrite migration file

* Revert "Add credentialId to action record - rewrite migration file"

This reverts commit 2843a92c61820e7f7a1614a557d3f7ea96342107.

* Revert "Add booking incomplete actions table"

This reverts commit 7ec75bef4a0f0a9d07be0142da64c49d739439ea.

* Revert "Add enabled field to incomplete booking action db record"

This reverts commit d279a1da05819eafa8fc5d664e3be733e0687e8d.

* Write migration in single commit

* Rename table

* Rename table - remove underscores

* Remove credential relationship

* Type fix - changing table name

* Fix table name

* Change writeToRecordObject to object

* Salesforce add incomplete booking, write to record

* Add incomplete booking actions to `triggerFormSubmittedNoEventWebhooks`

* Remove console.log

* Type fixes

* Type fixes

* Iterate if incompleteBookingActions

* Choose which credential to assign to action

* Save credential to action

* Fix getServerSideProp changes

* Type fix

* Type fix

* Type fix

* Type fix

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-14 23:17:16 +00:00
Praash 7a8b9ededb fix: html render from calendar invite (#18577) 2025-01-14 20:40:31 +00:00
Benny Joo 501c49f369 perf: no need for two query fetches for d link (private booking page) (#18659)
* return eventData

* use eventData and do not do extra query fetch
2025-01-14 17:15:56 +00:00
Amit Sharma 536604455b fix: BUG: adding filters to settings/teams/[team-id]/members page throws error (#18631) 2025-01-14 16:30:54 +00:00
MorganandPeer Richelsen cfb76a9689 chore: platform libraries readme publish reasons (#18646)
* chore: platform libraries readme publish reasons

* fixup! chore: platform libraries readme publish reasons

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-14 16:28:34 +00:00
Udit TakkarandPeer Richelsen 30180edc0b fix: phone number in confirmation email (#18642)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-14 16:27:24 +00:00
MorganandCarina Wollendorfer 2aa3d1d4e0 feat: get org ooo entries and filters/sort (#18645)
* feat: get org ooo entries and filters/sort

* remove console.log

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2025-01-14 15:33:22 +00:00
Eunjae Lee 21694c039a perf: improve routing insights view (2) (#18566) 2025-01-14 12:28:39 -03:00
Benny Joo a8c29b5459 chore: log error to Sentry in app router error page (#18637) 2025-01-14 15:23:07 +00:00
Anik Dhabal Babu 6cd7454f43 fix: /booking page datefilter (#18654)
* fix: /booking page datefilter

* Update get.handler.ts

* Update get.handler.ts

* shit
2025-01-14 15:14:28 +00:00
Calcom BotandReplexica 99b2419dd7 feat: update translations via @replexica (#18598)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2025-01-14 13:47:22 +00:00
Benny Joo f6d0a2ca09 fix: potential fix for flaky layout shift (#18651)
* potential fix: layout shift

* fix lint error
2025-01-14 13:36:20 +00:00
Alex van Andel f5ab9bd40b Allow lint to error but continue (unblock pipeline) 2025-01-14 13:12:24 +00:00
Benny JooandAlex van Andel 07085b9ffb chore: clean up config.matcher in middleware (#18638)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-14 10:24:36 +00:00
Benny Joo 14151b827f fix: make ESLint work and fix lint errors that were undetected before (#18639)
* fix eslint config

* fix

* add it to dev dep

* fix

* sync eslint version

* force ts-node to compile our ESLint plugin's TS files into CommonJS (which ESLint requires)

* fix some lint errors

* fix lint errors

* remove duplicate classname

* make @typescript-eslint/ban-types a warn for packages/trpc files

* fix lint errors in trpc

* fix lint errors in trpc - 2

* fix

* fix

* fix lint warnings
2025-01-14 08:58:38 +00:00
Kartik Saini 28b757f19c feat: do not show automation webhooks in webhook list (#18607) 2025-01-14 10:55:22 +05:30
Alex van Andel 68455db860 fix: main lint errors (#18634)
* fix: error-page.tsx related lint errors

* fix: lint no continue-on-error

* Adding more lint fixes

* Bring back annotate code linting results

* Bring back linting continue-on-error

* Slimline lint
2025-01-13 22:01:20 +00:00
Udit Takkar 96ff88d266 fix: disable sending sms when email is present (#18632)
* fix: disable sending sms when email is present

* fix: update test
2025-01-13 20:38:44 +00:00
GitHub Actions f88a91dad0 chore: release v4.8.18
Post release / release (push) Canceled after 0s
2025-01-13 18:12:32 +00:00
Keith WilliamsandAlex van Andel 35059d7727 revert: "feat: bulk shorten links with dub.links.createMany (#18539)" (#18587)
This reverts commit 4902c6a0f8.

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-13 18:07:27 +00:00
Carina WollendorferandCarinaWolli 68f76e2acc disable emails to all guests (#18628)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-01-13 15:58:43 +00:00
Benny Joo 80aac06ff3 chore: restore error pages for pages router (#18625) 2025-01-13 12:42:00 +00:00
sean-brydon 0f47528ba2 fix: parsing teamId (#18623) 2025-01-13 12:16:59 +00:00
Benny Joo 113dc5fca6 chore: try using custom 404 in pages/_error (#18622) 2025-01-13 11:35:10 +00:00
Benny Joo 0befdde5d7 chore: gracefully handle 404s from pages router's dynamic pages + tests (#18618)
* restore pages/_error

* set custom header in pages/_error

* handle it in middleware

* add test

* remove logs

* better test description
2025-01-13 15:46:56 +05:30
Benny Joo b3ed6e39e3 chore: redirect to /500 if pathname does not exist + better error handling (#18615)
* fix lint error

* fix booking page and better error handling
2025-01-13 00:04:36 +00:00
sean-brydon e07b1eda32 Async False (#18611) 2025-01-12 20:31:43 +00:00
Benny Joo 8262894bde fix: app/not-found cannot be a static page (#18610) 2025-01-12 20:21:03 +01:00
Benny Joo a3d7bcd22e feat: render custom error page for unexpected sever error + remove pages/_error (#18606)
* remove page/_error

* refactor app/error
2025-01-12 14:12:02 +01:00
Omar LópezandBenny Joo 62b753afba chore: app router 404 page (#18597)
* wip

* wip

* fix not found page

* render middleware for /settings pages

* fix

* remove global-error page

* add metadata to not-found page

* make not-found page static

* remove 404

* adding not-found to middleware is not necessary

* add every routes to config.matcher

* fix test

* fix style

* use i18n string

* fix tests

* fix

* fix

* revert unneeded changes

* fix

* fix

* fix

* fix style

* fix

* remove 404

* remove log

* fix

* fix

* fix

* fix

* better naming

* parallel testing

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-01-11 02:42:08 +00:00
bb40b2f6bf chore: app router - all sub-pages in /apps (#16976)
* chore: apps/[slug] remove pages router

* remove apps/[slug] pages from /future

* chore: apps/installed remove pages router

* chore: apps/installation remove pages router

* remove Head element

* fix metadata

* fix test

* fix another test

* chore: apps/categories remove pages router

* revert unneeded changes

* update middleware

* Remove <Head>

* remove unused import and code

* remove unused import and code again

* fix

* fix category page

* add split icon

* add /routing paths to middleware matcher

* wip

* remove HeadSeo from App.tsx

* clean up head-seo test

* add generateAppMetadata

* use generateAppMetadata in apps/[slug] page

* delete file

* remove log

* fix

* fix

* fix apps/installed pages

* fix cateogires pages

* fix

* fix imports

* wip

* fix

* fix

* fix metadata

* fix

* redirect /apps/routing-forms to /routing

* replace all usages of /apps/routing-forms to /routing

* better naming

* /routing -> /routing/forms

* fix

* fix

* fix

* fix

* remove backPath as it is irrelevant when withoutMain is true

* fix type checks

* fix type check in apps/[slug]

* refactors

* fix

* fix test

* fix

* fix

* fix

* Replace multiple leading slashes with a single slash

* migrate routing-forms too

* add re routing

* fix

* add redirection

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-01-10 20:16:11 -05:00
NizzyandOmar López f628dc83a8 detailed customer card (#18511)
Co-authored-by: Omar López <zomars@me.com>
2025-01-10 23:51:27 +00:00
sean-brydonandOmar López 2fd2578504 feat: Round Robin weights future members toggle (#17782)
Co-authored-by: Omar López <zomars@me.com>
2025-01-10 14:24:19 -07:00
Syed Ali ShahbazandCarinaWolli a29407349d docs: fix error introduced in #18578 (#18593)
* Fix error

* remove indent

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-01-10 19:48:12 +00:00
Joe Au-YeungandAnik Dhabal Babu e7df1ef493 fix: Create dub links if valid url (#18588)
* Create dub links if valid

* Map through filtered links

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-01-10 15:58:16 +00:00
Kartik Saini b652ae7714 fix: wrong Meeting Ended payload in zapier (#18202)
* fix: wrong Meeting Ended payload in zapier

* fix
2025-01-10 15:55:35 +01:00
Peer Richelsen cec1c9ee46 feat: granola.ai app (#18585)
* new granola app

* granola app
2025-01-10 14:47:18 +00:00
GitHub Actions 3813fd70f7 chore: release v4.8.16
Post release / release (push) Canceled after 0s
2025-01-10 14:01:39 +00:00
Morgan dabd3b0fc3 chore: improve ooo reason documentation apiv2 (#18582) 2025-01-10 13:20:39 +00:00
Calcom BotandReplexica 95f9a02d29 feat: update translations via @replexica (#18584)
Co-authored-by: Replexica <support@replexica.com>
2025-01-10 13:19:47 +00:00
Somay Chauhan f26d2998cb feat: added pagination to platform managed users table (#18523)
* feat: added pagination to platform managed users table

* removed console

* implemented feedback, removed unused components

* added label

* Update managed-users-view.tsx

* Update managed-users-view.tsx
2025-01-10 12:53:20 +00:00
Somay Chauhan f558f8aca5 fix: broken platform members table (#18583) 2025-01-10 12:46:48 +00:00
Syed Ali ShahbazandPeer Richelsen a6dd5185ea docs: fix footer socials in /help and other broken pages (#18578)
* Update mint.json

* fix broken links

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-10 12:50:45 +01:00
Anik Dhabal Babu 3da7694002 fix: flaky e2e test in saml.e2e (#18559)
* chore: skip failure e2e test

* Update .env

* Update manage-booking-questions.e2e.ts

* Update manage-booking-questions.e2e.ts

* Update manage-booking-questions.e2e.ts

* Update manage-booking-questions.e2e.ts

* Update manage-booking-questions.e2e.ts

* Update manage-booking-questions.e2e.ts

* Update .env

* Update manage-booking-questions.e2e.ts

* fix

* fix flake

* Update saml.e2e.ts
2025-01-10 17:17:12 +05:30
Eunjae Lee 1ffb376789 fix: improve filters at /bookings (#18581)
* fix: improve filters at /bookings

* fix type
2025-01-10 11:10:10 +00:00
Anik Dhabal BabuandUdit Takkar 8761b91f71 fix: unpublished org is visble as an option in workflow (#18522)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-01-10 10:49:08 +00:00
Nizzy 1dbef7f8f6 fix: billing plain chat working (#18530) 2025-01-10 10:12:30 +00:00
Anik Dhabal BabuandPeer Richelsen d4d5ac2b28 fix: date filter for the same day showing appointments of the day before (#18579)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-10 15:42:24 +05:30
Omar López 88c8ad8b5c chore: fix SVG warning on render (#18572) 2025-01-10 10:00:43 +00:00
Benny Joo 0a08680084 cleanup: remove-headseo-from-migrated-app-router-components (#18576) 2025-01-10 10:35:37 +01:00
Anik Dhabal Babu 65828545b9 fix: wip (#18574) 2025-01-10 00:38:04 -05:00
Omar López 3adb8e84fc chore: Webhook should timeout fast on e2e (#18570) 2025-01-09 20:52:55 -03:00
2a29155641 chore: Upgrade jsforce to 3.6.2 and use jsforce-node (#18363)
* Upgrade jsforce to 3.6.2

* Refactor connecting to Salesforce

* Revert yarn.lock changes

* Migrate callback endpoint

* Add jsforce node dependency

* Migrate add endpoint

* Import

* Import package into crmService

* Use new package types

* Type fix

* Update vite config

* Push updated lockfile

* Attempt to bump platform/libraries to unlock jsforce

* Also update lockfile, naturally

* bump platform libraries

* Update vite.config

* Add jsforce to vite config

* fixup! Merge branch 'main' into upgrade-jsforce-v3

* fixup! fixup! Merge branch 'main' into upgrade-jsforce-v3

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-01-09 20:24:06 +00:00
Steven TeyandPeer Richelsen 4902c6a0f8 feat: bulk shorten links with dub.links.createMany (#18539)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-09 20:04:55 +00:00
Joe Au-YeungandUdit Takkar a22fd0c151 chore: Salesforce - optimize getContact method [CAL-4940] (#18289)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-01-09 12:14:49 -07:00
Peer Richelsen b6bd0eda43 Documentation edits made through Mintlify web editor 2025-01-09 19:59:04 +01:00
mintlify[bot]andPeer Richelsen 25d052e8f0 mintlify help edits (#18562)
* Documentation edits made through Mintlify web editor

* Documentation edits made through Mintlify web editor

* Delete help/calendars/how-to-add-apple-calendar.mdx

* Delete help/calendars/how-to-add-google-calendar.mdx

---------

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-09 19:56:22 +01:00
mintlify[bot] f933432d2b Documentation edits made through Mintlify web editor (#18565)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2025-01-09 19:52:49 +01:00
Benny JooandAnik Dhabal Babu ccf1d4430a chore: clean up some code in app router (#18541)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-01-09 10:47:21 -07:00
2fd8d24204 perf: Improve routing insights view (#18557)
* perf: Improve routing insights view

* fix: remove old join

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-01-09 16:42:26 +00:00
Syed Ali Shahbaz b4c18f7bcc Update README.md (#18524) 2025-01-09 16:33:02 +00:00
sean-brydon 23c93d75ca fix: Handle booker UI busy or skip on timeslot (#18553) 2025-01-09 16:29:42 +00:00
Benny Joo 91526d0bc7 chore: unique key missing for child (#18542) 2025-01-09 16:20:05 +00:00
Anik Dhabal Babu a9f95937c4 chore: temporarily skip a failing E2E test (#18560) 2025-01-09 15:53:12 +00:00
Nizzy 74580157ba fix: added videos and fixed iframe sizes for help page docs (#18558) 2025-01-09 16:37:59 +01:00
mintlify[bot] fcdbcceac0 feat: Mintlify /help edits (#18555)
* Documentation edits made through Mintlify web editor

* Documentation edits made through Mintlify web editor

* Documentation edits made through Mintlify web editor

* Documentation edits made through Mintlify web editor

---------

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2025-01-09 15:36:56 +01:00
Peer Richelsen aa7c924c2a Documentation edits made through Mintlify web editor 2025-01-09 13:23:51 +01:00
Peer Richelsen 3406ca72bc Documentation edits made through Mintlify web editor 2025-01-09 13:22:18 +01:00
Peer Richelsen c29a2fc7cb Documentation edits made through Mintlify web editor 2025-01-09 13:20:03 +01:00
Peer Richelsen 0a4c7bcecc Documentation edits made through Mintlify web editor 2025-01-09 13:19:26 +01:00
Peer Richelsen 803dfa008c Documentation edits made through Mintlify web editor 2025-01-09 13:17:24 +01:00
Peer Richelsen 0eb6f1beb6 chore: mintlify updates (#18552) 2025-01-09 13:05:32 +01:00
Peer Richelsen edd2e5e584 chore: fixed help logo (#18551) 2025-01-09 12:57:09 +01:00
mintlify[bot] 99a99b4b7e Documentation edits made through Mintlify web editor (#18543) 2025-01-09 09:45:21 +01:00
Somay ChauhanandRajiv Sahal 2ba6f4cb24 fix: payment not working in booker (#18540)
* fix: payment not working in booker

* Update transformApiEventTypeForAtom.ts

---------

Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2025-01-09 07:32:13 +00:00
Udit TakkarandBailey Pumfleet 8a8c870513 chore: change identifier (#18518)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2025-01-08 23:39:56 -05:00
Peer Richelsen eba335c903 chore: moved docs into mintlify (#18536) 2025-01-08 23:01:53 +01:00
GitHub Actions 9b2161cc19 chore: release v4.8.15
Post release / release (push) Canceled after 0s
2025-01-08 18:53:07 +00:00
Morgan b997c656df feat: api v2 org ooo crud (#18499)
* feat: api v2 org ooo crud

* chore: add guard isUserOOO on ooo controller

* chore: add checks for ooo

* fixup! chore: add checks for ooo

* added e2e tests

* fixup! added e2e tests
2025-01-08 10:50:58 -05:00
Udit Takkarandsupalarry c9c5493de6 feat: support phone based booking api (#17635)
* chore: save progress

* feat: support phone based booking using API v2

* chore: change name of the variable

* fix: update event types and booking fields for improved validation and new properties

- Updated imports in input and output event types services to use the correct library version.
- Added new `avatarUrl` and `successRedirectUrl` properties to the Swagger documentation for user and booking schemas.
- Enhanced booking fields validation by changing the `required` property to a boolean type.
- Improved descriptions and examples in the OpenAPI specifications for better clarity.
- Refactored booking fields input and output classes to include new properties and ensure proper validation.

* fix: booking with phone number

* test: add event type update test

* test: add test for creating booking

* chore: update unit test

* fix: tests

* fix: add turbo

* chore: remove emial

* refactor: make email optional and move it to service

* fix: test

* chore: bump platform-libraries

* fix: e2e test

---------

Co-authored-by: supalarry <laurisskraucis@gmail.com>
2025-01-08 21:16:10 +05:30
Anwar Sadath 20a5775f31 fix: Description not rendered as html in select event types screen when installing an app (#18354) 2025-01-08 15:03:34 +00:00
Omar López b9b0a86b32 feat: Add auto payment methods for paid booking on Stripe (#18513) 2025-01-08 07:55:14 -07:00
Abhishek Singh ad93066cf6 feat-insihts-app (#18461) 2025-01-08 14:24:20 +00:00
Benny Joo e6d76b630c chore: Revert "chore: app router - /team, /org, /[user] booking pages (excl. embeds) (#18186) (#18526) 2025-01-08 11:19:45 -03:00
Eunjae Lee b941b716d5 fix: add index to AssignmentReason (#18520) 2025-01-08 12:32:28 +00:00
Eunjae LeeandUdit Takkar 1ab9622416 feat: add DataTableWrapper (#18503)
* feat: data table wrapper

* feat: add DataTableWrapper

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-01-08 09:12:55 +00:00
Benny Joo 15156c2225 perf: use react svg cache from root level (#18506) 2025-01-08 09:29:50 +01:00
Benny JooandPeer Richelsen cc202ed847 chore: app router - /event-types pages (#18181)
* refactor event type web wrapper

* finish migration

* move event-types folder out of future

* remove vars

* remove pages router

* fix

* fix

* fix test

* remove test tag

* refactor

* remove completely

* add withoutSeo prop for event type web page

* remove HeadSeo usage from event types listing view

* make onboarding e2e test faster

* remove useIntercom

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-07 20:39:05 -05:00
Joe Au-Yeung 79d51a4989 chore: Add Salesforce tests to getContacts method (#18225) 2025-01-07 14:14:34 -07:00
Calcom BotandReplexica 53a846d10c feat: update translations via @replexica (#18496)
Co-authored-by: Replexica <support@replexica.com>
2025-01-07 20:53:16 +00:00
GitHub Actions 1619f2daf2 chore: release v4.8.14
Post release / release (push) Canceled after 0s
2025-01-07 20:06:08 +00:00
Alex van Andel 57bd0b3df3 fix: type + build errors (#18502) 2025-01-07 18:35:06 +01:00
Udit Takkar e359d13d9a Revert "fix: improve useInViewObserver hook to trigger when observer is alrea…" (#18504)
This reverts commit 22f92f1ee3.
2025-01-07 16:41:55 +00:00
Hariom Balhara 3b7cb542ed fix checklyt tests (#18501) 2025-01-07 12:58:39 +00:00
Alex van Andel fd7f6c32ae chore: Remove all instances of useIntercom (#18500) 2025-01-07 07:43:46 -05:00
Hariom Balhara 06d8b98b15 fix: 404 on the public form page for a sub-team form when the owner itself was removed from the org (#18495)
* wip

* fix: 404 for a form of a member that has been removed.

- Moved existing authorization check to isAuthorizedToViewFormOnOrgDomain function to handle form access control
- Add comprehensive test suite for org domain authorization scenarios
- Refactor getServerSideProps to use the new authorization function
- Improve code organization by separating domain-specific authorization logic

Test Coverage:
- Non-org domain access
- Org member's form access
- Sub-team form access within org
- Access denial for non-org members/teams
2025-01-07 12:13:19 +00:00
Benny Joo 0c69c5dfea remove MainLayout / MainLayoutAppDir (#18498) 2025-01-07 12:13:00 +00:00
Benny Joo 00f7d8be0d feat: bookings - render layout only once (#18490) 2025-01-07 11:37:17 +00:00
Hariom Balhara 2e9beb8513 fix: Make it easier to manage group options and also improve attribute options performance with 100s of options (#18486) 2025-01-07 11:16:52 +00:00
Omar López b6c6fa7365 Update repository.ts (#18482) 2025-01-07 11:10:52 +00:00
Benny Joo c623785ec2 chore: remove unused code from SideBar (#18487) 2025-01-07 10:56:09 +00:00
Alex van Andel 6a381cdc79 perf: Remove zod from edge runtime (#18494) 2025-01-07 10:47:48 +00:00
Benny Joo 9d4496133e feat: Prevent re-renders (moving SessionProvider to Root level for both app router / pages router) (#18493) 2025-01-07 11:46:08 +01:00
Benny Joo 1452672495 withEmbedSsr is missing in a embed route (#18483) 2025-01-07 07:28:55 -03:00
Lauris Skraucis 9159c591ff fix: v2 slots timeZone parameter (#18488)
* fix: v2 slots timeZone parameter

* test: v2 slots
2025-01-07 15:24:20 +05:30
Benny Joo 408aee5851 insights layout (#18489) 2025-01-07 09:02:52 +00:00
Vishnu 29bbada77a fix: cancel workflows on cancel booking (#17871)
* feat: cancel scheduled email from sendGrid while cancelling the booking

* fix: remove deleteScheduledSend in scheduledEmailReminder cron api

* fix: use cancelScheduledEmailSend

* chore: remove cancelling email workflow in deleteScheduledEmailReminder

* refactor: bring back deleteScheduleSend

* fix: get only past reminders
2025-01-07 00:06:57 -05:00
PraashandHariom 6178092878 fix: Cannot read properties of undefined (reading 'timeZone') (#18107)
Co-authored-by: Hariom <hariombalhara@gmail.com>
2025-01-07 04:17:17 +00:00
Hariom BalharaandOmar López 6f531d0be5 feat: Single domain setup when only one org in the system (#18383)
Co-authored-by: Omar López <zomars@me.com>
2025-01-07 09:28:44 +05:30
GitHub Actions d7b723c914 chore: release v4.8.13
Post release / release (push) Canceled after 0s
2025-01-06 21:59:03 +00:00
Anik Dhabal BabuandAlex van Andel aad3988d60 fix: Zoho Calendar is allowing bookings during busy schedules (#18473)
* fix: zoho calendar allowing to book busy schedule

* Improved more readable version

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-06 21:00:05 +05:30
c8172ab790 refactor: v2 default booking fields (#18287)
* feat: allow title,notes,reschedule reason inputs

* refactor: v2 event type default booking fields as in web app

* wip: input validation

* input field validation

* fix: tests

* chore: bump libraries

* remove change in docs

* fix input type

* chore: ensure additional notes at the end

* fixup! Merge branch 'main' into lauris/cal-4932-platform-refactor-default-booking-fields

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-01-06 16:49:57 +02:00
657f9dfd41 chore: ignore oAuthClientId for Booker Embed bookings (#18314)
* chore: ignore oAuthClientId for Booker Embed bookings

* remove logs

* chore: isEmbed header and emails

* fixup! chore: isEmbed header and emails

* fixup! fixup! chore: isEmbed header and emails

* fixup! fixup! fixup! chore: isEmbed header and emails

* fixup! fixup! fixup! fixup! chore: isEmbed header and emails

* fixup! fixup! fixup! fixup! fixup! chore: isEmbed header and emails

---------

Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2025-01-06 16:21:38 +02:00
Calcom BotandReplexica c48b209c04 feat: update translations via @replexica (#18472)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2025-01-06 11:13:03 -03:00
Hariom BalharaandPeer Richelsen b9a460d8b4 chore: Bump version for embed (#18285)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-06 12:42:13 +00:00
a7727b5ca6 feat: shorten sms links using dub (#17925)
* feat: add getshortenLinks function

* feat: use shortenLinks in SMS Worflows

* chore: use getShortenLink and cleanup getShortenLinks

* fix: Duplicate keys error

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-06 12:39:54 +00:00
Lauris Skraucis 9b661b75f1 docs: OAuth client redirectUri with wildcard character (#18267) 2025-01-06 09:39:31 -03:00
Kartik SainiandPeer Richelsen 487cc0751d feat: add cancel and reschedule reason variables in workflows (#18305)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-06 11:56:06 +00:00
Akash Jayan f7f36978e1 fix: initialize useCustomTemplate state based on payloadTemplate prop… (#18257) 2025-01-06 12:37:23 +01:00
Amit SharmaandPeer Richelsen c1dbb7cffd feat: refund policies for payment apps (#18428)
* feat: refund policies for payment apps

* tests for payment refund

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-06 11:36:37 +00:00
Morgan e722e75a6f docs: cal-api-version header example for bookings controller v2 (#18271)
* docs: api v2 cal-api-version header example for bookings controller

* fixup! docs: api v2 cal-api-version header example for bookings controller
2025-01-06 11:36:03 +00:00
Amit SharmaandAnik Dhabal Babu 455572b5f4 feat: Info lost when navigating without warning routing form (#18386)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-01-06 12:19:58 +01:00
Alex van Andel 2fe4462f62 perf: Use improved checkIfAvailable and remove dupe code (#18463) 2025-01-06 09:53:39 +00:00
Amit Sharma f9dc788007 fix: It's hard to click on the last available Event Duration (#18462) 2025-01-04 15:07:36 +00:00
René Aaron 6e293d2985 fix: improve qr readability of alby payment integration (#18266)
* fix: optimize qr for better readability

* fix: optimize readability
2025-01-04 12:18:31 +01:00
Benny Joo 00334c5610 chore: app router - /signup page (#18151)
* app router - /signup page

* add to config.matcher

* fix
2025-01-03 22:27:03 +00:00
Benny JooandHariom Balhara 2f9c8cb441 chore: app router - /team, /org, /[user] booking pages (excl. embeds) (#18186)
* update env vars

* update middleware

* remove pages router and move pages to /app

* move to /team

* update imports

* fix

* remove pages router and move org pages to /app

* wip

* fix orgSlug/user pages

* fix orgSlug/user/type pages

* remove generateMetadata from embed pages

* fix

* remove pages router for /user pages

* generateMetadata is not needed in embed pages

* remove future/org

* no layout in [user] page

* simplify

* fix

* fix OG image for [user]

* fix OG images for org/user and team/slug

* fix OG images for booking page

* fix all metadata

* use isBrandingHidden

* remove BookerSeo and its usages

* rename excludeAppNameFromTitle -> hideBranding

* remove logic for meeting type in HeadSeo

* remove BookerSeo instances from team-view and users-public-view

* create generateMeetingMetadata util and use it to reduce duplicate code

* remove BookerSeo imports

* fix spacing

* remove constructMeetingImage mock from head-seo.test

* fetch avatarUrl using user id for user page metadata

* fix test

* remove unused test cases

* index and follow must be true by default

* invert noindex/nofollow flags

* remove HeadSeo for already migrated pages and refactor prepareMetadata

* fix organization-settings.e2e.ts

* fix order

* enable parallel test execution for dynamic-booking e2e test

* fix

* + could be %2B in app router

* refactor handling logic for embeds in app router

* fix isEmbed

* fix embed-core

* remove dead code

* move embed pages back to /future

* add back embed pages in pages router

* revert some changes

* fix import type checks

* simplify

* fix

* feat: Implement generateBookingPageMetadata function for improved SEO and metadata handling across user and event pages (#18440)

- Added a new utility function `generateBookingPageMetadata` to streamline the generation of metadata for booking and user profile pages.
- Updated multiple page components to utilize the new function, enhancing SEO indexing and metadata consistency.
- Removed redundant code related to previous metadata generation methods, improving code clarity and maintainability.

* fix dirs

* Pr-review-fixes-app-router-team-pages (#18450)

* Remove unnecessary getPublicEvent call

* Remove unused variable

* Fix 404 for team page (#18451)

* Remove console log

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-01-03 22:26:30 +00:00
GitHub Actions 39173900b1 chore: release v4.8.12
Post release / release (push) Canceled after 0s
2025-01-03 21:53:06 +00:00
Syed Ali Shahbaz bd61e01a5d fix: Show managed event bookings for team admins/owners (#18434)
* add managed event bookings for team admins/owners

* remove unnecessary OR

* address change suggestion

* improve readability
2025-01-03 21:43:05 +00:00
Cao Hoang aa0b3a2778 fix: improve useInViewObserver hook to trigger when observer is already in view on mount (#18455) 2025-01-03 21:20:59 +00:00
Joe Au-Yeung 170feeae5e chore: Salesforce - Add additional logging and error checks (#18448) 2025-01-03 12:57:24 -05:00
Eunjae Lee a67b57b216 feat: server side sorting for insights routing (#18113)
* feat: server side sorting for insights routing

^ Conflicts:
^	packages/prisma/playground.ts

* fix download csv

* fix hooks locations
2025-01-03 10:58:01 +00:00
821a16648f fix: Timezone in Email Embed not working #15525 (#15537)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2025-01-03 08:43:09 +00:00
Hariom Balhara 727f7df2c2 fix: Enhance SCIM payload handling and attribute creation (#18427)
* feat: Enhance SCIM user attribute handling and sync process

- Introduced a new PR_TODO.md file to track ongoing tasks and fixes.
- Updated `getAttributesFromScimPayload` to accept a directoryId and ignore core user attributes during syncing.
- Modified `handleUserEvents` to pass directoryId when syncing custom attributes.
- Improved attribute creation logic to handle unique options and added support for optional isGroup property in attribute options.
- Added utility function `getOptionsWithValidContains` to ensure valid sub-options in attribute options.
- Updated tests to reflect changes in attribute handling and ensure proper functionality.

This commit addresses issues with user attribute syncing and enhances the overall attribute management process.

* test: Add unit tests for getOptionsWithValidContains utility function

- Introduced a new test file for the getOptionsWithValidContains function.
- Added tests to verify the removal of duplicate options, initialization of empty contains arrays, filtering of non-existent sub-options, and handling of empty options arrays.
- Ensured comprehensive coverage of the utility's functionality to enhance reliability and maintainability.
2025-01-03 13:36:21 +05:30
Anik Dhabal Babu be5f7f83b5 fix: Zoho Bigin not pulling information from booking (#18445)
* fix: Zoho Bigin not pulling information from booking

* update

* missed

* fix contactcreation
2025-01-03 08:41:18 +05:30
amrit 02a49e2b4b fix: react incompatiblity with nextjs error (#18433) 2025-01-03 01:03:46 +00:00
Cao Hoang 22f92f1ee3 fix: improve useInViewObserver hook to trigger when observer is already in view on mount (#18446) 2025-01-02 23:58:40 +00:00
Udit Takkar c7a797c0e0 fix: get bookings ordering (#18439)
* fix: get bookings ordering

* chore: update query

* chore: add log for debuggin on prod

* chore
2025-01-02 23:41:30 +00:00
Calcom BotandReplexica 447436dfff feat: update translations via @replexica (#18438)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2025-01-02 22:23:30 +00:00
Eunjae LeeandUdit Takkar c415b2d7f5 feat: apply full width automatically for DataTable (#18357)
* feat: apply full width automatically for DataTable

* change implementation

* load all columns of insights routing table at the same time

* update team member list

* sticky columns for >= sm

* fix type error

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-01-02 14:32:39 +00:00
Lauris Skraucis c10c87c98e fix: booker atom booking fields (#18441) 2025-01-02 11:31:43 +00:00
Benny Joo 85f96f492d chore: migrate /icons page to App Router (#18436)
* migrate icons page

* migrate main index page

* remove /icons page

* use i18n string

* fix imports

* add icons_showcase i18n string

* add /app dir to tailwind preset config

* use classname

* do not migrate main index page
2025-01-02 15:18:19 +05:30
Morgan b1350d0764 fix: atoms globals.css invalid class format (#18295) 2025-01-02 09:32:50 +00:00
Hariom BalharaandUdit Takkar bac2d1f6f0 feat: Add dry run mode functionality and message display in booking process (#18429)
- Introduced a new `DryRunMessage` component to inform users when in dry run mode.
- Implemented utility function `isBookingDryRun` to check for dry run status based on URL search parameters.
- Updated the `Booker` component to conditionally render the dry run message.
- Adjusted booking mutation input mapping to reuse the utility

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-01-01 11:40:53 +00:00
Anik Dhabal Babu fec504496a fix: Add to google leads to a 403 page (#18430) 2024-12-31 14:11:42 +00:00
Alex van Andel 890fc92148 fix: Some recently discovered type errors breaking build (not sure why undetected before) (#18432) 2024-12-31 13:50:43 +00:00
Benny Joo 557bd8bf25 chore: Migrate /403 and /500 pages to App Router (#18424)
* global-error is not needed

* remove /403 and /500 pages from pages router

* add 403 and 500 pages in App Router

* add i18n strings

* fix i18n string

* fix styling
2024-12-31 11:43:15 +05:30
Cao HoangandUdit Takkar d8027fe248 Fix duplicated data returned by Prisma query in team availability view (#18416)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-30 21:15:27 +00:00
Anik Dhabal Babu 116c765c36 fix: Error changing location for meeting (#18401) 2024-12-30 14:59:07 +00:00
1112 changed files with 30603 additions and 9712 deletions
+7 -17
View File
@@ -112,7 +112,7 @@ NEXT_PUBLIC_POSTHOG_HOST=
PLAIN_API_KEY=
PLAIN_API_URL=https://api.plain.com/v1
PLAIN_HMAC_SECRET_KEY=
PLAIN_CHAT_ID=
NEXT_PUBLIC_PLAIN_CHAT_ID=
PLAIN_CHAT_HMAC_SECRET_KEY=
NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS=
@@ -242,6 +242,7 @@ E2E_TEST_MAILHOG_ENABLED=
# Cloudflare Turnstile
NEXT_PUBLIC_CLOUDFLARE_SITEKEY=
NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER=
CLOUDFLARE_TURNSTILE_SECRET=
# Set the following value to true if you wish to enable Team Impersonation
@@ -355,25 +356,9 @@ 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
# whether we redirect to the future/event-types from event-types or not
APP_ROUTER_EVENT_TYPES_ENABLED=0
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
APP_ROUTER_APPS_SLUG_ENABLED=0
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
# whether we redirect to the future/apps/categories from /apps/categories or not
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
APP_ROUTER_APPS_ENABLED=0
APP_ROUTER_TEAM_ENABLED=0
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
APP_ROUTER_AUTH_LOGIN_ENABLED=0
APP_ROUTER_AUTH_LOGOUT_ENABLED=0
APP_ROUTER_AUTH_NEW_ENABLED=0
APP_ROUTER_AUTH_SAML_ENABLED=0
APP_ROUTER_AUTH_ERROR_ENABLED=0
APP_ROUTER_AUTH_PLATFORM_ENABLED=0
APP_ROUTER_AUTH_OAUTH2_ENABLED=0
# disable setry server source maps
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
@@ -416,3 +401,8 @@ REPLEXICA_API_KEY=
# Comma-separated list of DSyncData.directoryId to log SCIM API requests for. It can be enabled temporarily for debugging the requests being sent to SCIM server.
DIRECTORY_IDS_TO_LOG=
# Set this when Cal.com is used to serve only one organization's booking pages
# Read more about it in the README.md
NEXT_PUBLIC_SINGLE_ORG_SLUG=
@@ -1,39 +0,0 @@
# .github/workflows/chromatic.yml
# Workflow name
name: "Chromatic"
# Event for the workflow
on:
pull_request_target: # So we can test on forks
branches:
- main
paths:
- apps/storybook/**
- packages/ui/**
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
# List of jobs
jobs:
chromatic-deployment:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
fetch-depth: 0
- name: Install dependencies
run: yarn
# 👇 Adds Chromatic as a step in the workflow
- name: Publish to Chromatic
uses: chromaui/action@v1
# Options required to the GitHub Chromatic Action
with:
# 👇 Chromatic projectToken, refer to the manage page to obtain it.
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
workingDir: "apps/storybook"
buildScriptName: "build"
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: replexica/replexica@main
- uses: lingodotdev/lingo.dev@main
env:
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
with:
+2 -12
View File
@@ -12,16 +12,6 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- name: Run Linting with Reports
run: yarn lint:report
- name: Run Lint
run: yarn lint
continue-on-error: true
- name: Merge lint reports
run: jq -s '[.[]]|flatten' lint-results/*.json &> lint-results/eslint_report.json
- name: Upload ESLint report
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: lint-results
path: lint-results
@@ -1,5 +1,5 @@
diff --git a/index.cjs b/index.cjs
index c83f700ae9998cd87b4c2d66ecbb2ad3d7b4603c..76a2200b57f0b9243e2c61464d578b67746ad5a4 100644
index c83f700..da6fc7e 100644
--- a/index.cjs
+++ b/index.cjs
@@ -13,8 +13,8 @@ function withMetadataArgument(func, _arguments) {
@@ -12,4 +12,4 @@ index c83f700ae9998cd87b4c2d66ecbb2ad3d7b4603c..76a2200b57f0b9243e2c61464d578b67
+// exports['default'] = min.parsePhoneNumberFromString
// `parsePhoneNumberFromString()` named export is now considered legacy:
// it has been promoted to a default export due to being too verbose.
// it has been promoted to a default export due to being too verbose.
+6
View File
@@ -0,0 +1,6 @@
undecided:
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
+6
View File
@@ -0,0 +1,6 @@
undecided:
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
View File
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+7
View File
@@ -0,0 +1,7 @@
undecided:
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@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"
View File
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
View File
+9
View File
@@ -485,6 +485,15 @@ Don't code but still want to contribute? Join our [Discussions](https://github.c
- Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
## Single Org Mode
If you want to have booker.yourcompany.com to be the domain used for both dashboard(e.g. https://booker.yourcompany.com/event-types) and booking pages(e.g. https://booker.yourcompany.com/john.joe/15min).
- Set the `NEXT_PUBLIC_SINGLE_ORG_SLUG` environment variable to the slug of the organization you want to use. `NEXT_PUBLIC_SINGLE_ORG_SLUG=booker`
- Set the `NEXT_PUBLIC_WEBAPP_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXT_PUBLIC_WEBAPP_URL=https://booker.yourcompany.com`.
- Set the `NEXT_PUBLIC_WEBSITE_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXT_PUBLIC_WEBSITE_URL=https://booker.yourcompany.com`.
- Set the `NEXTAUTH_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXTAUTH_URL=https://booker.yourcompany.com`.
Note: It causes root to serve the dashboard and not the organization profile page which shows all bookable users in the organization.
## Integrations
### Obtaining the Google API Credentials
+12 -10
View File
@@ -11,15 +11,15 @@ test.describe("Org", () => {
await expectPageToBeRenderedWithEmbedSsr(page);
});
test("Org User(Peer) Page should be embeddable", async ({ page }) => {
const response = await page.goto("https://i.cal.com/peer/embed");
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=Peer Richelsen")).toBeVisible();
await expect(page.locator("text=Used by Checkly")).toBeVisible();
await expectPageToBeRenderedWithEmbedSsr(page);
});
test("Org User Event(peer/meet) Page should be embeddable", async ({ page }) => {
const response = await page.goto("https://i.cal.com/peer/meet/embed");
test("Org User Event(/team-rick/test-event) Page should be embeddable", async ({ page }) => {
const response = await page.goto("https://i.cal.com/team-rick/test-event/embed");
expect(response?.status()).toBe(200);
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
@@ -58,10 +58,10 @@ test.describe("Org", () => {
expect(response?.status()).toBe(200);
expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Group Meeting");
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[0].name);
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[1].name);
// 2 users and 1 for the organization(2+1)
expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(3);
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(
"Join us for a meeting with multiple people"
);
expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(2);
});
});
@@ -75,7 +75,9 @@ test.describe("Org", () => {
test.describe("Browse the Engineering Team", async () => {
test("By User Navigation", async ({ page }) => {
await page.goto("https://i.cal.com");
const response = await page.goto("https://i.cal.com");
await page.waitForLoadState("networkidle");
expect(response?.status()).toBe(200);
await page.click('text="Engineering"');
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
});
+156 -5
View File
@@ -1,24 +1,34 @@
import type { RatelimitResponse } from "@unkey/ratelimit";
import type { Request, Response } from "express";
import type { NextApiResponse, NextApiRequest } from "next";
import { createMocks } from "node-mocks-http";
import { describe, it, expect, vi } from "vitest";
import { handleAutoLock } from "@calcom/lib/autoLock";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { HttpError } from "@calcom/lib/http-error";
import { rateLimitApiKey } from "~/lib/helpers/rateLimitApiKey";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
const testUserId = 123;
vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({
checkRateLimitAndThrowError: vi.fn(),
}));
vi.mock("@calcom/lib/autoLock", () => ({
handleAutoLock: vi.fn(),
}));
describe("rateLimitApiKey middleware", () => {
it("should return 401 if no apiKey is provided", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {},
userId: testUserId,
});
await rateLimitApiKey(req, res, vi.fn() as any);
@@ -31,6 +41,7 @@ describe("rateLimitApiKey middleware", () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
(checkRateLimitAndThrowError as any).mockResolvedValueOnce({
@@ -43,7 +54,7 @@ describe("rateLimitApiKey middleware", () => {
await rateLimitApiKey(req, res, vi.fn() as any);
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
identifier: "test-key",
identifier: testUserId.toString(),
rateLimitingType: "api",
onRateLimiterResponse: expect.any(Function),
});
@@ -53,19 +64,23 @@ describe("rateLimitApiKey middleware", () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
const rateLimiterResponse = {
const rateLimiterResponse: RatelimitResponse = {
limit: 100,
remaining: 99,
reset: Date.now(),
success: true,
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
onRateLimiterResponse(rateLimiterResponse);
});
(checkRateLimitAndThrowError as any).mockImplementationOnce(
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
onRateLimiterResponse(rateLimiterResponse);
}
);
// @ts-expect-error weird typing between middleware and createMocks
await rateLimitApiKey(req, res, vi.fn() as any);
@@ -79,6 +94,7 @@ describe("rateLimitApiKey middleware", () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
(checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded"));
@@ -89,4 +105,139 @@ describe("rateLimitApiKey middleware", () => {
expect(res._getStatusCode()).toBe(429);
expect(res._getJSONData()).toEqual({ message: "Rate limit exceeded" });
});
it("should lock API key when rate limit is repeatedly exceeded", async () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
const rateLimiterResponse: RatelimitResponse = {
success: false,
remaining: 0,
limit: 100,
reset: Date.now(),
};
// Mock rate limiter to trigger the onRateLimiterResponse callback
(checkRateLimitAndThrowError as any).mockImplementationOnce(
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
onRateLimiterResponse(rateLimiterResponse);
}
);
// Mock handleAutoLock to indicate the key was locked
vi.mocked(handleAutoLock).mockResolvedValueOnce(true);
// @ts-expect-error weird typing between middleware and createMocks
await rateLimitApiKey(req, res, vi.fn() as any);
expect(handleAutoLock).toHaveBeenCalledWith({
identifier: testUserId.toString(),
identifierType: "userId",
rateLimitResponse: rateLimiterResponse,
});
expect(res._getStatusCode()).toBe(429);
expect(res._getJSONData()).toEqual({ message: "Too many requests" });
});
it("should handle API key not found error during auto-lock", async () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
const rateLimiterResponse: RatelimitResponse = {
success: false,
remaining: 0,
limit: 100,
reset: Date.now(),
};
// Mock rate limiter to trigger the onRateLimiterResponse callback
(checkRateLimitAndThrowError as any).mockImplementationOnce(
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
onRateLimiterResponse(rateLimiterResponse);
}
);
// Mock handleAutoLock to throw a "No user found" error
vi.mocked(handleAutoLock).mockRejectedValueOnce(new Error("No user found for this API key."));
// @ts-expect-error weird typing between middleware and createMocks
await rateLimitApiKey(req, res, vi.fn() as any);
expect(handleAutoLock).toHaveBeenCalledWith({
identifier: testUserId.toString(),
identifierType: "userId",
rateLimitResponse: rateLimiterResponse,
});
expect(res._getStatusCode()).toBe(401);
expect(res._getJSONData()).toEqual({ message: "No user found for this API key." });
});
it("should continue if auto-lock returns false (not locked)", async () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
const rateLimiterResponse: RatelimitResponse = {
success: false,
remaining: 0,
limit: 100,
reset: Date.now(),
};
// Mock rate limiter to trigger the onRateLimiterResponse callback
(checkRateLimitAndThrowError as any).mockImplementationOnce(
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
onRateLimiterResponse(rateLimiterResponse);
}
);
// Mock handleAutoLock to indicate the key was not locked
vi.mocked(handleAutoLock).mockResolvedValueOnce(false);
const next = vi.fn();
// @ts-expect-error weird typing between middleware and createMocks
await rateLimitApiKey(req, res, next);
expect(handleAutoLock).toHaveBeenCalledWith({
identifier: testUserId.toString(),
identifierType: "userId",
rateLimitResponse: rateLimiterResponse,
});
// Verify headers were set but request continued
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
expect(next).toHaveBeenCalled();
});
it("should handle HttpError during rate limiting", async () => {
const { req, res } = createMocks({
method: "GET",
query: { apiKey: "test-key" },
userId: testUserId,
});
// Mock checkRateLimitAndThrowError to throw HttpError
vi.mocked(checkRateLimitAndThrowError).mockRejectedValueOnce(
new HttpError({
statusCode: 429,
message: "Custom rate limit error",
})
);
// @ts-expect-error weird typing between middleware and createMocks
await rateLimitApiKey(req, res, vi.fn() as any);
expect(res._getStatusCode()).toBe(429);
expect(res._getJSONData()).toEqual({ message: "Custom rate limit error" });
});
});
+27 -3
View File
@@ -1,23 +1,47 @@
import type { NextMiddleware } from "next-api-middleware";
import { handleAutoLock } from "@calcom/lib/autoLock";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { HttpError } from "@calcom/lib/http-error";
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
if (!req.userId) return res.status(401).json({ message: "No userId provided" });
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
// TODO: Add a way to add trusted api keys
try {
const identifier = req.userId.toString();
await checkRateLimitAndThrowError({
identifier: req.query.apiKey as string,
identifier,
rateLimitingType: "api",
onRateLimiterResponse: (response) => {
onRateLimiterResponse: async (response) => {
res.setHeader("X-RateLimit-Limit", response.limit);
res.setHeader("X-RateLimit-Remaining", response.remaining);
res.setHeader("X-RateLimit-Reset", response.reset);
try {
const didLock = await handleAutoLock({
identifier,
identifierType: "userId",
rateLimitResponse: response,
});
if (didLock) {
return res.status(429).json({ message: "Too many requests" });
}
} catch (error) {
if (error instanceof Error && error.message === "No user found for this API key.") {
return res.status(401).json({ message: error.message });
}
throw error;
}
},
});
} catch (error) {
res.status(429).json({ message: "Rate limit exceeded" });
if (error instanceof HttpError) {
return res.status(error.statusCode).json({ message: error.message });
}
return res.status(429).json({ message: "Rate limit exceeded" });
}
await next();
+17
View File
@@ -38,6 +38,23 @@ export const schemaBookingGetParams = z.object({
export type Status = z.infer<typeof schemaBookingGetParams>["status"];
export const bookingCancelSchema = z.object({
id: z.number(),
allRemainingBookings: z.boolean().optional(),
cancelSubsequentBookings: z.boolean().optional(),
cancellationReason: z.string().optional().default("Not Provided"),
seatReferenceUid: z.string().optional(),
cancelledBy: z.string().email({ message: "Invalid email" }).optional(),
internalNote: z
.object({
id: z.number(),
name: z.string(),
cancellationReason: z.string().optional().nullable(),
})
.optional()
.nullable(),
});
const schemaBookingEditParams = z
.object({
title: z.string().optional(),
@@ -1,9 +1,11 @@
import { z } from "zod";
// Helper schema for JSON fields
type Literal = boolean | number | string;
type Literal = boolean | number | string | null;
type Json = Literal | { [key: string]: Json } | Json[];
const literalSchema = z.union([z.string(), z.number(), z.boolean()]);
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
const jsonObjectSchema = z.record(z.lazy(() => jsonSchema));
const jsonArraySchema = z.array(z.lazy(() => jsonSchema));
export const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
z.union([literalSchema, jsonObjectSchema, jsonArraySchema])
);
@@ -2,8 +2,8 @@ import type { NextApiRequest } from "next";
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
import { defaultResponder } from "@calcom/lib/server";
import { bookingCancelSchema } from "@calcom/prisma/zod-utils";
import { bookingCancelSchema } from "~/lib/validations/booking";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
/**
@@ -70,6 +70,7 @@ async function handler(req: NextApiRequest) {
...req.query,
allRemainingBookings: req.query.allRemainingBookings === "true",
});
// Normalizing for universal handler
req.body = { id, allRemainingBookings, cancellationReason };
return await handleCancelBooking(req);
+5
View File
@@ -5,6 +5,7 @@ import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { CreationSource } from "@calcom/prisma/enums";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -213,6 +214,10 @@ import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
*/
async function handler(req: NextApiRequest) {
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
req.body = {
...req.body,
creationSource: CreationSource.API_V1,
};
if (isSystemWideAdmin) req.userId = req.body.userId || userId;
if (isOrganizationOwnerOrAdmin) {
+2
View File
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { CreationSource } from "@calcom/prisma/enums";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
import type { TInviteMemberInputSchema } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema";
@@ -43,6 +44,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
language: data.language,
teamId: data.teamId,
usernameOrEmail: data.usernameOrEmail,
creationSource: CreationSource.API_V1,
});
return { success: true, message: `${data.usernameOrEmail} has been invited.` };
+18 -2
View File
@@ -2,6 +2,7 @@ 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 { IS_PRODUCTION } from "@calcom/lib/constants";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
@@ -192,11 +193,26 @@ const generateTeamCheckoutSession = async ({
pendingPaymentTeamId: number;
ownerId: number;
}) => {
const customer = await getStripeCustomerIdFromUserId(ownerId);
const [customer, dubCustomer] = await Promise.all([
getStripeCustomerIdFromUserId(ownerId),
getDubCustomer(ownerId.toString()),
]);
const session = await stripe.checkout.sessions.create({
customer,
mode: "subscription",
allow_promotion_codes: true,
...(dubCustomer?.discount?.couponId
? {
discounts: [
{
coupon:
process.env.NODE_ENV !== "production" && dubCustomer.discount.couponTestId
? dubCustomer.discount.couponTestId
: dubCustomer.discount.couponId,
},
],
}
: { allow_promotion_codes: true }),
success_url: `${WEBAPP_URL}/api/teams/api/create?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${WEBAPP_URL}/settings/my-account/profile`,
line_items: [
+2 -1
View File
@@ -3,6 +3,7 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { CreationSource } from "@calcom/prisma/enums";
import { schemaUserCreateBodyParams } from "~/lib/validations/user";
@@ -92,7 +93,7 @@ async function postHandler(req: NextApiRequest) {
// If user is not ADMIN, return unauthorized.
if (!isSystemWideAdmin) throw new HttpError({ statusCode: 401, message: "You are not authorized" });
const data = await schemaUserCreateBodyParams.parseAsync(req.body);
const user = await prisma.user.create({ data });
const user = await prisma.user.create({ data: { ...data, creationSource: CreationSource.API_V1 } });
req.statusCode = 201;
return { user };
}
@@ -12,6 +12,7 @@ import { ErrorCode } from "@calcom/lib/errorCodes";
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
import prisma from "@calcom/prisma";
import type { Booking } from "@calcom/prisma/client";
import { CreationSource } from "@calcom/prisma/enums";
import handler from "../../../pages/api/bookings/_post";
@@ -220,6 +221,34 @@ describe.skipIf(true)("POST /api/bookings", () => {
});
expect(previousBooking?.status).toBe("cancelled");
});
test("Creates source as api_v1", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
name: "test",
start: dayjs().format(),
end: dayjs().add(1, "day").format(),
eventTypeId: 2,
email: "test@example.com",
location: "Cal.com Video",
timeZone: "America/Montevideo",
language: "en",
customInputs: [],
metadata: {},
userId: 4,
},
prisma,
});
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue(buildEventType());
prismaMock.booking.findMany.mockResolvedValue([]);
await handler(req, res);
createdBooking = JSON.parse(res._getData());
expect(createdBooking.creationSource).toEqual(CreationSource.API_V1);
expect(prismaMock.booking.create).toHaveBeenCalledTimes(1);
});
});
describe("Recurring event-type", () => {
+1 -1
View File
@@ -13,5 +13,5 @@
"setupFiles": ["<rootDir>/test/setEnvVars.ts", "jest-date-mock"],
"reporters": ["default", "jest-summarizing-reporter"],
"workerIdleMemoryLimit": "512MB",
"maxWorkers": 2
"maxWorkers": 8
}
+3 -1
View File
@@ -20,6 +20,7 @@
"test:cov": "yarn dev:build && jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192 --experimental-vm-modules' jest --ci --forceExit --config ./jest-e2e.json",
"test:e2e:local": "yarn test:e2e --maxWorkers=4",
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch",
"prisma": "yarn workspace @calcom/prisma prisma",
"generate-schemas": "yarn prisma generate && yarn prisma format",
@@ -29,7 +30,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.78",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.93",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -59,6 +60,7 @@
"googleapis": "^84.0.0",
"helmet": "^7.1.0",
"ioredis": "^5.3.2",
"jsforce": "^1.11.0",
"luxon": "^3.4.4",
"nest-winston": "^1.9.4",
"next-auth": "^4.22.1",
+5 -2
View File
@@ -14,6 +14,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { RateLimitRepositoryFixture } from "test/fixtures/repository/rate-limit.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import { User, PlatformOAuthClient, Team, RateLimit } from "@calcom/prisma/client";
@@ -24,7 +25,7 @@ describe("AppController", () => {
let userRepositoryFixture: UserRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let rateLimitRepositoryFixture: RateLimitRepositoryFixture;
const userEmail = "app-rate-limits-e2e@api.com";
const userEmail = `app-rate-limits-user-${randomString()}@api.com`;
let user: User;
let organization: Team;
@@ -94,7 +95,9 @@ describe("AppController", () => {
);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
organization = await organizationsRepositoryFixture.create({ name: "ecorp" });
organization = await organizationsRepositoryFixture.create({
name: `app-rate-limits-organization-${randomString()}`,
});
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
oAuthClient = await createOAuthClient(organization.id);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
+2
View File
@@ -18,6 +18,7 @@ import {
CAL_API_VERSION_HEADER,
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
X_CAL_PLATFORM_EMBED,
} from "@calcom/platform-constants";
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
@@ -45,6 +46,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
allowedHeaders: [
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
X_CAL_PLATFORM_EMBED,
CAL_API_VERSION_HEADER,
"Accept",
"Authorization",
@@ -8,6 +8,7 @@ import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/
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 { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
@@ -18,6 +19,7 @@ import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repo
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
@@ -35,7 +37,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let apiKeyString: string;
const userEmail = "bookings-controller-e2e@api.com";
const userEmail = `bookings-2024-04-15-user-${randomString()}@api.com`;
let user: User;
let eventTypeId: number;
@@ -67,13 +69,17 @@ describe("Bookings Endpoints 2024-04-15", () => {
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = keyString;
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `bookings-2024-04-15-schedule-${randomString()}-${describe.name}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
{
title: `bookings-2024-04-15-event-type-${randomString()}-${describe.name}`,
slug: `bookings-2024-04-15-event-type-${randomString()}-${describe.name}`,
length: 60,
},
user.id
);
eventTypeId = event.id;
@@ -343,94 +349,6 @@ describe("Bookings Endpoints 2024-04-15", () => {
});
});
// note(Lauris) : found this test broken here - first thing to fix is that recurring endpoint accepts an array not 1 object.
// it("should create a recurring booking", async () => {
// const bookingStart = "2040-05-25T09:30:00.000Z";
// const bookingEnd = "2040-05-25T10:30:00.000Z";
// const bookingEventTypeId = 7;
// const bookingTimeZone = "Europe/London";
// const bookingLanguage = "en";
// const bookingHashedLink = "";
// const bookingRecurringCount = 5;
// const currentBookingRecurringIndex = 0;
// const body = {
// start: bookingStart,
// end: bookingEnd,
// eventTypeId: bookingEventTypeId,
// timeZone: bookingTimeZone,
// language: bookingLanguage,
// metadata: {},
// hashedLink: bookingHashedLink,
// recurringCount: bookingRecurringCount,
// currentRecurringIndex: currentBookingRecurringIndex,
// };
// return request(app.getHttpServer())
// .post("/v2/bookings/recurring")
// .send(body)
// .expect(201)
// .then((response) => {
// const responseBody: ApiResponse<Awaited<ReturnType<typeof handleNewRecurringBooking>>> =
// response.body;
// expect(responseBody.status).toEqual("recurring");
// });
// });
// note(Lauris) : found this test broken here - first thing to fix is that the eventTypeId must be team event type, because
// instant bookings only work for teams.
// it("should create an instant booking", async () => {
// const bookingStart = "2040-05-25T09:30:00.000Z";
// const bookingEnd = "2040-25T10:30:00.000Z";
// const bookingEventTypeId = 7;
// const bookingTimeZone = "Europe/London";
// const bookingLanguage = "en";
// const bookingHashedLink = "";
// const body = {
// start: bookingStart,
// end: bookingEnd,
// eventTypeId: bookingEventTypeId,
// timeZone: bookingTimeZone,
// language: bookingLanguage,
// metadata: {},
// hashedLink: bookingHashedLink,
// };
// return request(app.getHttpServer())
// .post("/v2/bookings/instant")
// .send(body)
// .expect(201)
// .then((response) => {
// const responseBody: ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>> = response.body;
// expect(responseBody.status).toEqual("instant");
// });
// });
// cancelling a booking hangs the test for some reason
it.skip("should cancel a booking", async () => {
const bookingId = createdBooking.id;
const body = {
allRemainingBookings: false,
cancellationReason: "Was fighting some unforseen rescheduling demons",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${bookingId}/cancel`)
.send(body)
.expect(201)
.then((response) => {
const responseBody: ApiResponse<{ status: typeof SUCCESS_STATUS | typeof ERROR_STATUS }> =
response.body;
expect(bookingId).toBeDefined();
expect(responseBody.status).toEqual(SUCCESS_STATUS);
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
@@ -33,11 +33,12 @@ import {
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";
import { v4 as uuidv4 } from "uuid";
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
import { X_CAL_CLIENT_ID, X_CAL_PLATFORM_EMBED } from "@calcom/platform-constants";
import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants";
import {
handleNewRecurringBooking,
@@ -161,14 +162,15 @@ export class BookingsController_2024_04_15 {
async createBooking(
@Req() req: BookingRequest,
@Body() body: CreateBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<Partial<BookingResponse>>> {
const oAuthClientId = clientId?.toString();
const { orgSlug, locationUrl } = body;
req.headers["x-cal-force-slug"] = orgSlug;
try {
const booking = await handleNewBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed)
);
if (booking.userId && booking.uid && booking.startTime) {
void (await this.billingService.increaseUsageByUserId(booking.userId, {
@@ -192,13 +194,16 @@ export class BookingsController_2024_04_15 {
@Req() req: BookingRequest,
@Param("bookingId") bookingId: string,
@Body() _: CancelBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
const oAuthClientId = clientId?.toString();
if (bookingId) {
try {
req.body.id = parseInt(bookingId);
const res = await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
const res = await handleCancelBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed)
);
if (!res.onlyRemovedAttendee) {
void (await this.billingService.cancelUsageByBookingUid(res.bookingUid));
}
@@ -246,7 +251,8 @@ export class BookingsController_2024_04_15 {
async createRecurringBooking(
@Req() req: BookingRequest,
@Body() _: CreateRecurringBookingInput_2024_04_15[],
@Headers(X_CAL_CLIENT_ID) clientId?: string
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<BookingResponse[]>> {
const oAuthClientId = clientId?.toString();
try {
@@ -258,7 +264,7 @@ export class BookingsController_2024_04_15 {
}
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
await this.createNextApiRecurringBookingRequest(req, oAuthClientId)
await this.createNextApiRecurringBookingRequest(req, oAuthClientId, undefined, isEmbed)
);
createdBookings.forEach(async (booking) => {
@@ -284,13 +290,14 @@ export class BookingsController_2024_04_15 {
async createInstantBooking(
@Req() req: BookingRequest,
@Body() _: CreateBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
const oAuthClientId = clientId?.toString();
req.userId = (await this.getOwnerId(req)) ?? -1;
try {
const instantMeeting = await handleInstantMeeting(
await this.createNextApiBookingRequest(req, oAuthClientId)
await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed)
);
if (instantMeeting.userId && instantMeeting.bookingUid) {
@@ -332,8 +339,14 @@ export class BookingsController_2024_04_15 {
}
}
private async getOAuthClientsParams(clientId: string): Promise<OAuthRequestParams> {
private async getOAuthClientsParams(clientId: string, isEmbed = false): Promise<OAuthRequestParams> {
const res = { ...DEFAULT_PLATFORM_PARAMS };
if (isEmbed) {
// embed should ignore oauth client settings and enable emails by default
return { ...res, arePlatformEmailsEnabled: true };
}
try {
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
// fetch oAuthClient from db and use data stored in db to set these values
@@ -354,13 +367,14 @@ export class BookingsController_2024_04_15 {
private async createNextApiBookingRequest(
req: BookingRequest,
oAuthClientId?: string,
platformBookingLocation?: string
platformBookingLocation?: string,
isEmbed?: string
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
const requestId = req.get("X-Request-Id");
const clone = { ...req };
const userId = (await this.getOwnerId(req)) ?? -1;
const oAuthParams = oAuthClientId
? await this.getOAuthClientsParams(oAuthClientId)
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
: DEFAULT_PLATFORM_PARAMS;
this.logger.log(`createNextApiBookingRequest_2024_04_15`, {
requestId,
@@ -370,19 +384,24 @@ export class BookingsController_2024_04_15 {
...oAuthParams,
});
Object.assign(clone, { userId, ...oAuthParams, platformBookingLocation });
clone.body = { ...clone.body, noEmail: !oAuthParams.arePlatformEmailsEnabled };
clone.body = {
...clone.body,
noEmail: !oAuthParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
private async createNextApiRecurringBookingRequest(
req: BookingRequest,
oAuthClientId?: string,
platformBookingLocation?: string
platformBookingLocation?: string,
isEmbed?: string
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
const clone = { ...req };
const userId = (await this.getOwnerId(req)) ?? -1;
const oAuthParams = oAuthClientId
? await this.getOAuthClientsParams(oAuthClientId)
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
: DEFAULT_PLATFORM_PARAMS;
const requestId = req.get("X-Request-Id");
this.logger.log(`createNextApiRecurringBookingRequest_2024_04_15`, {
@@ -397,6 +416,7 @@ export class BookingsController_2024_04_15 {
...oAuthParams,
platformBookingLocation,
noEmail: !oAuthParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
});
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
@@ -424,4 +444,8 @@ export class BookingsController_2024_04_15 {
throw new InternalServerErrorException(errMsg);
}
private transformToBoolean(v?: string): boolean {
return v && typeof v === "string" ? v.toLowerCase() === "true" : false;
}
}
@@ -1,4 +1,4 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional, ApiHideProperty } from "@nestjs/swagger";
import { Transform, Type } from "class-transformer";
import {
IsBoolean,
@@ -126,4 +126,47 @@ export class CreateBookingInput_2024_04_15 {
@IsOptional()
@ApiPropertyOptional()
locationUrl?: string;
// note(rajiv): after going through getUrlSearchParamsToForward.ts we found out
// that the below properties were not being included inside of handleNewBooking :- cc @morgan
// cal.salesforce.rrSkipToAccountLookupField, cal.rerouting & cal.isTestPreviewLink
// hence no input values have been setup for them in CreateBookingInput_2024_04_15
@IsArray()
@Type(() => Number)
@IsOptional()
@ApiHideProperty()
routedTeamMemberIds?: number[];
@IsNumber()
@IsOptional()
@ApiHideProperty()
routingFormResponseId?: number;
@IsBoolean()
@IsOptional()
@ApiHideProperty()
skipContactOwner?: boolean;
@IsBoolean()
@IsOptional()
@ApiHideProperty()
_shouldServeCache?: boolean;
@IsBoolean()
@IsOptional()
@ApiHideProperty()
_isDryRun?: boolean;
// reroutingFormResponses is similar to rescheduling which can only be done by the organiser
// won't really be necessary here in our usecase though :- cc @Hariom
@IsObject()
@IsOptional()
@ApiHideProperty()
reroutingFormResponses?: Record<
string,
{
value: (string | number | string[]) & (string | number | string[] | undefined);
label?: string | undefined;
}
>;
}
@@ -5,7 +5,7 @@ import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/out
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
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 } from "@/lib/api-versions";
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
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";
@@ -65,6 +65,7 @@ import {
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-08-13\``,
example: VERSION_2024_08_13,
required: true,
})
export class BookingsController_2024_08_13 {
@@ -19,6 +19,7 @@ import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.rep
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { 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";
@@ -72,10 +73,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let apiKeyString: string;
const userEmail = "bookings-controller-e2e@api.com";
const userEmail = `api-key-bookings-2024-08-13-user-${randomString()}@api.com`;
let user: User;
let eventTypeId: number;
const eventTypeSlug = `api-key-bookings-2024-08-13-event-type-${randomString()}`;
let createdBooking: BookingOutput_2024_08_13;
let rescheduledBooking: BookingOutput_2024_08_13;
@@ -100,7 +102,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
organization = await teamRepositoryFixture.create({
name: `api-key-bookings-organization-${randomString()}`,
});
user = await userRepositoryFixture.create({
email: userEmail,
@@ -110,13 +114,17 @@ describe("Bookings Endpoints 2024-08-13", () => {
apiKeyString = keyString;
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `api-key-bookings-e2e-api-key-bookings-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
{
title: `api-key-bookings-e2e-api-key-bookings-2024-08-13-event-type-${randomString()}`,
slug: eventTypeSlug,
length: 60,
},
user.id
);
eventTypeId = event.id;
@@ -16,7 +16,7 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomNumber } from "test/utils/randomNumber";
import { 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";
@@ -26,9 +26,7 @@ import {
GetSeatedBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { BookingOutput_2024_08_13 } from "@calcom/platform-types";
import { Booking, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
const suffix = randomNumber();
import { Booking, PlatformOAuthClient, Team, User, EventType } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Booking fields", () => {
@@ -43,7 +41,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
const userEmail = `alice-${suffix}@api.com`;
const userEmail = `booking-fields-2024-08-13-user-${randomString()}@api.com`;
let user: User;
let bookingWithSplitName: Booking;
@@ -54,7 +52,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
let seatedBookingWithSplitName: Booking;
let eventTypeWithBookingFieldsId: number;
let eventTypeId: number;
let seatedEvent: EventType;
let eventTypeWithBookingFields: EventType;
const eventTypeSlug = `booking-fields-2024-08-13-event-type-${randomString()}`;
const eventTypeWithBookingFieldsSlug = `booking-fields-2024-08-13-event-type-${randomString()}`;
const seatedEventTypeSlug = `booking-fields-2024-08-13-event-type-${randomString()}`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -76,7 +79,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: `booking fields ${suffix}` });
organization = await teamRepositoryFixture.create({
name: `booking-fields-2024-08-13-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
@@ -89,109 +94,36 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `booking-fields-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: `normal-booking-${randomNumber()}`, length: 60 },
user.id
);
bookingWithSplitName = await bookingsRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2020, 0, 8, 12, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
title: "peer coding lets goo",
uid: "booking",
eventType: {
connect: {
id: event.id,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: splitName,
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie Goldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const seatedEvent = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: `seated-${randomNumber()}`, length: 60, seatsPerTimeSlot: 3 },
user.id
);
seatedBookingWithSplitName = await bookingsRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 15, 0, 0)),
title: "peer coding lets goo",
uid: "seated-booking",
eventType: {
connect: {
id: seatedEvent.id,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: splitName,
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie Goldie",
locale: "lv",
timeZone: "Europe/Rome",
bookingSeat: {
create: {
referenceUid: "unique-seat-uid",
data: {
responses: {
email: "oldie@gmail.com",
name: {
firstName: "Oldie",
lastName: "Goldie",
},
},
},
metadata: {},
booking: {
connect: {
uid: "seated-booking",
},
},
},
},
},
},
});
const eventTypeWithBookingFields = await eventTypesRepositoryFixture.create(
{
title: "peer coding with booking fields",
slug: `with-custom-booking-fields-${randomNumber()}`,
title: `booking-fields-2024-08-13-event-type-${randomString()}`,
slug: eventTypeSlug,
length: 60,
},
user.id
);
eventTypeId = event.id;
seatedEvent = await eventTypesRepositoryFixture.create(
{
title: `booking-fields-2024-08-13-event-type-${randomString()}`,
slug: seatedEventTypeSlug,
length: 60,
seatsPerTimeSlot: 3,
},
user.id
);
eventTypeWithBookingFields = await eventTypesRepositoryFixture.create(
{
title: `booking-fields-2024-08-13-event-type-${randomString()}`,
slug: eventTypeWithBookingFieldsSlug,
length: 60,
bookingFields: [
{
@@ -381,11 +313,93 @@ describe("Bookings Endpoints 2024-08-13", () => {
},
user.id
);
eventTypeWithBookingFieldsId = eventTypeWithBookingFields.id;
bookingWithSplitName = await bookingsRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2020, 0, 8, 12, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
title: "peer coding lets goo",
uid: "booking",
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: splitName,
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie Goldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
seatedBookingWithSplitName = await bookingsRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 15, 0, 0)),
title: "peer coding lets goo",
uid: "seated-booking",
eventType: {
connect: {
id: seatedEvent.id,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: splitName,
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie Goldie",
locale: "lv",
timeZone: "Europe/Rome",
bookingSeat: {
create: {
referenceUid: "unique-seat-uid",
data: {
responses: {
email: "oldie@gmail.com",
name: {
firstName: "Oldie",
lastName: "Goldie",
},
},
},
metadata: {},
booking: {
connect: {
uid: "seated-booking",
},
},
},
},
},
},
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
@@ -457,7 +471,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
it("should not be able to book an event type with custom required booking fields if they are missing in bookingFieldsResponses", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithBookingFieldsId,
eventTypeId: eventTypeWithBookingFields.id,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
@@ -476,7 +490,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
it("should be able to book an event type with custom required booking fields", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithBookingFieldsId,
eventTypeId: eventTypeWithBookingFields.id,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
@@ -16,6 +16,7 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type
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";
@@ -35,11 +36,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
const userEmail = "bookings-controller-e2e@api.com";
const userEmail = `confirm-bookings-2024-08-13-user-${randomString()}@api.com`;
let user: User;
let eventTypeId: number;
const eventTypeSlug = "peer-coding";
const eventTypeSlug = `confirm-bookings-2024-08-13-event-type-${randomString()}`;
let createdBooking1: Booking;
let createdBooking2: Booking;
@@ -64,7 +65,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
organization = await teamRepositoryFixture.create({
name: `confirm-bookings-2024-08-13-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
@@ -77,13 +80,17 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `confirm-bookings-2024-08-13-${randomString()}-schedule`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: eventTypeSlug, length: 60 },
{
title: `confirm-bookings-2024-08-13-${randomString()}-event-type`,
slug: `confirm-bookings-2024-08-13-${randomString()}-event-type-slug`,
length: 60,
},
user.id
);
eventTypeId = event.id;
@@ -20,6 +20,7 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type
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";
@@ -92,7 +93,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
let emailsEnabledSetup: EmailSetup;
let emailsDisabledSetup: EmailSetup;
const authEmail = `admin-${Math.floor(Math.random() * 1000)}@example.com`;
const authEmail = `confirm-emails-2024-08-13-admin-${randomString()}@api.com`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -114,7 +115,9 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
organization = await teamRepositoryFixture.create({
name: `confirm-emails-2024-08-13-organization-${randomString()}`,
});
await setupEnabledEmails();
await setupDisabledEmails();
@@ -139,7 +142,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
const user = await userRepositoryFixture.create({
email: `alice-${Math.floor(Math.random() * 1000)}@gmail.com`,
email: `confirm-emails-2024-08-13-user-${randomString()}@api.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
@@ -148,14 +151,19 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `confirm-emails-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60, requiresConfirmation: true },
{
title: "peer coding",
slug: `confirm-emails-2024-08-13-event-type-${randomString()}`,
length: 60,
requiresConfirmation: true,
},
user.id
);
@@ -171,7 +179,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
const user = await userRepositoryFixture.create({
email: `bob-${Math.floor(Math.random() * 1000)}@gmail.com`,
email: `confirm-emails-2024-08-13-user-${randomString()}@api.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
@@ -179,13 +187,18 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `confirm-emails-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60, requiresConfirmation: true },
{
title: "peer coding",
slug: `confirm-emails-2024-08-13-event-type-${randomString()}`,
length: 60,
requiresConfirmation: true,
},
user.id
);
@@ -23,6 +23,7 @@ import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-cli
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
@@ -103,7 +104,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
let emailsEnabledSetup: EmailSetup;
let emailsDisabledSetup: EmailSetup;
const authEmail = "admin@example.com";
const authEmail = "team-emails-2024-08-13-user-admin@example.com";
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -128,7 +129,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
organization = await teamRepositoryFixture.create({ name: `team-emails-organization-${randomString()}` });
await setupEnabledEmails();
await setupDisabledEmails();
@@ -152,7 +153,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
const team = await teamRepositoryFixture.create({
name: "team 1",
name: `team-emails-team-${randomString()}`,
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
@@ -163,7 +164,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
});
const member1 = await userRepositoryFixture.create({
email: "alice@gmail.com",
email: `team-emails-2024-08-13-member1-${randomString()}@api.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
@@ -172,7 +173,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
});
const member2 = await userRepositoryFixture.create({
email: "bob@gmail.com",
email: `team-emails-2024-08-13-member2-${randomString()}@api.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
@@ -181,7 +182,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `team-emails-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
@@ -237,8 +238,8 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
team: {
connect: { id: team.id },
},
title: "Collective Event Type",
slug: "collective-event-type",
title: `team-emails-2024-08-13-event-type-${randomString()}`,
slug: `team-emails-2024-08-13-event-type-${randomString()}`,
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
@@ -278,8 +279,8 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
team: {
connect: { id: team.id },
},
title: "Round Robin Event Type",
slug: "round-robin-event-type",
title: `team-emails-2024-08-13-event-type-${randomString()}`,
slug: `team-emails-2024-08-13-event-type-${randomString()}`,
length: 60,
assignAllTeamMembers: false,
bookingFields: [],
@@ -336,7 +337,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
const team = await teamRepositoryFixture.create({
name: "team 2",
name: `team-emails-2024-08-13-team-${randomString()}`,
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
@@ -347,7 +348,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
});
const member1 = await userRepositoryFixture.create({
email: "charlie@gmail.com",
email: `team-emails-2024-08-13-member1-${randomString()}@api.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
@@ -356,7 +357,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
});
const member2 = await userRepositoryFixture.create({
email: "dean@gmail.com",
email: `team-emails-2024-08-13-member2-${randomString()}@api.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
@@ -365,7 +366,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `team-emails-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
@@ -421,8 +422,8 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
team: {
connect: { id: team.id },
},
title: "Collective Event Type",
slug: "collective-event-type",
title: `team-emails-2024-08-13-event-type-${randomString()}`,
slug: `team-emails-2024-08-13-event-type-${randomString()}`,
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
@@ -462,8 +463,8 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
team: {
connect: { id: team.id },
},
title: "Round Robin Event Type",
slug: "round-robin-event-type",
title: `team-emails-2024-08-13-event-type-${randomString()}`,
slug: `team-emails-2024-08-13-event-type-${randomString()}`,
length: 60,
assignAllTeamMembers: false,
bookingFields: [],
@@ -965,17 +966,19 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
});
});
});
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(authEmail);
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.member1.email);
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.member1.email);
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.member2.email);
await bookingsRepositoryFixture.deleteAllBookings(
emailsEnabledSetup.member1.id,
emailsEnabledSetup.member1.email
);
await bookingsRepositoryFixture.deleteAllBookings(
emailsDisabledSetup.member1.id,
emailsDisabledSetup.member1.email
emailsDisabledSetup.member2.email
);
await app.close();
});
@@ -20,6 +20,7 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type
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 { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
@@ -101,7 +102,9 @@ describe("Bookings Endpoints 2024-08-13 user emails", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
organization = await teamRepositoryFixture.create({
name: `user-emails-2024-08-13-organization-${randomString()}`,
});
await setupEnabledEmails();
await setupDisabledEmails();
@@ -116,7 +119,7 @@ describe("Bookings Endpoints 2024-08-13 user emails", () => {
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
const user = await userRepositoryFixture.create({
email: `alice-${Math.floor(Math.random() * 1000)}@gmail.com`,
email: `user-emails-2024-08-13-user-${randomString()}@api.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
@@ -125,22 +128,26 @@ describe("Bookings Endpoints 2024-08-13 user emails", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `user-emails-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
{
title: `user-emails-2024-08-13-event-type-${randomString()}`,
slug: `user-emails-2024-08-13-event-type-${randomString()}`,
length: 60,
},
user.id
);
const recurringEvent = await eventTypesRepositoryFixture.create(
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
{
title: "peer coding recurring",
slug: "peer-coding-recurring",
title: `user-emails-2024-08-13-recurring-event-type-${randomString()}`,
slug: `user-emails-2024-08-13-recurring-event-type-${randomString()}`,
length: 60,
recurringEvent: { freq: 2, count: 3, interval: 1 },
},
@@ -160,7 +167,7 @@ describe("Bookings Endpoints 2024-08-13 user emails", () => {
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
const user = await userRepositoryFixture.create({
email: `bob-${Math.floor(Math.random() * 1000)}@gmail.com`,
email: `user-emails-2024-08-13-user-${randomString()}@api.com`,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
@@ -168,21 +175,25 @@ describe("Bookings Endpoints 2024-08-13 user emails", () => {
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `user-emails-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: "peer-coding", length: 60 },
{
title: `user-emails-2024-08-13-event-type-${randomString()}`,
slug: `user-emails-2024-08-13-event-type-${randomString()}`,
length: 60,
},
user.id
);
const recurringEvent = await eventTypesRepositoryFixture.create(
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
{
title: "peer coding recurring",
slug: "peer-coding-recurring",
title: `user-emails-2024-08-13-recurring-event-type-${randomString()}`,
slug: `user-emails-2024-08-13-recurring-event-type-${randomString()}`,
length: 60,
recurringEvent: { freq: 2, count: 3, interval: 1 },
},
@@ -21,6 +21,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
@@ -45,8 +46,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
const teamUserEmail = "orgUser1team1@api.com";
const teamUserEmail2 = "orgUser2team1@api.com";
const teamUserEmail = `reassign-bookings-2024-08-13-user1-${randomString()}@api.com`;
const teamUserEmail2 = `reassign-bookings-2024-08-13-user2-${randomString()}@api.com`;
let teamUser1: User;
let teamUser2: User;
@@ -78,11 +79,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" });
organization = await organizationsRepositoryFixture.create({
name: `reassign-bookings-2024-08-13-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
team = await teamRepositoryFixture.create({
name: "team 1",
name: `reassign-bookings-2024-08-13-team-${randomString()}`,
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
@@ -95,17 +98,17 @@ describe("Bookings Endpoints 2024-08-13", () => {
teamUser1 = await userRepositoryFixture.create({
email: teamUserEmail,
locale: "it",
name: "orgUser1team1",
name: `reassign-bookings-2024-08-13-user1-${randomString()}`,
});
teamUser2 = await userRepositoryFixture.create({
email: teamUserEmail2,
locale: "it",
name: "orgUser2team1",
name: `reassign-bookings-2024-08-13-user2-${randomString()}`,
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `reassign-bookings-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
@@ -164,8 +167,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
users: {
connect: [{ id: teamUser1.id }, { id: teamUser2.id }],
},
title: "Round Robin Event Type",
slug: "round-robin-event-type",
title: `reassign-bookings-2024-08-13-event-type-${randomString()}`,
slug: `reassign-bookings-2024-08-13-event-type-${randomString()}`,
length: 60,
assignAllTeamMembers: false,
bookingFields: [],
@@ -17,6 +17,7 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type
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";
@@ -41,11 +42,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
const userEmail = "bookings-controller-e2e@api.com";
const userEmail = `recurring-bookings-2024-08-13-user-${randomString()}@api.com`;
let user: User;
const maxRecurrenceCount = 3;
let recurringEventTypeId: number;
const recurringEventSlug = `recurring-bookings-2024-08-13-event-type-${randomString()}`;
let createdRecurringBooking: RecurringBookingOutput_2024_08_13[];
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -67,7 +70,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
organization = await teamRepositoryFixture.create({
name: `recurring-bookings-2024-08-13-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
@@ -80,7 +85,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `recurring-bookings-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
@@ -89,8 +94,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
const recurringEvent = await eventTypesRepositoryFixture.create(
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
{
title: "peer coding recurring",
slug: "peer-coding-recurring",
title: `recurring-bookings-2024-08-13-event-type-${randomString()}`,
slug: recurringEventSlug,
length: 60,
recurringEvent: { freq: 2, count: maxRecurrenceCount, interval: 1 },
},
@@ -344,7 +349,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
const userEmail = "bookings-controller-e2e@api.com";
const userEmail = `recurring-bookings-2024-08-13-user-${randomString()}@api.com`;
let user: User;
const maxRecurrenceCount = 4;
@@ -380,7 +385,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
organization = await teamRepositoryFixture.create({
name: `recurring-bookings-2024-08-13-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
@@ -393,17 +400,18 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `recurring-bookings-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const recurringEventSlug = `recurring-bookings-2024-08-13-event-type-${randomString()}`;
const recurringEvent = await eventTypesRepositoryFixture.create(
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
{
title: "peer coding recurring",
slug: "peer-coding-recurring",
title: `recurring-bookings-2024-08-13-event-type-${randomString()}`,
slug: recurringEventSlug,
length: 60,
recurringEvent: { freq: 2, count: maxRecurrenceCount, interval: 1 },
},
@@ -551,7 +559,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
const userEmail = "bookings-controller-e2e@api.com";
const userEmail = `recurring-bookings-2024-08-13-user-${randomString()}@api.com`;
let user: User;
const maxRecurrenceCount = 4;
@@ -587,7 +595,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
organization = await teamRepositoryFixture.create({
name: `recurring-bookings-2024-08-13-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
@@ -600,17 +610,18 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `recurring-bookings-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const recurringEventSlug = `recurring-bookings-2024-08-13-event-type-${randomString()}`;
const recurringEvent = await eventTypesRepositoryFixture.create(
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
{
title: "peer coding recurring",
slug: "peer-coding-recurring",
title: `recurring-bookings-2024-08-13-event-type-${randomString()}`,
slug: recurringEventSlug,
length: 60,
recurringEvent: { freq: 2, count: maxRecurrenceCount, interval: 1 },
},
@@ -19,6 +19,7 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type
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";
@@ -51,22 +52,23 @@ describe("Bookings Endpoints 2024-08-13", () => {
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
const userEmail = "seated-bookings-controller-e2e@api.com";
const userEmail = `seated-bookings-user-${randomString()}@api.com`;
let user: User;
const seatedTventTypeSlug = "peer-coding-seated";
const recurringSeatedTventTypeSlug = "peer-coding-recurring-seated";
let seatedEventTypeId: number;
let recurringSeatedEventTypeId: number;
const maxRecurrenceCount = 3;
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
const recurringSeatedEventSlug = `seated-bookings-event-type-${randomString()}`;
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
let createdRecurringSeatedBooking: CreateRecurringSeatedBookingOutput_2024_08_13[];
const emailAttendeeOne = "mr_proper_seated@gmail.com";
const nameAttendeeOne = "Mr Proper Seated";
const emailAttendeeTwo = "mr_proper_friend_seated@gmail.com";
const nameAttendeeTwo = "Mr Proper Friend Seated";
const emailAttendeeOne = `seated-bookings-attendee1-${randomString()}@api.com`;
const nameAttendeeOne = `Attendee One ${randomString()}`;
const emailAttendeeTwo = `seated-bookings-attendee2-${randomString()}@api.com`;
const nameAttendeeTwo = `Attendee Two ${randomString()}`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -88,7 +90,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
organization = await teamRepositoryFixture.create({
name: `seated-bookings-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
@@ -101,15 +105,15 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `seated-bookings-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const seatedEvent = await eventTypesRepositoryFixture.create(
{
title: "peer coding",
slug: seatedTventTypeSlug,
title: `seated-bookings-2024-08-13-event-type-${randomString()}`,
slug: seatedEventSlug,
length: 60,
seatsPerTimeSlot: 5,
seatsShowAttendees: true,
@@ -123,8 +127,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
const recurringSeatedEvent = await eventTypesRepositoryFixture.create(
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
{
title: "peer coding recurring",
slug: recurringSeatedTventTypeSlug,
title: `seated-bookings-2024-08-13-recurring-event-type-${randomString()}`,
slug: recurringSeatedEventSlug,
length: 60,
recurringEvent: { freq: 2, count: maxRecurrenceCount, interval: 1 },
seatsPerTimeSlot: 5,
@@ -200,7 +204,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.eventTypeId).toEqual(seatedEventTypeId);
expect(data.eventType).toEqual({
id: seatedEventTypeId,
slug: seatedTventTypeSlug,
slug: seatedEventSlug,
});
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
@@ -272,7 +276,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.eventTypeId).toEqual(seatedEventTypeId);
expect(data.eventType).toEqual({
id: seatedEventTypeId,
slug: seatedTventTypeSlug,
slug: seatedEventSlug,
});
expect(data.attendees.length).toEqual(2);
// note(Lauris): first attendee is from previous test request
@@ -366,7 +370,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
expect(firstBooking.eventType).toEqual({
id: recurringSeatedEventTypeId,
slug: recurringSeatedTventTypeSlug,
slug: recurringSeatedEventSlug,
});
expect(firstBooking.attendees.length).toEqual(1);
expect(firstBooking.attendees[0]).toEqual({
@@ -401,7 +405,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
expect(secondBooking.eventType).toEqual({
id: recurringSeatedEventTypeId,
slug: recurringSeatedTventTypeSlug,
slug: recurringSeatedEventSlug,
});
expect(secondBooking.attendees.length).toEqual(1);
expect(secondBooking.attendees[0]).toEqual({
@@ -575,7 +579,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.eventTypeId).toEqual(seatedEventTypeId);
expect(data.eventType).toEqual({
id: seatedEventTypeId,
slug: seatedTventTypeSlug,
slug: seatedEventSlug,
});
expect(data.attendees.length).toEqual(1);
const attendee = createdSeatedBooking.attendees.find((a) => a.seatUid === body.seatUid);
@@ -629,7 +633,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.eventTypeId).toEqual(seatedEventTypeId);
expect(data.eventType).toEqual({
id: seatedEventTypeId,
slug: seatedTventTypeSlug,
slug: seatedEventSlug,
});
expect(data.attendees.length).toEqual(0);
expect(data.location).toBeDefined();
@@ -681,22 +685,23 @@ describe("Bookings Endpoints 2024-08-13", () => {
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
const userEmail = "seated-bookings-controller-e2e@api.com";
const userEmail = `seated-bookings-user-${randomString()}@api.com`;
let user: User;
const seatedTventTypeSlug = "peer-coding-seated";
const recurringSeatedTventTypeSlug = "peer-coding-recurring-seated";
let seatedEventTypeId: number;
let recurringSeatedEventTypeId: number;
const maxRecurrenceCount = 3;
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
const recurringSeatedEventSlug = `seated-bookings-event-type-${randomString()}`;
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
let createdRecurringSeatedBooking: CreateRecurringSeatedBookingOutput_2024_08_13[];
const emailAttendeeOne = "mr_proper_first@gmail.com";
const nameAttendeeOne = "Mr Proper First";
const emailAttendeeTwo = "mr_proper_second@gmail.com";
const nameAttendeeTwo = "Mr Proper Second";
const emailAttendeeOne = `seated-bookings-attendee1-${randomString()}@api.com`;
const nameAttendeeOne = `Attendee One ${randomString()}`;
const emailAttendeeTwo = `seated-bookings-attendee2-${randomString()}@api.com`;
const nameAttendeeTwo = `Attendee Two ${randomString()}`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -718,7 +723,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
organization = await teamRepositoryFixture.create({
name: `seated-bookings-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
@@ -731,20 +738,17 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `seated-bookings-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const seatedEvent = await eventTypesRepositoryFixture.create(
{
title: "peer coding",
slug: seatedTventTypeSlug,
title: `seated-bookings-2024-08-13-event-type-${randomString()}`,
slug: seatedEventSlug,
length: 60,
seatsPerTimeSlot: 5,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
seatsPerTimeSlot: 3,
},
user.id
);
@@ -753,14 +757,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
const recurringSeatedEvent = await eventTypesRepositoryFixture.create(
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
{
title: "peer coding recurring",
slug: recurringSeatedTventTypeSlug,
title: `seated-bookings-2024-08-13-recurring-event-type-${randomString()}`,
slug: recurringSeatedEventSlug,
length: 60,
seatsPerTimeSlot: 3,
recurringEvent: { freq: 2, count: maxRecurrenceCount, interval: 1 },
seatsPerTimeSlot: 5,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
},
user.id
);
@@ -836,7 +837,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
expect(firstBooking.eventType).toEqual({
id: recurringSeatedEventTypeId,
slug: recurringSeatedTventTypeSlug,
slug: recurringSeatedEventSlug,
});
expect(firstBooking.attendees.length).toEqual(1);
expect(firstBooking.attendees[0]).toEqual({
@@ -871,7 +872,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
expect(secondBooking.eventType).toEqual({
id: recurringSeatedEventTypeId,
slug: recurringSeatedTventTypeSlug,
slug: recurringSeatedEventSlug,
});
expect(secondBooking.attendees.length).toEqual(1);
expect(secondBooking.attendees[0]).toEqual({
@@ -949,10 +950,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
expect(firstBooking.eventType).toEqual({
id: recurringSeatedEventTypeId,
slug: recurringSeatedTventTypeSlug,
slug: recurringSeatedEventSlug,
});
expect(firstBooking.attendees.length).toEqual(2);
expect(firstBooking.attendees[0]).toEqual({
const firstAttendee = firstBooking.attendees.find(
(attendee) => attendee.name === nameAttendeeOne
);
expect(firstAttendee).toEqual({
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: body.attendee.timeZone,
@@ -965,7 +969,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
},
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
});
expect(firstBooking.attendees[1]).toEqual({
const secondAttendee = firstBooking.attendees.find(
(attendee) => attendee.name === nameAttendeeTwo
);
expect(secondAttendee).toEqual({
name: nameAttendeeTwo,
email: emailAttendeeTwo,
timeZone: body.attendee.timeZone,
@@ -997,7 +1004,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
expect(secondBooking.eventType).toEqual({
id: recurringSeatedEventTypeId,
slug: recurringSeatedTventTypeSlug,
slug: recurringSeatedEventSlug,
});
expect(secondBooking.attendees.length).toEqual(2);
expect(secondBooking.attendees[0]).toEqual({
@@ -21,6 +21,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
@@ -52,13 +53,18 @@ describe("Bookings Endpoints 2024-08-13", () => {
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
const teamUserEmail = "orgUser1team1@api.com";
const teamUserEmail2 = "orgUser2team1@api.com";
const teamUserEmail = `team-bookings-user1-${randomString()}@api.com`;
const teamUserEmail2 = `team-bookings-user2-${randomString()}@api.com`;
let teamUser: User;
let teamUser2: User;
let team1EventTypeId: number;
let team2EventTypeId: number;
let phoneOnlyEventTypeId: number;
const team1EventTypeSlug = `team-bookings-event-type-${randomString()}`;
const team2EventTypeSlug = `team-bookings-event-type-${randomString()}`;
const phoneOnlyEventTypeSlug = `team-bookings-event-type-${randomString()}`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -84,11 +90,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" });
organization = await organizationsRepositoryFixture.create({
name: `team-bookings-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
team1 = await teamRepositoryFixture.create({
name: "team 1",
name: `team-bookings-team1-${randomString()}`,
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
@@ -99,7 +107,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
team2 = await teamRepositoryFixture.create({
name: "team 2",
name: `team-bookings-team2-${randomString()}`,
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
@@ -132,7 +140,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `team-bookings-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
@@ -195,8 +203,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
team: {
connect: { id: team1.id },
},
title: "Collective Event Type",
slug: "collective-event-type",
title: `team-bookings-2024-08-13-event-type-${randomString()}`,
slug: team1EventTypeSlug,
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
@@ -205,13 +213,90 @@ describe("Bookings Endpoints 2024-08-13", () => {
team1EventTypeId = team1EventType.id;
const phoneOnlyEventType = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "ROUND_ROBIN",
team: {
connect: { id: team1.id },
},
title: `team-bookings-2024-08-13-event-type-${randomString()}`,
slug: phoneOnlyEventTypeSlug,
length: 15,
assignAllTeamMembers: false,
hosts: {
connectOrCreate: [
{
where: {
userId_eventTypeId: {
userId: teamUser.id,
eventTypeId: team1EventTypeId,
},
},
create: {
userId: teamUser.id,
isFixed: true,
},
},
],
},
bookingFields: [
{
name: "name",
type: "name",
label: "your name",
sources: [{ id: "default", type: "default", label: "Default" }],
variant: "fullName",
editable: "system",
required: true,
defaultLabel: "your_name",
variantsConfig: {
variants: {
fullName: {
fields: [{ name: "fullName", type: "text", label: "your name", required: true }],
},
},
},
},
{
name: "email",
type: "email",
label: "your email",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system",
required: false,
defaultLabel: "email_address",
},
{
name: "attendeePhoneNumber",
type: "phone",
label: "phone_number",
sources: [{ id: "user", type: "user", label: "User", fieldRequired: true }],
editable: "user",
required: true,
placeholder: "",
},
{
name: "rescheduleReason",
type: "textarea",
views: [{ id: "reschedule", label: "Reschedule View" }],
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "reason_for_reschedule",
defaultPlaceholder: "reschedule_placeholder",
},
],
locations: [],
});
phoneOnlyEventTypeId = phoneOnlyEventType.id;
const team2EventType = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team2.id },
},
title: "Collective Event Type 2",
slug: "collective-event-type-2",
title: `team-bookings-2024-08-13-event-type-${randomString()}`,
slug: team2EventTypeSlug,
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
@@ -322,6 +407,60 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should create a phone based booking", async () => {
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",
timeZone: "Europe/Madrid",
language: "es",
},
meetingUrl: "https://meet.google.com/abc-def-ghi",
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(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(2030, 0, 8, 15, 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: body.attendee.name,
email: "919876543210@sms.cal.com",
phoneNumber: body.attendee.phoneNumber,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
expect(data.meetingUrl).toEqual(body.meetingUrl);
expect(data.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should create a team 2 booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
@@ -398,7 +537,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(1);
expect(data.length).toEqual(2);
expect(data[0].eventTypeId).toEqual(team1EventTypeId);
});
});
@@ -436,7 +575,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(2);
expect(data.length).toEqual(3);
expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined();
expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined();
});
@@ -21,6 +21,7 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type
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 {
@@ -58,12 +59,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
const userEmail = "bookings-controller-e2e@api.com";
const userEmail = `user-bookings-user-${randomString()}@api.com`;
let user: User;
let eventTypeId: number;
const eventTypeSlug = "peer-coding";
const eventTypeSlug = `user-bookings-event-type-${randomString()}`;
let recurringEventTypeId: number;
const recurringEventTypeSlug = `user-bookings-event-type-${randomString()}`;
let createdBooking: BookingOutput_2024_08_13;
let rescheduledBooking: BookingOutput_2024_08_13;
@@ -91,12 +93,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
organization = await teamRepositoryFixture.create({
name: `user-bookings-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
username: `bob-${Math.floor(Math.random() * 1000)}@gmail.com`,
username: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
@@ -105,13 +109,18 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `user-bookings-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "peer coding", slug: eventTypeSlug, length: 60 },
{
title: `user-bookings-2024-08-13-event-type-${randomString()}`,
slug: eventTypeSlug,
length: 60,
},
user.id
);
eventTypeId = event.id;
@@ -120,7 +129,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
{
title: "peer coding recurring",
slug: "peer-coding-recurring",
slug: recurringEventTypeSlug,
length: 60,
recurringEvent: { freq: 2, count: 3, interval: 1 },
},
@@ -332,6 +341,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
metadata: {
userId: "100",
},
guests: ["bob@gmail.com"],
};
const beforeCreate = new Date();
@@ -353,6 +363,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
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(2030, 0, 8, 14, 0, 0)).toISOString());
@@ -376,7 +387,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
name: body.attendee.name,
email: body.attendee.email,
...body.bookingFieldsResponses,
guests: body.guests,
});
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);
@@ -515,6 +528,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.uid).toEqual(createdBooking.uid);
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(createdBooking.status);
expect(data.start).toEqual(createdBooking.start);
expect(data.end).toEqual(createdBooking.end);
@@ -549,6 +563,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.id).toEqual(createdRecurringBooking[0].id);
expect(data.uid).toEqual(createdRecurringBooking[0].uid);
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(createdRecurringBooking[0].status);
expect(data.start).toEqual(createdRecurringBooking[0].start);
expect(data.end).toEqual(createdRecurringBooking[0].end);
@@ -1018,6 +1034,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual(createdBooking.status);
expect(data.duration).toEqual(createdBooking.duration);
expect(data.eventTypeId).toEqual(createdBooking.eventTypeId);
@@ -1075,6 +1093,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual(createdRecurringBooking[0].status);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 9, 15, 0, 0)).toISOString());
@@ -1155,6 +1175,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.id).toEqual(booking.id);
expect(data.uid).toEqual(booking.uid);
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(booking.status);
expect(data.start).toEqual(booking.start);
expect(data.end).toEqual(booking.end);
@@ -1187,6 +1209,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.id).toEqual(booking.id);
expect(data.uid).toEqual(booking.uid);
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(booking.status);
expect(data.start).toEqual(booking.start);
expect(data.end).toEqual(booking.end);
@@ -1225,6 +1249,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual("cancelled");
expect(data.cancellationReason).toEqual(body.cancellationReason);
expect(data.start).toEqual(rescheduledBooking.start);
@@ -17,6 +17,7 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type
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";
@@ -36,12 +37,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
const userEmail = "bookings-controller-e2e@api.com";
const userEmail = `variable-length-bookings-2024-08-13-user-${randomString()}@api.com`;
let user: User;
let variableLengthEventType: EventType;
const variableLengthEventTypeSlug = "variable-length-event";
const VARIABLE_LENGTH_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
let normalEventType: EventType;
const NORMAL_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -63,7 +65,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
organization = await teamRepositoryFixture.create({
name: `variable-length-bookings-2024-08-13-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
@@ -76,7 +80,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
name: `variable-length-bookings-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
@@ -84,8 +88,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
variableLengthEventType = await eventTypesRepositoryFixture.create(
{
title: "variable length event",
slug: variableLengthEventTypeSlug,
title: `variable-length-bookings-2024-08-13-event-type-${randomString()}`,
slug: VARIABLE_LENGTH_EVENT_TYPE_SLUG,
length: 15,
metadata: { multipleDuration: [15, 30, 60] },
},
@@ -93,7 +97,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
);
normalEventType = await eventTypesRepositoryFixture.create(
{ title: "normal event", slug: "normal-event", length: 15 },
{
title: `variable-length-bookings-2024-08-13-event-type-${randomString()}`,
slug: NORMAL_EVENT_TYPE_SLUG,
length: 15,
},
user.id
);
@@ -13,7 +13,6 @@ import { Request } from "express";
import { z } from "zod";
import {
handleNewBooking,
handleNewRecurringBooking,
getAllUserBookings,
handleInstantMeeting,
@@ -23,6 +22,7 @@ import {
handleMarkNoShow,
confirmBookingHandler,
} from "@calcom/platform-libraries";
import { handleNewBooking } from "@calcom/platform-libraries";
import {
CreateBookingInput_2024_08_13,
CreateBookingInput,
@@ -12,6 +12,7 @@ import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.se
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { CreationSource } from "@prisma/client";
import { Request } from "express";
import { DateTime } from "luxon";
import { NextApiRequest } from "next/types";
@@ -97,10 +98,14 @@ export class InputBookingsService_2024_08_13 {
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
newRequest.body = {
...bodyTransformed,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: false };
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
}
return newRequest as unknown as BookingRequest;
@@ -146,6 +151,8 @@ export class InputBookingsService_2024_08_13 {
);
const endTime = startTime.plus({ minutes: lengthInMinutes });
const guests = inputBooking.guests;
return {
start: startTime.toISO(),
end: endTime.toISO(),
@@ -154,15 +161,22 @@ export class InputBookingsService_2024_08_13 {
language: inputBooking.attendee.language || "en",
metadata: inputBooking.metadata || {},
hasHashedBookingLink: false,
guests: inputBooking.guests,
guests,
// note(Lauris): responses with name and email are required by the handleNewBooking
responses: inputBooking.bookingFieldsResponses
? {
...inputBooking.bookingFieldsResponses,
name: inputBooking.attendee.name,
email: inputBooking.attendee.email,
email: inputBooking.attendee.email ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
}
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email },
: {
name: inputBooking.attendee.name,
email: inputBooking.attendee.email ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
},
};
}
@@ -211,6 +225,7 @@ export class InputBookingsService_2024_08_13 {
newRequest.body = bodyTransformed.map((event) => ({
...event,
creationSource: CreationSource.API_V2,
}));
return newRequest as unknown as BookingRequest;
@@ -246,6 +261,8 @@ export class InputBookingsService_2024_08_13 {
inputBooking.attendee.timeZone
);
const guests = inputBooking.guests;
for (let i = 0; i < repeatsTimes; i++) {
const endTime = startTime.plus({ minutes: eventType.length });
@@ -258,15 +275,16 @@ export class InputBookingsService_2024_08_13 {
language: inputBooking.attendee.language || "en",
metadata: inputBooking.metadata || {},
hasHashedBookingLink: false,
guests: inputBooking.guests,
guests,
// note(Lauris): responses with name and email are required by the handleNewBooking
responses: inputBooking.bookingFieldsResponses
? {
...inputBooking.bookingFieldsResponses,
name: inputBooking.attendee.name,
email: inputBooking.attendee.email,
guests,
}
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email },
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email, guests },
schedulingType: eventType.schedulingType,
});
@@ -305,10 +323,14 @@ export class InputBookingsService_2024_08_13 {
const location = await this.getRescheduleBookingLocation(bookingUid);
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
newRequest.body = {
...bodyTransformed,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: false };
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
}
return newRequest as unknown as BookingRequest;
@@ -72,6 +72,7 @@ type DatabaseBooking = Booking & {
email: string;
timeZone: string;
locale: string | null;
phoneNumber?: string | null;
noShow: boolean | null;
bookingSeat?: BookingSeat | null;
}[];
@@ -121,6 +122,7 @@ export class OutputBookingsService_2024_08_13 {
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
phoneNumber: attendee.phoneNumber ?? undefined,
})),
guests: bookingResponses.guests,
location,
@@ -20,6 +20,7 @@ import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.reposit
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { CalendarsServiceMock } from "test/mocks/calendars-service-mock";
import { IcsCalendarServiceMock } from "test/mocks/ics-calendar-service-mock";
import { randomString } from "test/utils/randomString";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import {
@@ -71,9 +72,12 @@ describe("Platform Calendars Endpoints", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
organization = await teamRepositoryFixture.create({ name: `calendars-organization-${randomString()}` });
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.createOAuthManagedUser("office365-connect@gmail.com", oAuthClient.id);
user = await userRepositoryFixture.createOAuthManagedUser(
`calendars-user-${randomString()}@api.com`,
oAuthClient.id
);
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClient.id);
accessTokenSecret = tokens.accessToken;
refreshTokenSecret = tokens.refreshToken;
@@ -22,6 +22,7 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type
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 {
@@ -77,8 +78,8 @@ describe("Event types Endpoints", () => {
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
const userEmail = "event-types-test-e2e@api.com";
const name = "bob-the-builder";
const userEmail = `event-types-2024-04-15-user-${randomString()}@api.com`;
const name = `event-types-2024-04-15-user-${randomString()}`;
const username = name;
let eventType: EventType;
let user: User;
@@ -105,7 +106,9 @@ describe("Event types Endpoints", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
organization = await teamRepositoryFixture.create({
name: `event-types-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
@@ -139,7 +142,7 @@ describe("Event types Endpoints", () => {
it("should create an event type", async () => {
const body: CreateEventTypeInput_2024_04_15 = {
title: "Test Event Type",
slug: "test-event-type",
slug: `event-types-event-type-${randomString()}`,
description: "A description of the test event type.",
length: 60,
hidden: false,
@@ -399,8 +399,8 @@ class PublicEventTypeOutput {
workflows!: any[];
@IsArray()
@ApiProperty()
hosts!: any[];
@ApiPropertyOptional()
hosts?: any[];
@ValidateNested()
@Type(() => Owner)
@@ -427,8 +427,8 @@ class PublicEventTypeOutput {
@IsArray()
@ValidateNested({ each: true })
@Type(() => User)
@ApiProperty({ type: [User] })
users!: User[];
@ApiPropertyOptional({ type: [User] })
users?: User[];
@IsObject()
@ApiProperty({ type: Object })
@@ -19,6 +19,7 @@ import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repo
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
@@ -33,11 +34,18 @@ import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
NameFieldInput_2024_06_14,
NameDefaultFieldInput_2024_06_14,
NotesDefaultFieldInput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { SchedulingType } from "@calcom/prisma/enums";
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
if (a.slug < b.slug) return -1;
if (a.slug > b.slug) return 1;
return 0;
};
describe("Event types Endpoints", () => {
describe("Not authenticated", () => {
let app: INestApplication;
@@ -79,9 +87,9 @@ describe("Event types Endpoints", () => {
let schedulesRepostoryFixture: SchedulesRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
const userEmail = "event-types-test-e2e@api.com";
const falseTestEmail = "false-event-types@api.com";
const name = "bob-the-builder";
const userEmail = `event-types-2024-06-14-user-${randomString()}@api.com`;
const falseTestEmail = `event-types-2024-06-14-false-user-${randomString()}@api.com`;
const name = `event-types-2024-06-14-user-${randomString()}`;
const username = name;
let eventType: EventTypeOutput_2024_06_14;
let user: User;
@@ -93,6 +101,67 @@ describe("Event types Endpoints", () => {
let orgUserEventType1: EventType;
let orgUserEventType2: EventType;
let orgUserEventType3: EventType;
const defaultResponseBookingFieldName = {
isDefault: true,
type: "name",
slug: "name",
required: true,
disableOnPrefill: false,
};
const defaultResponseBookingFieldEmail = {
isDefault: true,
type: "email",
slug: "email",
required: true,
disableOnPrefill: false,
};
const defaultResponseBookingFieldLocation = {
isDefault: true,
type: "radioInput",
slug: "location",
required: false,
hidden: false,
};
const defaultResponseBookingFieldTitle = {
isDefault: true,
type: "text",
slug: "title",
required: true,
hidden: true,
disableOnPrefill: false,
};
const defaultResponseBookingFieldNotes = {
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
hidden: false,
disableOnPrefill: false,
};
const defaultResponseBookingFieldGuests = {
isDefault: true,
type: "multiemail",
slug: "guests",
required: false,
hidden: false,
disableOnPrefill: false,
};
const defaultResponseBookingFieldRescheduleReason = {
isDefault: true,
type: "textarea",
slug: "rescheduleReason",
required: false,
hidden: false,
disableOnPrefill: false,
};
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
@@ -118,8 +187,8 @@ describe("Event types Endpoints", () => {
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({
name: "organization",
slug: "event-type-2024-06-14-org-slug",
name: `event-types-2024-06-14-organization-${randomString()}`,
slug: `event-type-2024-06-14-org-slug-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
@@ -129,9 +198,9 @@ describe("Event types Endpoints", () => {
});
orgUser = await userRepositoryFixture.create({
email: "event-types-2024-06-14-org-user@example.com",
name: "event-types-2024-06-14-org-user",
username: "event-types-2024-06-14-org-user",
email: `event-types-2024-06-14-org-user-${randomString()}@example.com`,
name: `event-types-2024-06-14-org-user-${randomString()}`,
username: `event-types-2024-06-14-org-user-${randomString()}`,
});
profileRepositoryFixture.create({
@@ -150,17 +219,32 @@ describe("Event types Endpoints", () => {
});
orgUserEventType1 = await eventTypesRepositoryFixture.create(
{ title: "orgUserEventType1", slug: "org-event-type-1", length: 60, locations: [] },
{
title: `event-types-2024-06-14-event-type-${randomString()}`,
slug: `event-types-2024-06-14-event-type-${randomString()}`,
length: 60,
locations: [],
},
orgUser.id
);
orgUserEventType2 = await eventTypesRepositoryFixture.create(
{ title: "orgUserEventType2", slug: "org-event-type-2", length: 60, locations: [] },
{
title: `event-types-2024-06-14-event-type-${randomString()}`,
slug: `event-types-2024-06-14-event-type-${randomString()}`,
length: 60,
locations: [],
},
orgUser.id
);
orgUserEventType3 = await eventTypesRepositoryFixture.create(
{ title: "orgUserEventType3", slug: "org-event-type-3", length: 60, locations: [] },
{
title: `event-types-2024-06-14-event-type-${randomString()}`,
slug: `event-types-2024-06-14-event-type-${randomString()}`,
length: 60,
locations: [],
},
orgUser.id
);
@@ -173,25 +257,25 @@ describe("Event types Endpoints", () => {
falseTestUser = await userRepositoryFixture.create({
email: falseTestEmail,
name: "false-test",
name: `event-types-2024-06-14-false-test-user-${randomString()}`,
username: falseTestEmail,
});
firstSchedule = await schedulesRepostoryFixture.create({
userId: user.id,
name: "work",
name: `event-types-2024-06-14-schedule-work-${randomString()}`,
timeZone: "Europe/Rome",
});
secondSchedule = await schedulesRepostoryFixture.create({
userId: user.id,
name: "chill",
name: `event-types-2024-06-14-schedule-chill-${randomString()}`,
timeZone: "Europe/Rome",
});
falseTestSchedule = await schedulesRepostoryFixture.create({
userId: falseTestUser.id,
name: "work",
name: `event-types-2024-06-14-schedule-work-${randomString()}`,
timeZone: "Europe/Rome",
});
@@ -253,7 +337,7 @@ describe("Event types Endpoints", () => {
});
it("should create an event type", async () => {
const nameBookingField: NameFieldInput_2024_06_14 = {
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
type: "name",
label: "Your name sir / madam",
placeholder: "john doe",
@@ -379,30 +463,25 @@ describe("Event types Endpoints", () => {
);
expect(createdEventType.color).toEqual(body.color);
const requestBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
// note(Lauris): location booking field is added if multiple locations are passed
{ ...defaultResponseBookingFieldName, ...nameBookingField },
{ ...defaultResponseBookingFieldEmail },
{ ...defaultResponseBookingFieldLocation },
{
isDefault: true,
required: false,
slug: "location",
type: "radioInput",
disableOnPrefill: false,
type: "select",
label: "select which language you want to learn",
slug: "select-language",
required: true,
placeholder: "select language",
options: ["javascript", "python", "cobol"],
disableOnPrefill: true,
hidden: false,
isDefault: false,
},
{
isDefault: true,
required: false,
slug: "rescheduleReason",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
...requestBookingFields
.filter((field) => field.type !== "name" && field.type !== "email")
.map((field) => ({ isDefault: false, ...field })),
{ ...defaultResponseBookingFieldTitle },
{ ...defaultResponseBookingFieldNotes },
{ ...defaultResponseBookingFieldGuests },
{ ...defaultResponseBookingFieldRescheduleReason },
];
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
@@ -432,7 +511,9 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
@@ -455,7 +536,6 @@ describe("Event types Endpoints", () => {
});
it(`/GET/event-types by username and orgSlug`, async () => {
console.log(organization);
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
@@ -636,7 +716,6 @@ describe("Event types Endpoints", () => {
.expect(201);
const createdEventType = createResponse.body.data;
console.log("createdEventType: ", createdEventType);
expect(createdEventType).toMatchObject({
id: expect.any(Number),
@@ -779,13 +858,20 @@ describe("Event types Endpoints", () => {
});
it("should update event type", async () => {
const nameBookingField: NameFieldInput_2024_06_14 = {
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
type: "name",
label: "Your name sir / madam",
placeholder: "john doe",
disableOnPrefill: true,
};
const notesBookingField: NotesDefaultFieldInput_2024_06_14 = {
slug: "notes",
label: "lemme take some notes",
placeholder: "write your notes here",
required: true,
};
const newTitle = "Coding class in Italian!";
const body: UpdateEventTypeInput_2024_06_14 = {
@@ -804,6 +890,7 @@ describe("Event types Endpoints", () => {
disableOnPrefill: false,
hidden: false,
},
notesBookingField,
],
bookingLimitsCount: {
day: 4,
@@ -866,21 +953,25 @@ describe("Event types Endpoints", () => {
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(updatedEventType.locations).toEqual(eventType.locations);
const requestBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{ ...defaultResponseBookingFieldName, ...nameBookingField },
{ ...defaultResponseBookingFieldEmail },
{ ...defaultResponseBookingFieldLocation },
{
isDefault: true,
required: false,
slug: "rescheduleReason",
type: "textarea",
type: "select",
label: "select which language you want to learn",
slug: "select-language",
required: true,
placeholder: "select language",
options: ["javascript", "python", "cobol"],
disableOnPrefill: false,
hidden: false,
isDefault: false,
},
...requestBookingFields
.filter((field) => field.type !== "name" && field.type !== "email")
.map((field) => ({ isDefault: false, ...field })),
{ ...defaultResponseBookingFieldTitle },
{ ...defaultResponseBookingFieldNotes, ...notesBookingField },
{ ...defaultResponseBookingFieldGuests },
{ ...defaultResponseBookingFieldRescheduleReason },
];
expect(updatedEventType.bookingFields).toEqual(expectedBookingFields);
@@ -997,7 +1088,9 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
@@ -1085,20 +1178,9 @@ describe("Event types Endpoints", () => {
type: "radioInput",
slug: "location",
required: false,
disableOnPrefill: false,
hidden: false,
label: "",
placeholder: "",
},
{ isDefault: true, required: true, slug: "title", type: "text", disableOnPrefill: false, hidden: true },
{
isDefault: true,
required: false,
slug: "notes",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
required: false,
@@ -1115,7 +1197,22 @@ describe("Event types Endpoints", () => {
disableOnPrefill: false,
hidden: false,
},
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false, hidden: true },
{
disableOnPrefill: false,
isDefault: true,
type: "phone",
slug: "attendeePhoneNumber",
required: false,
hidden: true,
},
{
isDefault: true,
required: false,
slug: "notes",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
];
beforeAll(async () => {
@@ -1140,7 +1237,9 @@ describe("Event types Endpoints", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
organization = await teamRepositoryFixture.create({
name: `event-types-2024-06-14-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
@@ -1239,15 +1338,6 @@ describe("Event types Endpoints", () => {
defaultLabel: "what_is_this_meeting_about",
defaultPlaceholder: "",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
{
name: "guests",
type: "multiemail",
@@ -1283,6 +1373,15 @@ describe("Event types Endpoints", () => {
required: false,
defaultLabel: "phone_number",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
],
};
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
@@ -1369,15 +1468,6 @@ describe("Event types Endpoints", () => {
defaultLabel: "what_is_this_meeting_about",
defaultPlaceholder: "",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
{
name: "guests",
type: "multiemail",
@@ -1413,6 +1503,15 @@ describe("Event types Endpoints", () => {
required: false,
defaultLabel: "phone_number",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
],
};
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
@@ -1426,19 +1525,21 @@ describe("Event types Endpoints", () => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
...expectedReturnSystemFields,
{
isDefault: false,
type: userDefinedBookingField.type,
slug: userDefinedBookingField.name,
label: userDefinedBookingField.label,
required: userDefinedBookingField.required,
placeholder: userDefinedBookingField.placeholder,
disableOnPrefill: false,
hidden: false,
},
]);
expect(fetchedEventType.bookingFields.sort(orderBySlug)).toEqual(
[
{
isDefault: false,
type: userDefinedBookingField.type,
slug: userDefinedBookingField.name,
label: userDefinedBookingField.label,
required: userDefinedBookingField.required,
placeholder: userDefinedBookingField.placeholder,
disableOnPrefill: false,
hidden: false,
},
...expectedReturnSystemFields,
].sort(orderBySlug)
);
});
});
@@ -1530,7 +1631,6 @@ describe("Event types Endpoints", () => {
type: "radioInput",
slug: "location",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
@@ -1626,7 +1726,9 @@ describe("Event types Endpoints", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
organization = await teamRepositoryFixture.create({
name: `event-types-2024-06-14-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
@@ -5,16 +5,19 @@ import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, BadRequestException } from "@nestjs/common";
import {
transformBookingFieldsApiRequestToInternal,
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
transformRecurrenceApiToInternal,
systemBeforeFieldName,
systemBeforeFieldEmail,
systemBeforeFieldLocation,
systemAfterFieldTitle,
systemAfterFieldNotes,
systemAfterFieldGuests,
systemAfterFieldRescheduleReason,
EventTypeMetaDataSchema,
systemBeforeFieldLocation,
transformBookerLayoutsApiToInternal,
transformConfirmationPolicyApiToInternal,
transformEventColorsApiToInternal,
@@ -217,27 +220,46 @@ export class InputEventTypesService_2024_06_14 {
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
hasMultipleLocations: boolean
) {
const customFields: (SystemField | CustomField)[] = inputBookingFields
? transformBookingFieldsApiRequestToInternal(inputBookingFields)
const internalFields: (SystemField | CustomField)[] = inputBookingFields
? transformBookingFieldsApiToInternal(inputBookingFields)
: [];
const customFieldsWithoutNameEmail = customFields.filter(
(field) => field.type !== "name" && field.type !== "email"
const systemCustomFields = internalFields.filter((field) => !this.isUserCustomField(field));
const userCustomFields = internalFields.filter((field) => this.isUserCustomField(field));
const systemCustomNameField = systemCustomFields?.find((field) => field.type === "name");
const systemCustomEmailField = systemCustomFields?.find((field) => field.type === "email");
const systemCustomTitleField = systemCustomFields?.find((field) => field.name === "title");
const systemCustomNotesField = systemCustomFields?.find((field) => field.name === "notes");
const systemCustomGuestsField = systemCustomFields?.find((field) => field.name === "guests");
const systemCustomRescheduleReasonField = systemCustomFields?.find(
(field) => field.name === "rescheduleReason"
);
const customNameField = customFields?.find((field) => field.type === "name");
const customEmailField = customFields?.find((field) => field.type === "email");
const defaultFieldsBefore: (SystemField | CustomField)[] = [
customNameField || systemBeforeFieldName,
customEmailField || systemBeforeFieldEmail,
systemCustomNameField || systemBeforeFieldName,
systemCustomEmailField || systemBeforeFieldEmail,
systemBeforeFieldLocation,
];
// note(Lauris): if event type has multiple locations then a radio button booking field has to be displayed to allow booker to pick location
if (hasMultipleLocations) {
defaultFieldsBefore.push(systemBeforeFieldLocation);
}
const defaultFieldsAfter = [systemAfterFieldRescheduleReason];
const defaultFieldsAfter = [
systemCustomTitleField || systemAfterFieldTitle,
systemCustomNotesField || systemAfterFieldNotes,
systemCustomGuestsField || systemAfterFieldGuests,
systemCustomRescheduleReasonField || systemAfterFieldRescheduleReason,
];
return [...defaultFieldsBefore, ...customFieldsWithoutNameEmail, ...defaultFieldsAfter];
return [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
}
isUserCustomField(field: SystemField | CustomField): field is CustomField {
return (
field.type !== "name" &&
field.type !== "email" &&
field.name !== "title" &&
field.name !== "notes" &&
field.name !== "guests" &&
field.name !== "rescheduleReason"
);
}
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
@@ -128,6 +128,7 @@ export class OutputEventTypesService_2024_06_14 {
const bookingFields = databaseEventType.bookingFields
? this.transformBookingFields(databaseEventType.bookingFields)
: this.getDefaultBookingFields(isOrgTeamEvent);
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
const users = this.transformUsers(databaseEventType.users || []);
@@ -15,6 +15,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -29,7 +30,7 @@ describe("Me Endpoints", () => {
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
const userEmail = "me-controller-e2e@api.com";
const userEmail = `me-controller-user-${randomString()}@api.com`;
let user: User;
let org: Team;
@@ -58,7 +59,7 @@ describe("Me Endpoints", () => {
});
org = await organizationsRepositoryFixture.create({
name: "Test org team",
name: `me-controller-organization-${randomString()}`,
isOrganization: true,
isPlatform: true,
});
@@ -16,6 +16,7 @@ import { User } from "@prisma/client";
import * as request from "supertest";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.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_04_15 } from "@calcom/platform-constants";
@@ -28,10 +29,11 @@ describe("Schedules Endpoints", () => {
let userRepositoryFixture: UserRepositoryFixture;
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
const userEmail = "schedules-controller-e2e@api.com";
const userEmail = `schedules-2024-04-15-user-${randomString()}@api.com`;
let user: User;
let createdSchedule: CreateScheduleOutput_2024_04_15["data"];
const scheduleName = `schedules-2024-04-15-schedule-${randomString()}`;
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
const defaultAvailabilityStartTime = "1970-01-01T09:00:00.000Z";
const defaultAvailabilityEndTime = "1970-01-01T17:00:00.000Z";
@@ -67,7 +69,6 @@ describe("Schedules Endpoints", () => {
});
it("should not create an invalid schedule", async () => {
const scheduleName = "schedule-name";
const scheduleTimeZone = "Europe/Rome";
const isDefault = true;
@@ -98,13 +99,11 @@ describe("Schedules Endpoints", () => {
});
it("should create a default schedule", async () => {
const scheduleName = "schedule-name";
const scheduleTimeZone = "Europe/Rome";
const isDefault = true;
const body: CreateScheduleInput_2024_04_15 = {
name: scheduleName,
timeZone: scheduleTimeZone,
timeZone: "Europe/Rome",
isDefault,
};
@@ -118,7 +117,7 @@ describe("Schedules Endpoints", () => {
expect(responseData.status).toEqual(SUCCESS_STATUS);
expect(responseData.data).toBeDefined();
expect(responseData.data.isDefault).toEqual(isDefault);
expect(responseData.data.timeZone).toEqual(scheduleTimeZone);
expect(responseData.data.timeZone).toEqual("Europe/Rome");
expect(responseData.data.name).toEqual(scheduleName);
const schedule = responseData.data.schedule;
@@ -177,7 +176,7 @@ describe("Schedules Endpoints", () => {
});
it("should update schedule name", async () => {
const newScheduleName = "new-schedule-name";
const newScheduleName = `schedules-2024-04-15-schedule-${randomString()}`;
const body: UpdateScheduleInput_2024_04_15 = {
name: newScheduleName,
@@ -32,11 +32,11 @@ describe("Schedules Endpoints", () => {
let userRepositoryFixture: UserRepositoryFixture;
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
const userEmail = "schedules-controller-e2e@api.com";
const userEmail = `schedules-2024-06-11-user@api.com`;
let user: User;
const createScheduleInput: CreateScheduleInput_2024_06_11 = {
name: "work",
name: `schedules-2024-06-11-work`,
timeZone: "Europe/Rome",
isDefault: true,
};
@@ -41,6 +41,18 @@ describe("isOriginAllowed", () => {
const origin = "https://sub.mydomain.com/path/to/resource";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should handle patterns with wildcard for routes correctly", () => {
const allowedOrigins = ["https://domain.com/*"];
const origin = "https://domain.com/dashboard";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should handle patterns with wildcard for root route correctly", () => {
const allowedOrigins = ["https://domain.com*"];
const origin = "https://domain.com";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
});
describe("is not allowed", () => {
@@ -23,6 +23,7 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { MockedRedisService } from "test/mocks/mock-redis-service";
import { randomString } from "test/utils/randomString";
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
@@ -39,9 +40,9 @@ describe("ApiAuthStrategy", () => {
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
const validApiKeyEmail = "api-key-user-email@example.com";
const validAccessTokenEmail = "access-token-user-email@example.com";
const validOAuthEmail = "oauth-user@example.com";
const validApiKeyEmail = `api-auth-api-key-user-${randomString()}@api.com`;
const validAccessTokenEmail = `api-auth-access-token-user-${randomString()}@api.com`;
const validOAuthEmail = `api-auth-oauth-user-${randomString()}@api.com`;
let validApiKeyUser: User;
let validAccessTokenUser: User;
@@ -83,7 +84,7 @@ describe("ApiAuthStrategy", () => {
teamRepositoryFixture = new TeamRepositoryFixture(module);
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
organization = await teamRepositoryFixture.create({ name: "organization" });
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-${randomString()}` });
validApiKeyUser = await userRepositoryFixture.create({
email: validApiKeyEmail,
});
@@ -179,7 +179,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
if (origin && !isOriginAllowed(origin, client.redirectUris)) {
throw new UnauthorizedException(
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${origin}' to the 'Redirect uris' of your OAuth client.`
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${origin}' to the 'Redirect uris' of your OAuth client with ID '${client.id}'`
);
}
@@ -16,13 +16,14 @@ import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-cli
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { Team, PlatformOAuthClient, PlatformBilling } from "@calcom/prisma/client";
describe("Platform Billing Controller (e2e)", () => {
let app: INestApplication;
const userEmail = "platform-billing-controller-e2e@api.com";
const userEmail = `billing-user-${randomString()}@api.com`;
let user: UserWithProfile;
let billing: PlatformBilling;
let userRepositoryFixture: UserRepositoryFixture;
@@ -47,7 +48,9 @@ describe("Platform Billing Controller (e2e)", () => {
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
organization = await organizationsRepositoryFixture.create({ name: "platform billing" });
organization = await organizationsRepositoryFixture.create({
name: `billing-organization-${randomString()}`,
});
user = await userRepositoryFixture.create({
email: userEmail,
@@ -11,6 +11,7 @@ import { User } from "@prisma/client";
import * as request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import {
@@ -30,7 +31,7 @@ describe("Conferencing Endpoints", () => {
let userRepositoryFixture: UserRepositoryFixture;
let credentialsRepositoryFixture: CredentialsRepositoryFixture;
const userEmail = "conferencing-controller-user-e2e@api.com";
const userEmail = `conferencing-user-${randomString()}@api.com`;
let user: User;
beforeAll(async () => {
@@ -17,6 +17,7 @@ import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-cli
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -55,9 +56,14 @@ describe("Platform Destination Calendar Endpoints", () => {
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
organization = await teamRepositoryFixture.create({
name: `destination-calendars-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.createOAuthManagedUser("office365-connect@gmail.com", oAuthClient.id);
user = await userRepositoryFixture.createOAuthManagedUser(
`destination-calendars-user-${randomString()}@api.com`,
oAuthClient.id
);
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClient.id);
accessTokenSecret = tokens.accessToken;
refreshTokenSecret = tokens.refreshToken;
@@ -4,6 +4,7 @@ import { BillingModule } from "@/modules/billing/billing.module";
import { ConferencingModule } from "@/modules/conferencing/conferencing.module";
import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { RouterModule } from "@/modules/router/router.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { TimezoneModule } from "@/modules/timezones/timezones.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
@@ -24,6 +25,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
AtomsModule,
StripeModule,
ConferencingModule,
RouterModule,
],
})
export class EndpointsModule implements NestModule {
@@ -17,13 +17,14 @@ import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { EventType, Webhook } from "@calcom/prisma/client";
describe("EventTypes WebhooksController (e2e)", () => {
let app: INestApplication;
const userEmail = "event-types-webhook-controller-e2e@api.com";
const userEmail = `event-types-webhooks-user-${randomString()}@api.com`;
let user: UserWithProfile;
let otherUser: UserWithProfile;
let eventType: EventType;
@@ -55,14 +56,14 @@ describe("EventTypes WebhooksController (e2e)", () => {
});
otherUser = await userRepositoryFixture.create({
email: "other-user-webhook-controller@api.com",
username: "other-user-webhook-controller@api.com",
email: `event-types-webhooks-other-user-${randomString()}@api.com`,
username: `event-types-webhooks-other-user-${randomString()}@api.com`,
});
eventType = await eventTypeRepositoryFixture.create(
{
title: "Event Type 1",
slug: "webhook-event-type-1",
slug: `event-types-webhooks-event-type-${randomString()}`,
length: 60,
},
user.id
@@ -71,7 +72,7 @@ describe("EventTypes WebhooksController (e2e)", () => {
eventType2 = await eventTypeRepositoryFixture.create(
{
title: "Event Type 2",
slug: "webhook-event-type-2",
slug: `event-types-webhooks-event-type-${randomString()}`,
length: 60,
},
user.id
@@ -80,7 +81,7 @@ describe("EventTypes WebhooksController (e2e)", () => {
otherEventType = await eventTypeRepositoryFixture.create(
{
title: "Other Event Type ",
slug: "other-webhook-event-type",
slug: `event-types-webhooks-other-event-type-${randomString()}`,
length: 60,
},
otherUser.id
@@ -22,6 +22,7 @@ import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repo
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiSuccessResponse } from "@calcom/platform-types";
@@ -84,10 +85,10 @@ describe("OAuth Client Users Endpoints", () => {
let postResponseData: CreateManagedUserOutput["data"];
const platformAdminEmail = "platform-sensei@mail.com";
const platformAdminEmail = `oauth-client-users-admin-${randomString()}@api.com`;
let platformAdmin: User;
const userEmail = "oauth-client-user@gmail.com";
const userEmail = `oauth-client-users-user-${randomString()}@api.com`;
const userTimeZone = "Europe/Rome";
beforeAll(async () => {
@@ -110,7 +111,7 @@ describe("OAuth Client Users Endpoints", () => {
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
organization = await teamRepositoryFixture.create({
name: "organization",
name: `oauth-client-users-organization-${randomString()}`,
isPlatform: true,
isOrganization: true,
});
@@ -220,6 +221,7 @@ describe("OAuth Client Users Endpoints", () => {
await userConnectedToOAuth(responseBody.data.user.email);
await userHasDefaultEventTypes(responseBody.data.user.id);
await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId);
await userHasOnlyOneSchedule(responseBody.data.user.id);
});
async function userConnectedToOAuth(userEmail: string) {
@@ -260,6 +262,11 @@ describe("OAuth Client Users Endpoints", () => {
expect(schedule?.userId).toEqual(userId);
}
async function userHasOnlyOneSchedule(userId: number) {
const schedules = await schedulesRepositoryFixture.getByUserId(userId);
expect(schedules?.length).toEqual(1);
}
it(`should fail /POST using already used managed user email`, async () => {
const requestBody: CreateManagedUserInput = {
email: userEmail,
@@ -386,7 +393,7 @@ describe("OAuth Client Users Endpoints", () => {
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let postResponseData: CreateManagedUserOutput["data"];
const userEmail = "oauth-client-users-user@gmail.com";
const userEmail = `oauth-client-users-user-${randomString()}@api.com`;
const userTimeZone = "Europe/Rome";
beforeAll(async () => {
@@ -405,20 +412,20 @@ describe("OAuth Client Users Endpoints", () => {
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({
name: "Testy Organization",
isOrganization: true,
name: `oauth-client-users-organization-${randomString()}`,
isPlatform: true,
isOrganization: true,
});
owner = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
email: `oauth-client-users-admin-${randomString()}@api.com`,
username: `oauth-client-users-admin-${randomString()}@api.com`,
organization: { connect: { id: organization.id } },
});
await profileRepositoryFixture.create({
uid: `usr-${owner.id}`,
username: userEmail,
username: `oauth-client-users-admin-${randomString()}@api.com`,
organization: {
connect: {
id: organization.id,
@@ -20,14 +20,15 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withNextAuth } from "test/utils/withNextAuth";
import { PlatformOAuthClient, Team, Webhook } from "@calcom/prisma/client";
describe("OAuth client WebhooksController (e2e)", () => {
let app: INestApplication;
const userEmail = "oauth-client-webhook-controller-e2e@api.com";
const otherUserEmail = "other-oauth-client-webhook-controller-e2e@api.com";
const userEmail = `oauth-client-webhooks-user-${randomString()}@api.com`;
const otherUserEmail = `oauth-client-webhooks-other-user-${randomString()}@api.com`;
let user: UserWithProfile;
let otherUser: UserWithProfile;
let oAuthClient: PlatformOAuthClient;
@@ -69,7 +70,7 @@ describe("OAuth client WebhooksController (e2e)", () => {
});
org = await orgRepositoryFixture.create({
name: "apiOrg",
name: `oauth-client-webhooks-organization-${randomString()}`,
isOrganization: true,
metadata: {
isOrganization: true,
@@ -80,7 +81,7 @@ describe("OAuth client WebhooksController (e2e)", () => {
isPlatform: true,
});
otherOrg = await orgRepositoryFixture.create({
name: "otherOrg",
name: `oauth-client-webhooks-other-organization-${randomString()}`,
isOrganization: true,
metadata: {
isOrganization: true,
@@ -18,6 +18,7 @@ import { MembershipRepositoryFixture } from "test/fixtures/repository/membership
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { NextAuthMockStrategy } from "test/mocks/next-auth-mock.strategy";
import { randomString } from "test/utils/randomString";
import { withNextAuth } from "test/utils/withNextAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -68,7 +69,7 @@ describe("OAuth Clients Endpoints", () => {
let user: User;
let org: Team;
let app: INestApplication;
const userEmail = "oauth-clients-test-e2e@api.com";
const userEmail = `oauth-clients-user-${randomString()}@api.com`;
beforeAll(async () => {
const moduleRef = await withNextAuth(
@@ -89,7 +90,7 @@ describe("OAuth Clients Endpoints", () => {
email: userEmail,
});
org = await teamFixtures.create({
name: "apiOrg",
name: `oauth-clients-organization-${randomString()}`,
isOrganization: true,
metadata: {
isOrganization: true,
@@ -126,7 +127,7 @@ describe("OAuth Clients Endpoints", () => {
let user: User;
let org: Team;
let app: INestApplication;
const userEmail = "test-e2e@api.com";
const userEmail = `oauth-clients-user-${randomString()}@api.com`;
beforeAll(async () => {
const moduleRef = await withNextAuth(
@@ -147,7 +148,7 @@ describe("OAuth Clients Endpoints", () => {
email: userEmail,
});
org = await teamFixtures.create({
name: "apiOrg",
name: `oauth-clients-organization-${randomString()}`,
isOrganization: true,
metadata: {
isOrganization: true,
@@ -211,7 +212,7 @@ describe("OAuth Clients Endpoints", () => {
describe("User is part of an organization as Admin", () => {
let membership: Membership;
let client: { clientId: string; clientSecret: string };
const oAuthClientName = "test-oauth-client-admin";
const oAuthClientName = `oauth-clients-admin-${randomString()}`;
beforeAll(async () => {
membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true);
@@ -264,7 +265,7 @@ describe("OAuth Clients Endpoints", () => {
});
});
it(`/PUT/:id`, () => {
const clientUpdatedName = "test-oauth-client-updated";
const clientUpdatedName = `oauth-clients-admin-updated-${randomString()}`;
const body: UpdateOAuthClientInput = { name: clientUpdatedName };
return request(app.getHttpServer())
.patch(`/api/v2/oauth-clients/${client.clientId}`)
@@ -289,7 +290,7 @@ describe("OAuth Clients Endpoints", () => {
describe("User is part of an organization as Owner", () => {
let membership: Membership;
let client: { clientId: string; clientSecret: string };
const oAuthClientName = "test-oauth-client-owner";
const oAuthClientName = `oauth-clients-owner-${randomString()}`;
const oAuthClientPermissions = 32;
beforeAll(async () => {
@@ -346,7 +347,7 @@ describe("OAuth Clients Endpoints", () => {
});
});
it(`/PUT/:id`, () => {
const clientUpdatedName = "test-oauth-client-updated";
const clientUpdatedName = `oauth-clients-owner-updated-${randomString()}`;
const body: UpdateOAuthClientInput = { name: clientUpdatedName };
return request(app.getHttpServer())
.patch(`/api/v2/oauth-clients/${client.clientId}`)
@@ -19,6 +19,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withNextAuth } from "test/utils/withNextAuth";
import { X_CAL_SECRET_KEY } from "@calcom/platform-constants";
@@ -70,7 +71,7 @@ describe("OAuthFlow Endpoints", () => {
let refreshToken: string;
beforeAll(async () => {
const userEmail = "developer@platform.com";
const userEmail = `oauth-flow-user-${randomString()}@api.com`;
const moduleRef: TestingModule = await withNextAuth(
userEmail,
@@ -92,7 +93,9 @@ describe("OAuthFlow Endpoints", () => {
email: userEmail,
});
organization = await organizationsRepositoryFixture.create({ name: "organization" });
organization = await organizationsRepositoryFixture.create({
name: `oauth-flow-organization-${randomString()}`,
});
await profilesRepositoryFixture.create({
uid: "asd-asd",
username: userEmail,
@@ -6,7 +6,7 @@ import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-us
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
import { User } from "@prisma/client";
import { User, CreationSource } from "@prisma/client";
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
@@ -46,6 +46,7 @@ export class OAuthClientUsersService {
role: "MEMBER",
},
],
creationSource: CreationSource.API_V2,
teamId: organizationId,
isOrg: true,
parentId: null,
@@ -0,0 +1,30 @@
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";
@Injectable()
export class IsUserOOO implements CanActivate {
constructor(private oooRepo: UserOOORepository) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const oooId: string = request.params.oooId;
const userId: string = request.params.userId;
if (!userId) {
throw new ForbiddenException("No user id found in request params.");
}
if (!oooId) {
throw new ForbiddenException("No ooo entry id found in request params.");
}
const ooo = await this.oooRepo.getUserOOOByIdAndUserId(Number(oooId), Number(userId));
if (ooo) {
return true;
}
throw new ForbiddenException("This OOO entry does not belong to this user.");
}
}
@@ -0,0 +1,128 @@
import { BadRequestException } from "@nestjs/common";
import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsDate, IsInt, IsOptional, IsString, IsEnum, isDate } from "class-validator";
import { SkipTakePagination } from "@calcom/platform-types";
export enum OutOfOfficeReason {
UNSPECIFIED = "unspecified",
VACATION = "vacation",
TRAVEL = "travel",
SICK_LEAVE = "sick",
PUBLIC_HOLIDAY = "public_holiday",
}
export type OutOfOfficeReasonType = `${OutOfOfficeReason}`;
const isDateString = (dateString: string) => {
try {
const isoDateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{3})?Z$/;
return isoDateRegex.test(dateString);
} catch {
throw new BadRequestException("Invalid Date.");
}
};
export class CreateOutOfOfficeEntryDto {
@Transform(({ value }: { value: string }) => {
if (isDateString(value)) {
const date = new Date(value);
date.setUTCHours(0, 0, 0, 0);
return date;
}
throw new BadRequestException("Invalid Date.");
})
@IsDate()
@ApiProperty({
description: "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
example: "2023-05-01T00:00:00.000Z",
})
start!: Date;
@Transform(({ value }: { value: string }) => {
if (isDateString(value)) {
const date = new Date(value);
date.setUTCHours(23, 59, 59, 999);
return date;
}
throw new BadRequestException("Invalid Date.");
})
@IsDate()
@ApiProperty({
description: "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
example: "2023-05-10T23:59:59.999Z",
})
end!: Date;
@IsString()
@IsOptional()
@ApiPropertyOptional({
description: "Optional notes for the out of office entry.",
example: "Vacation in Hawaii",
})
notes?: string;
@IsInt()
@IsOptional()
@ApiPropertyOptional({
description: "The ID of the user covering for the out of office period, if applicable.",
example: 2,
})
toUserId?: number;
@IsEnum(OutOfOfficeReason)
@IsOptional()
@ApiPropertyOptional({
description: "the reason for the out of office entry, if applicable",
example: "vacation",
enum: OutOfOfficeReason,
})
reason?: OutOfOfficeReasonType;
}
export class UpdateOutOfOfficeEntryDto extends PartialType(CreateOutOfOfficeEntryDto) {}
export enum SortOrder {
asc = "asc",
desc = "desc",
}
type SortOrderType = keyof typeof SortOrder;
export class GetOutOfOfficeEntryFiltersDTO extends SkipTakePagination {
@IsOptional()
@IsEnum(SortOrder, {
message: 'SortStart must be either "asc" or "desc".',
})
@ApiProperty({
required: false,
description: "Sort results by their start time in ascending or descending order.",
example: "?sortStart=asc OR ?sortStart=desc",
enum: SortOrder,
})
sortStart?: SortOrderType;
@IsOptional()
@IsEnum(SortOrder, {
message: 'SortEnd must be either "asc" or "desc".',
})
@ApiProperty({
required: false,
description: "Sort results by their end time in ascending or descending order.",
example: "?sortEnd=asc OR ?sortEnd=desc",
enum: SortOrder,
})
sortEnd?: SortOrderType;
}
export class GetOrgUsersOutOfOfficeEntryFiltersDTO extends GetOutOfOfficeEntryFiltersDTO {
@IsString()
@IsOptional()
@ApiProperty({
type: String,
required: false,
description: "Filter ooo entries by the user email address. user must be within your organization.",
example: "example@domain.com",
})
email?: string;
}
@@ -0,0 +1,100 @@
import { OutOfOfficeReason } from "@/modules/ooo/inputs/ooo.input";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsInt, IsEnum, ValidateNested, IsString, IsDateString, IsOptional } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class UserOooOutputDto {
@IsInt()
@Expose()
@ApiProperty({
description: "The ID of the user.",
example: 2,
})
readonly userId!: number;
@IsInt()
@IsOptional()
@ApiPropertyOptional({
description: "The ID of the user covering for the out of office period, if applicable.",
example: 2,
})
@Expose()
readonly toUserId?: number;
@IsInt()
@Expose()
@ApiProperty({
description: "The ID of the ooo entry.",
example: 2,
})
readonly id!: number;
@IsString()
@Expose()
@ApiProperty({
description: "The UUID of the ooo entry.",
example: 2,
})
readonly uuid!: string;
@IsDateString()
@ApiProperty({
description: "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
example: "2023-05-01T00:00:00.000Z",
})
@Expose()
start!: Date;
@IsDateString()
@ApiProperty({
description: "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
example: "2023-05-10T23:59:59.999Z",
})
@Expose()
end!: Date;
@IsString()
@IsOptional()
@ApiPropertyOptional({
description: "Optional notes for the out of office entry.",
example: "Vacation in Hawaii",
})
@Expose()
notes?: string;
@IsEnum(OutOfOfficeReason)
@IsOptional()
@ApiPropertyOptional({
description: "the reason for the out of office entry, if applicable",
example: "vacation",
enum: OutOfOfficeReason,
})
@Expose()
reason?: OutOfOfficeReason;
}
export class UserOooOutputResponseDto {
@Expose()
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Expose()
@ValidateNested()
@Type(() => UserOooOutputDto)
data!: UserOooOutputDto;
}
export class UserOoosOutputResponseDto {
@Expose()
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Expose()
@ValidateNested()
@Type(() => UserOooOutputDto)
data!: UserOooOutputDto[];
}
@@ -0,0 +1,107 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
import { v4 as uuidv4 } from "uuid";
import { Prisma } from "@calcom/prisma/client";
import { PrismaWriteService } from "../../prisma/prisma-write.service";
type OOOInputData = Omit<Prisma.OutOfOfficeEntryCreateInput, "user" | "toUser" | "reason" | "uuid"> & {
toUserId?: number;
userId: number;
reasonId?: number;
};
@Injectable()
export class UserOOORepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async createUserOOO(data: OOOInputData) {
const uuid = uuidv4();
return this.dbWrite.prisma.outOfOfficeEntry.create({
data: { ...data, uuid },
include: { reason: true },
});
}
async updateUserOOO(oooId: number, data: Partial<OOOInputData>) {
return this.dbWrite.prisma.outOfOfficeEntry.update({
where: { id: oooId },
data,
include: { reason: true },
});
}
async getUserOOOById(oooId: number) {
return this.dbRead.prisma.outOfOfficeEntry.findFirst({
where: { id: oooId },
include: { reason: true },
});
}
async getUserOOOByIdAndUserId(oooId: number, userId: number) {
return this.dbRead.prisma.outOfOfficeEntry.findFirst({
where: { id: oooId, userId },
include: { reason: true },
});
}
async getUserOOOPaginated(
userId: number,
skip: number,
take: number,
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" }
) {
return this.dbRead.prisma.outOfOfficeEntry.findMany({
where: { userId },
skip,
take,
include: { reason: true },
...(sort?.sortStart && { orderBy: { start: sort.sortStart } }),
...(sort?.sortEnd && { orderBy: { end: sort.sortEnd } }),
});
}
async deleteUserOOO(oooId: number) {
return this.dbWrite.prisma.outOfOfficeEntry.delete({
where: { id: oooId },
include: { reason: true },
});
}
async findExistingOooRedirect(userId: number, start: Date, end: Date, toUserId?: number) {
const existingOutOfOfficeEntry = await this.dbRead.prisma.outOfOfficeEntry.findFirst({
select: {
userId: true,
toUserId: true,
},
where: {
...(toUserId && { userId: toUserId }),
toUserId: userId,
// Check for time overlap or collision
OR: [
// Outside of range
{
AND: [{ start: { lte: end } }, { end: { gte: start } }],
},
// Inside of range
{
AND: [{ start: { gte: start } }, { end: { lte: end } }],
},
],
},
});
return existingOutOfOfficeEntry;
}
async getOooByUserIdAndTime(userId: number, start: Date, end: Date) {
return await this.dbRead.prisma.outOfOfficeEntry.findFirst({
where: {
userId,
start,
end,
},
});
}
}
@@ -0,0 +1,142 @@
import {
CreateOutOfOfficeEntryDto,
UpdateOutOfOfficeEntryDto,
OutOfOfficeReason,
GetOutOfOfficeEntryFiltersDTO,
SortOrder,
} from "@/modules/ooo/inputs/ooo.input";
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
import { OutOfOfficeEntry } from "@calcom/prisma/client";
const OOO_REASON_ID_TO_REASON = {
1: OutOfOfficeReason["UNSPECIFIED"],
2: OutOfOfficeReason["VACATION"],
3: OutOfOfficeReason["TRAVEL"],
4: OutOfOfficeReason["SICK_LEAVE"],
5: OutOfOfficeReason["PUBLIC_HOLIDAY"],
};
const OOO_REASON_TO_REASON_ID = {
[OutOfOfficeReason["UNSPECIFIED"]]: 1,
[OutOfOfficeReason["VACATION"]]: 2,
[OutOfOfficeReason["TRAVEL"]]: 3,
[OutOfOfficeReason["SICK_LEAVE"]]: 4,
[OutOfOfficeReason["PUBLIC_HOLIDAY"]]: 5,
};
@Injectable()
export class UserOOOService {
constructor(
private readonly oooRepository: UserOOORepository,
private readonly usersRepository: UsersRepository
) {}
formatOooReason(ooo: OutOfOfficeEntry) {
return {
...ooo,
reason: ooo.reasonId
? OOO_REASON_ID_TO_REASON[ooo.reasonId as keyof typeof OOO_REASON_ID_TO_REASON]
: OOO_REASON_ID_TO_REASON[1],
};
}
isStartBeforeEnd(start?: Date, end?: Date) {
if ((end && !start) || (start && !end)) {
throw new BadRequestException("Please specify both ooo start and end time.");
}
if (start && end) {
if (start.getTime() > end.getTime()) {
throw new BadRequestException("Start date must be before end date.");
}
}
return true;
}
async checkUserEligibleForRedirect(userId: number, toUserId?: number) {
if (toUserId) {
const user = await this.usersRepository.findUserOOORedirectEligible(userId, toUserId);
if (!user) {
throw new BadRequestException("Cannot redirect to this user.");
}
}
}
async checkExistingOooRedirect(userId: number, start?: Date, end?: Date, toUserId?: number) {
if (start && end) {
const existingOooRedirect = await this.oooRepository.findExistingOooRedirect(
userId,
start,
end,
toUserId
);
if (existingOooRedirect) {
throw new BadRequestException("Booking redirect infinite not allowed.");
}
}
}
async checkDuplicateOOOEntry(userId: number, start?: Date, end?: Date) {
if (start && end) {
const duplicateEntry = await this.oooRepository.getOooByUserIdAndTime(userId, start, end);
if (duplicateEntry) {
throw new ConflictException("Ooo entry already exists.");
}
}
}
checkRedirectToSelf(userId: number, toUserId?: number) {
if (toUserId && toUserId === userId) {
throw new BadRequestException("Cannot redirect to self.");
}
}
async checkIsValidOOO(userId: number, ooo: CreateOutOfOfficeEntryDto | UpdateOutOfOfficeEntryDto) {
this.isStartBeforeEnd(ooo.start, ooo.end);
await this.checkExistingOooRedirect(userId, ooo.start, ooo.end, ooo.toUserId);
await this.checkDuplicateOOOEntry(userId, ooo.start, ooo.end);
await this.checkRedirectToSelf(userId, ooo.toUserId);
await this.checkUserEligibleForRedirect(userId, ooo.toUserId);
}
async createUserOOO(userId: number, body: CreateOutOfOfficeEntryDto) {
await this.checkIsValidOOO(userId, body);
const { reason, ...rest } = body;
const ooo = await this.oooRepository.createUserOOO({
...rest,
userId,
reasonId: OOO_REASON_TO_REASON_ID[reason ?? OutOfOfficeReason["UNSPECIFIED"]],
});
return this.formatOooReason(ooo);
}
async updateUserOOO(userId: number, oooId: number, body: UpdateOutOfOfficeEntryDto) {
await this.checkIsValidOOO(userId, body);
const { reason, ...rest } = body;
const data = reason
? { ...rest, reasonId: OOO_REASON_TO_REASON_ID[reason ?? OutOfOfficeReason["UNSPECIFIED"]] }
: rest;
const ooo = await this.oooRepository.updateUserOOO(oooId, data);
return this.formatOooReason(ooo);
}
async deleteUserOOO(oooId: number) {
const ooo = await this.oooRepository.deleteUserOOO(oooId);
return this.formatOooReason(ooo);
}
async getUserOOOPaginated(
userId: number,
skip: number,
take: number,
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" }
) {
const ooos = await this.oooRepository.getUserOOOPaginated(userId, skip, take, sort);
return ooos.map((ooo) => this.formatOooReason(ooo));
}
}
@@ -14,6 +14,7 @@ import { AttributeRepositoryFixture } from "test/fixtures/repository/attributes.
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -26,7 +27,7 @@ describe("Organizations Attributes Options Endpoints", () => {
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
const userEmail = "member@attributes-options-api.com";
const userEmail = `organization-attributes-options-member-${randomString()}@api.com`;
let user: User;
let org: Team;
let membership: Membership;
@@ -45,7 +46,7 @@ describe("Organizations Attributes Options Endpoints", () => {
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "AttributesOptionsCorp",
name: `organization-attributes-options-organization-${randomString()}`,
isOrganization: true,
});
@@ -97,7 +98,7 @@ describe("Organizations Attributes Options Endpoints", () => {
let membershipFixtures: MembershipRepositoryFixture;
let attributeRepositoryFixture: AttributeRepositoryFixture;
const userEmail = "admin@attributes-options-api.com";
const userEmail = `organization-attributes-options-admin-${randomString()}@api.com`;
let user: User;
let org: Team;
let membership: Membership;
@@ -123,7 +124,7 @@ describe("Organizations Attributes Options Endpoints", () => {
attributeRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "AttributesOptionsCorp",
name: `organization-attributes-options-admin-organization-${randomString()}`,
isOrganization: true,
});
@@ -12,6 +12,7 @@ import * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -24,7 +25,7 @@ describe("Organizations Attributes Endpoints", () => {
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
const userEmail = "member@attributes-api.com";
const userEmail = `organization-attributes-member-${randomString()}@api.com`;
let user: User;
let org: Team;
let membership: Membership;
@@ -42,7 +43,7 @@ describe("Organizations Attributes Endpoints", () => {
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "AttributesCorp",
name: `organization-attributes-organization-${randomString()}`,
isOrganization: true,
});
@@ -88,7 +89,7 @@ describe("Organizations Attributes Endpoints", () => {
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
const userEmail = "admin@attributes-api.com";
const userEmail = `organization-attributes-admin-${randomString()}@api.com`;
let user: User;
let org: Team;
let membership: Membership;
@@ -116,7 +117,7 @@ describe("Organizations Attributes Endpoints", () => {
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "AttributesCorp",
name: `organization-attributes-admin-organization-${randomString()}`,
isOrganization: true,
});
@@ -149,10 +149,14 @@ export class OrganizationsEventTypesController {
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
): Promise<GetTeamEventTypesOutput> {
const { eventSlug } = queryParams;
const { eventSlug, hostsLimit } = queryParams;
if (eventSlug) {
const eventType = await this.organizationsEventTypesService.getTeamEventTypeBySlug(teamId, eventSlug);
const eventType = await this.organizationsEventTypesService.getTeamEventTypeBySlug(
teamId,
eventSlug,
hostsLimit
);
return {
status: SUCCESS_STATUS,
@@ -14,6 +14,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -48,12 +49,12 @@ describe("Organizations Event Types Endpoints", () => {
let falseTestOrg: Team;
let falseTestTeam: Team;
const userEmail = "org-admin-event-types-controller-e222e@api.com";
const userEmail = `organizations-event-types-admin-${randomString()}@api.com`;
let userAdmin: User;
const teammate1Email = "teammate111@team.com";
const teammate2Email = "teammate221@team.com";
const falseTestUserEmail = "false-user@false-team.com";
const teammate1Email = `organizations-event-types-teammate1-${randomString()}@api.com`;
const teammate2Email = `organizations-event-types-teammate2-${randomString()}@api.com`;
const falseTestUserEmail = `organizations-event-types-false-user-${randomString()}@api.com`;
let teammate1: User;
let teammate2: User;
let falseTestUser: User;
@@ -98,23 +99,23 @@ describe("Organizations Event Types Endpoints", () => {
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
name: `organizations-event-types-organization-${randomString()}`,
isOrganization: true,
});
falseTestOrg = await organizationsRepositoryFixture.create({
name: "False test org",
name: `organizations-event-types-false-org-${randomString()}`,
isOrganization: true,
});
team = await teamsRepositoryFixture.create({
name: "Test org team",
name: `organizations-event-types-team-${randomString()}`,
isOrganization: false,
parent: { connect: { id: org.id } },
});
falseTestTeam = await teamsRepositoryFixture.create({
name: "Outside org team",
name: `organizations-event-types-false-team-${randomString()}`,
isOrganization: false,
parent: { connect: { id: falseTestOrg.id } },
});
@@ -237,7 +238,7 @@ describe("Organizations Event Types Endpoints", () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
successRedirectUrl: "https://masterchief.com/argentina/flan/video/1234",
title: "Coding consultation collective",
slug: "coding-consultation collective",
slug: `organizations-event-types-collective-${randomString()}`,
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
@@ -343,7 +344,7 @@ describe("Organizations Event Types Endpoints", () => {
it("should create a managed team event-type", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: "Coding consultation managed",
slug: "coding-consultation-managed",
slug: `organizations-event-types-managed-${randomString()}`,
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
@@ -577,6 +578,100 @@ describe("Organizations Event Types Endpoints", () => {
});
});
it("should be able to configure phone-only event type", async () => {
const body: UpdateTeamEventTypeInput_2024_06_14 = {
bookingFields: [
{
type: "email",
required: false,
label: "Email",
},
{
type: "phone",
slug: "attendeePhoneNumber",
required: true,
label: "Phone number",
hidden: true,
},
],
};
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`)
.send(body)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.bookingFields).toEqual([
{
isDefault: true,
type: "name",
slug: "name",
required: true,
disableOnPrefill: false,
},
{
isDefault: true,
type: "email",
slug: "email",
required: false,
label: "Email",
disableOnPrefill: false,
},
{
isDefault: true,
type: "radioInput",
slug: "location",
required: false,
hidden: false,
},
{
isDefault: true,
type: "phone",
slug: "attendeePhoneNumber",
required: true,
hidden: true,
label: "Phone number",
disableOnPrefill: false,
},
{
isDefault: true,
type: "text",
slug: "title",
required: true,
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "multiemail",
slug: "guests",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "textarea",
slug: "rescheduleReason",
required: false,
disableOnPrefill: false,
hidden: false,
},
]);
});
});
it("should assign all members to managed event-type", async () => {
const body: UpdateTeamEventTypeInput_2024_06_14 = {
assignAllTeamMembers: true,
@@ -657,10 +752,10 @@ describe("Organizations Event Types Endpoints", () => {
it("should return event type with default bookingFields if they are not defined", async () => {
const eventTypeInput = {
title: "unknown field event type two",
slug: `organizations-event-types-unknown-${randomString()}`,
description: "unknown field event type description two",
length: 40,
hidden: false,
slug: "unknown-field-type-two",
locations: [],
schedulingType: SchedulingType.ROUND_ROBIN,
};
@@ -677,21 +772,10 @@ describe("Organizations Event Types Endpoints", () => {
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{
isDefault: true,
type: "name",
slug: "name",
required: true,
disableOnPrefill: false,
},
{
isDefault: true,
type: "email",
slug: "email",
required: true,
disableOnPrefill: false,
},
{
isDefault: true,
type: "phone",
slug: "attendeePhoneNumber",
@@ -703,38 +787,37 @@ describe("Organizations Event Types Endpoints", () => {
type: "radioInput",
slug: "location",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "text",
slug: "title",
required: true,
slug: "title",
type: "text",
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
type: "textarea",
required: false,
slug: "notes",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "multiemail",
slug: "guests",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "textarea",
slug: "rescheduleReason",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
required: false,
slug: "guests",
type: "multiemail",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
required: false,
slug: "rescheduleReason",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
@@ -14,6 +14,7 @@ import { MembershipRepositoryFixture } from "test/fixtures/repository/membership
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -33,10 +34,9 @@ describe("Organizations Memberships Endpoints", () => {
let membership2: Membership;
let membershipCreatedViaApi: Membership;
const userEmail = "org-admin-membership-controller-e2e@api.com";
const userEmail2 = "org-member-membership-controller-e2e@api.com";
const invitedUserEmail = "org-member-invited-membership-controller-e2e@api.com";
const userEmail = `organizations-memberships-admin-${randomString()}@api.com`;
const userEmail2 = `organizations-memberships-member-${randomString()}@api.com`;
const invitedUserEmail = `organizations-memberships-invited-${randomString()}@api.com`;
let user: User;
let user2: User;
@@ -70,7 +70,7 @@ describe("Organizations Memberships Endpoints", () => {
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
name: `organizations-memberships-organization-${randomString()}`,
isOrganization: true,
});
@@ -217,7 +217,7 @@ describe("Organizations Memberships Endpoints", () => {
let org: Team;
let membership: Membership;
const userEmail = "org-member-memberships-controller-e2e@api.com";
const userEmail = `organizations-memberships-member-${randomString()}@api.com`;
let user: User;
beforeAll(async () => {
@@ -238,7 +238,7 @@ describe("Organizations Memberships Endpoints", () => {
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
name: `organizations-memberships-organization-${randomString()}`,
isOrganization: true,
});
@@ -12,6 +12,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -35,7 +36,7 @@ describe("Organizations Schedules Endpoints", () => {
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
const userEmail = "mr-robot@schedules-api.com";
const userEmail = `organizations-schedules-member-${randomString()}@api.com`;
let user: User;
let org: Team;
let membership: Membership;
@@ -53,7 +54,7 @@ describe("Organizations Schedules Endpoints", () => {
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "Ecorp",
name: `organizations-schedules-organization-${randomString()}`,
isOrganization: true,
});
@@ -115,12 +116,15 @@ describe("Organizations Schedules Endpoints", () => {
let membershipFixtures: MembershipRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
const userEmail = "mr-robot@schedules-api.com";
const username = "mr-robot";
const userEmail = `organizations-schedules-admin-${randomString()}@api.com`;
const userEmail2 = `organizations-schedules-member-${randomString()}@api.com`;
let user: User;
let user2: User;
let org: Team;
let profile: Profile;
let membership: Membership;
let membership2: Membership;
let profile: Profile;
let profile2: Profile;
let createdSchedule: ScheduleOutput_2024_06_11;
@@ -152,7 +156,7 @@ describe("Organizations Schedules Endpoints", () => {
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "Ecorp",
name: `organizations-schedules-admin-organization-${randomString()}`,
isOrganization: true,
});
@@ -164,7 +168,7 @@ describe("Organizations Schedules Endpoints", () => {
profile = await profileRepositoryFixture.create({
uid: `usr-${user.id}`,
username: username,
username: userEmail,
organization: {
connect: {
id: org.id,
@@ -17,6 +17,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -44,11 +45,10 @@ describe("Organizations Teams Memberships Endpoints", () => {
let membership2: Membership;
let membershipCreatedViaApi: OrgTeamMembershipOutputDto;
const userEmail = "org-admin-membership-teams-controller-e2e@api.com";
const userEmail2 = "org-member-membership-teams-controller-e2e@api.com";
const nonOrgUserEmail = "non-org-member-membership-teams-controller-e2e@api.com";
const invitedUserEmail = "org-member-invited-membership-teams-controller-e2e@api.com";
const userEmail = `organizations-teams-memberships-admin-${randomString()}@api.com`;
const userEmail2 = `organizations-teams-memberships-member-${randomString()}@api.com`;
const nonOrgUserEmail = `organizations-teams-memberships-non-org-${randomString()}@api.com`;
const invitedUserEmail = `organizations-teams-memberships-invited-${randomString()}@api.com`;
let user: User;
let user2: User;
@@ -92,12 +92,12 @@ describe("Organizations Teams Memberships Endpoints", () => {
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
name: `organizations-teams-memberships-organization-${randomString()}`,
isOrganization: true,
});
orgTeam = await teamsRepositoryFixture.create({
name: "Org Team",
name: `organizations-teams-memberships-team-${randomString()}`,
isOrganization: false,
parent: { connect: { id: org.id } },
});
@@ -129,7 +129,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
});
nonOrgTeam = await teamsRepositoryFixture.create({
name: "Non Org Team",
name: `organizations-teams-memberships-non-org-team-${randomString()}`,
isOrganization: false,
});
@@ -15,6 +15,7 @@ import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-cli
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { SUCCESS_STATUS, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
@@ -36,7 +37,7 @@ describe("Organizations Team Endpoints", () => {
let teamCreatedViaApi: Team;
let teamCreatedViaApi2: Team;
const userEmail = "org-admin-teams-controller-e2e@api.com";
const userEmail = `organizations-teams-admin-${randomString()}@api.com`;
let user: User;
beforeAll(async () => {
@@ -58,7 +59,7 @@ describe("Organizations Team Endpoints", () => {
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
name: `organizations-teams-organization-${randomString()}`,
isOrganization: true,
});
@@ -69,13 +70,13 @@ describe("Organizations Team Endpoints", () => {
});
team = await teamsRepositoryFixture.create({
name: "Test org team",
name: `organizations-teams-team1-${randomString()}`,
isOrganization: false,
parent: { connect: { id: org.id } },
});
team2 = await teamsRepositoryFixture.create({
name: "Test org team 2",
name: `organizations-teams-team2-${randomString()}`,
isOrganization: false,
parent: { connect: { id: org.id } },
});
@@ -133,10 +134,11 @@ describe("Organizations Team Endpoints", () => {
});
it("should create the team of the org", async () => {
const teamName = `organizations-teams-api-team1-${randomString()}`;
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.send({
name: "Team created via API",
name: teamName,
slug: "team-created-via-api",
bio: "This is our test team created via API",
} satisfies CreateOrgTeamDto)
@@ -145,7 +147,7 @@ describe("Organizations Team Endpoints", () => {
const responseBody: ApiSuccessResponse<Team> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
teamCreatedViaApi = responseBody.data;
expect(teamCreatedViaApi.name).toEqual("Team created via API");
expect(teamCreatedViaApi.name).toEqual(teamName);
expect(teamCreatedViaApi.slug).toEqual("team-created-via-api");
expect(teamCreatedViaApi.bio).toEqual("This is our test team created via API");
expect(teamCreatedViaApi.parentId).toEqual(org.id);
@@ -171,10 +173,11 @@ describe("Organizations Team Endpoints", () => {
});
it("should update the team of the org", async () => {
const updatedTeamName = `organizations-teams-api-team1-${randomString()}-updated`;
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/teams/${teamCreatedViaApi.id}`)
.send({
name: "Team created via API Updated",
name: updatedTeamName,
weekStart: "Monday",
logoUrl: "https://i.cal.com/api/avatar/b0b58752-68ad-4c0d-8024-4fa382a77752.png",
bannerUrl: "https://i.cal.com/api/avatar/949be534-7a88-4185-967c-c020b0c0bef3.png",
@@ -184,7 +187,7 @@ describe("Organizations Team Endpoints", () => {
const responseBody: ApiSuccessResponse<Team> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
teamCreatedViaApi = responseBody.data;
expect(teamCreatedViaApi.name).toEqual("Team created via API Updated");
expect(teamCreatedViaApi.name).toEqual(updatedTeamName);
expect(teamCreatedViaApi.weekStart).toEqual("Monday");
expect(teamCreatedViaApi.logoUrl).toEqual(
"https://i.cal.com/api/avatar/b0b58752-68ad-4c0d-8024-4fa382a77752.png"
@@ -219,10 +222,11 @@ describe("Organizations Team Endpoints", () => {
});
it("should create the team of the org without auto-accepting creator", async () => {
const teamName = `organizations-teams-api-team2-${randomString()}`;
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.send({
name: "Team II created via API",
name: teamName,
autoAcceptCreator: false,
} satisfies CreateOrgTeamDto)
.expect(201)
@@ -230,7 +234,7 @@ describe("Organizations Team Endpoints", () => {
const responseBody: ApiSuccessResponse<Team> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
teamCreatedViaApi2 = responseBody.data;
expect(teamCreatedViaApi2.name).toEqual("Team II created via API");
expect(teamCreatedViaApi2.name).toEqual(teamName);
expect(teamCreatedViaApi2.parentId).toEqual(org.id);
const membership = await membershipsRepositoryFixture.getUserMembershipByTeamId(
user.id,
@@ -265,7 +269,7 @@ describe("Organizations Team Endpoints", () => {
let team: Team;
let team2: Team;
const userEmail = "org-member-teams-controller-e2e@api.com";
const userEmail = `organizations-teams-member-${randomString()}@api.com`;
let user: User;
beforeAll(async () => {
@@ -287,7 +291,7 @@ describe("Organizations Team Endpoints", () => {
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
name: `organizations-teams-organization-${randomString()}`,
isOrganization: true,
});
@@ -298,13 +302,13 @@ describe("Organizations Team Endpoints", () => {
});
team = await teamsRepositoryFixture.create({
name: "Test org team",
name: `organizations-teams-team1-${randomString()}`,
isOrganization: false,
parent: { connect: { id: org.id } },
});
team2 = await teamsRepositoryFixture.create({
name: "Test org team 2",
name: `organizations-teams-team2-${randomString()}`,
isOrganization: false,
parent: { connect: { id: org.id } },
});
@@ -338,7 +342,7 @@ describe("Organizations Team Endpoints", () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.send({
name: "Team created via API",
name: `organizations-teams-api-team1-${randomString()}`,
} satisfies CreateOrgTeamDto)
.expect(403);
});
@@ -347,7 +351,7 @@ describe("Organizations Team Endpoints", () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/teams/${team.id}`)
.send({
name: "Team created via API Updated",
name: `organizations-teams-api-team1-${randomString()}-updated`,
} satisfies CreateOrgTeamDto)
.expect(403);
});
@@ -379,7 +383,7 @@ describe("Organizations Team Endpoints", () => {
let team: Team;
let team2: Team;
const userEmail = "org-member-teams-owner-controller-e2e@api.com";
const userEmail = `organizations-teams-owner-${randomString()}@api.com`;
let user: User;
beforeAll(async () => {
@@ -401,7 +405,7 @@ describe("Organizations Team Endpoints", () => {
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
name: `organizations-teams-organization-${randomString()}`,
isOrganization: true,
});
@@ -412,13 +416,13 @@ describe("Organizations Team Endpoints", () => {
});
team = await teamsRepositoryFixture.create({
name: "Test org team",
name: `organizations-teams-team1-${randomString()}`,
isOrganization: false,
parent: { connect: { id: org.id } },
});
team2 = await teamsRepositoryFixture.create({
name: "Test org team 2",
name: `organizations-teams-team2-${randomString()}`,
isOrganization: false,
parent: { connect: { id: org.id } },
});
@@ -464,7 +468,7 @@ describe("Organizations Team Endpoints", () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.send({
name: "Team created via API",
name: `organizations-teams-api-team1-${randomString()}`,
} satisfies CreateOrgTeamDto)
.expect(403);
});
@@ -473,7 +477,7 @@ describe("Organizations Team Endpoints", () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/teams/${team.id}`)
.send({
name: "Team created via API Updated",
name: `organizations-teams-api-team1-${randomString()}-updated`,
} satisfies CreateOrgTeamDto)
.expect(403);
});
@@ -509,7 +513,7 @@ describe("Organizations Team Endpoints", () => {
let team1: Team;
let team2: Team;
const userEmail = "platform-org-member-teams-owner-controller-e2e@api.com";
const userEmail = `organizations-teams-platform-owner-${randomString()}@api.com`;
let user: User;
beforeAll(async () => {
@@ -531,7 +535,7 @@ describe("Organizations Team Endpoints", () => {
});
org = await orgRepositoryFixture.create({
name: "Platform Test Organization",
name: `organizations-teams-platform-organization-${randomString()}`,
isOrganization: true,
});
@@ -570,7 +574,7 @@ describe("Organizations Team Endpoints", () => {
});
it("should create first oAuth client team", async () => {
const teamName = "Platform team created via API";
const teamName = `organizations-teams-platform-api-team1-${randomString()}`;
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.set(X_CAL_CLIENT_ID, oAuthClient1.id)
@@ -593,7 +597,7 @@ describe("Organizations Team Endpoints", () => {
});
it("should create second oAuth client team", async () => {
const teamName = "Platform team II created via API";
const teamName = `organizations-teams-platform-api-team2-${randomString()}`;
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.set(X_CAL_CLIENT_ID, oAuthClient2.id)
@@ -14,6 +14,7 @@ import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repo
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -37,11 +38,10 @@ describe("Organizations Teams Schedules Endpoints", () => {
let nonOrgTeam: Team;
let user2Schedule: Schedule;
const userEmail = "org-admin-schedules-teams-controller-e2e@api.com";
const userEmail2 = "org-member-schedules-teams-controller-e2e@api.com";
const nonOrgUserEmail = "non-org-member-schedules-teams-controller-e2e@api.com";
const invitedUserEmail = "org-member-invited-schedules-teams-controller-e2e@api.com";
const userEmail = `organizations-teams-schedules-admin-${randomString()}@api.com`;
const userEmail2 = `organizations-teams-schedules-member-${randomString()}@api.com`;
const nonOrgUserEmail = `organizations-teams-schedules-non-org-${randomString()}@api.com`;
const invitedUserEmail = `organizations-teams-schedules-invited-${randomString()}@api.com`;
let user: User;
let user2: User;
@@ -79,7 +79,7 @@ describe("Organizations Teams Schedules Endpoints", () => {
id: user2.id,
},
},
name: "User 2 schedule",
name: `organizations-teams-schedules-user2-schedule-${randomString()}`,
timeZone: "America/New_York",
});
@@ -94,18 +94,18 @@ describe("Organizations Teams Schedules Endpoints", () => {
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
name: `organizations-teams-schedules-organization-${randomString()}`,
isOrganization: true,
});
orgTeam = await teamsRepositoryFixture.create({
name: "Org Team",
name: `organizations-teams-schedules-team-${randomString()}`,
isOrganization: false,
parent: { connect: { id: org.id } },
});
nonOrgTeam = await teamsRepositoryFixture.create({
name: "Non Org Team",
name: `organizations-teams-schedules-non-org-team-${randomString()}`,
isOrganization: false,
});
@@ -0,0 +1,142 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
import { IsUserOOO } from "@/modules/ooo/guards/is-user-ooo";
import {
CreateOutOfOfficeEntryDto,
UpdateOutOfOfficeEntryDto,
GetOutOfOfficeEntryFiltersDTO,
GetOrgUsersOutOfOfficeEntryFiltersDTO,
} from "@/modules/ooo/inputs/ooo.input";
import {
UserOooOutputDto,
UserOooOutputResponseDto,
UserOoosOutputResponseDto,
} from "@/modules/ooo/outputs/ooo.output";
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
import { OrgUsersOOOService } from "@/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service";
import {
Controller,
UseGuards,
Get,
Post,
Patch,
Delete,
Param,
ParseIntPipe,
Body,
UseInterceptors,
Query,
} from "@nestjs/common";
import { ClassSerializerInterceptor } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@Controller({
path: "/v2/organizations/:orgId",
version: API_VERSIONS_VALUES,
})
@UseInterceptors(ClassSerializerInterceptor)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@UseGuards(IsOrgGuard)
@DocsTags("Orgs / Users / OOO")
export class OrganizationsUsersOOOController {
constructor(
private readonly userOOOService: UserOOOService,
private readonly orgUsersOOOService: OrgUsersOOOService
) {}
@Get("/users/:userId/ooo")
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsUserInOrg)
@ApiOperation({ summary: "Get all ooo entries of a user" })
async getOrganizationUserOOO(
@Param("userId", ParseIntPipe) userId: number,
@Query() query: GetOutOfOfficeEntryFiltersDTO
): Promise<UserOoosOutputResponseDto> {
const { skip, take, ...rest } = query ?? { skip: 0, take: 250 };
const ooos = await this.userOOOService.getUserOOOPaginated(userId, skip ?? 0, take ?? 250, rest);
return {
status: SUCCESS_STATUS,
data: ooos.map((ooo) => plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" })),
};
}
@Post("/users/:userId/ooo")
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsUserInOrg)
@ApiOperation({ summary: "Create an ooo entry for user" })
async createOrganizationUserOOO(
@Param("userId", ParseIntPipe) userId: number,
@Body() input: CreateOutOfOfficeEntryDto
): Promise<UserOooOutputResponseDto> {
const ooo = await this.userOOOService.createUserOOO(userId, input);
return {
status: SUCCESS_STATUS,
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
};
}
@Patch("/users/:userId/ooo/:oooId")
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsUserInOrg, IsUserOOO)
@ApiOperation({ summary: "Update ooo entry of a user" })
async updateOrganizationUserOOO(
@Param("userId", ParseIntPipe) userId: number,
@Param("oooId", ParseIntPipe) oooId: number,
@Body() input: UpdateOutOfOfficeEntryDto
): Promise<UserOooOutputResponseDto> {
const ooo = await this.userOOOService.updateUserOOO(userId, oooId, input);
return {
status: SUCCESS_STATUS,
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
};
}
@Delete("/users/:userId/ooo/:oooId")
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsUserInOrg, IsUserOOO)
@ApiOperation({ summary: "Delete ooo entry of a user" })
async deleteOrganizationUserOOO(
@Param("oooId", ParseIntPipe) oooId: number
): Promise<UserOooOutputResponseDto> {
const ooo = await this.userOOOService.deleteUserOOO(oooId);
return {
status: SUCCESS_STATUS,
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
};
}
@Get("/ooo")
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@ApiOperation({ summary: "Get all OOO entries of org users" })
async getOrganizationUsersOOO(
@Param("orgId", ParseIntPipe) orgId: number,
@Query() query: GetOrgUsersOutOfOfficeEntryFiltersDTO
): Promise<UserOoosOutputResponseDto> {
const { skip, take, email, ...rest } = query ?? { skip: 0, take: 250 };
const ooos = await this.orgUsersOOOService.getOrgUsersOOOPaginated(orgId, skip ?? 0, take ?? 250, rest, {
email,
});
return {
status: SUCCESS_STATUS,
data: ooos.map((ooo) => plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" })),
};
}
}
@@ -0,0 +1,446 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { UserOooOutputDto } from "@/modules/ooo/outputs/ooo.output";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { Team } from "@calcom/prisma/client";
describe("Organizations User OOO Endpoints", () => {
describe("User Authentication - User is Org Admin", () => {
let app: INestApplication;
let oooCreatedViaApiId: number;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let org: Team;
let team: Team;
let falseTestOrg: Team;
let falseTestTeam: Team;
const userEmail = `organizations-users-ooo-admin-${randomString()}@api.com`;
let userAdmin: User;
const teammate1Email = `organizations-users-ooo-member1-${randomString()}@api.com`;
const teammate2Email = `organizations-users-ooo-member2-${randomString()}@api.com`;
const falseTestUserEmail = `organizations-users-ooo-false-user-${randomString()}@api.com`;
let teammate1: User;
let teammate2: User;
let falseTestUser: User;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
userAdmin = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
role: "ADMIN",
});
teammate1 = await userRepositoryFixture.create({
email: teammate1Email,
username: teammate1Email,
});
teammate2 = await userRepositoryFixture.create({
email: teammate2Email,
username: teammate2Email,
});
falseTestUser = await userRepositoryFixture.create({
email: falseTestUserEmail,
username: falseTestUserEmail,
});
org = await organizationsRepositoryFixture.create({
name: `organizations-users-ooo-organization-${randomString()}`,
isOrganization: true,
});
falseTestOrg = await organizationsRepositoryFixture.create({
name: `organizations-users-ooo-false-org-${randomString()}`,
isOrganization: true,
});
team = await teamsRepositoryFixture.create({
name: `organizations-users-ooo-team-${randomString()}`,
isOrganization: false,
parent: { connect: { id: org.id } },
});
falseTestTeam = await teamsRepositoryFixture.create({
name: `organizations-users-ooo-false-team-${randomString()}`,
isOrganization: false,
parent: { connect: { id: falseTestOrg.id } },
});
await profileRepositoryFixture.create({
uid: `usr-${userAdmin.id}`,
username: userEmail,
organization: {
connect: {
id: org.id,
},
},
user: {
connect: {
id: userAdmin.id,
},
},
});
await profileRepositoryFixture.create({
uid: `usr-${teammate1.id}`,
username: teammate1Email,
organization: {
connect: {
id: org.id,
},
},
user: {
connect: {
id: teammate1.id,
},
},
});
await membershipsRepositoryFixture.create({
role: "ADMIN",
user: { connect: { id: userAdmin.id } },
team: { connect: { id: org.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: teammate1.id } },
team: { connect: { id: team.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: teammate2.id } },
team: { connect: { id: team.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: falseTestUser.id } },
team: { connect: { id: falseTestTeam.id } },
accepted: true,
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(organizationsRepositoryFixture).toBeDefined();
expect(userAdmin).toBeDefined();
expect(org).toBeDefined();
});
it("should create a ooo entry without redirect", async () => {
const body = {
start: "2025-05-01T01:00:00.000Z",
end: "2025-05-10T13:59:59.999Z",
notes: "ooo numero uno",
};
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
.send(body)
.expect(201)
.then((response) => {
const responseBody = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data as UserOooOutputDto;
expect(data.reason).toEqual("unspecified");
expect(data.userId).toEqual(teammate1.id);
expect(data.start).toEqual("2025-05-01T00:00:00.000Z");
expect(data.end).toEqual("2025-05-10T23:59:59.999Z");
oooCreatedViaApiId = data.id;
});
});
it("should create a ooo entry with redirect", async () => {
const body = {
start: "2025-08-01T01:00:00.000Z",
end: "2025-10-10T13:59:59.999Z",
notes: "ooo numero dos with redirect",
toUserId: teammate2.id,
};
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
.send(body)
.expect(201)
.then((response) => {
const responseBody = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data as UserOooOutputDto;
expect(data.reason).toEqual("unspecified");
expect(data.userId).toEqual(teammate1.id);
expect(data.start).toEqual("2025-08-01T00:00:00.000Z");
expect(data.end).toEqual("2025-10-10T23:59:59.999Z");
expect(data.toUserId).toEqual(teammate2.id);
});
});
it("should fail to create a ooo entry with start after end", async () => {
const body = {
start: "2025-07-01T00:00:00.000Z",
end: "2025-05-10T23:59:59.999Z",
notes: "ooo numero uno duplicate",
};
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
.send(body)
.expect(400);
});
it("should fail to create a duplicate ooo entry", async () => {
const body = {
start: "2025-05-01T00:00:00.000Z",
end: "2025-05-10T23:59:59.999Z",
notes: "ooo numero uno duplicate",
};
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
.send(body)
.expect(409);
});
it("should fail to create an ooo entry that redirects to self", async () => {
const body = {
start: "2025-05-02T00:00:00.000Z",
end: "2025-05-03T23:59:59.999Z",
notes: "ooo infinite redirect",
toUserId: teammate1.id,
};
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
.send(body)
.expect(400);
});
it("should fail to create an ooo entry that redirects to member outside of org", async () => {
const body = {
start: "2025-05-02T00:00:00.000Z",
end: "2025-05-03T23:59:59.999Z",
notes: "ooo invalid redirect",
toUserId: falseTestUser.id,
};
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
.send(body)
.expect(400);
});
it("should update a ooo entry without redirect", async () => {
const body = {
start: "2025-06-01T01:00:00.000Z",
end: "2025-06-10T13:59:59.999Z",
notes: "ooo numero uno",
reason: "vacation",
};
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
.send(body)
.expect(200)
.then((response) => {
const responseBody = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data as UserOooOutputDto;
expect(data.reason).toEqual("vacation");
expect(data.userId).toEqual(teammate1.id);
expect(data.start).toEqual("2025-06-01T00:00:00.000Z");
expect(data.end).toEqual("2025-06-10T23:59:59.999Z");
});
});
it("should fail to update a ooo entry with redirect outside of org", async () => {
const body = {
toUserId: falseTestUser.id,
};
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
.send(body)
.expect(400);
});
it("should fail to update a ooo entry with start after end ", async () => {
const body = {
start: "2025-07-01T00:00:00.000Z",
end: "2025-05-10T23:59:59.999Z",
};
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
.send(body)
.expect(400);
});
it("should fail to update a ooo entry with redirect to self ", async () => {
const body = {
toUserId: teammate1.id,
};
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
.send(body)
.expect(400);
});
it("should fail to update a ooo entry with duplicate time", async () => {
const body = {
start: "2025-06-01T01:00:00.000Z",
end: "2025-06-10T13:59:59.999Z",
};
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
.send(body)
.expect(409);
});
it("should update a ooo entry without redirect", async () => {
const body = {
toUserId: teammate2.id,
};
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
.send(body)
.expect(200)
.then((response) => {
const responseBody = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data as UserOooOutputDto;
expect(data.reason).toEqual("vacation");
expect(data.toUserId).toEqual(teammate2.id);
expect(data.userId).toEqual(teammate1.id);
expect(data.start).toEqual("2025-06-01T00:00:00.000Z");
expect(data.end).toEqual("2025-06-10T23:59:59.999Z");
});
});
it("should get 2 ooo entries", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/ooo?sortEnd=desc&email=${teammate1Email}`)
.expect(200)
.then((response) => {
const responseBody = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data as UserOooOutputDto[];
expect(data.length).toEqual(2);
const oooUno = data.find((ooo) => ooo.id === oooCreatedViaApiId);
expect(oooUno).toBeDefined();
if (oooUno) {
expect(oooUno.reason).toEqual("vacation");
expect(oooUno.toUserId).toEqual(teammate2.id);
expect(oooUno.userId).toEqual(teammate1.id);
expect(oooUno.start).toEqual("2025-06-01T00:00:00.000Z");
expect(oooUno.end).toEqual("2025-06-10T23:59:59.999Z");
}
// test sort
expect(data[1].id).toEqual(oooCreatedViaApiId);
});
});
it("should get 2 ooo entries", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
.expect(200)
.then((response) => {
const responseBody = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data as UserOooOutputDto[];
expect(data.length).toEqual(2);
const oooUno = data.find((ooo) => ooo.id === oooCreatedViaApiId);
expect(oooUno).toBeDefined();
if (oooUno) {
expect(oooUno.reason).toEqual("vacation");
expect(oooUno.toUserId).toEqual(teammate2.id);
expect(oooUno.userId).toEqual(teammate1.id);
expect(oooUno.start).toEqual("2025-06-01T00:00:00.000Z");
expect(oooUno.end).toEqual("2025-06-10T23:59:59.999Z");
}
});
});
it("should delete ooo entry", async () => {
return request(app.getHttpServer())
.delete(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
.expect(200);
});
it("user should have 1 ooo entries", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
.expect(200)
.then((response) => {
const responseBody = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data as UserOooOutputDto[];
expect(data.length).toEqual(1);
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(userAdmin.email);
await userRepositoryFixture.deleteByEmail(teammate1.email);
await userRepositoryFixture.deleteByEmail(teammate2.email);
await userRepositoryFixture.deleteByEmail(falseTestUser.email);
await teamsRepositoryFixture.delete(team.id);
await teamsRepositoryFixture.delete(falseTestTeam.id);
await organizationsRepositoryFixture.delete(org.id);
await organizationsRepositoryFixture.delete(falseTestOrg.id);
await app.close();
});
});
});
@@ -0,0 +1,33 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrgUsersOOORepository {
constructor(private readonly dbRead: PrismaReadService) {}
async getOrgUsersOOOPaginated(
orgId: number,
skip: number,
take: number,
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" },
filters?: { email?: string }
) {
console.log({ sort, filters });
return this.dbRead.prisma.outOfOfficeEntry.findMany({
where: {
user: {
...(filters?.email && { email: filters.email }),
profiles: {
some: {
organizationId: orgId,
},
},
},
},
skip,
take,
include: { reason: true },
...(sort?.sortStart && { orderBy: { start: sort.sortStart } }),
...(sort?.sortEnd && { orderBy: { end: sort.sortEnd } }),
});
}
}
@@ -0,0 +1,26 @@
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
import { OrgUsersOOORepository } from "@/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrgUsersOOOService {
constructor(
private readonly oooRepository: UserOOORepository,
private readonly oooUserService: UserOOOService,
private readonly usersRepository: UsersRepository,
private readonly orgUsersOOORepository: OrgUsersOOORepository
) {}
async getOrgUsersOOOPaginated(
orgId: number,
skip: number,
take: number,
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" },
filters?: { email?: string }
) {
const ooos = await this.orgUsersOOORepository.getOrgUsersOOOPaginated(orgId, skip, take, sort, filters);
return ooos.map((ooo) => this.oooUserService.formatOooReason(ooo));
}
}
@@ -15,6 +15,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -29,7 +30,7 @@ describe("Organizations Users Endpoints", () => {
let membershipFixtures: MembershipRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
const userEmail = "member1@org.com";
const userEmail = `organizations-users-member-${randomString()}@api.com`;
let user: User;
let org: Team;
@@ -47,7 +48,7 @@ describe("Organizations Users Endpoints", () => {
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "Test org 3",
name: `organizations-users-organization-${randomString()}`,
isOrganization: true,
});
@@ -118,24 +119,24 @@ describe("Organizations Users Endpoints", () => {
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
const userEmail = "admin1@org.com";
const nonMemberEmail = "non-member@test.com";
const userEmail = `organizations-users-admin-${randomString()}@api.com`;
const nonMemberEmail = `organizations-users-non-member-${randomString()}@api.com`;
let user: User;
let org: Team;
let createdUser: User;
const orgMembersData = [
{
email: "member1@org.com",
username: "member1@org.com",
email: `organizations-users-member1-${randomString()}@api.com`,
username: `organizations-users-member1-${randomString()}@api.com`,
},
{
email: "member2@org.com",
username: "member2@org.com",
email: `organizations-users-member2-${randomString()}@api.com`,
username: `organizations-users-member2-${randomString()}@api.com`,
},
{
email: "member3@org.com",
username: "member3@org.com",
email: `organizations-users-member3-${randomString()}@api.com`,
username: `organizations-users-member3-${randomString()}@api.com`,
},
];
@@ -154,7 +155,7 @@ describe("Organizations Users Endpoints", () => {
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "Test org 2",
name: `organizations-users-admin-organization-${randomString()}`,
isOrganization: true,
});
@@ -243,9 +244,9 @@ describe("Organizations Users Endpoints", () => {
{ userData },
userData.map((u) => u.profile)
);
expect(userData.find((u) => u.profile.username === "member1@org.com")).toBeDefined();
expect(userData.find((u) => u.profile.username === "member2@org.com")).toBeDefined();
expect(userData.find((u) => u.profile.username === "member3@org.com")).toBeDefined();
expect(userData.find((u) => u.profile.username === orgMembersData[0].username)).toBeDefined();
expect(userData.find((u) => u.profile.username === orgMembersData[1].username)).toBeDefined();
expect(userData.find((u) => u.profile.username === orgMembersData[2].username)).toBeDefined();
expect(userData.filter((user: { email: string }) => user.email === nonMemberEmail).length).toBe(0);
});
@@ -304,7 +305,7 @@ describe("Organizations Users Endpoints", () => {
it("should create a new org user", async () => {
const newOrgUser = {
email: "new-org-member-b@org.com",
email: `organizations-users-new-member-${randomString()}@api.com`,
organizationRole: "MEMBER",
autoAccept: true,
};
@@ -327,7 +328,7 @@ describe("Organizations Users Endpoints", () => {
usernameOrEmail: newOrgUser.email,
orgName: org.name,
orgId: org.id,
inviterName: "admin1@org.com",
inviterName: userEmail,
locale: null,
});
createdUser = userData;
@@ -366,7 +367,7 @@ describe("Organizations Users Endpoints", () => {
let membershipFixtures: MembershipRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
const authEmail = "auth@org.com";
const authEmail = `organizations-users-auth-${randomString()}@api.com`;
let user: User;
let org: Team;
let team: Team;
@@ -390,12 +391,12 @@ describe("Organizations Users Endpoints", () => {
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "Test org 4",
name: `organizations-users-organization-${randomString()}`,
isOrganization: true,
});
team = await teamsRepositoryFixture.create({
name: "Test org 4 team",
name: `organizations-users-team-${randomString()}`,
isOrganization: false,
parent: { connect: { id: org.id } },
});
@@ -464,7 +465,7 @@ describe("Organizations Users Endpoints", () => {
it("should create a new org user with team event-types", async () => {
const newOrgUser = {
email: "new-org-member-d@org.com",
email: `organizations-users-new-member-${randomString()}@api.com`,
organizationRole: "MEMBER",
autoAccept: true,
};
@@ -17,13 +17,14 @@ import { MembershipRepositoryFixture } from "test/fixtures/repository/membership
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { Team, Webhook } from "@calcom/prisma/client";
describe("WebhooksController (e2e)", () => {
let app: INestApplication;
const userEmail = "webhook-controller-e2e@api.com";
const userEmail = `organizations-webhooks-admin-${randomString()}@api.com`;
let org: Team;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
@@ -53,7 +54,7 @@ describe("WebhooksController (e2e)", () => {
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
name: `organizations-webhooks-organization-${randomString()}`,
isOrganization: true,
});
@@ -3,6 +3,8 @@ import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/
import { EmailModule } from "@/modules/email/email.module";
import { EmailService } from "@/modules/email/email.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
import { OrganizationsOptionsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes-options.controller";
import { OrganizationsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes.controller";
import { OrganizationsEventTypesController } from "@/modules/organizations/controllers/event-types/organizations-event-types.controller";
@@ -12,6 +14,9 @@ import { OrganizationsSchedulesController } from "@/modules/organizations/contro
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/controllers/teams/schedules/organizations-teams-schedules.controller";
import { OrganizationsUsersOOOController } from "@/modules/organizations/controllers/users/ooo/organizations-users-ooo-controller";
import { OrgUsersOOORepository } from "@/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository";
import { OrgUsersOOOService } from "@/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service";
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
@@ -86,6 +91,10 @@ import { Module } from "@nestjs/common";
WebhooksRepository,
WebhooksService,
OutputTeamEventTypesResponsePipe,
UserOOOService,
UserOOORepository,
OrgUsersOOOService,
OrgUsersOOORepository,
],
exports: [
OrganizationsService,
@@ -118,6 +127,7 @@ import { Module } from "@nestjs/common";
OrganizationsOptionsAttributesController,
OrganizationsWebhooksController,
OrganizationsTeamsSchedulesController,
OrganizationsUsersOOOController,
],
})
export class OrganizationsModule {}
@@ -3,6 +3,7 @@ import { UpdateOrganizationUserInput } from "@/modules/organizations/inputs/upda
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { CreationSource } from "@prisma/client";
@Injectable()
export class OrganizationsUsersRepository {
@@ -70,7 +71,7 @@ export class OrganizationsUsersRepository {
async createOrganizationUser(orgId: number, createUserBody: CreateOrganizationUserInput) {
const createdUser = await this.dbWrite.prisma.user.create({
data: createUserBody,
data: { ...createUserBody, creationSource: CreationSource.API_V2 },
});
return createdUser;
@@ -64,8 +64,12 @@ export class OrganizationsEventTypesService {
return this.teamsEventTypesService.getTeamEventType(teamId, eventTypeId);
}
async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string): Promise<DatabaseTeamEventType | null> {
return this.teamsEventTypesService.getTeamEventTypeBySlug(teamId, eventTypeSlug);
async getTeamEventTypeBySlug(
teamId: number,
eventTypeSlug: string,
hostsLimit?: number
): Promise<DatabaseTeamEventType | null> {
return this.teamsEventTypesService.getTeamEventTypeBySlug(teamId, eventTypeSlug, hostsLimit);
}
async getTeamEventTypes(teamId: number): Promise<DatabaseTeamEventType[]> {

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