Compare commits

..
315 Commits
Author SHA1 Message Date
GitHub Actions 0d2ec20f66 chore: release v5.0.4
Post release / release (push) Canceled after 0s
2025-02-20 22:50:23 +00:00
Benny JooandKeith Williams 01fc501cb9 chore: Block POST requests to all page routes except a whitelist (#19346)
* chore: block POST requests to next.js routes except a whitelist

* update config.matcher

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-02-20 17:19:35 -03:00
Nayan Bagale 1be002c33c fix: Issues with scheduling timezone change without end time (#19103)
* fix: Issues with scheduling timezone change without end time

* test: remove DatePicker component tests

* feat: implement DatePicker component with tests and update button data-testid
2025-02-20 19:19:55 +00:00
Benny Joo 8a4b03833d chore: remove org/team booking routes from middleware (#19349)
* chore: remove user routes from middleware

* chore: remove org and team routes from middleware
2025-02-20 15:30:13 -03:00
Calcom BotandLingo.dev 8714dda047 feat: update translations via @lingodotdev (#19417)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-02-20 18:18:14 +00:00
5b89123307 fix: fix youtube link and referral url (#19410)
* feat: Add 'Monobot CX' web application into cal.com app-store

* rollback yarn.lock

* fix: youtube link

* fix: change redirect url

* fix: config change redirect url

---------

Co-authored-by: Volodymyr Gusakovskyi <v.gusakovskyi@asferro.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-02-20 17:48:47 +00:00
Benny Joo 3221379584 chore: remove user routes from middleware (#19345) 2025-02-20 14:39:40 -03:00
Amit SharmaandTushar Bhatt 578f5dc16b feat: implement mintlify AI search into CMD+K (#18749)
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-02-20 16:07:13 +00:00
amritandTushar Bhatt 3f042a1077 fix: reduce abnormal spacing in calendar invite (#19290)
* fix: reduce abnormal spacing in calendar invite

* add test for CalEventParser's getRichDescription

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-02-20 21:27:33 +05:30
Anik Dhabal Babu a44e7b2371 fix: multiselect is broken (#19351) 2025-02-20 13:22:34 +00:00
Hariom Balhara b06a94ab57 More dry run fixes (#18865) 2025-02-20 10:21:28 +00:00
Rajiv Sahal ec55454e43 separate phone input for web and platform (#19396) 2025-02-20 10:23:59 +01:00
Hariom Balhara 9ec0217858 chore: Add logs more (#19397) 2025-02-20 10:17:05 +01:00
amrit 66d69f5a06 fix: impersonation button alignment (#19383) 2025-02-20 06:45:45 +00:00
GitHub Actions b7b58c2dc7 chore: release v5.0.3
Post release / release (push) Canceled after 0s
2025-02-19 22:38:11 +00:00
Vachmara de1e19ee6d fix: Intercom app - book an event inside the messenger (#19391) 2025-02-19 21:21:22 +00:00
mintlify[bot] a366cb7c89 Documentation edits made through Mintlify web editor (#19084)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2025-02-19 14:33:22 +00:00
yuriisilkov1986andVolodymyr Gusakovskyi dac336894c feat: Add 'Monobot CX' web application into cal.com app-store (#18505)
* feat: Add 'Monobot CX' web application into cal.com app-store

* rollback yarn.lock

---------

Co-authored-by: Volodymyr Gusakovskyi <v.gusakovskyi@asferro.com>
2025-02-19 13:40:07 +00:00
Harshit Singh 373c17a46d fix: Improved EventsLimit page for mobile viewports (#19270)
* Fixed mobile responsiveness UI  bugs

* Minor fix
2025-02-19 18:50:34 +05:30
Peer Richelsen 4c00c1ac2b added millis ai (#19357) 2025-02-19 18:20:47 +05:30
Özer 050a030ff0 fix(ui): prevent text descender cutoff in user dropdown (#19309) 2025-02-19 12:44:21 +00:00
Benny Joo dfb2232a59 chore: deprecate requiresLicense = true (#19374) 2025-02-19 13:33:03 +01:00
Amit SharmaandAnik Dhabal Babu 984ccf7c80 feat: Duration select dropdown multiple durations email embed (#17432)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-02-19 12:29:52 +00:00
Hariom Balhara e87168322c fix: Dont crash entire listing if one form has faulty JSON fields (#19368)
* Dont hit members matching endpoint for regular team forms

* fix tests

* Dont crash and instead show the form in readonly mode
2025-02-19 11:05:29 +00:00
Anwar Sadath bf9e9b4ae1 fix: Show loading indicator when delete account API is in progress (#19361) 2025-02-19 10:20:20 +00:00
Peer Richelsen 6d4b05cf1d Update README.md 2025-02-19 10:48:15 +01:00
Peer Richelsen bd1af4027f Update README.md 2025-02-19 10:47:45 +01:00
Peer Richelsen 3872175393 Update LICENSE 2025-02-19 10:27:35 +01:00
Peer Richelsen 5ca063d4e8 Update custom-css.mdx 2025-02-19 10:24:27 +01:00
Peer Richelsen 112aa8201f Update introduction.mdx 2025-02-19 10:22:21 +01:00
Hariom Balhara 17fcc2c213 fix: Test preview - Routing forms crash for regular teams(that are not sub-teams) (#19353) 2025-02-19 08:20:26 +00:00
amritandPeer Richelsen d3f1dc1b32 fix: tanstack getRight error, focus in user impersonation, missing space after input fields, border in my-profile email (#19350)
* fix: shadow after email and before the primary label

* fix: autofill input shadow in impersonation view

* fix: improve spacing and layout in EditUserForm

* fix: error due to getRight

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-02-19 13:23:11 +05:30
Hariom BalharaandhbjORbj 25f3368fa1 fix: Plain Chat loading in embed (#19348)
* PlainChat has to be on dashboard and not on booking pages

* improve useIsBookingPage

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-02-19 06:40:32 +00:00
9a271f397b fix: empty hosts array in filterHostsByLeadThreshold (#19343)
* return early if hosts is empty

* feedback

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

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-02-18 18:19:44 +00:00
GitHub Actions 6847781a7f chore: release v5.0.2
Post release / release (push) Canceled after 0s
2025-02-18 16:07:40 +00:00
amritandPeer Richelsen 2d9a3bd741 fix: EditLocationDialog footer positioning and structure (#19335)
* fix: EditLocationDialog footer positioning and structure

* fix: make RescheduleDialog footer layout and button structure right

* fix: input dialogue glitch now works great

* made tests happy

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-02-18 15:23:40 +00:00
649faf4ff6 fix: Fairness when routing (#18550)
* Partial push of new filters

* Let's always return the passed host array when a filter doesn't need to be applied

* fix: isFixed typing in same round robin host check

* Add main bulk of rewritten logic for routed fairness support

* Remove console.log

* Add getAttributeWithEnabledWeights to AttributeRepository

* Add OOO entry repository

* Use getOrderedListOfLuckyUsers in lead threshold filtering

* Shortfall already includes weight

* test against minShortfall

* first attempt to fix fallbacks

* Added testcase updates

* Partially fix filterHosts test

* Some test updates

* Removed email field from normalization

* fix no available users found error

* fix unit test

* remove fallback from same host reschedule return

* fixes and improvements for findQualifiedHosts

* include fallback logic in handleNewBooking

* use fallback in handleNewBooking

* fix for contact owner in handleNewBooking

* WIP - comment

* pass routingFormResponseId

* fix type error

* pass routingFormResponse

* fixes in treshold logic

* remove reason

* merge qualified and fallback availabilities

* fix type error

* add and fix tests

* clean up test file

* fix filterHostsBySameRoundRobinHost tests

* fix findQualifiedHosts.test.ts

* fix getRoutedUsers.test.ts

* fix tests

* fix isRerouting check

* fix start time for two weeks check

* fix fallback hosts

* fix test

* fallback hosts should also include qualified hosts

* fix type error

* fix typos

* add contact owner to fallbacks

* fix typo

* fix qualifiedRRUsers

* set fixed users when no hosts

* add tets + code clean up

* add test for reschedule with same host

* fix typo

* add more tests for findQualifiedHosts

* remove skip from test

* add comment

* remove unused code

* fix fallback users in loadAndValidateUsers

* improve naming for fallback

* rename missed fallback variable

* fix reschedule with same rr host in handleNewBooking

* fixes for fallback slots

* add more tests + small fixes for contact owner

* fix tests and type errors

* add logs for fallback

* add type:json to logger

* Fix small merge issue

* fix end time if two weeks are cut off

* add more tests for contact owner availability check

* add new logic for two weeks fallback

* fix reschedule same RR host test

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2025-02-18 14:59:48 +00:00
Peer Richelsen 03346f04ed chore: fixed tooltip position for routing form (#19267)
* fixed tooltip position for routing form

* fixed offset
2025-02-18 14:11:09 +00:00
Anik Dhabal BabuandUdit Takkar 99260974f4 fix: Issues with date changes in column view (#19258)
* fix: coulmn view date change

* Update store.ts

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-02-18 18:46:46 +05:30
Peer Richelsen 4a493c0a0e chore: v3 fixed filters, new profile view (#19312)
* fixed filters with badge

* aligned recurrence in bookings

* removed new badge

* new profile view

* fixed try for free button

* fixed form input
2025-02-18 12:23:30 +00:00
Benny Joo 47435c36ff fix: redirect logic in /settings/admin/apps (#19332) 2025-02-18 11:24:13 +00:00
Benny Joo 599744b31c chore: remove WithLayout from not-found.tsx (#19331) 2025-02-18 11:07:16 +00:00
Amit SharmaandTushar Bhatt d2c4c45137 fix: Can't use unsaved booking question as variable in event name (#18604)
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-02-18 16:11:40 +05:30
Benny Joo 43ba47a122 perf: Use one PageWrapper instance for all settings pages (#18752)
* 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

* move settings/(settings-layout) to use-page-wrapper

* move settings/(admin-layout) to use-page-wrapper

* move remaining settings pages to (use-page-wrapper)

* fix test

* remove unused code

* remove unused files

* fix type check

* remove test tag

* remove page

* RSC should not default export client components
2025-02-18 04:43:25 -05:00
amritandTushar Bhatt 0048b2d92c feat: make first tab active by default (#19315)
* feat: make first tab active by default

* perf: optimize event types tabs rendering with useMemo

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-02-18 14:42:03 +05:30
amrit 9dc0570d38 fix: timezone input text is now visible in dark mode (#19325) 2025-02-18 05:46:10 +00:00
Anik Dhabal Babu 1fbfca958a fix (#19298) 2025-02-18 11:08:10 +05:30
Joe Au-Yeung 7a6c93795a feat: Create UserCreationService and use in API V1 create user endpoint (#19150)
* Create `createUser` method in `userCreationService`

* Refactor `UserRepository` create method to accept prisma input and remove business logic

* API use `UserCreationService`

* Move slugify to service

* Use hashPassword instead

* Type fixes in `UserCreationService`

* Add `userCreationService` tests

* API accept data object

* Type fixes

* Add user _post test

* Add test for locked user

* Add locked param to log

* Add user repository tests

* Do not return locked status

* Explicitly pass `locked` prop

* Fix tests when locked isn't returned

* Fix tests

* Pass locked prop

* Edit test name

* Use logger

* Fix passing hashed password
2025-02-17 22:30:39 -05:00
GitHub Actions 4481b232cc chore: release v5.0.1
Post release / release (push) Canceled after 0s
2025-02-17 20:02:39 +00:00
Alex van Andel 84b265cd54 refactor: Clean out old code, no longer needed (#19323)
* refactor: Clean out old code, no longer needed

* Removed a sneaky one keen to stay behind
2025-02-17 19:39:20 +00:00
Rajiv Sahal eb86064995 feat: atoms adjustments with ui v3 styles (#19320) 2025-02-17 16:58:47 +02:00
Sanchit TewariandTushar Bhatt 39fdf5a74e feat: prevent automatic query parameters when opening booking page (#18132)
- Resolve the user hindrance where the event's booking page should not have search parameters such as date and month in the initial page load
- Contains changes for the fix of issue #18094 as well as it is needed

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-02-17 14:41:00 +00:00
Tushar BhattandAnik Dhabal Babu f28c605b50 fix: bulk-update-in-scheduleId-in-detailed-view (#19295)
* fix: bulk-update-in-scheduleId-in-detailed-view

* minor-change

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-02-17 14:39:27 +00:00
Anik Dhabal Babu 63d26c4fba fix: Can't change availability for managed event type (#19006)
* fix: flaky test organization.spec

* fix
2025-02-17 13:59:39 +00:00
Eunjae Lee 14bba343a4 fix: nested scrolls on /bookings (#19284) 2025-02-17 10:25:42 +01:00
amrit e441f3308f fix: cut off and alignment bug (#19317)
* fix: assignment label cut-off

* fix: add all team members label is vertically aligned
2025-02-17 08:21:26 +00:00
Benny Joo b4aad453cd chore: migrate api for team upgrade to App Router (#19100)
* remove old pages router api

* add new app router api

* fix

* fix redirect

* refactor
2025-02-17 12:07:13 +05:30
Benny JooandHariom Balhara dc7a1c4680 chore: migrate /apps/routing-forms/[...pages] to App Router (#18956)
* wip

* fix

* fix

* refactor

* rename to routingServerSidePropsConfig

* refactor

* refactor

* add routing-link to useIsBookingPage hook

* remove Head component

* fix

* redirect user to apps/routing-forms/forms if user goes to apps/routing-forms

* refactor

* remove log

* remove client component not needed

* clean up code

* remove unneeded metadata

* clean up further

* clean up further 2

* fix type check

* routing-link does not need shell

* Fix title for Routing Form public link and also remove any

* Remove ; in HTML

* Remove unnecessary page reload

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
2025-02-17 11:15:54 +05:30
Anik Dhabal Babu fd651133c2 fix flake (#19306) 2025-02-16 07:27:34 -03:00
GitHub Actions f0d2a1a051 chore: release v5.0.0
Post release / release (push) Canceled after 0s
2025-02-15 10:26:34 +00:00
Tushar Bhatt 62c7627775 fix: removing-all-members-toggle-for-fh (#19277)
* fix : removing all members toggle for fh

* ux enhancements

* adding placeholder
2025-02-15 10:14:56 +00:00
be53a20429 feat: Use Folders to organize SMS links on Dub (#19248)
* feat: Use Folders to organize SMS links on Dub

* Update yarn.lock

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2025-02-15 10:06:28 +00:00
Benny JooandAnik Dhabal Babu 5c6f52f1d4 perf: separate client vs server code from insights layout (#19285)
* add UpgradeTipWrapper

* refactor insights/layout.tsx

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-02-15 06:43:37 -03:00
Carina WollendorferandCarinaWolli 0e9e3e5012 add ireland country code (#19117)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-02-15 06:28:44 -03:00
5d709cf63c feat: v3 of UI components (#19136)
* ui playground

* add types to ui

* push avatar ignore ts and eslint errors

* feat: V3 UI avatar  (#18997)

* avatar playground

* add overflow-hidden back for loading indicator

* update lock

* feat: v3/badge  (#18999)

* badge intro

* fix text on small badge

* add dark mode warning

* feat: v3/button (#19014)

* TOC

* wip buttons

* shadows

* secondary

* semantic colours

* minimal buttons

* update disabled states

* checkbox

* dropdown v3

* load icons

* fix design sections

* fix icon gap

* fix

* remove client component from rsc

* use peer as avatar

* gif

* fix button gap - dropdown iocn

* support start icon in dropdowns

* add alert demo + v3 component + darken semantic text

* base inputs and text field

* wip input

* input field addon/suffix

* toast page

* wip select

* match designs for input and select rounded

* fix

* fix select isloading transform

* fix multi icons transition rotation

* fix group heading

* range slider demo + range slider

* range slider popover design

* v3 dialog

* fix slider thumb size

* wrap in suspense for useSearchParam difference in next14+

* base of tabs + horizontal tabs

* fix select sizing to match input 36px

* fix dark mode hover on button

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

* fix default bg on popover

* lightmode toasts + eleveation token

* empty screen

* intro to split button concept

* mass colour updates

* intro to themes

* remove i18n from label with name

* bouncy buttons

* feat: small select

* nav item dumb component

* full demo

* settings demo

* fix tabs

* fix mt

* fix dropdown feedback from loom

* alert fixes mentioned in loom

* push font-cal

* move to fumadocs

* fix avatar

* colors table

* add typeography

* icon + spacing guide

* add border radius and shadow tables

* fix colours and border radius

* remove prose undline via fumas fork

* fix icon buttons sizes

* button group component

* fix horizontal tabs

* tab fixes

* switches

* pagination component

* show toasts components raw

* sync with airtable

* add use client

* 10px on input again

* 10px in select also

* fix input height

* fix files merge

* fix loading state for icon buttons

* fix button fab

* Enable addon to now overflow to next line in DS

* use cal font for dialog text

* add avatar square variant

* fix select padding and menu size

* fix type errors

* fix type errors

* add switch small size and fix type errors

* fix ref types on input

* update web styles

* remove addonFilled to fix type errors

* push dialog fixes

* fix checkbox display

* remove prose from alert examples

* remove prose from all our component examples

* badge rounded-lg on lg size

* update colors

* force w/h on icon buttons instead of inheriting

* remove border-focus

* remove border-focus from tailwind config

* update tokens to match new colours

* add new border subtle tokens and add to toasts

* fix lg button padding

* fix and drastically improve rtl rests

* multi select use badges

* use badge design for select pills

* fix avatar alignment

* fix tests and remove redudant ones

* fix tests and remove redudant ones

* update atoms globals css

* extract tokens.css

* extract app.css

* force flex for atoms

* add @calcom/ui/css

* revert ui playground styles abstraction

* fix input focus states

* fix focus states for buttons

* feat: v3/app work (#19233)

* globals.css for app + event types listing search

* few app wide NITS

* fix select foucs within styling

* fix formbuilder switch styles

* fix disabled state of nested inputs

* workflow fixes

* webhook form fixes

* i18n on tabs info

* booking filters + tab

* fix filters spacing

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

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

* profile page

* update editor br

* button nits in existing places in the app

* fix timezone select and multilist in guest form

* fix text area on public booking page

* size small event types create

* margin on routing filter

* filters improvments

* routing insights fixes

* fix date filter range

* ooo fixes

* fix teams/members bulk actions

* create team UI

* i18n in useTabs instead of in tab component now

* Add event type setup border to availability

* fix bulk actions on org members table

* onboarding fixes

* fix input types with ref stripe comparision

* i18n fixes for platform

* update globals of atoms

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

* fix select

* Revert "add @calcom/ui/css"

This reverts commit 0dd8f9012907a278cfede4a98f289cb0744c9244.

* revert tokens css

* Revert "extract app.css"

This reverts commit 10b998604816ad3310c24b36a02a638dabc5be24.

* linear nits

* workflow dropdown

* fix page theme heading sizes to match cards

* fix stepper for team creation

* adjust margin to FilterSearchField

* fix ooo icon

* fix booker and dark mode due to darkgray 50

* more dark mode fixes

* add size prop

---------

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

* remove ui-playground

* fix grow missing from merge

* add search icon data-test-id back to search

* fix FormAction style

* fix datatestid for apps and tabs

* change selector for team invitation e2e

* fix use data-testid from item instaed of using names

* rename upper case O

* fix unit tests data-testid

* fix unit tests data-testid

* more data-testid changes

* add data-testid to apps tab

* data-testid to installed appPage

* fix embed tests

* fix filter-popover-trigger

* use text-error and use dataId for locator

* more e2e improvment

* pass in single testId as button is now duplicate

* show field-error instead

* add test to eventtype tabs

* Add css vars to embed too\

* add scrollbar and change dialog overlay colour

* fix type

* use attribute instead of class for tab detection

* await promise

* use new tokens for sprite generation

* add correct testing Ids to e2e instead of button:text

* use testIds for creation of orgs

* Webhook receiver wait for 10 seconds

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2025-02-14 16:31:21 -05:00
MorganandBailey Pumfleet 0f67faeef3 chore: calendars controller v2 docs (#19291)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2025-02-14 17:54:23 +00:00
1ff4f557f6 fix: flaky test (#19294)
Post release / release (push) Canceled after 0s
* fix: flaky test

* fix another test

* update

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: unknown <adhabal2002@gmail.com>
2025-02-14 16:25:54 +00:00
GitHub Actions 7e2c0bd29e chore: release v4.9.13 2025-02-14 16:25:10 +00:00
Alex van Andel 5b59cf9d9c revert: Reapply "fix: Multiple rr-hosts combine to create erroneous availabil… (#19263)
* Reapply "fix: Multiple rr-hosts combine to create erroneous availability (#18772)"

This reverts commit 5dbc6d9ca6.

* Only thing left is a diff to show which slots are removed

* Add logging around time slot results

* Setup logging through axiom

* .... forgot

* fix: type errors

* Only log when there are differences

* Updated from old which had a failing test to new logic
2025-02-14 12:34:56 -03:00
Lauris Skraucis d5420eda00 chore: disable v2 booking seats safe parse error logs (#19292) 2025-02-14 10:59:10 +00:00
c17ac67b3f feat: add CSAT to V2 bookings return (#19278)
* init

* test: v2 bookings returns rating

---------

Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
2025-02-14 09:47:54 +00:00
VijayandPeer Richelsen b2763a6274 fix: Default people filter in bookings list for admin (#18035)
* default people filter for admin

* chore

* type check

* added test case

* made test concise to reduce flakiness

* add timeout to wait for second get response before counting

* update timeout

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-02-14 10:08:57 +01:00
Morgan 08b6f60407 fix: missing await router controller (#19276) 2025-02-13 21:38:43 +00:00
466b3e4c08 fix: Salesforce - if no contact throw Error (#19271)
* Throw error if no contact is passed to createEvent

* chore: update error message

* chore

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-02-13 16:01:46 -05:00
GitHub Actions 401209810a chore: release v4.9.12
Post release / release (push) Canceled after 0s
2025-02-13 17:53:44 +00:00
Benny Joo 9f34a840d0 chore: Refactor to remove getServerSideProps for /teams route (#19286)
* remove getServerSideProps

* refactor teams route
2025-02-13 17:46:56 +00:00
Udit Takkar 37f1535e1a refactor: remove depreceated fields (#19279)
* refactor: remove depreceated fields

* chore: use v2 endpoint

* chore: update V2 Api type

* chore
2025-02-13 17:34:33 +02:00
Lauris Skraucis 5dc81e8677 feat: v2 slots new version (#18758)
* refactor: version old slots

* feat: init new slots endpoints

* chore: range format

* fix: duratin

* fix: duratin

* test: slot releaseAt

* refactor: reserve slot response

* refactor variable name

* docs: have new slots controller in docs

* feat: crud for slots reservations

* refactor: use exclude all for response

* docs

* chore: slots input service

* refactor mini

* refactor: remove unused imports

* docs

* handle orgSlug for dynamic events

* refactor: correct name

* docs

* add optional organizationSlug to BySlug search

* refactor: slot output format

* refactor: return seated slot info

* docs

* rename functions

* refactor: slots seated response

* fix: handle same username in org and non org

* refactor: test format

* fix: allow reservationDuration only for authed requests

* chore: check if auth user has permissions to define custom reservation duration

* refactor: split e2e test files

* refactor: invalid request exception codes

* chore: old slots controller uses all old versions
2025-02-13 14:17:00 +00:00
Eunjae Lee 5a166ee430 fix: remove section on /bookings if no data exists (#19261) 2025-02-13 11:40:17 +01:00
Hariom BalharaandHariom Balhara f6f964d495 feat: Use new /routing URL for routing forms (#19235)
* feat: Use new /routing URL for routing forms

* Fixes

* Fix bug

* precent crash

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
2025-02-13 15:13:13 +05:30
Joe Au-Yeung 6364e85293 Handle cancelled status (#19272) 2025-02-13 12:59:41 +05:30
GitHub Actions 46add2bebc chore: release v4.9.11
Post release / release (push) Canceled after 0s
2025-02-12 19:22:01 +00:00
Syed Ali Shahbaz 8ada940dc7 hotfix (#19266) 2025-02-12 19:16:52 +00:00
d027f8004a perf: Move PlainChat up the hierarchy to remove rerenders (#19255)
* perf: Move PlainChat up the hierarchy to remove rerenders

* rmeove yarn lock

---------

Co-authored-by: Nizzy <nizabizaher@gmail.com>
Co-authored-by: nizzy <140507264+nizzyabi@users.noreply.github.com>
2025-02-12 19:13:54 +00:00
Udit Takkar cc2f16411b feat: add country code (#19264) 2025-02-12 20:09:37 +01:00
nizzy 15b58c1c13 fix: added youtube videos to tips.tsx sidebar (#19245)
* added youtube videos to tips.tsx sidebar

* replace video links with dubco go.cal.com links for tracking
2025-02-12 21:04:38 +05:30
Eunjae Lee 2d12ea7372 fix: path to impersonation page on side bar (#19260) 2025-02-12 13:43:30 +00:00
Eunjae Lee 06bf9cfbfe fix: indicate hidden columns on filter dropdown (#19259) 2025-02-12 13:33:10 +00:00
Alex van Andel 5dbc6d9ca6 Revert "fix: Multiple rr-hosts combine to create erroneous availability (#18772)"
This reverts commit f5dc22f145.
2025-02-12 13:15:04 +00:00
Morgan 486e3a386e chore: salesforce for Router / Booker atoms and api v2 (#19217)
* wip

* small refactor

* small refactor

* fixup! Merge branch 'main' into enable-salesforce-router-apiv2-atom

* crm props

* fixup! crm props

* bump platform libraries

* bump platform libraries
2025-02-12 15:04:38 +02:00
Tushar Bhatt 3a29327761 fix: overlapping-dropdown-bookings-page (#19216) 2025-02-12 10:45:47 +00:00
Benny Joo 4f226cd653 chore: remove unneeded props from AuthContainer and make its usages cleaner (#19191)
* refactor AuthContainer by removing app-dir related props

* fix
2025-02-12 05:19:16 -05:00
Benny Joo 207804bd97 perf: remove WithLayout HOC from private booking page and booking embed (#19189) 2025-02-12 11:15:08 +01:00
Benny Joo 7969789d60 wip (#19190) 2025-02-12 05:06:53 -05:00
Lauris Skraucis 67fb0e6719 fix: v2 slots not returning attendees and bookingUid info (#18858)
* fix: v2 slots not returning attendees and bookingUid info

* rename file

* finish merge main
2025-02-12 15:06:52 +05:30
Anik Dhabal Babuandsean-brydon 7f390b147a fix: CTA is hidden on mobile (#19241)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-02-12 09:26:08 +00:00
Benny Joo 1dc869971e fix: small ui bug in 404 pages (#19254) 2025-02-12 08:30:00 +00:00
Joe Au-YeungandUdit Takkar 3fe83bb45c fix: Salesforce bugs (#19250)
* incompleteBooking only search for leads if contact is not found

* Fix logic

* Abstract to functions

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-02-12 08:24:57 +00:00
Benny Joo 4fbeff31e7 fix: decode params used in query object in buildLegacyCtx (#19253)
* fix: decode params used in query object in buildLegacyCtx

* add tests

* fix
2025-02-12 07:58:46 +00:00
GitHub Actions e800341485 chore: release v4.9.10
Post release / release (push) Canceled after 0s
2025-02-11 19:48:09 +00:00
Alex van Andel f5dc22f145 fix: Multiple rr-hosts combine to create erroneous availability (#18772)
* wip: Open draft PR with passing test (should fail)

* Fix aggregatedAvailability by not merging rr host availability

* Added test for validating fixed host behaviour

* Add test for combined rr and fixed hosts

* Check date-ranges arent returned multiple times when offered by multiple rr hosts

* Also sort date ranges

* Adding failing test that should result in 5 slots, but returns 7 as it returns the same slots twice.

* Slots returns duplicate slots for overlapping date ranges

* As mergeOverlappingDateRanges enabled the unified start time, we need to reproduce this

* Do not 'prettify' slots to the same degree if they fall on different days

* Added test to prove an end date before start also works

* Use a string instead of number as key

* chore: Deprecate organizerTimeZone, effectively unused

* Adjust all dateRanges similarly

* Bring back eventTimeZone

* Remove adjustDateRanges in favour of slot generation changes

* Convert to tz after all logic is finished.

* clearer code flow

* Remove console.log + add failing test due to inverse order

* Address issue with inversion when date ranges are given out of order
2025-02-11 19:35:56 +00:00
Keith WilliamsandJoe Au-Yeung 4fd31db6ee perf: Use a limit for searching website in Salesforce (#19244)
* perf: Avoid leading wildcard for Salesforce

* use limit of 1 since we need 1 record

* use LOWER to check exact website match

* Revert the usage of LOWER since we need wider searches

* Add limits to queries

* Update packages/app-store/salesforce/lib/CrmService.ts

* Remove unneeded LIMIT statements

* Update packages/app-store/salesforce/lib/CrmService.ts

* Apply suggestions from code review

* Update packages/app-store/salesforce/lib/CrmService.ts

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2025-02-11 19:17:23 +00:00
Keith Williams f58d3cf837 revert: "fix: bookings for guests added by organizer get cancelled wh… (#19243)
This reverts commit e29b42a02b.
2025-02-11 14:53:03 +00:00
Keith Williams fc79ea898a chore: Update CODEOWNERS to include more booking files (#19240) 2025-02-11 15:08:28 +01:00
Eunjae Lee 5a602f10e6 fix: select "Your account" for members by default on insights (#19236) 2025-02-11 12:39:40 +00:00
Kartik SainiandTushar Bhatt e29b42a02b fix: bookings for guests added by organizer get cancelled when rescheduled (#17820)
* fix: bookings for guests added by organizer get cancelled when rescheduled

* chore

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-02-11 17:56:09 +05:30
Benny Joo cdb57eaa59 fix: get user loggedin if accouont is found + add better error handling for new user creation (#19229) 2025-02-11 11:41:18 +00:00
Peer Richelsen 662ba8dfc6 chore: added impersonation to sidebar for admins (#19228) 2025-02-11 11:39:09 +00:00
Hariom BalharaandHariom Balhara 552d88ad6d Fallback if embedLibUrl is empty string (#19223)
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
2025-02-11 14:56:31 +05:30
Lauris Skraucis 08bcc77e2c feat: v2 GET bookings updated at filters and sort (#19133)
* fix: make Booking.updatedAt update automatically

* feat: return updatedAt by bookings endpoints

* feat: GET bookings beforeUpdatedAt, afterUpdatedAt, sortUpdated params

* docs: api

* chore: bump platform-libraries

* refactor: sortUpdated -> sortUpdatedAt query param

* chore: delete wrong migration
2025-02-11 08:39:50 +00:00
anushka-0099 c211f181fc fix: the wprd on confirm booking button in japanese (#19230) 2025-02-11 04:16:05 +00:00
Rajiv SahalandMorgan Vernay cc6016aef3 chore: atoms and docs update (#19137)
* hide docs for old google connect endpoint

* update docs for atoms

* fix css styles for week view

* update v2 reference

* add dutch translations in cal provider

* fix: add nl i18n in BaseCalProvider

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-02-10 20:05:28 +00:00
Pavan NaikandPavan 77d0fa9ff8 fix: the set as default apps issue with links #19176 (#19198)
* fix the set as default apps issue with links

* Allow (. _ -) in the Mirotalk link

---------

Co-authored-by: Pavan <pavannaik20@gmail.com>
2025-02-10 20:29:22 +01:00
Eunjae LeeandBenny Joo 720136fcf9 fix: replace filter implementations on /insights (#19033)
* fix: replace filter implementations on /insights

* add authorization for teamIds

* replace all the implementations

* provide eventTypeId

* remove log

* fix teamId for isAll

* fix Download button

* remove legacy implementations

* clean up

* nullish check

* remove unusable filters

* revert style

* fix type error

* fix e2e test

* fix type error

* fix type error

* fix e2e

* extract util method

* fix type error

* fix type error

* fix type error

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-02-10 20:28:48 +01:00
Benny Joo 9bf3732717 fix: RSC boundary error from auth/oauth2/authorize (#19226) 2025-02-10 19:18:25 +00:00
GitHub Actions cd721308d0 chore: release v4.9.9
Post release / release (push) Canceled after 0s
2025-02-10 17:03:41 +00:00
Tushar Bhatt 74c292cc57 feat: remove tabs when only user and no team (#19222) 2025-02-10 16:32:08 +00:00
Alex van Andel 8e26882b2d fix: TS error related to non-existant ReasonEnum 2025-02-10 16:08:19 +00:00
Joe Au-Yeung a775bec902 fix: Disable round robin reassignment if reroute is available (#19192)
* If booking is routed from form, disable round robin reassign

* Pass `bookingFromRoutingForm` prop from `BookingListItem`

* Add `ReassignType` enum

* Refactor `isBookingReroutable`
2025-02-10 15:05:18 +00:00
amrit 75ff5f6fb1 fix: update hungarian translation for booking and cancellation texts (#19218) 2025-02-10 14:27:43 +01:00
Morgan 7e20b74674 chore: move getTeamMemberEmailFromCrm to package/lib (#19174) 2025-02-10 13:02:29 +00:00
Peer Richelsen 111f292c0d chore: removed props.hideHeadingOnMobile (#19208)
* removed props.hideHeadingOnMobile

* made sure CTA works
2025-02-10 11:06:16 +00:00
Peer Richelsen 52775dd27c chore: changed "routing" icon (#19211)
* changed routing icon

* new routing badge
2025-02-10 08:54:32 +00:00
Calcom BotandLingo.dev 668304a7cd feat: update translations via @lingodotdev (#19214)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-02-10 08:49:41 +00:00
Mohammad Aquib 378917ba19 fix: typo in conferencing.repository.ts filename (#19161) (#19173)
* fix: typo in conferencing.repository.ts filename

* fix: rename conferencing.respository.ts to conferencing.repository.ts
2025-02-10 07:56:43 +00:00
luzpaz 15334ff20b fix: typos in apps/web (#19194)
Found via `codespell -q 3 -S "*.svg,./apps/web/public/static/locales,./packages/app-store/stripepayment/lib/currencyOptions.ts,./packages/lib/freeEmailDomainCheck/freeEmailDomains.ts" -L afterall,atleast,datea,fo,incase,ist,nam,notin,optionel,perview,reccuring`

Closes #19193
2025-02-10 03:35:17 +00:00
Udit TakkarandhbjORbj 76c09ca7bf perf: fetch only by id (#19154)
* perf: fetch only by id

* remove getServerSession

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-02-10 02:14:57 +05:30
Peer Richelsen 0fb0974769 feat: move apps to more (#19206) 2025-02-09 20:36:23 +01:00
Benny Joo 2840d64197 chore: migrate user booking pages to App Router (#19069)
* add user pages to config.matcher in middleware

* add app router user booking pages

* remove HeadSeo from users-public-view component

* remove pages router user booking pages

* fix type check

* fix embed

* fix test

* chore: remove BookerSEO from users-type-public-view

* fix
2025-02-07 16:52:56 -05:00
Benny Joo 77a00b3e8c chore: migrate orgDomain instant meeting route to App Router (#19067)
* Remove BookerSeo from instant-meeting-view

* remove route from pages router

* make eventData passed from getServerSideProps

* refactor route in app router and move it outside of /future
2025-02-07 21:51:35 +00:00
Tushar Bhatt cd04b17113 fix: placeholder added (#19184) 2025-02-07 20:19:55 +00:00
Joe Au-Yeung cbf2604029 chore: SF logging (#19180)
* More logging

* More logging

* Clean up logging
2025-02-07 13:01:23 -05:00
Eunjae Lee 403cfebb89 fix: resize columns when column visibility changes (#19172)
* fix: resize columns when column visibility changes

* fill initial column size if missing

* use different variable
2025-02-07 17:12:42 +00:00
Eunjae Lee 5d2c8a49ba fix: null check for useSearchParams() (#19175) 2025-02-07 15:19:15 +00:00
Anik Dhabal Babu 87f02f5588 fix: Lock time-zone not working properly (#19152)
Post release / release (push) Canceled after 0s
* fix: flaky test organization.spec

* fix
2025-02-07 13:55:58 +00:00
Anik Dhabal Babu 4671ffc4f4 fix (#19156) 2025-02-07 13:54:41 +00:00
Eunjae Lee 6cad7ac1d1 fix: selected filters for numeric and text filters (#19129)
* fix: selected filters for numeric and text filters

* little adjustment
2025-02-07 13:43:25 +00:00
GitHub Actions aff2c7cdb4 chore: release v4.9.8 2025-02-07 13:36:35 +00:00
luzpaz 787ff543df fix: source typo in bookingScenario.ts test (#19162) 2025-02-07 12:56:48 +00:00
luzpaz 01e442df17 fix: source typo in EditUserForm.tsx (#19157) 2025-02-07 12:48:24 +00:00
808de26401 feat: add option to disable sending transcription emails (#19065)
* feat: add option to disable sending transcription emails

* fix: types and tests

* fix: type error

* fix: type err

* fix: type err

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-02-07 12:33:38 +00:00
6ca76707da test: Scenario that replicates the crash in production (#19071)
* Add a case that crashes

* Add proper crash test case

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-02-07 11:20:29 +00:00
Benny Joo e79af63e6c chore: refactor apiRouteMiddleware and add it to some api routes in App Router (#19146)
* chore: remove try and catch from api routes in app router

* refactor

* refactor apiRouteMiddleware

* refactor
2025-02-07 11:04:34 +00:00
Hariom BalharaandHariom Balhara 24a080b988 Add documentation to pass metadata (#19124)
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
2025-02-07 14:40:22 +05:30
luzpaz 8612f77c07 fix: typos in docs/ (#19144)
Found via `codespell -q 3 -S "*.svg,./apps/web/public/static/locales" -L afterall,datea,fo,ist,optionel`
2025-02-07 06:45:34 +00:00
Benny Joo 1babf5bf50 chore: Remove HeadSeo components where not needed (#19151) 2025-02-07 04:11:26 +00:00
Benny Joo e8890156a1 chore: migrate api/cancel to App Router (#19074)
* Add app/api/cancel/route.ts

* remove pages router api/cancel

* refactors needed

* refactor
2025-02-06 23:16:38 +00:00
Benny Joo c9bfd977b1 perf: make metadata generation light for event type single page (#19143)
* add findTitleById method to Repository

* refactor route
2025-02-07 00:26:29 +02:00
Benny Joo 19921ab7c9 chore: migrate api/availability/calendar to App Router (#19099)
* add app/api/availability/calendar/route.ts

* remove pages router api

* refactor
2025-02-06 15:58:05 -05:00
Benny Joo 239c8a6fa9 chore: migrate /api/team/create (#19105) 2025-02-06 15:57:03 -05:00
Udit Takkar cb9dfaa76b fix: logo for org team event types (#19131)
* fix: logo on booking page

* fix: logo for org team event tyoes

* chore
2025-02-06 20:50:15 +05:30
Lauris Skraucis 0b46415856 feat: track who updated routing form (#19079)
* chore: updatedBy App_RoutingForms_Form schema field

* feat: track who updated routing form
2025-02-06 11:52:53 -03:00
sean-brydon 3a522320e7 change error message (#19135) 2025-02-06 14:06:52 +00:00
Joe Au-Yeung 89033c0e9c chore: Salesforce add more logging (#19106)
* Add more logs

* Remove unused method

* Fallback when creating an event

* Type fix

* Type fix

* Type fix

* Add more logging

* More logging
2025-02-06 10:25:38 -03:00
Benny Joo 3972972d57 fix: do not export client components as default in RSC (#19130) 2025-02-06 12:15:37 +00:00
Lauris Skraucis 695d2c0f39 feat: v2 bookings return cancelledBy and rescheduledBy (#19110) 2025-02-06 12:45:13 +02:00
Eunjae Lee 4c32d0c499 fix: check searchParams existence (#19128) 2025-02-06 10:28:46 +00:00
Somay Chauhan 3252efebb1 feat: enable booker atom for non org team events (#19125)
* feat: enable booker atom for non org team events

* Update useTeamEventType.ts
2025-02-06 12:32:05 +05:30
Keith Williams a6fa9d7d60 chore: Trigger Docker release on publish (#19121) 2025-02-05 21:51:33 +00:00
GitHub Actions 38a80c90bb chore: release v4.9.7
Post release / release (push) Canceled after 0s
2025-02-05 21:27:34 +00:00
Morgan 5f897837f2 chore: create event-type input lengthInMinutesOptions (#19114) 2025-02-05 21:39:16 +02:00
Tushar Bhatt 510e73b579 fix: startdate filter and ui sync on bookings page (#19113)
* fix: date-filter issues

* fixed comment

* fixed comment

* removing extra node
2025-02-05 16:03:52 +00:00
Joe Au-YeungandKeith Williams 1787f6d25d Flatmap today's bookings (#19104)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-02-05 14:35:06 +00:00
Somay Chauhan afa83ddb7f fix: add orgId to EventTeamAssignmentTabPlatformWrapper (#19109) 2025-02-05 18:19:31 +05:30
Eunjae Lee 744359dcdf fix: null check (#19111) 2025-02-05 10:27:42 +00:00
Keith WilliamsandPeer Richelsen b7bf58d560 chore: Don't require checks for /help updates (#19089)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-02-05 06:43:43 -03:00
Lauris Skraucis da02cd06d6 feat: v2 organization bookings endpoints (#18875)
* chore: extra team bookings e2e tests

* feat: v2 organizations bookings endpoints

* feat: v2 organizations bookings endpoints

* test

* revert seed.ts change
2025-02-05 10:58:36 +02:00
Morgan 14ec3813ee chore: enable more router props (#19088)
* chore: enable more router props

* chore: expose booker props via router atom
2025-02-05 08:36:34 +00:00
Hariom BalharaandHariom Balhara 288248d94f Prevent xss in preview aand load resources from known URL (#18978)
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
2025-02-05 12:17:56 +05:30
Peer Richelsen 1232eefece feat: attio app (#19102)
* feat: attio app

* added help page

* fixed redirect
2025-02-04 20:45:44 +00:00
Calcom BotandLingo.dev 24abcc3fb2 feat: update translations via @lingodotdev (#19098)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-02-04 19:06:53 +00:00
Eunjae Lee 935e4c52a4 fix: nullish check on insights routing (#19097) 2025-02-04 16:46:01 +00:00
Sanchit Tewariandamrit 9a8ec4d281 fix: Ensure correct fallback to current month when 'month' query param is missing (#18097)
Updated the query parameter handling logic to properly format the 'date' param
as 'YYYY-MM' in case month param is missing.  This resolves an issue where previous slots were not appearing.

Co-authored-by: amrit <iamamrit27@gmail.com>
2025-02-04 20:33:13 +05:30
Morgan 259624d44d fix: do not force cancellation reason for platform (#19087) 2025-02-04 14:55:05 +00:00
Carina WollendorferandCarinaWolli 7e0845ccf4 run cron job every minute (#19086)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-02-04 14:15:22 +00:00
Tushar Bhatt 0522af0c88 fix: empty state search messaging event page (#19064)
* test

* fix: adding empty search headline

* revert a change
2025-02-04 10:14:09 -03:00
Keith Williams f080a59c89 chore: Update CODEOWNERS for Platform and infra-related code (#18938)
* chore: Update CODEOWNERS for Platform and infra-related code

* Added more high-risk files/folders
2025-02-04 08:39:26 -03:00
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
1191 changed files with 44285 additions and 19017 deletions
+8 -13
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,21 +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
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_SAML_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
@@ -399,6 +388,12 @@ BLACKLISTED_GUEST_EMAILS=
NEXT_PUBLIC_VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
# Mintlify chat api
# Power AI chat in for docs
NEXT_PUBLIC_MINTLIFY_CHAT_API_KEY=
NEXT_PUBLIC_CHAT_API_URL=
NEXT_PUBLIC_DOCS_URL=
# Custom privacy policy / terms URLs (for self-hosters: change to your privacy policy / terms URLs)
NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL=
NEXT_PUBLIC_WEBSITE_TERMS_URL=
+12
View File
@@ -1,3 +1,6 @@
/apps/api/**/* @calcom/Platform
/deploy/**/* @calcom/Foundation
/infra/**/* @calcom/Foundation
/packages/app-store/applecalendar/**/* @calcom/Foundation
/packages/app-store/caldavcalendar/**/* @calcom/Foundation
/packages/app-store/exchange2013calendar/**/* @calcom/Foundation
@@ -9,3 +12,12 @@
/packages/app-store/larkcalendar/**/* @calcom/Foundation
/packages/app-store/office365calendar/**/* @calcom/Foundation
/packages/app-store/zohocalendar/**/* @calcom/Foundation
/packages/core/getAggregatedAvailability/index.ts @calcom/Foundation
/packages/core/getAggregateWorkingHours.ts @calcom/Foundation
/packages/core/getUserAvailability.ts @calcom/Foundation
/packages/features/bookings/lib/**/* @calcom/Foundation
/packages/lib/server/getLuckyUser.ts @calcom/Foundation
/packages/lib/slots.ts @calcom/Foundation
/packages/platform/**/* @calcom/Platform
/packages/prisma/**/* @calcom/Foundation
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
@@ -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"
+2 -2
View File
@@ -4,8 +4,8 @@ on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “every 5 minutes” (see https://crontab.guru)
- cron: "*/5 * * * *"
# Runs “every minute” (see https://crontab.guru)
- cron: "* * * * *"
jobs:
cron-webhookTriggers:
env:
+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:
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
with:
filters: |
has-files-requiring-all-checks:
- "!(**.md|.github/CODEOWNERS|docs/**|apps/web/public/static/locales/**/common.json)"
- "!(**.md|.github/CODEOWNERS|docs/**|help/**|apps/web/public/static/locales/**/common.json)"
- name: Get Latest Commit SHA
id: get_sha
run: |
+1
View File
@@ -4,6 +4,7 @@ on: # yamllint disable-line rule:truthy
release:
types:
- created
- published
# in case manual trigger is needed
workflow_dispatch:
inputs:
@@ -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.
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@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
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
View File
+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"
View File
View File
View File
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
View File
+8 -6
View File
@@ -14,7 +14,7 @@ test.describe("Org", () => {
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=Rick in i.cal.com")).toBeVisible();
await expect(page.locator("text=Used by Checkly")).toBeVisible();
await expectPageToBeRenderedWithEmbedSsr(page);
});
@@ -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.` };
+5 -2
View File
@@ -2,7 +2,8 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { UserCreationService } from "@calcom/lib/server/service/userCreationService";
import { CreationSource } from "@calcom/prisma/enums";
import { schemaUserCreateBodyParams } from "~/lib/validations/user";
@@ -92,7 +93,9 @@ 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 UserCreationService.createUser({
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", () => {
+134
View File
@@ -0,0 +1,134 @@
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, test, expect, vi } from "vitest";
import handler from "../../../pages/api/users/_post";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
vi.mock("@calcom/lib/server/i18n", () => {
return {
getTranslation: (key: string) => {
return () => key;
},
};
});
vi.stubEnv("CALCOM_LICENSE_KEY", undefined);
describe("POST /api/users", () => {
test("should throw 401 if not system-wide admin", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
email: "test@example.com",
username: "test",
},
});
req.isSystemWideAdmin = false;
await handler(req, res);
expect(res.statusCode).toBe(401);
});
test("should throw a 400 if no email is provided", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
username: "test",
},
});
req.isSystemWideAdmin = true;
await handler(req, res);
expect(res.statusCode).toBe(400);
});
test("should throw a 400 if no username is provided", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
email: "test@example.com",
},
});
req.isSystemWideAdmin = true;
await handler(req, res);
expect(res.statusCode).toBe(400);
});
test("should create user successfully", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
email: "test@example.com",
username: "test",
},
prisma: prismock,
});
req.isSystemWideAdmin = true;
await handler(req, res);
expect(res.statusCode).toBe(200);
const userQuery = await prismock.user.findFirst({
where: {
email: "test@example.com",
},
});
expect(userQuery).toEqual(
expect.objectContaining({
email: "test@example.com",
username: "test",
locked: false,
organizationId: null,
})
);
});
test("should auto lock user if email is in watchlist", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
email: "test@example.com",
username: "test",
},
prisma: prismock,
});
req.isSystemWideAdmin = true;
await prismock.watchlist.create({
data: {
type: "EMAIL",
value: "test@example.com",
severity: "CRITICAL",
createdById: 1,
},
});
await handler(req, res);
expect(res.statusCode).toBe(200);
const userQuery = await prismock.user.findFirst({
where: {
email: "test@example.com",
},
});
expect(userQuery).toEqual(
expect.objectContaining({
email: "test@example.com",
username: "test",
locked: true,
organizationId: null,
})
);
});
});
+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.85",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.99",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -60,6 +61,7 @@
"helmet": "^7.1.0",
"ioredis": "^5.3.2",
"jsforce": "^1.11.0",
"lodash": "^4.17.21",
"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 -2
View File
@@ -8,13 +8,13 @@ used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Cal.com Subscription Terms available
at https://cal.com/terms, or other agreements governing
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
and otherwise have a valid Cal.com Enterprise Edition subscription ("Commercial Subscription")
and otherwise have a valid Cal.com Commercial License ("Commercial License")
for the correct number of hosts as defined in the "Commercial Terms ("Hosts"). Subject to the foregoing sentence,
you are free to modify this Software and publish patches to the Software. You agree
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
and to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Commercial Subscription for the correct number of hosts.
exploited with a valid Commercial License for the correct number of hosts.
Notwithstanding the foregoing, you may copy and modify the Software for development
and testing purposes, without requiring a subscription. You agree that Cal.com and/or
its licensors (as applicable) retain all right, title and interest in and to all such
+5 -5
View File
@@ -4,15 +4,15 @@
<img src="https://user-images.githubusercontent.com/8019099/133430653-24422d2a-3c8d-4052-9ad6-0580597151ee.png" alt="Logo">
</a>
<a href="https://console.cal.com/">Get a License Key</a>
<a href="https://go.cal.com/get-license">Get a License Key</a>
</div>
# Enterprise Edition of API
# Commercial License of API
Welcome to the Enterprise Edition ("/ee") of the Cal.com API.
Welcome to the Commercial License of the Cal.com API.
Our philosophy is simple, all "Singleplayer APIs" are open-source under AGPLv3. All "Multiplayer APIs" are under a commercial license.
The [/ee](https://github.com/calcom/cal.com/tree/main/apps/api/v2/ee) subfolder is the place for all the **Enterprise Edition** features from our [hosted](https://cal.com/pricing) plan and enterprise-grade features for [Enterprise](https://cal.com/enterprise) such as SSO, SAML, OIDC, SCIM, SIEM and much more or [Platform](https://cal.com/platform) plan to build a marketplace.
The [/ee](https://github.com/calcom/cal.com/tree/main/apps/api/v2/ee) subfolder is the place for all the **Commercial License** features from our [hosted](https://cal.com/pricing) plan.
> _❗ WARNING: This repository is copyrighted (unlike our [main repo](https://github.com/calcom/cal.com)). You are not allowed to use this code to host your own version of app.cal.com without obtaining a proper [license](https://console.cal.com/) first❗_
> _❗ WARNING: This repository is copyrighted (unlike our [main repo](https://github.com/calcom/cal.com)). You are not allowed to use this code to host your own version of app.cal.com without obtaining a proper [license](https://go.cal.com/get-license) first❗_
@@ -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,6 +33,7 @@ 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";
@@ -383,7 +384,11 @@ 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;
}
@@ -411,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;
}
@@ -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,62 @@ 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;
}
>;
@IsString()
@IsOptional()
@ApiPropertyOptional()
teamMemberEmail?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
crmAppSlug?: string;
@IsString()
@IsOptional()
@ApiPropertyOptional()
crmOwnerRecordType?: string;
}
@@ -32,5 +32,6 @@ import { Module } from "@nestjs/common";
ApiKeyRepository,
],
controllers: [BookingsController_2024_08_13],
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
})
export class BookingsModule_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();
});
@@ -441,7 +455,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
// eslint-disable-next-line
// @ts-ignore
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
console.log("asap data", JSON.stringify(data, null, 2));
expect(data.attendees[0].name).toEqual(`${splitName.firstName} ${splitName.lastName}`);
expect(data.attendees[0].bookingFieldsResponses.name).toEqual(splitName);
} else {
@@ -457,7 +470,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 +489,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,7 +950,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(2);
const firstAttendee = firstBooking.attendees.find(
@@ -1003,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,8 +53,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 = `team-bookings-user1-${randomString()}@api.com`;
const teamUserEmail2 = `team-bookings-user2-${randomString()}@api.com`;
let teamUser: User;
let teamUser2: User;
@@ -61,6 +62,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
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(
teamUserEmail,
@@ -85,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: {
@@ -100,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: {
@@ -133,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,
};
@@ -196,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: [],
@@ -211,8 +218,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
team: {
connect: { id: team1.id },
},
title: "Phone Only Event Type",
slug: "phone-only-event-type",
title: `team-bookings-2024-08-13-event-type-${randomString()}`,
slug: phoneOnlyEventTypeSlug,
length: 15,
assignAllTeamMembers: false,
hosts: {
@@ -288,8 +295,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
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: [],
@@ -554,6 +561,43 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should get bookings by teamId and eventTypeId", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamId=${team2.id}&eventTypeId=${team2EventTypeId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(1);
expect(data[0].eventTypeId).toEqual(team2EventTypeId);
});
});
it("should not get bookings by teamId and non existing eventTypeId", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamId=${team2.id}&eventTypeId=90909`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(0);
});
});
it("should should get bookings by teamIds", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamIds=${team1.id},${team2.id}`)
@@ -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 },
},
@@ -158,6 +167,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
},
},
rating: 10,
});
app = moduleRef.createNestApplication();
@@ -354,6 +364,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());
@@ -386,6 +397,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
// Check updatedAt date is between the time of the request and after the request
expect(data.updatedAt).toBeDefined();
const updatedAtDate = data.updatedAt ? new Date(data.updatedAt) : null;
expect(updatedAtDate?.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(updatedAtDate?.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
expect(data.metadata).toEqual(body.metadata);
createdBooking = data;
} else {
@@ -518,6 +535,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);
@@ -535,6 +553,30 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get a booking with rating", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${bookingInThePast.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toEqual(bookingInThePast.id);
expect(data.uid).toEqual(bookingInThePast.uid);
expect(data.rating).toEqual(bookingInThePast.rating);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should should get 1 recurrence of a recurring booking", async () => {
const recurrenceUid = createdRecurringBooking[0].uid;
return request(app.getHttpServer())
@@ -552,6 +594,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);
@@ -856,7 +900,63 @@ describe("Bookings Endpoints 2024-08-13", () => {
it("should should get bookings by before specified end time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?afterStart=${createdRecurringBooking[0].start}`)
.get(`/v2/bookings?beforeEnd=${createdRecurringBooking[0].end}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(3);
});
});
it("should should get bookings after specified update time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?afterUpdatedAt=${createdRecurringBooking[1].updatedAt}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(2);
});
});
it("should should get bookings before specified update time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?beforeUpdatedAt=${createdRecurringBooking[0].updatedAt}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(3);
});
});
it("should should get bookings in update range", async () => {
return request(app.getHttpServer())
.get(
`/v2/bookings?afterUpdatedAt=${createdRecurringBooking[0].updatedAt}&beforeUpdatedAt=${createdRecurringBooking[2].updatedAt}`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
@@ -991,6 +1091,46 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data[1].start).toEqual(createdBooking.start);
});
});
it("should should sort bookings by updated in descending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortUpdatedAt=desc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(2);
expect(data[0].updatedAt).toEqual(createdBooking.updatedAt);
expect(data[1].updatedAt).toEqual(bookingInThePast.updatedAt?.toISOString());
});
});
it("should should sort bookings by updated in ascending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortUpdatedAt=asc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(2);
expect(data[0].updatedAt).toEqual(bookingInThePast.updatedAt?.toISOString());
expect(data[1].updatedAt).toEqual(createdBooking.updatedAt);
});
});
});
describe("reschedule bookings", () => {
@@ -1021,6 +1161,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);
@@ -1078,6 +1220,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());
@@ -1158,6 +1302,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);
@@ -1190,6 +1336,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);
@@ -1228,6 +1376,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);
@@ -1285,6 +1435,108 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("who cancelled and rescheduled a booking", () => {
it("should return who cancelled a booking", async () => {
const cancelledByEmail = `user-bookings-canceller-${randomString()}@canceller.com`;
const cancelledBooking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${eventTypeId}`,
title: "booking title",
startTime: "2050-09-05T11:00:00.000Z",
endTime: "2050-09-05T12:00:00.000Z",
eventType: {
connect: {
id: eventTypeId,
},
},
status: "CANCELLED",
cancelledBy: cancelledByEmail,
metadata: {},
responses: {
name: "tester",
email: "tester@example.com",
guests: [],
},
user: {
connect: {
id: user.id,
},
},
});
return request(app.getHttpServer())
.get(`/v2/bookings/${cancelledBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.uid).toEqual(cancelledBooking.uid);
expect(data.cancelledByEmail).toEqual(cancelledByEmail);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should return who rescheduled a booking", async () => {
const rescheduledByEmail = `user-bookings-rescheduler-${randomString()}@rescheduler.com`;
const rescheduledBooking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${eventTypeId}`,
title: "booking title",
startTime: "2050-09-05T11:00:00.000Z",
endTime: "2050-09-05T12:00:00.000Z",
eventType: {
connect: {
id: eventTypeId,
},
},
status: "CANCELLED",
rescheduledBy: rescheduledByEmail,
metadata: {},
responses: {
name: "tester",
email: "tester@example.com",
guests: [],
},
user: {
connect: {
id: user.id,
},
},
});
return request(app.getHttpServer())
.get(`/v2/bookings/${rescheduledBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
await bookingsRepositoryFixture.deleteById(rescheduledBooking.id);
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.uid).toEqual(rescheduledBooking.uid);
expect(data.rescheduledByEmail).toEqual(rescheduledByEmail);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
return (
!Array.isArray(data) &&
@@ -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
);
@@ -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;
@@ -220,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;
@@ -317,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;
@@ -453,11 +463,18 @@ export class InputBookingsService_2024_08_13 {
teamIds: queryParams.teamsIds || (queryParams.teamId ? [queryParams.teamId] : undefined),
eventTypeIds:
queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined),
afterUpdatedDate: queryParams.afterUpdatedAt,
beforeUpdatedDate: queryParams.beforeUpdatedAt,
};
}
transformGetBookingsSort(queryParams: GetBookingsInput_2024_08_13) {
if (!queryParams.sortStart && !queryParams.sortEnd && !queryParams.sortCreated) {
if (
!queryParams.sortStart &&
!queryParams.sortEnd &&
!queryParams.sortCreated &&
!queryParams.sortUpdatedAt
) {
return undefined;
}
@@ -465,6 +482,7 @@ export class InputBookingsService_2024_08_13 {
sortStart: queryParams.sortStart,
sortEnd: queryParams.sortEnd,
sortCreated: queryParams.sortCreated,
sortUpdated: queryParams.sortUpdatedAt,
};
}
@@ -108,7 +108,9 @@ export class OutputBookingsService_2024_08_13 {
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
cancelledByEmail: databaseBooking.cancelledBy || undefined,
reschedulingReason: bookingResponses?.rescheduledReason,
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
end: databaseBooking.endTime,
@@ -130,6 +132,8 @@ export class OutputBookingsService_2024_08_13 {
meetingUrl: location,
absentHost: !!databaseBooking.noShowHost,
createdAt: databaseBooking.createdAt,
updatedAt: databaseBooking.updatedAt,
rating: databaseBooking.rating,
};
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
@@ -198,7 +202,9 @@ export class OutputBookingsService_2024_08_13 {
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
cancelledByEmail: databaseBooking.cancelledBy || undefined,
reschedulingReason: bookingResponses?.rescheduledReason,
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
end: databaseBooking.endTime,
@@ -221,6 +227,8 @@ export class OutputBookingsService_2024_08_13 {
absentHost: !!databaseBooking.noShowHost,
bookingFieldsResponses: databaseBooking.responses,
createdAt: databaseBooking.createdAt,
updatedAt: databaseBooking.updatedAt,
rating: databaseBooking.rating,
};
const bookingTransformed = plainToClass(RecurringBookingOutput_2024_08_13, booking, {
@@ -267,6 +275,8 @@ export class OutputBookingsService_2024_08_13 {
meetingUrl: location,
absentHost: !!databaseBooking.noShowHost,
createdAt: databaseBooking.createdAt,
updatedAt: databaseBooking.updatedAt,
rating: databaseBooking.rating,
};
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
@@ -276,7 +286,8 @@ export class OutputBookingsService_2024_08_13 {
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData
defaultSeatedBookingData,
false
);
const attendeeData = {
@@ -293,7 +304,8 @@ export class OutputBookingsService_2024_08_13 {
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata
defaultSeatedBookingMetadata,
false
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
@@ -372,6 +384,8 @@ export class OutputBookingsService_2024_08_13 {
recurringBookingUid: databaseBooking.recurringEventId,
absentHost: !!databaseBooking.noShowHost,
createdAt: databaseBooking.createdAt,
updatedAt: databaseBooking.updatedAt,
rating: databaseBooking.rating,
};
const parsed = plainToClass(GetRecurringSeatedBookingOutput_2024_08_13, booking, {
@@ -383,7 +397,8 @@ export class OutputBookingsService_2024_08_13 {
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData
defaultSeatedBookingData,
false
);
const attendeeData = {
@@ -400,7 +415,8 @@ export class OutputBookingsService_2024_08_13 {
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata
defaultSeatedBookingMetadata,
false
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
@@ -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;
@@ -34,7 +34,7 @@ import {
Post,
Body,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { plainToClass } from "class-transformer";
import { Request } from "express";
@@ -123,10 +123,15 @@ export class CalendarsController {
};
}
@ApiParam({
enum: [OFFICE_365_CALENDAR, GOOGLE_CALENDAR],
type: String,
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@Get("/:calendar/connect")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Get connect URL" })
@ApiOperation({ summary: "Get oAuth connect URL" })
async redirect(
@Req() req: Request,
@Headers("Authorization") authorization: string,
@@ -146,10 +151,15 @@ export class CalendarsController {
}
}
@ApiParam({
enum: [OFFICE_365_CALENDAR, GOOGLE_CALENDAR],
type: String,
name: "calendar",
})
@Get("/:calendar/save")
@HttpCode(HttpStatus.OK)
@Redirect(undefined, 301)
@ApiOperation({ summary: "Save a calendar" })
@ApiOperation({ summary: "Save an oAuth calendar credentials" })
async save(
@Query("state") state: string,
@Query("code") code: string,
@@ -177,6 +187,11 @@ export class CalendarsController {
}
}
@ApiParam({
enum: [APPLE_CALENDAR],
type: String,
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@Post("/:calendar/credentials")
@ApiOperation({ summary: "Sync credentials" })
@@ -198,6 +213,11 @@ export class CalendarsController {
}
}
@ApiParam({
enum: [APPLE_CALENDAR, GOOGLE_CALENDAR, OFFICE_365_CALENDAR],
type: String,
name: "calendar",
})
@Get("/:calendar/check")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard, PermissionsGuard)
@@ -219,6 +239,11 @@ export class CalendarsController {
}
}
@ApiParam({
enum: [APPLE_CALENDAR, GOOGLE_CALENDAR, OFFICE_365_CALENDAR],
type: String,
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@Post("/:calendar/disconnect")
@HttpCode(HttpStatus.OK)
@@ -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";
@@ -86,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;
@@ -186,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({
@@ -197,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({
@@ -218,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
);
@@ -241,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",
});
@@ -1221,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,
@@ -1708,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,
@@ -7,11 +7,7 @@ import { InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
@Injectable()
export class EventTypesRepository_2024_06_14 {
constructor(
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService,
private usersService: UsersService
) {}
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async createUserEventType(
userId: number,
@@ -47,6 +43,13 @@ export class EventTypesRepository_2024_06_14 {
});
}
async getEventTypeWithHosts(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { hosts: true },
});
}
async getUserEventType(userId: number, eventTypeId: number) {
return this.dbRead.prisma.eventType.findFirst({
where: {
@@ -1,6 +1,6 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsString, ValidateNested } from "class-validator";
import { IsEnum, IsString, ValidateNested, IsOptional } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
@@ -10,8 +10,9 @@ class Data {
callId!: string;
@IsString()
@IsOptional()
@ApiProperty()
agentId!: string;
agentId?: string;
}
export class CreatePhoneCallOutput {
+3 -2
View File
@@ -26,7 +26,8 @@ import {
Headers,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiExcludeController } from "@nestjs/swagger";
import { ApiOperation } from "@nestjs/swagger";
import { Request } from "express";
import { APPS_READ, GOOGLE_CALENDAR_TYPE, SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -41,7 +42,7 @@ const CALENDAR_SCOPES = [
path: "/v2/gcal",
version: API_VERSIONS_VALUES,
})
@DocsTags("Platform / Google Calendar")
@ApiExcludeController(true)
export class GcalController {
private readonly logger = new Logger("Platform Gcal Provider");
@@ -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,
});
@@ -8,10 +8,11 @@ import { MeModule } from "@/ee/me/me.module";
import { ProviderModule } from "@/ee/provider/provider.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { SlotsModule } from "@/modules/slots/slots.module";
import { SlotsModule_2024_04_15 } from "@/modules/slots/slots-2024-04-15/slots.module";
import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { TeamsModule } from "@/modules/teams/teams/teams.module";
import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module";
import { TeamsModule } from "@/modules/teams/teams/teams.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { Module } from "@nestjs/common";
@@ -29,7 +30,8 @@ import { Module } from "@nestjs/common";
BookingsModule_2024_04_15,
BookingsModule_2024_08_13,
TeamsMembershipsModule,
SlotsModule,
SlotsModule_2024_04_15,
SlotsModule_2024_09_04,
TeamsModule,
],
})
@@ -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,
};
+3
View File
@@ -6,6 +6,7 @@ import {
VERSION_2024_06_11,
VERSION_2024_06_14,
VERSION_2024_08_13,
VERSION_2024_09_04,
} from "@calcom/platform-constants";
export const API_VERSIONS_VALUES: VersionValue = API_VERSIONS as unknown as VersionValue;
@@ -13,8 +14,10 @@ export const VERSION_2024_06_14_VALUE: VersionValue = VERSION_2024_06_14 as unkn
export const VERSION_2024_06_11_VALUE: VersionValue = VERSION_2024_06_11 as unknown as VersionValue;
export const VERSION_2024_04_15_VALUE: VersionValue = VERSION_2024_04_15 as unknown as VersionValue;
export const VERSION_2024_08_13_VALUE: VersionValue = VERSION_2024_08_13 as unknown as VersionValue;
export const VERSION_2024_09_04_VALUE: VersionValue = VERSION_2024_09_04 as unknown as VersionValue;
export { VERSION_2024_04_15 };
export { VERSION_2024_06_11 };
export { VERSION_2024_06_14 };
export { VERSION_2024_08_13 };
export { VERSION_2024_09_04 };
+15 -8
View File
@@ -3,20 +3,27 @@ import { ZodSchema } from "zod";
const logger = new Logger("safeParse");
export function safeParse<T>(schema: ZodSchema<T>, value: unknown, defaultValue: T): T {
export function safeParse<T>(
schema: ZodSchema<T>,
value: unknown,
defaultValue: T,
logError = true
): T {
const result = schema.safeParse(value);
if (result.success) {
return result.data;
} else {
const errorStack = new Error().stack;
logger.error(
`Zod parsing failed.\n` +
`1. Schema: ${schema.description || "UnnamedSchema"}\n` +
`2. Input: ${JSON.stringify(value, null, 2)}\n` +
`3. Zod Error: ${result.error}\n` +
`4. Call Stack: ${errorStack}`
);
if (logError) {
logger.error(
`Zod parsing failed.\n` +
`1. Schema: ${schema.description || "UnnamedSchema"}\n` +
`2. Input: ${JSON.stringify(value, null, 2)}\n` +
`3. Zod Error: ${result.error}\n` +
`4. Call Stack: ${errorStack}`
);
}
return defaultValue;
}
@@ -0,0 +1,30 @@
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { ExecutionContext } from "@nestjs/common";
import { createParamDecorator } from "@nestjs/common";
export const GetOptionalUser = createParamDecorator<
keyof ApiAuthGuardUser | (keyof ApiAuthGuardUser)[],
ExecutionContext
>((data, ctx) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user as ApiAuthGuardUser;
if (!user) {
return null;
}
if (Array.isArray(data)) {
return data.reduce((prev, curr) => {
return {
...prev,
[curr]: user[curr],
};
}, {});
}
if (data) {
return user[data];
}
return user;
});
@@ -0,0 +1,14 @@
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { NO_AUTH_PROVIDED_MESSAGE } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
export class OptionalApiAuthGuard extends ApiAuthGuard {
handleRequest(error: Error, user: any) {
// note(Lauris): optional means that auth is not required but if it is invalid then still throw error.
const noAuthProvided = error && error.message === NO_AUTH_PROVIDED_MESSAGE;
if (user || noAuthProvided || !error) {
return user || null;
} else {
throw error;
}
}
}
@@ -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,
});
@@ -19,6 +19,8 @@ import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
export const NO_AUTH_PROVIDED_MESSAGE =
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers";
@Injectable()
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
constructor(
@@ -62,9 +64,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
return await this.authenticateNextAuth(nextAuthToken);
}
throw new UnauthorizedException(
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers"
);
throw new UnauthorizedException(NO_AUTH_PROVIDED_MESSAGE);
} catch (err) {
if (err instanceof Error) {
return this.error(err);
@@ -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,
@@ -1,6 +1,6 @@
import { AppsRepository } from "@/modules/apps/apps.repository";
import { ConferencingController } from "@/modules/conferencing/controllers/conferencing.controller";
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
import { Office365VideoService } from "@/modules/conferencing/services/office365-video.service";
@@ -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 () => {
@@ -1,4 +1,4 @@
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
import { UserWithProfile } from "@/modules/users/users.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, InternalServerErrorException, Logger } from "@nestjs/common";
@@ -1,4 +1,4 @@
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, InternalServerErrorException, Logger } from "@nestjs/common";
@@ -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,8 @@ 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 { OrganizationsTeamsBookingsModule } from "@/modules/organizations/controllers/teams/bookings/organizations-teams-bookings.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 +26,8 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
AtomsModule,
StripeModule,
ConferencingModule,
OrganizationsTeamsBookingsModule,
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
@@ -1,10 +1,11 @@
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { MembershipsService } from "@/modules/memberships/services/memberships.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [MembershipsRepository],
exports: [MembershipsRepository],
providers: [MembershipsRepository, MembershipsService],
exports: [MembershipsRepository, MembershipsService],
})
export class MembershipsModule {}
@@ -0,0 +1,24 @@
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { Injectable } from "@nestjs/common";
import { intersectionBy } from "lodash";
@Injectable()
export class MembershipsService {
constructor(private readonly membershipsRepository: MembershipsRepository) {}
async haveMembershipsInCommon(firstUserId: number, secondUserId: number) {
const memberships = await this.membershipsInCommon(firstUserId, secondUserId);
return memberships.length > 0;
}
async membershipsInCommon(firstUserId: number, secondUserId: number) {
const firstUserMemberships = await this.membershipsRepository.findUserMemberships(firstUserId);
const secondUserMemberships = await this.membershipsRepository.findUserMemberships(secondUserId);
return intersectionBy(
firstUserMemberships.filter((m) => m.accepted),
secondUserMemberships.filter((m) => m.accepted),
"teamId"
);
}
}
@@ -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,
@@ -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: [
@@ -751,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,
};
@@ -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,
@@ -0,0 +1,179 @@
import { ApiProperty } from "@nestjs/swagger";
import { Transform, Type } from "class-transformer";
import {
ArrayMinSize,
ArrayNotEmpty,
IsArray,
IsEnum,
IsInt,
IsISO8601,
IsNumber,
IsOptional,
IsString,
Max,
Min,
} from "class-validator";
enum Status {
upcoming = "upcoming",
recurring = "recurring",
past = "past",
cancelled = "cancelled",
unconfirmed = "unconfirmed",
}
type StatusType = keyof typeof Status;
enum SortOrder {
asc = "asc",
desc = "desc",
}
type SortOrderType = keyof typeof SortOrder;
export class GetOrganizationsTeamsBookingsInput_2024_08_13 {
// note(Lauris): filters
@IsOptional()
@Transform(({ value }) => {
if (typeof value === "string") {
return value.split(",").map((status: string) => status.trim());
}
return value;
})
@ArrayNotEmpty({ message: "status cannot be empty." })
@IsEnum(Status, {
each: true,
message: "Invalid status. Allowed are upcoming, recurring, past, cancelled, unconfirmed",
})
@ApiProperty({
required: false,
description:
"Filter bookings by status. If you want to filter by multiple statuses, separate them with a comma.",
example: "?status=upcoming,past",
enum: Status,
isArray: true,
})
status?: StatusType[];
@IsString()
@IsOptional()
@ApiProperty({
type: String,
required: false,
description: "Filter bookings by the attendee's email address.",
example: "example@domain.com",
})
attendeeEmail?: string;
@IsString()
@IsOptional()
@ApiProperty({
type: String,
required: false,
description: "Filter bookings by the attendee's name.",
example: "John Doe",
})
attendeeName?: string;
@IsOptional()
@Transform(({ value }) => {
if (typeof value === "string") {
return value.split(",").map((eventTypeId: string) => parseInt(eventTypeId));
}
return value;
})
@IsArray()
@IsNumber({}, { each: true })
@ArrayMinSize(1, { message: "eventTypeIds must contain at least 1 event type id" })
@ApiProperty({
type: String,
required: false,
description:
"Filter bookings by event type ids belonging to the team. Event type ids must be separated by a comma.",
example: "?eventTypeIds=100,200",
})
eventTypeIds?: number[];
@IsInt()
@IsOptional()
@Type(() => Number)
@ApiProperty({
type: String,
required: false,
description: "Filter bookings by event type id belonging to the team.",
example: "?eventTypeId=100",
})
eventTypeId?: number;
@IsOptional()
@IsISO8601({ strict: true }, { message: "fromDate must be a valid ISO 8601 date." })
@ApiProperty({
type: String,
required: false,
description: "Filter bookings with start after this date string.",
example: "?afterStart=2025-03-07T10:00:00.000Z",
})
afterStart?: string;
@IsOptional()
@IsISO8601({ strict: true }, { message: "toDate must be a valid ISO 8601 date." })
@ApiProperty({
type: String,
required: false,
description: "Filter bookings with end before this date string.",
example: "?beforeEnd=2025-03-07T11:00:00.000Z",
})
beforeEnd?: string;
// note(Lauris): sort
@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;
@IsOptional()
@IsEnum(SortOrder, {
message: 'SortCreated must be either "asc" or "desc".',
})
@ApiProperty({
required: false,
description:
"Sort results by their creation time (when booking was made) in ascending or descending order.",
example: "?sortCreated=asc OR ?sortCreated=desc",
enum: SortOrder,
})
sortCreated?: SortOrderType;
// note(Lauris): pagination
@ApiProperty({ required: false, description: "The number of items to return", example: 10 })
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@Min(1)
@Max(250)
@IsOptional()
take?: number;
@ApiProperty({ required: false, description: "The number of items to skip", example: 0 })
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@Min(0)
@IsOptional()
skip?: number;
}
@@ -0,0 +1,364 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/controllers/teams/bookings/organizations-teams-bookings.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import * as request from "supertest";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { 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 { withApiAuth } from "test/utils/withApiAuth";
import {
CAL_API_VERSION_HEADER,
SUCCESS_STATUS,
VERSION_2024_08_13,
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
} from "@calcom/platform-constants";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RecurringBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Organizations TeamsBookings Endpoints 2024-08-13", () => {
describe("Organization Team bookings", () => {
let app: INestApplication;
let organization: Team;
let team1: Team;
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let hostsRepositoryFixture: HostsRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
const teamUserEmail = "orgUser1team1@api.com";
const teamUserEmail2 = "orgUser2team1@api.com";
let teamUser: User;
let teamUser2: User;
let team1EventTypeId: number;
beforeAll(async () => {
const moduleRef = await withApiAuth(
teamUserEmail,
Test.createTestingModule({
imports: [AppModule, OrganizationsTeamsBookingsModule],
})
)
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" });
oAuthClient = await createOAuthClient(organization.id);
team1 = await teamRepositoryFixture.create({
name: "team 1",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClient.id,
},
},
});
teamUser = await userRepositoryFixture.create({
email: teamUserEmail,
locale: "it",
name: "orgUser1team1",
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
teamUser2 = await userRepositoryFixture.create({
email: teamUserEmail2,
locale: "es",
name: "orgUser2team1",
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: "working time",
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(teamUser.id, userSchedule);
await schedulesService.createUserSchedule(teamUser2.id, userSchedule);
await profileRepositoryFixture.create({
uid: `usr-${teamUser.id}`,
username: teamUserEmail,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: teamUser.id,
},
},
});
await profileRepositoryFixture.create({
uid: `usr-${teamUser2.id}`,
username: teamUserEmail2,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: teamUser2.id,
},
},
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: teamUser.id } },
team: { connect: { id: team1.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: teamUser.id } },
team: { connect: { id: organization.id } },
accepted: true,
});
const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team1.id },
},
title: "Collective Event Type",
slug: "collective-event-type",
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
});
team1EventTypeId = team1EventType.id;
await hostsRepositoryFixture.create({
isFixed: true,
user: {
connect: {
id: teamUser.id,
},
},
eventType: {
connect: {
id: team1EventType.id,
},
},
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
describe("create team bookings", () => {
it("should create a team 1 booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: team1EventTypeId,
attendee: {
name: "alice",
email: "alice@gmail.com",
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, 14, 0, 0)).toISOString());
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(team1EventTypeId);
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
});
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"
);
}
});
});
});
describe("get team bookings", () => {
it("should should get bookings by teamId", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${organization.id}/teams/${team1.id}/bookings`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(1);
expect(data[0].eventTypeId).toEqual(team1EventTypeId);
});
});
it("should get bookings by teamId and eventTypeId", async () => {
return request(app.getHttpServer())
.get(
`/v2/organizations/${organization.id}/teams/${team1.id}/bookings?eventTypeId=${team1EventTypeId}`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(1);
expect(data[0].eventTypeId).toEqual(team1EventTypeId);
});
});
it("should not get bookings by teamId and non existing eventTypeId", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${organization.id}/teams/${team1.id}/bookings?eventTypeId=90909`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(0);
});
});
it("should not get bookings by non existing teamId", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${organization.id}/teams/90909/bookings`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(404);
});
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(teamUser.email);
await userRepositoryFixture.deleteByEmail(teamUserEmail2);
await bookingsRepositoryFixture.deleteAllBookings(teamUser.id, teamUser.email);
await app.close();
});
});
});

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