Compare commits

...
282 Commits
Author SHA1 Message Date
GitHub Actions 53a06797a9 chore: release v5.3.4
Post release / release (push) Canceled after 0s
2025-05-22 23:03:42 +00:00
Carina WollendorferandCarinaWolli 99eb743772 add verifiedAt timestamp (#21477)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-05-22 22:42:25 +00:00
73adfb0cec fix: header in routing forms (#21445)
* fix header

* remove unused variables

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-05-22 19:47:10 +00:00
Rajiv SahalandMorgan 7127c60116 fix: stripe integration in EventTypeSettings atom (#20919)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-05-22 12:18:28 -07:00
Joe Au-Yeung a6d37fd167 feat: Round robin, when attendee books same timeslot return original booking (#21455) 2025-05-22 12:06:28 -07:00
Benny Joo 16ea15076d fix: increase timeout to 800 seconds for workflow reminders (#21476) 2025-05-22 18:31:02 +00:00
Anik Dhabal Babu 041871f1b6 fix: only fixed hosts added (#21407) 2025-05-22 18:14:46 +00:00
Carina WollendorferandCarinaWolli 8c8ae127fe feat: allow custom SMS templates for free users (#21469)
* charge free users for SMS to CA an US

* allow custom template for free users

* fix backend + some UI fixes

* make text editable

* rename back to needsTeamsUpgrade

* add twilio webhook handler tests

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-05-22 19:09:49 +01:00
357c82129c feat: Dub App integration (#21321)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-05-22 09:18:45 -07:00
Eunjae Lee 324ac691eb fix: use local date range on /insights (based on profile's timezone) (#21472)
* fix: use local date range on /insights (based on profile's timezone)

* update test case
2025-05-22 16:03:56 +00:00
devin-ai-integration[bot]andeunjae@cal.com da99001a7a - Prefer the implementation in packages/trpc/react/hooks as it's used by most of the codebase (#21468)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <hey@eunjae.dev>
2025-05-22 11:24:58 -04:00
Anik Dhabal Babu bd10d07083 fix: delete dilog button not visible (#21470) 2025-05-22 14:42:35 +00:00
Hariom BalharaandUdit Takkar 0e9b419a07 fix: Delegation Credentials - Use Cal Video if set as the default conferencing app (#21431)
* fix google-meet being used instead of Default Cal Video preference if
set

* Review addressed

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-05-22 08:21:25 -04:00
Hariom Balhara d5bd08d279 improve-delegation-disable-email-communication (#21456) 2025-05-22 09:06:31 +00:00
devin-ai-integration[bot]andeunjae@cal.com 4cc83530f9 refactor(e2e): use filter helper methods instead of raw selectors (#21460)
* refactor(e2e): use filter helper methods instead of raw selectors

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor(e2e): rename selectFilter to addFilter and update all usages

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <hey@eunjae.dev>
2025-05-22 08:46:51 +00:00
Hariom BalharaandOmar López 4fcf4081d0 feat: Send emails to potentially affected Organization Members on disabling delegation credential (#21276)
* mvp

* Improve UI

* wip

* self-review

* self-review

---------

Co-authored-by: Omar López <zomars@me.com>
2025-05-22 03:28:21 +00:00
GitHub Actions d84d80eea6 chore: release v5.3.3
Post release / release (push) Canceled after 0s
2025-05-22 01:05:29 +00:00
f3e1d67de1 feat: internationalize user table column headers (#21444)
* feat: internationalize user table column headers

Co-Authored-By: benny@cal.com <benny@cal.com>

* revert

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: benny@cal.com <benny@cal.com>
Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-05-21 20:10:28 +00:00
Benny Joo 2ce25f54d4 refactor: simplify arg of useBookerLayout hook (#21425) 2025-05-21 10:29:02 -07:00
Benny Joo 25eac5d29b chore: remove last booking response dead code (#21426) 2025-05-21 10:27:52 -07:00
Benny Joo 1710e30719 perf: use server fetched data in instant-meeting and private booking pages (#21424)
* remove useEvent hook

* refactor

* use repository functions

* remove trpc event

* wip

* fix

* remove

* fix

* revert some files

* revert Embed

* revert useEvent hook

* fix

* fix

* revert

* refactor
2025-05-21 10:27:23 -07:00
devin-ai-integration[bot]andeunjae@cal.com 6d970d0b57 fix: improve date selection behavior in DateRangePicker (#21438)
* fix: implement Airbnb-style date selection in DateRangePicker

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: add behavior for clicking on start/end date in DateRangePicker

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <hey@eunjae.dev>
2025-05-21 16:13:55 +00:00
41a6035a12 fix: replace bookingTimeStatus with bookingTimeStatusDenormalized (#21028)
* test: Add unit tests for buildBaseWhereCondition function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Export buildBaseWhereCondition function instead of copying it

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Remove implementation detail checks from tests

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: Fix tests to match actual function behavior

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: Fix test issues with buildBaseWhereCondition

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: Update tests to match actual function behavior

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: Address security issues in buildBaseWhereCondition function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Simplify buildBaseWhereCondition function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Simplify code and remove isEmptyResponse property

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Use array of conditions approach for buildBaseWhereCondition

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Make buildBaseWhereCondition implementation consistent and update tests

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: Fix incorrect usage of buildBaseWhereCondition function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* chore: Revert yarn.lock changes

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: Comment out non-existent Sentry API method to fix type error

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* Revert "fix: Comment out non-existent Sentry API method to fix type error"

This reverts commit 44c21a7396a518c63047b33967cd8af7e6e4f268.

* remove unnecessary if-statement

* fix: replace bookingTimeStatus with bookingTimeStatusDenormalized

* change column

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <eunjae@cal.com>
2025-05-21 16:10:43 +00:00
Akarsh Jain f84c8239e7 feat(bookings): implement Booking UID filter for bookings data table. (#21380)
Fixes: #21357
2025-05-21 11:51:50 -04:00
sean-brydon 1dca160db0 perf: use app dir to fetch team feature flags in settings (#21433)
* use app dir to fetch team feature flags

* guard against type error if teamId does not exist in teamfeatures

* remove redundant router
2025-05-21 12:41:45 +00:00
Carina WollendorferandCarinaWolli 4ff366cd99 fix: canceling workflow reminders (#21416)
* fix deleting reminders

* schedule reminders if iffy is disabled

* fix unit test

* clean up code

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-05-21 12:35:31 +00:00
Carina WollendorferandCarinaWolli 21a4711f07 fix: don't schedule past workflow reminders with tasker (#21434)
* don't schedule past reminders

* add test

* fix dates in tests

* add early return

* fix sms test

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-05-21 12:24:34 +00:00
Anik Dhabal Babu f1dd217de7 fix: don’t let the booking page crash on invalid metadata (#21410)
* fix: booking tab crash

* Update zod-utils.ts

* Update zod-utils.ts

* update
2025-05-21 09:44:17 +01:00
Anik Dhabal BabuandCarinaWolli 3e8f0b125d fix: WhatsApp workflows are not working (#21421)
* fix: whatsapp template

* update

* fixes for sending whatsapp messages

* add message service id when scheduling

* add timezone to startTime variable

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-05-21 08:12:12 +00:00
devin-ai-integration[bot]andeunjae@cal.com 25246ef8ff test: add e2e tests for filters on /insights/routing (#21332)
* test: add e2e tests for insights routing filters

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: improve stability of insights routing filters e2e tests

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: address PR feedback for insights routing filters e2e tests

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: add explicit page navigation to resolve timeout issues in filters.e2e.ts

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: navigate to routing tab after loading insights page

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix e2e tests

* fix tests

* rename

* fix tests

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <hey@eunjae.dev>
2025-05-20 18:23:57 -04:00
Benny Joo bb98143e2e perf: Server Fetching for platform members and org members pages (#21342)
* remove members page

* refactor to server fetch

* same for /members

* use server fetches for all queires

* fix

* add skeletons

* refactor

* fix loading page

* fix

* fix type checks

* fix details

* refactor platform members page
2025-05-20 16:36:34 -04:00
devin-ai-integration[bot]andeunjae@cal.com 6258ca14d0 feat: add segments prop to DataTableProvider (#21409)
* feat: add segments prop to DataTableProvider

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: modify useSegments hook instead of creating a new one

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: implement PR feedback and fix failing test

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* clean up

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <hey@eunjae.dev>
2025-05-20 22:30:32 +02:00
Eunjae Lee d0e17e8611 fix: add migration to backfill BookingDenormalized (#21420) 2025-05-20 10:01:09 -07:00
Eunjae Lee 79dc76239b fix: apply transaction to public methods of CreditService (#21356) 2025-05-20 18:20:45 +02:00
Eunjae Lee 95d62354d9 test: update flaky tests (#21419)
* test: update flaky integration tests

* change limit
2025-05-20 14:57:43 +00:00
b9fadf82a7 feat: pbac (#21296)
* inital pbac setup

* fix tests

* docs intro

* more docs

* add comments in docs

* revert pr_todo

* update to use action + custom as defaults

* use enums

* move to change user role + multi check

* add beforeEach import

* docs: add PERMISSIONS.md documenting team/organization role usages

Co-Authored-By: sean@cal.com <sean@cal.com>

* docs: add permission string alternatives for helper functions

Co-Authored-By: sean@cal.com <sean@cal.com>

* feat: enhance permission registry and default role permissions

Co-Authored-By: sean@cal.com <sean@cal.com>

* add routers

* add hooks + rsc. Also adjsuted test to use prismock

* make hooks type safe + add some tests

* add a permission context - trpc router to populate context

* update docs

* explore global and default roles member,admin,owner

* fix tests

* use kysley and abstract uril

* tidy up

* update trpc to use pbac route in endpoint + distinct on teamId

* make transform util

* remove role service dependancy from permission check class

* checkTeamId fall back to orgId if team is parent

* todos

* remove manual migrations to launch softly

* address feedback

* move permission checks to a repo

* move role service database calls to repo. improve mocks

* update tests to have describe import

* fix types

* remove router that is not used to pass build

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-05-20 15:22:52 +01:00
devin-ai-integration[bot]andalex@cal.com 1740e34c79 perf: remove unused submitFeedback tRPC route to reduce type generation load (#21418)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: alex@cal.com <me@alexvanandel.com>
2025-05-20 15:14:51 +01:00
Eunjae Lee 9e09066e99 chore: use correct import path for AppRouter (#21415) 2025-05-20 13:50:20 +00:00
Benny Joo 0a9cfd3a08 perf: cache TRPC queries in /availability RSC (#21398)
* cache trpc queries in /availability

* fix

* fix

* add comments

* use undefined
2025-05-20 08:04:52 +01:00
Benny Joo 0be711ad6e perf: cache TRPC queries in /event-types/[eventTypeId] RSC (#21401)
* cache /event-types details page

* fix
2025-05-20 08:04:01 +01:00
Alex van Andel 1c507b8169 chore: Increase workflows/trpc timeout to 800s (#21397) 2025-05-20 06:44:12 +00:00
81bdb3dc91 fix: Handle invalid non-delegation credential in cron (#21270)
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2025-05-20 03:17:13 +00:00
devin-ai-integration[bot]andkeith@cal.com 5efc7f3856 perf: optimize O(n²) algorithms in slot generation to O(n) or O(n log n) (#21373)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keithwillcode@gmail.com>
2025-05-19 18:40:18 -04:00
Benny Joo 0afecec607 perf: follow up of server query caching in /event-types (#21402) 2025-05-19 19:39:51 +00:00
Benny Joo 920574de65 perf: cache TRPC queries in /event-types RSC (#21381)
* improve trpc funcs

* cache /event-types

* remove unused code

* add comments

* add keys

* improve typing
2025-05-19 18:23:06 +00:00
GitHub Actions d15490d5aa chore: release v5.3.2
Post release / release (push) Canceled after 0s
2025-05-19 16:45:11 +00:00
Lauris Skraucis ba6e547f6c chore: bump platform libraries for v2 (#21391) 2025-05-19 16:30:13 +00:00
Anik Dhabal Babu 2493315f92 fix: wrong event details (#21390) 2025-05-19 16:20:07 +00:00
Keith Williams ec117d4034 revert: "perf: add p-limit to _getUsersAvailability to limit concurrent executions to 10 (#21374)" (#21396)
This reverts commit 6cb3bdc567.
2025-05-19 11:47:27 -04:00
Eunjae Lee 646058b8a1 chore: add missing migration for RoutingFormResponseDenormalized (#21388)
* chore: add missing migration for RoutingFormResponseDenormalized

* more changes

* remove cascade deleting

* update migration

* updates

* update

* update schema

* update

* update

* update

* update
2025-05-19 11:28:57 -04:00
Keith Williams 06c359ac9b revert: "chore: release v5.3.2"
This reverts commit 0d96d9c44c.
2025-05-19 08:44:37 -04:00
GitHub Actions 0d96d9c44c chore: release v5.3.2 2025-05-19 12:43:24 +00:00
Udit Takkar e5f8d4a1ac feat: support no show webhook for instant meetings (#21251)
* feat: support no show webhook for instant meetings

* fix: type error
2025-05-19 13:05:06 +04:00
Alex van Andel 19c72323b8 chore: Delete .github/workflows/advisories-to-slack.yml 2025-05-18 23:17:33 +01:00
Alex van Andel f19b2804b6 Update advisories-to-slack.yml (#21384) 2025-05-18 20:03:49 +01:00
Alex van Andel f387fdb166 chore: Add workflow to post security advisories to Slack (#21383) 2025-05-18 17:45:17 +00:00
ec8f9d5f7a feat: option to enforce language for bookingPage (#18782)
* feature eventType specific language or bookingPage language

* chore

* fix type checks

* chore

* chore: remove log

* improvement: this updates in DatePicker is not required with context provider method

* added e2e tests for InterfaceLanguage feature

* corrected prev merge conflict issues

* update to align with hooks usage pattern

* chore

* unrelated auto-formatting changes

* unrelated auto-formatting changes

* unrelated auto-formatting changes

* update to affect interfaceLanguage on success page

* undone prettier changes

* update test for success page translation

* chore

* return null from memo

* fix typecheck error due to atom imports in useLocale

* set default interfaceLanguage as null

* updated to use null instead of constant

* enhancements

* fix latest unit test, use prop instead of useSession hook

* fix build issue

* chore

* fix atom build issue

* fix unit test with reqd mock

* update to use server i18n rendering

* nit

* Move import localeOptions to EventSetupTabWebWrapper

* make sure we only display interface language option in web app

* using customI18nProvider

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Tushar <tusharbhatt0135@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-05-18 01:12:26 -04:00
devin-ai-integration[bot]andkeith@cal.com 6cb3bdc567 perf: add p-limit to _getUsersAvailability to limit concurrent executions to 10 (#21374)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keithwillcode@gmail.com>
2025-05-17 17:05:45 -04:00
NISHANT SINGH d574840c3f fix: make 'Send reset email' button text visible on hover (#21334) (#21335)
* fix: ensure button text remains visible on hover in forgot password page

Before: Button text turned black on hover, blending with the dark background.
After: Added 'enabled:hover:text-white' to maintain readability.

* Re-update forgot-password-view.tsx

* Update globals.css brand-text

* Update globals.css --cal-brand-text: white

* Reverted globals.css update

* Update forgot-password-view.tsx , change brand-text color

change "--cal-brand-text": 
white to Black
2025-05-17 09:48:21 +00:00
devin-ai-integration[bot]andkeith@cal.com 6a85d0e10b perf: optimize getSlots performance by selectively merging overlapping date ranges (#21370)
* perf: optimize getSlots performance by selectively merging overlapping date ranges

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: handle date ranges with end time before start time in filterRedundantDateRanges

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* perf: apply filterRedundantDateRanges to all scheduling types

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: preserve uniqueness check before filtering redundant date ranges

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keithwillcode@gmail.com>
2025-05-17 03:44:56 +00:00
devin-ai-integration[bot]andkeith@cal.com 816df3704e perf: Optimize getSlots function to handle large dateRanges arrays efficiently (#21371)
* perf: Optimize getSlots function to handle large dateRanges arrays efficiently

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Update packages/lib/slots.test.ts

* test: Update performance test to use 2000 date ranges

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* test: Update performance test to process 2000 date ranges across multiple days

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keithwillcode@gmail.com>
2025-05-17 04:00:13 +01:00
Alex van Andel badd9a5698 perf: Better perf by going event type first w/o booking (#21369)
* perf: Better perf by going event type first w/o booking

* minor fixup

* Use an IN with children instead of an OR with parent

* Turns out we already had a variable with the right values

* Make sure the right event type is used even for children

* few fixes

* Full select is needed to keep typings working

* Fix last remaining issue
2025-05-16 17:55:57 -07:00
GitHub Actions 4e02fe4251 chore: release v5.3.1
Post release / release (push) Canceled after 0s
2025-05-17 00:03:32 +00:00
Benny Joo 01b9780938 perf: ignore Object Not Found Matching Id error (#21365) 2025-05-16 23:11:33 +00:00
Anik Dhabal Babu 6be144f95f fix: unable to send verifiation code (#21367) 2025-05-16 23:09:19 +00:00
Anik Dhabal Babu 37dd6d888b fix: add reschedule by (#21363) 2025-05-16 17:14:06 +00:00
Lauris Skraucis f5ffee0a20 feat: automated changelogs and releases by changesets (#21299)
* feat: automated changesets

* chore: update changesets to latest

* readme
2025-05-16 12:52:45 -04:00
Carina WollendorferandCarinaWolli 6cd51e0a8d remove disabled state in addActionDialog (#21362)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-05-16 16:36:29 +00:00
Lauris Skraucis 80fad2aab9 fix: create event type with output of another create event type (#21273)
* feat: accept location booking field with label customization

* feat: return location label

* chore: bump libraries

* test

* fix: tests
2025-05-16 16:34:44 +03:00
Lauris Skraucis 1ccfc65766 fix: POST /v2/bookings validate submitted booking fields (#21163) 2025-05-16 13:00:58 +00:00
Lauris Skraucis 86918b03cf fix: booker atom seated events reservation (#21339)
* fix: reserving first seated booking slot

* fix: input type in useReserveSlot

* fix: handleReserveSlot submit bookingUid to api

* null -> undefined

* dont touch web app
2025-05-16 14:32:06 +02:00
a5b684af1f test: add unit tests for getWhereForTeamOrAllTeams function (#21152)
* test: add unit tests for getWhereForTeamOrAllTeams function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: remove implementation details and add tests for invalid parameter combinations

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: update mock values to use more distinct numbers

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: update mock values to use completely distinct numbers

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: update mock values to maintain data integrity

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: convert to integration tests for getWhereForTeamOrAllTeams function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* chore: remove integration test file

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: reorganize tests with proper describe blocks

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: move test file to __tests__ directory

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: update import path for routing-events in test file

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <eunjae@cal.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-05-16 08:19:17 +00:00
8889e8566c fix: eventType filter on /insights and add unit tests for buildBaseWhereCondition (#21154)
* test: Add unit tests for buildBaseWhereCondition function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Export buildBaseWhereCondition function instead of copying it

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Remove implementation detail checks from tests

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: Fix tests to match actual function behavior

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: Fix test issues with buildBaseWhereCondition

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: Update tests to match actual function behavior

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: Address security issues in buildBaseWhereCondition function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Simplify buildBaseWhereCondition function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Simplify code and remove isEmptyResponse property

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Use array of conditions approach for buildBaseWhereCondition

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Make buildBaseWhereCondition implementation consistent and update tests

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: Fix incorrect usage of buildBaseWhereCondition function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* chore: Revert yarn.lock changes

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: Comment out non-existent Sentry API method to fix type error

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* Revert "fix: Comment out non-existent Sentry API method to fix type error"

This reverts commit 44c21a7396a518c63047b33967cd8af7e6e4f268.

* remove unnecessary if-statement

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <eunjae@cal.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-05-16 10:10:57 +02:00
fab364f084 fix: Allow zero routingFormResponseId in API v2 available endpoint (#21301)
* Allow zero routingFormResponseId

* fix: conditionally validate routingFormResponseId to allow 0 only for dry run operations

Co-Authored-By: hariom@cal.com <hariom@cal.com>

* fix: pass _isDryRun parameter to API v2 available endpoint

Co-Authored-By: hariom@cal.com <hariom@cal.com>

* fix runtime error and improve message

* Add e2e test to verify the constraint works

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: hariom@cal.com <hariom@cal.com>
2025-05-16 12:23:11 +05:30
Devanshu Sharma 5bc9d71a88 fix: (hu) correct typo 'ektávolítás' to 'eltávolítás' in Hungarian translation (#21348) 2025-05-16 03:27:58 +00:00
GitHub Actions 2b5ea649d4 chore: release v5.3.0
Post release / release (push) Canceled after 0s
2025-05-15 20:43:11 +00:00
Benny Joo 0802ebb17b refactor: check insights feature flag in higher stack (#21341) 2025-05-15 12:41:22 -07:00
Anik Dhabal Babu 29b8d68f45 fix: unable to save workflow with verified secondary email (#21338)
* fix: unable to save workflow

* Update util.ts
2025-05-15 19:40:45 +00:00
Lauris Skraucis 6994cac5e0 fix: v2 token secret conflicts (#21279)
* fix: accessToken.create unique constraint failed on the secret field

* fix: force refresh tokens within single transaction
2025-05-15 16:04:50 +00:00
Carina Wollendorfer df9b72583d add learn more help link (#21333) 2025-05-15 11:02:57 +00:00
Carina WollendorferandCarinaWolli 553eab59e6 fix: sms credits follow up (#21304)
* address mrge review comments

* code clean up

* fix type error

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-05-15 08:33:38 +00:00
Joe Au-Yeung 8d675fd8b6 feat: Add AssignmentReason to BOOKING_CREATED webhook (#21315)
* AssignmentREasonRecorder methods return the reason generated

* Add assignmentReason to `EventPayloadType`

* Pass `assignmentReason` to webhook payload data

* Undo constants commit
2025-05-14 23:33:25 +00:00
GitHub Actions a5639a8290 chore: release v5.2.17
Post release / release (push) Canceled after 0s
2025-05-14 23:10:04 +00:00
Anik Dhabal Babu 44d9c4bcc5 fix: Seated events and round robin - all round robin hosts are being added to the calendar event (#21317)
* fix: add org email into ics

* fix: Seated events and round robin - all round robin hosts are being added to the calendar event

* added test
2025-05-14 22:52:26 +00:00
Anik Dhabal Babu 1b586560f8 fix: userprimaryemail not updating (#21318) 2025-05-14 22:01:43 +00:00
Benny Joo e082cc7d3d refactor: Handle apiv2 team slots in useSchedule hook (#21309) 2025-05-14 10:12:09 -07:00
Keith Williams d411e70ea3 chore: Add @calcom/Consumer as CODEOWNER for ui-playground (#21312) 2025-05-14 16:17:13 +00:00
Keith Williams 8dffa4d371 chore: Back down @calcom/Consumer CODEOWNERS (#21311) 2025-05-14 16:09:58 +00:00
Eunjae Lee b7bf6e80e6 fix: case insensitive filter for attendees on /bookings (#21302) 2025-05-14 15:51:22 +00:00
Eunjae Lee 2a93e661f9 feat: add denormalized table for RoutingFormResponse view (#21009)
* feat: add denormalized table for RoutingFormResponse

* WIP

* improve

* update schema

* update schema

* improvement

* fix type of bookingStatus

* cascading DELETE

* split update trigger

* denormalize fields into a separate table

* error handling

* address feedback

* fix

* fix constraints and relations

* delete rows when rows are deleted in the joined tables

* add tests and make booking nullable in RoutingFormResponseDenormalized

* add tests

* update test

* use e2e tests instead

* fix type error

* address feedback

* move e2e to integration test

* add missing tests

* move index migration to another file

* clean up migrations
2025-05-14 10:41:28 -04:00
Lauris Skraucis bfb51b82b2 fix: useLocale hook is being used outside of App Router & time zone dropdown timeout (#21306) 2025-05-14 14:07:35 +00:00
Keith Williams f50be6c08b chore: Lower number of errors sent to Sentry (#21300) 2025-05-14 12:21:13 +00:00
Hariom Balhara df75df7008 fix: DelegationCredential - Duplicate GoogleMeet and unnecessary serviceAccount key access (#21269)
* Remove unused return statement

* Fix return from fn instead of continuing the loop

* Ensure userId is set for existing records on update

* ensure that serviceAccountKey is only retrieved whenb assked
2025-05-14 09:49:47 +00:00
Eunjae Lee deb55c0536 fix: sticky columns in DataTable (#21278)
* fix: sticky columns in DataTable

* update th color

* remove rounded corner

* border change
2025-05-14 08:34:50 +00:00
Lauris Skraucis 7fc11ff341 fix: v2 bookings with sms reminders (#21191)
* fix: v2 bookings with sms reminders

* fix: falsey booking field values
2025-05-14 10:25:29 +03:00
Lauris Skraucis 1a70f83d34 fix: v2 sentry errors (#21214)
* fix: PATCH, POST event-types Address location must have an address

* fix: GET event-types Address location must have an address

* fix: POST /v2/bookings Address location must have an address

* refactor: GET /v2/slots Invalid time range given.

* fix: POST v2/event-types Cannot read properties of null (reading 'type')

* fix: POST /v2/bookings/:bookingUid/mark-absent Failed to update no-show status
2025-05-14 10:11:35 +03:00
39077a08f1 docs: correct documentation for slot format in 2024-09-04 API (#21196)
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-05-14 06:57:41 +00:00
Benny Joo 9a671f9063 fix: add arg validation to ensureValidPhoneNumber (#21259) 2025-05-13 18:57:00 -07:00
3bc9fa11d0 chore: remove @calcom/lib barrel file (#21267)
* refactor: replace imports from @calcom/lib barrel file with direct imports

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: add deprecated barrel file with warning to support tests

Co-Authored-By: benny@cal.com <benny@cal.com>

* remove barrel file

* fix: restore barrel file with deprecation notice and add CreditType enum export

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: add CreditType enum definition to barrel file

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: add mock for CreditType enum in credit-service test

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: use importOriginal in CreditType enum mock

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: remove barrel file completely

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: restore barrel file with deprecation notice to support tests

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: update test-setup to use direct import from @calcom/ui/classNames

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: completely remove barrel file

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: update package.json to remove references to index.ts

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: remove main and types fields from package.json

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: restore barrel file with deprecation notice to support tests

Co-Authored-By: benny@cal.com <benny@cal.com>

* remove barrel file

* fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: benny@cal.com <benny@cal.com>
Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-05-13 21:55:58 -04:00
Joe Au-Yeung 7e699ab810 fix: Salesforce - fix option to create contact if account exists (#21288)
* Add `onCheckedChange`

* Remove console.logs
2025-05-13 22:17:57 +00:00
GitHub Actions 1bd17ba365 chore: release v5.2.16
Post release / release (push) Canceled after 0s
2025-05-13 21:54:23 +00:00
Benny Joo 4d5246cf4d chore: Remove bcryptjs dependency from @calcom/lib (#21286)
* remove bcryptjs lib

* update yarn lock
2025-05-13 22:41:42 +01:00
devin-ai-integration[bot]andzomars@cal.com 5d988453fa fix: add --ignore-missing flag to sentry-cli set-commits command (#21285)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: zomars@cal.com <zomars@cal.com>
2025-05-13 20:48:14 +00:00
JamieandTushar Bhatt 85b9782264 fix: use actual event duration mins for templated event names (#21263)
* fix: use actual event duration mins for templated names

* fix: use actual event duration mins for templated event name

* fix: use actual event duration for calendar links

* fix: use actual event duration for round robin reassignment

* chore: rm console logs

* fix: use actual event duration for emails and rr reassignment

* chore: remove unnecessary dayjs calls

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-05-13 18:51:43 +00:00
Udit Takkar c2b091aa67 perf: don't load ring.mp3 on booking page (#21272) 2025-05-13 16:48:14 +00:00
Farhan Javed 7093e1e4c6 fix: corrected path for cron in documentation (#21274)
* path-fix

* path-update2
2025-05-13 16:02:40 +00:00
ead42c1a9c feat: SMS credits for free users (#21245)
* Add credits section to billing

* create seperate router for credits

* add stripe checkout session

* schema changes + code improvements

* rename to creditBalance

* custom quantify input with error message

* add checkout session completed webhook endpoint

* fix typo

* UI fixes

* add payCredits handler

* add error toast message

* allow scheduling sms up as close to 15 minutes in the future

* schedule at most 2 hours in advance

* webhook to pay for sent sms

* continued work on twilio callback

* code clean up

* further implementation for credit handling

* add migration

* object as param for scheduleSMS

* object as param for sendSMS

* fix TrpcSessionUser imports

* fix imports

* add db changes

* add cron job for price setting

* twilio status callback to create expense log

* remove unused code

* set up low credit balance email

* fixes for buying credits

* fixes in api/twilio/webhook

* add test to save credits to credits balance

* fix typos

* add new helper function chargeCredits

* expand twilioProvider

* fix type errors

* adjust tests

* type errors

* clean up

* clean up

* fix subscription active check

* remove some user/org related code

* more changes to remove user/org support

* send emails seperatly to admins

* fixes for team billing page

* fix stripe success url

* fixes to creating expense log

* email imrovements and more

* get monthly team price from stripe

* fix import

* fix monthly credits calculation

* finsih low credit balance warning email

* credit balance limit reached email

* create CreditService

* cancel SMS and send as email instead

* add messageDispatcher

* fix type error

* fix type error

* fix type error

* fix import

* fix unit test

* clean up twilioProvider

* clean up chckSmsPrices/route

* add missing translations

* add skeleton loader

* add admin check to get handler

* code clean up + fixes

* improve scheduling with fallback

* fix type error

* add bookingUid to handleSendingSMS

* add unit tests for creditService

* add more tests to credit-service.test.ts

* add test for cancelScheduledMessagesAndScheduleEmails

* fix test and type error

* add back resolve

* fix empty resolve

* adjust limitReachedAt logic

* address mrge comment on styling

* add getAdminMembership to repository

* twilio/webhook clean up (feedback)

* feedback - clean up

* remove todo comment

* clean up twilio/webhook

* code clean up

* add use client

* add createOneTimeCheckout to stripe service

* refactor repository pattern

* small fixes + clean up

* fix type error

* add missing import

* fix hasAvailableCredits for user

* force-dynamic

* rename credits to creditBalance

* fix stripe import

* remove not needed code

* fix e2e tests

* improve low balance warning email

* dynamic-import CreditService

* index.ts

* add user logic checkSmsPrices endpoint

* fix e2e tests

* remove dynamic import CreditService

* Revert "remove dynamic import CreditService"

This reverts commit e272978a7ff3fc5a04139e656c9f8d2c84a40dda.

* no need to dynamic-import credit service

* Revert "no need to dynamic-import credit service"

This reverts commit ba5ae488d08979a65fb47b5d0722cda9f45d6ea0.

* fix twilio webhook

* add userId support in checkout.session.completed

* clean up code

* only select id in getAdminMembership

* revert billing/package.json

* fix type checks

* fix type checks

* adjust hasAvailableCredits function

* fixes for checkout sessioned completed

* add UI for user

* fix type errors

* adds requires credits badge

* remove team check from update.handler

* clean up inlcude statements

* fix credit-service tests

* add tests

* fix type errors

* fix type errors

* fix and add tests

* imrove badge

* code clean up

* add reminderScheduler test

* add additional credits as title

* fixes for warningSentAt and limitReachedAt

* mock stripe

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: hbjORbj <sldisek783@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-05-13 15:55:19 +02:00
Somay Chauhan e669996442 fix: Inconsistent Location Handling for Event Types API - Map office365-video to msteams in Validation Handling (#21255)
* refactor: rename office365-video to msteams across API and platform code

* Revert "refactor: rename office365-video to msteams across API and platform code"

This reverts commit 7a823ab6aa7ecd739da941d3b315735220bd99de.

* Fix: inconsistency in default location values for create and update event-types
2025-05-13 13:04:03 +00:00
Somay Chauhan 0e6f365dc2 fix: dont default to cal-video in case locations is not provided (#21256) 2025-05-13 09:57:16 +00:00
devin-ai-integration[bot]andbenny@cal.com 08b501625d chore: remove @calcom/lib/server/queries barrel file (#21268)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: benny@cal.com <benny@cal.com>
2025-05-13 05:18:00 -04:00
Benny Joo b89ff43921 chore: Remove @calcom/lib/schedules barrel file (#21266) 2025-05-13 05:17:01 -04:00
fadfba8830 feat: Credit System (SMS) (#20126)
* Add credits section to billing

* create seperate router for credits

* add stripe checkout session

* schema changes + code improvements

* rename to creditBalance

* custom quantify input with error message

* add checkout session completed webhook endpoint

* fix typo

* UI fixes

* add payCredits handler

* add error toast message

* allow scheduling sms up as close to 15 minutes in the future

* schedule at most 2 hours in advance

* webhook to pay for sent sms

* continued work on twilio callback

* code clean up

* further implementation for credit handling

* add migration

* object as param for scheduleSMS

* object as param for sendSMS

* fix TrpcSessionUser imports

* fix imports

* add db changes

* add cron job for price setting

* twilio status callback to create expense log

* remove unused code

* set up low credit balance email

* fixes for buying credits

* fixes in api/twilio/webhook

* add test to save credits to credits balance

* fix typos

* add new helper function chargeCredits

* expand twilioProvider

* fix type errors

* adjust tests

* type errors

* clean up

* clean up

* fix subscription active check

* remove some user/org related code

* more changes to remove user/org support

* send emails seperatly to admins

* fixes for team billing page

* fix stripe success url

* fixes to creating expense log

* email imrovements and more

* get monthly team price from stripe

* fix import

* fix monthly credits calculation

* finsih low credit balance warning email

* credit balance limit reached email

* create CreditService

* cancel SMS and send as email instead

* add messageDispatcher

* fix type error

* fix type error

* fix type error

* fix import

* fix unit test

* clean up twilioProvider

* clean up chckSmsPrices/route

* add missing translations

* add skeleton loader

* add admin check to get handler

* code clean up + fixes

* improve scheduling with fallback

* fix type error

* add bookingUid to handleSendingSMS

* add unit tests for creditService

* add more tests to credit-service.test.ts

* add test for cancelScheduledMessagesAndScheduleEmails

* fix test and type error

* add back resolve

* fix empty resolve

* adjust limitReachedAt logic

* address mrge comment on styling

* add getAdminMembership to repository

* twilio/webhook clean up (feedback)

* feedback - clean up

* remove todo comment

* clean up twilio/webhook

* code clean up

* add use client

* add createOneTimeCheckout to stripe service

* refactor repository pattern

* small fixes + clean up

* fix type error

* add missing import

* fix hasAvailableCredits for user

* force-dynamic

* rename credits to creditBalance

* fix stripe import

* remove not needed code

* fix e2e tests

* improve low balance warning email

* dynamic-import CreditService

* index.ts

* fix e2e tests

* remove dynamic import CreditService

* Revert "remove dynamic import CreditService"

This reverts commit e272978a7ff3fc5a04139e656c9f8d2c84a40dda.

* no need to dynamic-import credit service

* Revert "no need to dynamic-import credit service"

This reverts commit ba5ae488d08979a65fb47b5d0722cda9f45d6ea0.

* only select id in getAdminMembership

* revert billing/package.json

* fix type checks

* fix type checks

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: hbjORbj <sldisek783@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-05-13 09:11:44 +02:00
Benny Joo 43be9bd6a8 fix: localStorage error in AvailableTimeSlots (#21265) 2025-05-13 11:31:25 +05:30
Hariom Balhara caf184151d fix: Credentials cron for DelegationCredential not working (#21239)
* Remove unused return statement

* Fix return from fn instead of continuing the loop

* Ensure userId is set for existing records on update
2025-05-13 09:00:08 +05:30
GitHub Actions 22e3338709 chore: release v5.2.15
Post release / release (push) Canceled after 0s
2025-05-12 23:05:02 +00:00
048e56806c feat: useApiV2AvailableSlots (#21138)
* feat: useApiV2AvailableSlots

* feat: useApiV2AvailableSlots

* fix type check

* remove log

* fix unit tests

* simplify

* feat: add missing params to the slots endpoint

* updated documentation

* _shouldServeCache is correct

* fix types

* fix enabled prop

* fix type

* add embedConnectVersion to query key

* add teamId to slot type

* fix

* add fallback to trpc query if apiv2 fails

* add comment

* remove logs

* fix

* refactor: default to isTeamEvent in query params

* fix: only return v2 slots once fetch is success

* fix last e2e failing test

* add feature flag

* add migration sql

* Update packages/prisma/migrations/20250512153630_add_use_api_v2_for_team_slots_feature_flag/migration.sql

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-05-12 20:48:29 +00:00
Calcom BotandLingo.dev ade64c0d09 feat: update translations via @LingoDotDev (#20599)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-05-12 14:20:33 -04:00
Benny Joo 5beb2943b9 fix: localStorage error in useTheme (#21258) 2025-05-12 17:59:37 +00:00
Omar López b3d3cb1dee refactor: Uses withReporting to force named functions for tracing (#21219) 2025-05-12 13:22:52 -04:00
devin-ai-integration[bot] 79d695236f test: add e2e tests for filter segment functionality (#21160) 2025-05-12 12:52:54 -04:00
luzpazandAlex van Andel 2dbbdc21f2 fix: typos in apps/ui-playground (#21223)
Found via codespell

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-05-12 08:49:35 +00:00
luzpazandAlex van Andel e4254ef7cc fix: typos in apps/web (#21224)
Found via codespell

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-05-12 08:47:02 +00:00
luzpazandUdit Takkar 7f9709bdaa fix: typos in packages/ (#21216)
Found via codespell

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-05-12 09:30:43 +01:00
Morgan 77054d92df feat: add pagination to get bookings endpoints [v2] (#21173)
* feat: add pagination to get bookings endpoints [v2]

* add decorators for pagination

* fix: ensure correct values

* code review comment

* fixup! code review comment
2025-05-12 10:09:56 +02:00
Benny Joo 1277f5e9f7 chore: migrate signup API to appDir (#19975)
* refactor calcomHandler

* refactor selfHostedHandler

* fix

* migrate to appdir

* fix type checks
2025-05-12 09:38:03 +02:00
Rajiv SahalandMorgan 109c303b60 feat: active managed user billing (v1) (#20499)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-05-10 16:13:09 +00:00
jen chanandamrit a52a0e90e7 feat: add autocomplete to login and signup (#21065)
Co-authored-by: amrit <iamamrit27@gmail.com>
2025-05-10 17:52:27 +02:00
+25 558140fdd6 fix: sentry builds (#20310)
* fix: sentry builds

* Update yarn.lock

* Upgrade Sentry

* Breaking changes

* Discard changes to apps/web/modules/connect-and-join/connect-and-join-view.tsx

* Discard changes to apps/web/modules/signup-view.tsx

* Discard changes to packages/app-store/make/pages/setup/index.tsx

* Discard changes to packages/app-store/zapier/pages/setup/index.tsx

* Discard changes to packages/emails/src/templates/AdminOrganizationNotificationEmail.tsx

* Discard changes to packages/features/eventtypes/components/tabs/apps/EventAppsTab.tsx

* Discard changes to packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx

* Discard changes to packages/features/eventtypes/components/tabs/webhooks/EventWebhooksTab.tsx

* Discard changes to packages/features/eventtypes/components/tabs/advanced/DisableAllEmailsSetting.tsx

* Discard changes to packages/emails/src/templates/BrokenIntegrationEmail.tsx

* Discard changes to packages/emails/src/templates/OrganizationCreationEmail.tsx

* Discard changes to packages/emails/src/templates/SlugReplacementEmail.tsx

* Discard changes to packages/emails/src/templates/TeamInviteEmail.tsx

* Discard changes to packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx

* Discard changes to packages/features/bookings/Booker/components/OutOfOfficeInSlots.tsx

* Discard changes to packages/features/ee/teams/components/MemberInvitationModal.tsx

* Discard changes to packages/features/eventtypes/components/EditWeightsForAllTeamMembers.tsx

* Discard changes to packages/features/eventtypes/components/Locations.tsx

* Discard changes to packages/features/eventtypes/components/dialogs/ManagedEventDialog.tsx

* Discard changes to packages/features/eventtypes/components/tabs/advanced/RequiresConfirmationController.tsx

* Discard changes to packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx

* Discard changes to packages/features/settings/BookerLayoutSelector.tsx

* Discard changes to packages/features/settings/outOfOffice/OutOfOfficeEntriesList.tsx

* chore: upgrade dependencies and improve Sentry configuration

- Upgrade @sentry/nextjs to 9.13.0

- Upgrade next.js to 15.3.0

- Upgrade sharp to 0.34.1

- Update Sentry configuration files

- Add Sentry example files for testing

* chore: upgrade dependencies

- Upgrade next.js to 15.3.0

- Upgrade sharp to 0.34.1

* Update next.config.js

* Discard changes to packages/platform/atoms/connect/apple/AppleConnect.tsx

* build(apps/web): enabled Sentry source maps uploading.

* Create sentry-sourcemaps.yml

* fix: buggy skeleton rendering for month view (#20744)

* fix: trigger lingo.dev by removing duplicate value

* fix: buggy skeleton monthly view

* fix: Wrong schedule ID set for child managed event (#20745)

* fix: wrong schule id set for managed event

* Update handleChildrenEventTypes.ts

* feat: v2 book team events using teamSlug & eventTypeSlug (#20585)

* feat: v2 book team events using teamSlug & eventTypeSlug

* fix: tests

* fix: failing e2e test (#20751)

* feat: filter users by assigned attribute options (#20659)

* feat: assigned attributes options base

* feat: assigned attributes options base e2e

* feat: filter assigned options by other assigned options

* feat: filter assigned options by other assigned options

* fixup! feat: filter assigned options by other assigned options

* feat: get org users by assigned attributes

* adding missing input file

* push generated docs

* feat: add filter org users by teamIds

* feat: add filter assigned attribute options by teamIds

* chore; update doc

* chore: remove console log

* chore: minor e2e fix

* chore: minor e2e fix

* chore: improve docs

* fix: assigned options by teamIds + optionIds

* fix: Admin/owner unable to confirm managed bookings (#20743)

* fix: Admin/owner can't confirm managed booking

* Update confirm.handler.ts

* fix: v2 automatic round robin reassign error message (#20755)

* fix: return react fragment from all layout.tsx

* feat: add guards to stripe teams controller (#20540)

* feat: add guards to stripe teams controller

* remove logs and comments

* fix return type status

* refactor: move PlatformSubscription to a dedicated module

* reroute to `organizations/stripe/save` for teams

* fix: type errors

* feat: fixed it for conferencing apps

* feat: Add error handling and fallback URL support in Stripe callback

* Refactor OAuth callback handling and move token validation to service layer

* Add documentation for OAuth callback proxying in conferencing and stripe controllers

* Move OAuthCallbackState type from organizations to stripe service module

* chore: release v5.2.2

* perf: slim down middleware matcher (#20735)

* wip

* wip

* refactor

* add pathname

* fix

* fix: manually construct pathname for metadata functions

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: add pathname parameter to all metadata functions

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix: add pathname parameter to remaining metadata functions

Co-Authored-By: benny@cal.com <benny@cal.com>

* fix

* fix

* fix type checks

* Refactor

* fix

* fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: benny@cal.com <benny@cal.com>

* Skip flakey tests (#20761)

* Write UTM params to Salesforce (#20759)

* chore: Add consent message when SMS workflows are related (#20741)

* Add consent message

* Add consent message if using SMS workflow

* Skip flakey tests

* Revert "Skip flakey tests"

This reverts commit 3bd6f3fd71469df4dbd67b69b8e17011657df9d8.

* chore: Remove verbose logging (#20732)

* fix: offer seats broken (#20758)

* Remove trailing } from utm param (#20763)

* fix: only managed users assignable to managed team event (#20754)

* fix: only managed users assignable to managed team event

* chore: bump platform libraries

* feat: make skeleton for booking embed accurate (#20765)

* fix: trigger lingo.dev by removing duplicate value

* feat: make skeleton for booking embed accurate

* fix: calendar connect atoms (#20766)

* fix: header overlaps in month view on short viewport (#20749)

* fix: header overlaps in month view on short viewport

* refactor: Merge classes into same condition

* fix: Remove booker margin top if isEmbed

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: amrit <iamamrit27@gmail.com>

* fx 404 for embed (#20120)

* Update rateLimit.ts (#20785)

* fix: v2 Sentry errors (#20788)

* fix: Only Team Event Types are supported for Instant Meeting

* refactor: Cannot read properties of undefined (reading 'timeZone')

* fix: Cannot read properties of undefined (reading 'map')

* refactor: rearrange function order

* refactor: booking_seats_full_error

* fix: invalid_round_robin_host

* refactor: booking_time_out_of_bounds_error

* refactor: Attempting to book a meeting in the past.

* fix: obj.phoneNumber.trim is not a function

* fix: Cannot use 'in' operator to search for 'disabled' in 1

* fix: Org onboarding handle error due to platform team being moved to regular org (#20786)

* Add better error logs and handle platform team

* Add better error logs and handle platform team

* fix: Wrong text EmbedTheme -> Theme (#20790)

* refactor: replace timeZone string validation with timeZoneSchema (#20719)

* refactor: replace timeZone string validation with timeZoneSchema - Replace all instances of timeZone: z.string() with timeZoneSchema - Preserve modifiers (.optional(), .default(), .nullish()) where present - Fix VideoApiAdapter to use organizer's timezone value - Remove redundant isSupportedTimeZone import from booking.ts - Add timeZoneSchema import where needed

* Update index.ts

* fix: conflicts

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

* Update yarn.lock

* Discard changes to yarn.lock

* Update booking.ts

* Discard changes to packages/platform/types/me.ts

* Discard changes to packages/platform/types/me/outputs/me.ts

* Discard changes to packages/platform/types/schedules/schedules-2024-04-15/schedules.ts

* Discard changes to packages/features/ee/cal-ai-phone/zod-utils.ts

---------

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

* fix: improved 'use-identity-login' error message for users trying to signInn using different provider (#20582)

* improved 'use-identity-login' error message for users trying to signin using different provider

* passing original identity provider as query parameter

* Update packages/features/auth/lib/next-auth-options.ts

* passed different error names for different types of error

---------

Co-authored-by: romit <romitgabani1@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>

* fix: Location not changing when rescheduling (#20404)

* fix: location not chnging

* update

* update

* Update EventManager.ts

* update

* update

* Update reschedule.test.ts

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* fix: sms workflow api v2 (#20795)

* fix: sms workflow api v2

* fixup! fix: sms workflow api v2

* fix: semi-colon in the bottom of layout UI (#20799)

* chore: Add layout.tsx to Foundation team CODEOWNERS (#20802)

* chore: remove swagger app and its dependencies (#20798)

* chore: release v5.2.3

* fix: Convert an error in webhook to info (#20806)

* Convert error to info

* Convert error to info

* feat: add "React (Atom)" to embed generator and rename rest (#20618)

* fix: trigger lingo.dev by removing duplicate value

* feat: add support for React Atom embed options

* fix: update locator for React code tab in embed code generator E2E tests

* fix: update embed tab names to clarify iframe usage

* fix failing test

* fix: indentation and installation commands for atoms

* fix: add note about iframe usage in Preview tab

* refactor: replace Cal component with BookerEmbed in EmbedCodes

* fix: Add EmbedConfig type import in Embed.tsx

- Added missing EmbedConfig type to the existing types import
- Resolves TypeScript error "Cannot find name 'EmbedConfig'"

* fix: update BookerEmbed usage in EmbedCodes component

* fix: enhance BookerEmbed implementation and update installation instructions in EmbedTabs

* fix: improve comments in EmbedCodes for clarity on layout options and styling references

* fix: streamline BookerEmbed usage in EmbedCodes with improved class handling and layout options

* fix: embed type selection

* fix: update layout selection in EmbedCodes to use dynamic configuration

* fix: update embed configuration to use theme instead of layout

* fix: conditionally render embed options based on if we are selecting atom

* fix: use simple props instead of embed specific logic to fix the active embed bug

* Fix double tabs activeness

* Revert isCurrent change

---------

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

* refactor: remove iframe usage note from Preview tab in EmbedTabs component (#20810)

* fix: trigger lingo.dev by removing duplicate value

* refactor: remove iframe usage note from Preview tab in EmbedTabs component

* fix: api v2 license check (#20813)

* fix: api v2 license check

* update cache key to avoid conflicts

* chore: Add more Foundation and Consumer team CODEOWNERS configuration (#20816)

* refactor: forbidden 403 v2 guard messages (#20794)

* refactor: delete unused proxy guard

* refactor: roles.guard.ts

* refactor: is-org.guard.ts

* refactor: permissions.guard.ts

* refactor: oauth-client-guard.ts

* refactor: is-user-event-type-webhook-guard.ts

* refactor: platform-plan.guard.ts

* refactor: roles.guard.ts

* refactor: cache guard result if canAccess=true

* refactor: is-admin-api-enabled.guard.ts

* refactor: is-team-in-org.guard.ts

* refactor: platform-plan.guard.ts

* refactor: is-webhook-in-org.guard.ts

* refactor: is-user-in-org.guard.ts

* refactor: is-user-webhook-guard.ts

* refactor: is-user-in-org-team.guard.ts

* refactor: is-membership-in-org.guard.ts

* refactor: is-oauth-client-webhook-guard.ts

* refactor: is-routing-form-in-team.guard.ts

* refactor: is-managed-org-in-manager-org.guard.ts

* fill the gaps

* refactor: api-auth.strategy.ts

* fix: test

* implement feedback

* fix: tests

* fix: refactor

* fix: test

* feat: update DeleteSegmentDialog to use ConfirmationDialogContent CAL-5399 (#20820)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <eunjae@cal.com>

* feat: open dropdown right after adding a filter (#20819)

* feat: open dropdown right after adding a filter

* clean up timeout

* fix: remove window usage in not-found client (#20822)

* remove window usage in not-found client

* fix

* fix: user is already part of a team (#20774)

* better error message

* redirect to login if no user session

* fixup

* remove callback url since the same function is used by org and platform both

* remove extra exclamation mark

* implement PR feedback

* refactor: Remove kysely as its unused (#20817)

* perf: Fetch data server-side for Api Keys page (#20677)

* server-side fetching for api keys page

* refactor billing page

* fix

* simplify metadata in /availability/[schedule] route (#20827)

* feat: Optout of SMS workflows (#20769)

* Do not call dub if API key isn't present

* Add `WorkflowOptOutContact` table

* Add `sendTo` column to `WorkflowReminder`

* Add `workflowOptOutContactRepository`

* Add `WorkflowReminderRepository`

* Write `sendTo` number when scheduling SMS messages

* Add `p-limit` package

* Add `determineOptOutType` to `TwilioProvider`

* Add `deleteMultipleScheduledSMS`

* Create `WorkflowOptOutService`

* Add endpoint handler for Twilio SMS responses

* Add `isOptedOut` method to `workflowOptOutContactRepository`

* Verify phone number is not opted out before scheduling SMS

* Use `smsReminderNumber` instead of `sendTo`

* Type fix

* Add .env variable if opt out is available

* Add opt out message to SMS

* Import `pLimit` directly in method

* Address select comment

* Guard against undefined phone number from form

* Add early return to `deleteWorkflowReminders`

* Add request validation from Twilio

* Add fallback message as i18N string

* Only delete SMS attendee scheduled reminders

* chore: release v5.2.4

* fix: caching for sprite.svg (#20832)

* update cache-control header for sprite.svg

* updated regex

* fix: platform org members cannot access their public booking page (#20821)

* fix: platform member event pages

* chore: update slots docs

* refactor: user repository call

* refactor: remove newline

* refactor: Returns 400 error for ZodErrors (#20831)

* fix: eventType filter on /insights (#20823)

* fix: eventType filter on /insights

* remove log

* feat: custom reply To Email (#20771)

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

* feat(insights): add progress indicator to download button on /insights and /insights/routing (CAL-5408) (#20631)

* feat(insights): add progress indicator to routing form responses download button (CAL-5408)

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: address PR feedback - remove initial progress update and ensure no decimal parts in percentage

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* feat(insights): add progress indicator to Download component

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* feat: paginate rawData endpoint and implement batch fetching in Download component

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: simplify pagination logic in trpc-router.ts

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* show progress on tooltip

* fix download for /insights

* fix download when there is no data

* feat: replace tooltip progress indicator with toast notification

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: remove duplicate ProgressToast component from showToast.tsx

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: export ProgressToast component from index.ts

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: add missing translation for downloading_data

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* update progress toast implementation

* fix error

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <eunjae@cal.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>

* perf: SSR for /teams  (#20824)

* finish

* improve

* refactor

* add revalidations

* /settings/teams is not needed

* refactor

* rename

* redundant type casting

* perf: SSR - /availablilty/[schedule] (#20829)

* ssr for schedule page

* improve skeleton

* improve skeleton

* improve skeleton

* fix: getBookings max 1 month in the past when no date filters specified (#20911)

* fix: getBookings max 1 month in the past

* bump platform libraries

* Pass customReplyTo as a managed event type prop (#20909)

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

* chore: release v5.2.5

* fix: Docs for deleting reserved slot (#20922)

* feat: ability to hide organizer email (#20782)

* feat: hide orgainzer email

* fix type error

* update

* update

* Update schema.prisma

* perf: Slim down trpc queries in Settings Layout   (#20918)

* slim down settings layout by removing server queries

* add check method

* fix type check

* refactor

* perf: remove next.redirect by going straight to event-types (#20920)

* perf: ssr for /settings/organizations/teams/other (#20927)

* refactor: v2 automatic team slugs (#20897)

* refactor: automatic team slugs

* refactor: automatic organizations team slugs

* refactor: automatic managed organizations slugs

* docs

* refactor: test

* feat: add loading component for /apps main page (#20924)

* perf: server-side fetching for /availability (#20804)

* availability

* refactor

* add loader

* improve

* update

* fix loader

* rename

* satisfying types

* refactor

* remove log

* add CTA to skeleton

* refactor

* fix revalidations

* refactor

* update

* finish

* fix: Include ordered attendees in booking select query (#20935)

* fix: correct ordering

* another

* docs: global platform event types and custom booking flows (#20940)

* docs: global event types

* docs: custom booking flow

* chore: release v5.2.6

* fix: v2 sentry errors (#20902)

* refactor: no_available_users_found_error for team event

* fix: hosts_unavailable_for_booking

* fix: Cannot read properties of undefined (reading 'replace')

* fix: Cannot read properties of undefined (reading 'phoneNumber')

* fix: No SelectedCalendar found.

* fix: Cannot read properties of undefined (reading 'length')

* refactor: add bookings errors service

* feat: add membership creation date to Organization Member List table (CAL-5406) (#20595)

* feat: add membership creation date to Organization Member List table (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* feat: add migration for membership creation date (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* feat: make createdAt and updatedAt nullable (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* feat: add updatedAt column to Organization Member List table (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: use type assertion to access createdAt and updatedAt fields (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: display N/A for null date values (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: use proper type assertions for createdAt and updatedAt fields (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: add createdAt and updatedAt to UserTableUser mock in test (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* feat: add PostgreSQL trigger for membership timestamps (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: use empty string instead of N/A and add translations for column headers (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* add i18n text

* clean up type issue

* feat: add translation keys for column headers (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* disable sort

* remove duplicated i18n texts

* feat: add filters for lastActiveAt, createdAt, and updatedAt (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* support date range filter

* fix date range for end date

* hide columns by default

* revert wrong change

* add missing selects

* fix e2e test

* fix: remove PostgreSQL trigger and let application handle timestamps (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* feat: add application-level timestamp handling for Membership model (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* add more timestamp handling

* refactor: use Prisma's built-in decorators for Membership timestamps (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* refactor: remove application-level timestamp handling in favor of Prisma decorators (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* refactor: remove more application-level timestamp handling in favor of Prisma decorators (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix e2e test

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keith@cal.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>

* fix: Offset is required but not passed everywhere, default to zero. (#20917)

* fix: exclude email from replyTo (#20953)

* fix: exclude email from replyTo

* update

* Update scheduleEmailReminders.ts

* fix: added order by (#20959)

* fix: double end-date parsing when saving availability (#20963)

* fix: double end-date parsing when saving availablity

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

* fix: cache hits in current and next month

* Update CalendarService.test.ts

---------

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

* fix: Don't delete Cal video room upon requesting reschedule (#20969)

* Update requestReschedule.handler.ts

* Update requestReschedule.handler.ts

* chore: leverage error.tsx in app router (#20955)

* chore: better code - migrate getServerSideProps file directly to RSC for /apps (#20926)

* migrate getServerSideProps file directly to RSC for /apps

* fix

* fix: dont persist last booking response, temporarily (#20975)

* dont persist last booking response

* remove dead code to fix ts error

* perf: Improve performance of period limits (#20974)

* perf: Improve performance of period limits

* Type fixes

* Fixed correct check for instanceof -> isDayjs

* revert: "feat: add membership creation date to Organization Member List table (CAL-5406) (#20595)" (#20978)

This reverts commit d21f7ccbc8.

* perf: Add credentialId index to selectedCalendar (#20979)

* docs: add JSDoc documentation to FeaturesRepository class and methods (#20980)

* chore: release v5.2.7

* Handle acme.cal.local root path (#20981)

* Update sms-manager.ts (#20985)

* Do not retrieve already persisted data as well (#20982)

* perf: workflows—remove data fetching for metadata (#20734)

* Remove 30 seats minimum text from organization setup page (#20983)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: hariom@cal.com <hariom@cal.com>

* fix: few details not populate (#20987)

* fix: cancel platform subscription on dashboard (#20787)

* add new endpoint to cancel user subscription

* remove comments

* add delete button to frontend

* update correct request method

* fixup

* frontend for cancelling subscription

* better warning message

* better function name

* update locales

* better error handling in case stripe api call fails

* rename teamId to organizationId

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>

* feat: added `hideOrganizerEmail` to api/v2/event-types (#21005)

* feat: added `hideOrganizerEmail` to api/v2/event-types

* updated docs

* fix: Skip already completed org onboarding, as webhook request can come when invoice is paid again next month (#20984)

* there is nothing to do for repeat invoice payment

* there is nothing to do for repeat invoice payment

* fix: wrong translation value (#17923)

* feat: ability to remove attendees from booking page

* update

* fix

* fix: unable to create meeting with ms teams (#20993)

* fix: few details not populate

* fix: unable to create meeting with ms teams

* Update CrmService.ts

* update

* fix: encoding bug (#21004)

* fix: encoding bug

* fix: use escapeValue

* perf: SSR - /event-types and /event-types/[type] (#20952)

* add revalidation

* ssr for event-types

* wip

* wip

* fix types

* fix skeleton

* better error component

* make skeleton better

* no need to export

* call revalidations early

* remove revalidations if not needed

* address comments

* address

* fix: schedule unscheduled emails with tasker in cron job (#20748)

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

* fix: hide orgnizer email in cal video sidebar (#21011)

* refactor: Use Salesforce GraphQL for account ownership routing (#20655)

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

* fix: Handle calendar-cache with Delegation Credentials

Fixes CAL-5372

# Delegation Credentials with CalendarCache.

Following content is a snapshot of the [internal document](https://calendso.slack.com/docs/T08B8KA2BNF/F08L5JYU3V3)



**Problem-1 :** 

CalendarCache needs SelectedCalendar records to work but SelectedCalendar record is only created when a user connects their calendar and then enables some calendar for conflict checking. Because with Delegation, no manual connection is done by any of the members, we need a way to create SelectedCalendar records automatically.

**Problem-2**

CalendarCache connects to credential(regular credential) which doesn’t exist for Delegation Credential scenario. Also, DelegationCredential is common for all the members(different from Credential which is different for different members) of the organization and we need to identify to which user the CalendarCache belongs.  

**Solution for both problems**
- Create credential records for Delegation Credentials as well - Through Cron(new - we could schedule it every 5mins)
- Now create SelectedCalendar  records for those Credential records -  Through another Cron(new - we could schedule it every 5mins)
- Now CalendarCache records will automatically be created for those SelectedCalendar records -existing cron

## Fixed some Delegation Credentials bugs unrelated to calendar-cache
- If DestinationCalendar wasn't set(which is possible only with Delegation Credentials), then Google Meet wasn't used as a conferencing app - [Added a test]
- If no SelectedCalendar is there but Google Calendar connection exists(possible only with Delegation Credential) then we were not doing conflict checking. It is expected to not do it for Regular Credentials, but for Delegation Credential we must check for conflict in that case too [Added a test]
- Earlier if a user has Regular Credential as well as Delegation Credential for the same external id which is the member email(say member1@acme.com) then availability were retrieved twice because we weren't deduplicating credentials as it wasn't a trivial thing to do. Now that is being done.


**Env Variables:**
Note this PR doesn't introduce any new env variable. The existing env variable has been added to .env.example. But if this env variable isn't already set, it must be set.

`CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY={SAME_AS_SET_FOR_V2_API}`

**Deployment Plan:**
1. Add Observability for SelectedCalendar when _error_ field is set
2. Follow https://github.com/calcom/cal.com/blob/calendar-cache-dwd-support/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/delegation-credential/delegation-credential.md#setting-up-delegation-credential-for-google-calendar-api to enable Delegation Credential for i.cal.com
3. Note that to be able to see the option to enable Delegation Credential for an organization, you need to enable `teamFeature` and `feature` for `delegation-credential`

## Automation Tests
- Introduced tests for calendar-cache.repository.ts
   - Tests all methods of the repository
- Added more tests for handleNewBooking/delegation-credential flow.
   - Added test to verify the bug fix when no DestinationCalendar exists and Google Meet should be used still 
- Added more tests for Google Calendar/CalendarService targeting DelegationCredential
- Added more tests for getCalendarsEvents. 
    - To test the new logic of calling getAvailability still if there are no selectedCalendars in case of Delegation Credential
    - Also introduced tests for `getAvailabitlityWithTimezones` which was an existing function but now has some new changes.
- Added tests for deduplication logic in CalendarManager.ts

## How to Test
Enable Calendar Cache and Delegation Credential feature for acme org through `features` and `teamFeatures` tables.
- Enable Delegation Credential for acme org
- Enable atleast 1 calendar for conflict checking for one of the users(say owner1)
- Ensure GOOGLE_WEBHOOK_TOKEN is set in .env file
- Ensure GOOGLE_WEBHOOK_URL is set to ngrok url of webapp in .env file
- Hit cron endpoint `curl http://localhost:3000/api/calendar-cache/cron\?apiKey\={API_KEY}` that would cache the freebusy result for the selected calendars



Followup 
- https://github.com/calcom/cal.com/pull/20698
- https://github.com/calcom/cal.com/pull/18619/files#r2046795643

* fix: UI Bug - ToggleGroup (#20997)

* fix #20986: UI Bug - ToggleGroup

* some changes

* some changes

* feat: /skip trial for teams (#20753)

* skip team trials

* add loading state

* update mocks

* cleanup

* add import oops

* fix tests files

---------

Co-authored-by: Sean Brydon <sean@Seans-Mac-mini.local>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* fix: cleanup html reports when pr is merged/closed (#20089)

Co-authored-by: anik@cal.com <anik@cal.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: allow only platform organisation or non-platform organisation (#20972)

* fix: platform customers cant subscribe to organizations plan

* fix: organization customers cant subscribe to platform plan

* chore: format calendar events to remove oauthClientIds (#21040)

* fix: meeting link fails when host requests to reschedule (#20999)

* fix: meeting link fails when host requests to reschedule

* Update EventManager.ts

* fixes

* Incorportate prisma integration

* Update package.json

* Delete apps/web/app/sentry-example-page/page.tsx

* Discard changes to packages/features/insights/server/trpc-router.ts

* Delete .github/workflows/sentry-sourcemaps.yml

* Delete apps/web/app/api/sentry-example-api/route.ts

* Update turbo.json

* Delete sentry.client.config.ts

* Delete sentry.client.config.ts

* fix

* Update package.json

* Update next.config.js

* Update package.json

* Update yarn.lock

* Discard changes to .env.example

* Discard changes to packages/embeds/embed-react/package.json

* Discard changes to packages/platform/atoms/package.json

* Discard changes to packages/features/ee/teams/components/TeamsListing.tsx

* Update instrumentation-client.ts

* Update instrumentation-client.ts

* Update sentry.edge.config.ts

* Update next.config.js

---------

Signed-off-by: Omar López <zomars@me.com>
Co-authored-by: Yadong Zhang <zhyd007@gmail.com>
Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: GitHub Actions <github-actions@github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: benny@cal.com <benny@cal.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Evan Liu <evanliu.dev@gmail.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Ronit Raj <ronitrajofficial7@gmail.com>
Co-authored-by: Romit Gabani <85230081+romitg2@users.noreply.github.com>
Co-authored-by: romit <romitgabani1@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: eunjae@cal.com <eunjae@cal.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Vijay <vijayraghav22@gmail.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: keith@cal.com <keith@cal.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: hariom@cal.com <hariom@cal.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Sean Brydon <sean@Seans-Mac-mini.local>
Co-authored-by: anik@cal.com <anik@cal.com>
2025-05-09 19:44:48 +00:00
GitHub Actions fa09fd66ff chore: release v5.2.14
Post release / release (push) Canceled after 0s
2025-05-09 17:03:57 +00:00
Joe Au-Yeung b1cbeebe48 feat: Add option to include no shows in RR calculations (#21063)
* Show distribution option for RR scheduling type

* Add `includeNoShowInRRCalculation` to event type schema

* Add `includeNoShowInRRCalculation` initial event type data

* Add FE option for `includeNoShowInRRCalculation`

* Consider `includeNoShowInRRCalculation` when building `where` clause when getting bookings for RR

* Add `includeNoShowInRRCalculation` param to `getAllBookingsForRoundRobin`

* Add `includeNoShow...` param to `GetLuckyUserParams`

* Pass `includeNoShow...` param to `getBookingsOfInterval` calls

* Add FE option for `includeNoShow...` (actual commit)

* Type fixes

* Fix brackets

* Add booking repository test

* Remove unused method

* Add translation

* Type fix

* Type fix

* Test passing

* Fix tests

* Fix tests

* Fix test

* Type fixes
2025-05-09 16:29:57 +00:00
devin-ai-integration[bot] 93bad282d7 feat: add SMS_ATTENDEE action to activateEventType.handler.ts (#21201) 2025-05-09 18:08:21 +02:00
Anik Dhabal Babu 47d941676e fix: add org email into ics when hideorganizer email is true (#21212)
* fix: add org email into ics

* update test
2025-05-09 15:57:24 +00:00
Keith Williams 54c4b0b65e chore: Remove Sentry replay integration (#21211) 2025-05-09 11:44:30 -04:00
Udit Takkar 0f8356f417 chore: add some console logs for debugging (#21130)
* chore: add some console logs for debugging

* chore: log only essential
2025-05-09 08:26:12 +00:00
Hariom BalharaandTushar Bhatt cecfcc7c6f Setup delegation credential related crons (#21176)
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-05-09 07:59:31 +00:00
amritandTushar Bhatt 0b3f9d06a9 test: add tests for the search dropdown in /bookings/upcoming filter (#21141)
* fix: trigger lingo.dev by removing duplicate value

* feat: add filter dropdown item search tests for bookings list

* refactor: update page navigation to wait for DOM content in bookings list tests and add new filter item search test

* remove comments

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-05-09 11:12:30 +05:30
Yadong (Adam) ZhangandOmar López 71e7300830 build(apps/web): enabled client code Sentry source maps uploading. (#21108)
Co-authored-by: Omar López <zomars@me.com>
2025-05-08 20:14:47 -07:00
Omar López bccffb328b refactor: removes copy-webpack-plugin (#21200) 2025-05-09 01:01:53 +00:00
Carina WollendorferandCarinaWolli 80fa172f79 feat: add feature flag for sending workflow emails with smtp (#21187)
* add feature flag

* add unit tests

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-05-08 22:39:14 +00:00
Joe Au-Yeung 50173658f8 refactor: Use Iffy for workflow body scanning (#21170)
* Add Iffy API key variable

* WIP using Iffy to scan comments

* Use Iffy for workflow body scanning

* Update entity

* Clean up

* Update test

* Fix client id

* Fix test
2025-05-08 23:38:49 +01:00
Lauris Skraucis dddbd7dad4 feat: v2 membership attributes (#21131)
* refactor: getByUserId.handler.ts

* feat: return org membership attributes

* docs

* export new library function

* test: attributes

* chore: bump libraries

* refactor: value -> option
2025-05-08 23:37:24 +01:00
Benny Joo ae2d85d81b chore: Remove @calcom/features/schedules barrel files (#21199)
* remove barrel files

* update imports
2025-05-08 23:15:51 +01:00
GitHub Actions 80c407a4b7 chore: release v5.2.13
Post release / release (push) Canceled after 0s
2025-05-08 21:44:17 +00:00
6e9ec113fe feat: Prerender booking link and reuse with headless router (#20720)
* mvp done

* wip

* fix ts errors and other code improvements

* fix ts errors

* ensure mobile layout support

* Make skeleton responsive on screen resize

* refactor

* Add test for EmbedElement

* make skeleton closer to pixel perfect

* Address PR feedback

* Router-preloading
## What does this PR do?

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

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

## Visual Demo (For contributors especially)

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

#### Video Demo (if applicable):

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

#### Image Demo (if applicable):

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

## Mandatory Tasks (DO NOT REMOVE)

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

## How should this be tested?

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

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

## Checklist

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

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

* wip\

* wip

* fix mrge.io feedback

* wip

* Add README and lifecycle

* Add README and lifecycle

* Update routing form-seed and some other fixes

* remove linkFailed fix from the branch

* self-review

* self-review-2

* self-review-3

* Handle soft connect\

* Update README and fix a bug with query parmas

* Add one more case in routing-html playground

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: amrit <iamamrit27@gmail.com>
2025-05-08 19:59:26 +01:00
Hariom Balhara 3ee6a7ecde update delegation creential readme (#21178) 2025-05-08 17:40:38 +00:00
Somay Chauhan 8ad62d183c feat: booking references api type filter (#21179)
* feat: add `type` filter for booking references API endpointa

* Update openapi.json

* feat: add enum validation for booking reference types in filter input

* refactor: rename externalUid to eventUid and add destinationCalendarId in booking references

* refactor: replace enum with const array for BookingReferenceType

* fix: type safety for booking references where clause using Prisma types

* refactor: rename BookingReferenceType to BookingReferences
2025-05-08 17:23:09 +00:00
Benny Joo f047799299 chore: remove @calcom/lib/server barrel file (#21190) 2025-05-08 22:36:30 +05:30
Lauris SkraucisandMorgan 3a9e353443 fix: don't allow double reserving already reserved slot (#21139)
* feat: prevent double reserving slots

* tests

* remove log

* logs

* refactor: releaseAt utc check

* fix: check overlap when updating slots

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-05-08 14:39:33 +02:00
Udit Takkar b73a3aeb0c feat: enforce log in cal video (#21082)
* feat: enforce log in cal video

* chore: remove un necessary text

* fix: type err

* fix: pass user name
2025-05-08 12:37:50 +00:00
Anik Dhabal Babu b6ff64ed74 fix: attendees not visible for hosts (#21156) 2025-05-08 12:23:27 +00:00
Anik Dhabal Babu 1b281fe269 fix: unable to change the workflow action (#21185) 2025-05-08 10:06:02 +00:00
Somay Chauhan 024913592b feat: Add handleSlotSelect prop for custom timeslot selection/reservation handling (#21133)
* feat: Add `handleSlotSelect` prop for custom timeslot selection/reservation handling

* Update custom-slot-select-flow.mdx

* Update custom-slot-select-flow.mdx

* refactor: rename handleSlotSelect to handleSlotReservation for clearer intent

* docs: add API reference link for slot reservation api
2025-05-08 15:13:58 +05:30
Udit TakkarandAnik Dhabal Babu 78a34038d2 test: add e2e test for round robin event types (#21159)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-05-08 11:41:21 +05:30
Romit GabaniandTushar Bhatt 5f770a9789 fix: hiding And connect with specific team members option for non org teams (#21083)
* fix: hiding And connect with specific team members option for non org teams

* removed console logs

* added comment

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-05-08 05:09:11 +00:00
devin-ai-integration[bot]andmorgan@cal.com 24a43877ce perf: optimize event type query by using team IDs instead of complex join (#21162)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: morgan@cal.com <morgan@cal.com>
2025-05-07 13:00:49 -07:00
GitHub Actions 4a96be4175 chore: release v5.2.12
Post release / release (push) Canceled after 0s
2025-05-07 19:34:41 +00:00
594bb606b8 perf: Refactor bookings queries for API (#16407)
* perf: Refactor bookings queries for API

* fix syntax

* fixing vars

* Inverted the filter by attendees if

* fixed prisma filter issue

* Defaulting list of emails to empty array

* Only running 2 queries if we have data to filter by

* Reverted changes that have been separated out to other PR

* fix: ts build error

* adds tests + extracts logic for units

* fix integration tests

* fix tests expectes

* Fix test

* switching back

* attempt : fix tests

* test: add test for duplicate bookings (#21165)

* feat: Add tests for booking and duration limits (#21158)

* Add tests for booking and duration limits in getSchedule.test.ts

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: use SchedulingType enum instead of string for schedulingType

Co-Authored-By: keith@cal.com <keith@cal.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keith@cal.com>

* test: add test to ensure no duplicate bookings when merging query results

Co-Authored-By: keith@cal.com <keith@cal.com>

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keith@cal.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* fix: deduplicate bookings when merging results from multiple queries

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: ensure pagination works correctly when merging results from multiple queries (#21167)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keith@cal.com>

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Tushar <tusharbhatt0135@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keith@cal.com>
2025-05-07 11:44:56 -07:00
Omar López 1c088f65c5 chore: Removes around app (#20650) 2025-05-07 14:41:46 -04:00
devin-ai-integration[bot]andkeith@cal.com 7d7a429780 feat: Add tests for booking and duration limits (#21158)
* Add tests for booking and duration limits in getSchedule.test.ts

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: use SchedulingType enum instead of string for schedulingType

Co-Authored-By: keith@cal.com <keith@cal.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keith@cal.com>
2025-05-07 13:06:10 -04:00
GiridharandTushar Bhatt d3228441aa fix:decline-booking-reason-missing (#21144)
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-05-07 21:01:33 +05:30
Morgan 21d6bf1fca fix: api v2 routing form responses flaky tests (#21150) 2025-05-07 16:22:02 +03:00
a4134aa5a3 feat: add 'View Responses' menu item to Routing Form dropdown (#21114)
* feat: add 'View Responses' menu item to Routing Form dropdown

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: use nuqs createSerializer for URL generation

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: move serializer to data-table lib and use proper type constants

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: type errors in serializers and FormActions

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: use parseAsArrayOf with parseAsJson for proper nuqs serialization

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: update to use dataTableQueryParamsSerializer with proper nuqs pattern

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: extract parsers from DataTableProvider to be reused in serializers

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: remove deprecated activeFiltersSerializer and use empty array instead of null

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: only show View Responses menu when form id and teamId exist

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* remove teamId condition

* refactor: improve viewResponses URL generation with cleaner conditional

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* Update packages/features/data-table/lib/parsers.ts

* refactor: make constants in parsers.ts non-exported

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: add explicit type annotations to constants in parsers.ts

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: add explicit type annotations and export DEFAULT_PAGE_SIZE in parsers.ts

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: add explicit type assertions to DEFAULT_ACTIVE_FILTERS and DEFAULT_SORTING

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <eunjae@cal.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-05-07 13:10:21 +00:00
Anik Dhabal Babu 61397e5184 fix: e2e flakes (#20630)
* fix

* Update managed-event-types.e2e.ts

* fix
2025-05-07 18:04:06 +05:30
Somay Chauhan 6143cd60e5 feat: Add booking references API endpoints (#21127)
* feat: Add booking references API endpoints

* refactor: Implement booking-reference service and repositories

* Add description and reorder properties in BookingReference schema

* Refactor booking references service to separate output service
2025-05-07 12:30:53 +00:00
Eunjae Lee ac821f6b13 chore: convert booking_denormalized e2e test to integration test (#21148) 2025-05-07 14:23:25 +02:00
sean-brydon 2bce38fd2a feat(v3): table designs (#21147) 2025-05-07 09:54:36 +00:00
Hariom BalharaandOmar López 390ee79134 Reduce payload size (#21093)
Co-authored-by: Omar López <zomars@me.com>
2025-05-07 03:16:07 +00:00
cfc9f0bfc3 refactor: Consolidate date parameter extraction and booking period check logic (#21142)
Co-authored-by: keith@cal.com <keith@cal.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2025-05-06 16:33:22 -07:00
Morgan 58d5f2a17c fix: advanced text filters getBookings with kysely (#21143) 2025-05-06 21:08:14 +00:00
Anwar Sadath 37f271bd74 fix: delete organization user endpoint not working in api v2 (#21136) 2025-05-06 19:10:55 +00:00
devin-ai-integration[bot]andkeith@cal.com d770cfc65e perf: Optimize team bookings query by fetching data for multiple users at once (#21137)
Co-authored-by: keith@cal.com <keith@cal.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-05-06 11:47:06 -07:00
Eunjae Leeandsean-brydon ab8556790e feat: add denormalized table for BookingTimeStatus view (#20947)
* feat: add denormalized table for BookingTimeStatus

* Update packages/prisma/migrations/20240320000001_add_booking_time_status_denormalized/migration.sql

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

* add more indexes

* improvement

* update schema

* improve indexes

* apply feedback from mrge

* split update trigger for better perf

* remove unnecessary checks

* split migration into two

* less logging

* address feedback

* add updatedAt and userUsername

* remove timeStatus

* add deletion trigger

* clean up denorm table on row deletion at EventType or users

* add timeStatus back as a part of a new view

* keep user and eventType nullable

* rename triggers

* add tests

* clean up migrations

* update tests

* update tests

* update test clean up

* one more try

* similar updates

* use e2e test instead

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-05-06 20:34:45 +02:00
Hariom Balhara 3d89b37cc6 Adding already released embed version (#21033) 2025-05-06 15:19:47 +00:00
GitHub Actions 78b213321c chore: release v5.2.11
Post release / release (push) Canceled after 0s
2025-05-06 14:43:34 +00:00
Carina WollendorferandCarinaWolli 276ba1179f fix: improvements for scheduling Email reminders (#21129)
* improvements cron job

* improvements emailReminderManager

* order bookings for reminders

* fix cleaning up past cancelled reminders

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-05-06 15:23:48 +01:00
Hariom BalharaandAnik Dhabal Babu 0109d166a1 Update delegation-credential.md (#21124)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-05-06 13:29:13 +00:00
Morgan 779c014256 fix: get bookings advanced email and attendee filter (#21125)
* fix: get bookings advanced email and attendee filter

* bump libraries

* bump libraries
2025-05-06 09:28:57 +01:00
sean-brydonandUdit Takkar 073faf9e10 feat: Salesforce Event Type App v3 designs (#21100)
* section component

* more section UI

* wip

* core components styled

* all components using sections

* match icons to figma

* small inputs

* fix select small to be the same as small text

* center flex rows

* enforce inputs to be full spanning grid

* tidy up section container for single inputs

* fix select and i18n

* wip

* install available apps

* raw heading

* app install card

* fix app logos width x height

* fix gap

* match design for other button

* fix prop name

* push i18n string

* fix doc snippets

* fix docs

* chore: remove yarn.lock changes

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2025-05-06 08:21:20 +00:00
GitHub Actions d0e39cf2dd chore: release v5.2.10
Post release / release (push) Canceled after 0s
2025-05-06 02:50:01 +00:00
Benny Joo f9488a1866 fix: theme search param crashing booking page (#21120)
* fix

* fix
2025-05-06 00:31:40 +00:00
MorganandKeith Williams d4a3894a9e chore: get bookings with kysely (#21084)
* chore: get bookings with kysely

* fixup! chore: get bookings with kysely

* replace left joins with inner joins

* add missing filter and count with kysely

* main query done

* kysely use connection string

* fix attendee emails operator

* fix immutability and add attendee email filter

* chore: everything working but date filters

* limit offset

* chore: bump platform libraries

* fix bookingseat / attendee user email filter

* refactor, clean, and subquery updatedAt/createdAt

* fix import kysely types path

* fix missing references and payments fields

* create kysely module in apiv2

* fix before created at filter

* fix types

* update yarn.lock

* update yarn.lock

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-05-05 17:21:15 -07:00
Omar López 91288b41cd refactor: centralize feature flag operations in FeaturesRepository (#21032) 2025-05-05 19:06:41 -04:00
GitHub Actions 1aa2f68e73 chore: release v5.2.9
Post release / release (push) Canceled after 0s
2025-05-05 17:55:27 +00:00
Udit Takkar ad707b0ee4 chore: improve error message (#21112) 2025-05-05 16:41:44 +00:00
Alex van Andel 49a2f4d4f7 chore: Add proton.me to email provider list (#21117) 2025-05-05 16:09:43 +00:00
Benny Joo f576979345 perf: remove unnecessary routes from middleware (#21089)
* perf: Remove unnecessary routes from middleware

* remove tests not needed
2025-05-05 11:47:27 -04:00
Keith Williams b45399ab78 chore: Update CODEOWNERS for booking tRPC handlers (#21118) 2025-05-05 15:40:42 +00:00
306dc5e4fb fix: add back round robin data to insights/router-position (#21116)
* add back all data to router-position

* fix type error

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-05-05 15:30:55 +00:00
Anik Dhabal Babu 605304b93b fix: availability view error (#21115)
* fix: avaiability view error

* Update list.handler.ts
2025-05-05 14:41:17 +00:00
amritandTushar Bhatt d6a17b2b1e feat: booking URL validation to accept links without explicit mention of https:// (#21110)
* fix: trigger lingo.dev by removing duplicate value

* feat: allow urls without an explicit https://

* fixed the test that ensures malformed URLs like "http:/nope" are rejected

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-05-05 12:49:19 +00:00
Yadong (Adam) Zhang 4bee875a8c build(packages/features): resolved type error when building apps/web. (#21094) 2025-05-05 11:20:05 +00:00
Anik Dhabal Babu fe4ae3ae62 fix: {Organizer} placeholder in event title not updating on reschedule (#21034) 2025-05-05 10:22:29 +00:00
Anik Dhabal Babu a245578ff2 fix: meeting link fails when manually reassign (#21106)
* fix: meeting link fails when manually reassign

* update

* Update booking.ts
2025-05-05 09:32:56 +00:00
sean-brydonandEunjae Lee f5ec696e15 fix: button fixes v3 (#20557)
* fix button styling

* add button usage

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-05-05 09:31:14 +00:00
sean-brydonandUdit Takkar ec9384858b move form builder email input away from routing form widget (#21077)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-05-05 09:05:01 +00:00
Eunjae Lee 47613f796e feat: add membership creation date to Organization Member List table (CAL-5406) (#21008)
* feat: add membership creation date to Organization Member List table (CAL-5406)

This reverts commit 5aaca2b7aa.

* fill createdAt
2025-05-05 10:15:30 +02:00
luzpaz 446c780d85 fix: typos in apps/api/v2 (#21105)
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 #21104
2025-05-04 15:58:37 +00:00
Shashwatt 615968c640 fix: add missing 'add' in profile username tip (#21049) (#21097) 2025-05-04 20:13:55 +05:30
Morgan fc89436b6a chore: bump platform libraries (#21101) 2025-05-04 10:13:28 +00:00
Joe Au-Yeung 3c1015cb11 Get correct userId (#21092) 2025-05-03 00:43:05 +00:00
Anik Dhabal Babu 46c513330c test: retry times for api v2 e2e test in CI (#21087)
* test: test retry times for api v2 test

* Update jest.setup-e2e.ts
2025-05-02 19:42:36 +00:00
sean-brydon ef1598faeb fix return (#21088) 2025-05-02 20:06:58 +01:00
Anik Dhabal Babu 42a6903057 fix: wrong info on cancelled email (#21085) 2025-05-02 17:19:19 +00:00
Morgan 0f0c2b8b92 feat: verified resources endpoint [v2] (#21006)
* feat: verified resources endpoint [v2]

* add tests and fixes

* chore: add endpoint throttler decorator and handling

* chore: code review comments

* chore: code review comments

* chore: code review comments

* bump platform libraries

* chore: add tests and fix issues
2025-05-02 13:37:19 +03:00
VijayandTushar Bhatt c400c36306 fix: between operator for attribute matching (#21031)
* update json logic for raqb between operator

* nit

* add tests

* e2e tests

* after merge: updated tests due to latest changes in routingforms v3

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-05-02 10:29:48 +00:00
sean-brydon 6410bd2eaf fix: rf v3 tidy up mobile issues (#21078)
* remove incomplete booking from mobile

* add incomplete booking to dropdown on mobile

* padding
2025-05-02 10:01:07 +00:00
Alex van Andel 2ddc5317b8 fix: disabled sms hanging, by calling resolve() (#21070) 2025-05-02 02:23:42 +00:00
Joe Au-Yeung 6fffe3377d refactor: Replace attendee email with user id in idempotency key (#21058)
* Use userId instead of attendee email

* Only write if booking is accepted

* Create `getBookingRequest` function

* Return already requested booking

* Type fixes

* Type fix

* Type fix

* Pass empty seatReferenceId

* Handle no userId

* Update test
2025-05-02 00:46:19 +01:00
GitHub Actions eedef35d02 chore: release v5.2.8
Post release / release (push) Canceled after 0s
2025-05-01 22:09:04 +00:00
Keith Williams 08d232a816 revert: "feat: Ability to set a locked timezone for event type (#20425)" (#21066)
This reverts commit 03240709dc.
2025-05-01 21:51:20 +00:00
Udit TakkarandSyed Ali Shahbaz d9b3f8657f feat: add setting to disable phone only sms notification (#21038)
* feat: add setting to disable phone only sms notification

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

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

---------

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2025-05-01 18:18:52 +00:00
Eunjae Lee 99dc5837f5 revert: "fix: eventType filter on /insights (#20823)" (#21059)
This reverts commit 45904c07bc.
2025-05-01 15:51:49 +00:00
Tushar Bhatt 5a35540522 feat: show-utm-params (#20733)
* feat:utm

* locale

* yarn.lock

* fix-typecheck

* indentation

* edge-case

* Update apps/web/modules/bookings/views/bookings-single-view.tsx

* Update apps/web/modules/bookings/views/bookings-single-view.tsx
2025-05-01 14:46:42 +00:00
Keith Williams c01fc08f67 chore: Allow Foundation to approve on behalf of Consumer (#21057) 2025-05-01 14:37:35 +00:00
Peer Richelsen 7397722eb3 chore: fixed headline of timelessui (#21051)
* Update page.tsx

* Update apps/ui-playground/app/(home)/page.tsx
2025-05-01 13:23:22 +01:00
d51e03b14b feat: routing v3 design (#20607)
Co-authored-by: Sean Brydon <sean@Seans-Mac-mini.local>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-05-01 10:35:17 +01:00
Anik Dhabal Babu c4af180a5b fix: attendeePhoneNumber not appearing in calendar event (#21035) 2025-05-01 07:48:16 +00:00
Hariom Balhara ee5f1dc879 fix unit test bug (#21046) 2025-05-01 07:05:12 +00:00
Rajiv Sahal 15b43176ff uncomment props (#20818) 2025-05-01 11:47:21 +05:30
Anik Dhabal Babu 03240709dc feat: Ability to set a locked timezone for event type (#20425)
* feat: ability to set locked timezone

* update

* Update update-event-type.input.ts

* Update get-event-type-public.output.ts

* Update documentation.json

* revert

* nit
2025-05-01 11:09:57 +05:30
Romit GabaniandHariom Balhara 7ee451ca02 fix: restricting platform user from creating another regular org (#20929)
* restricting platform user from creating another regular org

* changed as suggested, additional check of membership is accepted

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-05-01 03:59:32 +00:00
Alex van Andel 55b066c475 fix: cal.cache not working (#21044)
* fix: cal.cache not working

* Thanks mrge
2025-05-01 00:42:57 +00:00
Anik Dhabal Babu c52bbc4be3 fix: meeting link fails when host requests to reschedule (#20999)
* fix: meeting link fails when host requests to reschedule

* Update EventManager.ts
2025-04-30 17:18:48 +00:00
Morgan 31b1f31d82 chore: format calendar events to remove oauthClientIds (#21040) 2025-04-30 16:01:34 +01:00
Lauris Skraucis ab1c1bb015 fix: allow only platform organisation or non-platform organisation (#20972)
* fix: platform customers cant subscribe to organizations plan

* fix: organization customers cant subscribe to platform plan
2025-04-30 10:33:01 +02:00
45219f4736 fix: cleanup html reports when pr is merged/closed (#20089)
Co-authored-by: anik@cal.com <anik@cal.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-04-29 18:48:05 +00:00
3296255f80 feat: /skip trial for teams (#20753)
* skip team trials

* add loading state

* update mocks

* cleanup

* add import oops

* fix tests files

---------

Co-authored-by: Sean Brydon <sean@Seans-Mac-mini.local>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2025-04-29 11:48:41 +00:00
Ronit Raj 81b4651d6e fix: UI Bug - ToggleGroup (#20997)
* fix #20986: UI Bug - ToggleGroup

* some changes

* some changes
2025-04-29 03:01:20 +00:00
Hariom Balhara e3bd90c4f2 fix: Handle calendar-cache with Delegation Credentials
Fixes CAL-5372

# Delegation Credentials with CalendarCache.

Following content is a snapshot of the [internal document](https://calendso.slack.com/docs/T08B8KA2BNF/F08L5JYU3V3)



**Problem-1 :** 

CalendarCache needs SelectedCalendar records to work but SelectedCalendar record is only created when a user connects their calendar and then enables some calendar for conflict checking. Because with Delegation, no manual connection is done by any of the members, we need a way to create SelectedCalendar records automatically.

**Problem-2**

CalendarCache connects to credential(regular credential) which doesn’t exist for Delegation Credential scenario. Also, DelegationCredential is common for all the members(different from Credential which is different for different members) of the organization and we need to identify to which user the CalendarCache belongs.  

**Solution for both problems**
- Create credential records for Delegation Credentials as well - Through Cron(new - we could schedule it every 5mins)
- Now create SelectedCalendar  records for those Credential records -  Through another Cron(new - we could schedule it every 5mins)
- Now CalendarCache records will automatically be created for those SelectedCalendar records -existing cron

## Fixed some Delegation Credentials bugs unrelated to calendar-cache
- If DestinationCalendar wasn't set(which is possible only with Delegation Credentials), then Google Meet wasn't used as a conferencing app - [Added a test]
- If no SelectedCalendar is there but Google Calendar connection exists(possible only with Delegation Credential) then we were not doing conflict checking. It is expected to not do it for Regular Credentials, but for Delegation Credential we must check for conflict in that case too [Added a test]
- Earlier if a user has Regular Credential as well as Delegation Credential for the same external id which is the member email(say member1@acme.com) then availability were retrieved twice because we weren't deduplicating credentials as it wasn't a trivial thing to do. Now that is being done.


**Env Variables:**
Note this PR doesn't introduce any new env variable. The existing env variable has been added to .env.example. But if this env variable isn't already set, it must be set.

`CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY={SAME_AS_SET_FOR_V2_API}`

**Deployment Plan:**
1. Add Observability for SelectedCalendar when _error_ field is set
2. Follow https://github.com/calcom/cal.com/blob/calendar-cache-dwd-support/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/delegation-credential/delegation-credential.md#setting-up-delegation-credential-for-google-calendar-api to enable Delegation Credential for i.cal.com
3. Note that to be able to see the option to enable Delegation Credential for an organization, you need to enable `teamFeature` and `feature` for `delegation-credential`

## Automation Tests
- Introduced tests for calendar-cache.repository.ts
   - Tests all methods of the repository
- Added more tests for handleNewBooking/delegation-credential flow.
   - Added test to verify the bug fix when no DestinationCalendar exists and Google Meet should be used still 
- Added more tests for Google Calendar/CalendarService targeting DelegationCredential
- Added more tests for getCalendarsEvents. 
    - To test the new logic of calling getAvailability still if there are no selectedCalendars in case of Delegation Credential
    - Also introduced tests for `getAvailabitlityWithTimezones` which was an existing function but now has some new changes.
- Added tests for deduplication logic in CalendarManager.ts

## How to Test
Enable Calendar Cache and Delegation Credential feature for acme org through `features` and `teamFeatures` tables.
- Enable Delegation Credential for acme org
- Enable atleast 1 calendar for conflict checking for one of the users(say owner1)
- Ensure GOOGLE_WEBHOOK_TOKEN is set in .env file
- Ensure GOOGLE_WEBHOOK_URL is set to ngrok url of webapp in .env file
- Hit cron endpoint `curl http://localhost:3000/api/calendar-cache/cron\?apiKey\={API_KEY}` that would cache the freebusy result for the selected calendars



Followup 
- https://github.com/calcom/cal.com/pull/20698
- https://github.com/calcom/cal.com/pull/18619/files#r2046795643
2025-04-28 18:11:29 -03:00
Joe Au-YeungandOmar López 17f1932467 refactor: Use Salesforce GraphQL for account ownership routing (#20655)
Co-authored-by: Omar López <zomars@me.com>
2025-04-28 20:54:58 +00:00
Anik Dhabal Babu 1b04fe82a7 fix: hide orgnizer email in cal video sidebar (#21011) 2025-04-28 16:34:32 +00:00
Carina WollendorferandCarinaWolli ffb36509ff fix: schedule unscheduled emails with tasker in cron job (#20748)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-04-28 08:56:13 -07:00
Benny Joo dc361f7879 perf: SSR - /event-types and /event-types/[type] (#20952)
* add revalidation

* ssr for event-types

* wip

* wip

* fix types

* fix skeleton

* better error component

* make skeleton better

* no need to export

* call revalidations early

* remove revalidations if not needed

* address comments

* address
2025-04-28 16:58:53 +02:00
Udit Takkar 0b59a5d184 fix: encoding bug (#21004)
* fix: encoding bug

* fix: use escapeValue
2025-04-28 14:56:12 +00:00
Anik Dhabal Babu 54c17eb827 fix: unable to create meeting with ms teams (#20993)
* fix: few details not populate

* fix: unable to create meeting with ms teams

* Update CrmService.ts

* update
2025-04-28 13:41:02 +00:00
Anik Dhabal Babu bd1142ea7a fix: wrong translation value (#17923)
* feat: ability to remove attendees from booking page

* update

* fix
2025-04-28 13:16:18 +00:00
Hariom Balhara 26051833cf fix: Skip already completed org onboarding, as webhook request can come when invoice is paid again next month (#20984)
* there is nothing to do for repeat invoice payment

* there is nothing to do for repeat invoice payment
2025-04-28 18:36:14 +05:30
Somay Chauhan 1d8bc6ca49 feat: added hideOrganizerEmail to api/v2/event-types (#21005)
* feat: added `hideOrganizerEmail` to api/v2/event-types

* updated docs
2025-04-28 15:39:51 +03:00
Rajiv SahalandMorgan fbe8cf8223 fix: cancel platform subscription on dashboard (#20787)
* add new endpoint to cancel user subscription

* remove comments

* add delete button to frontend

* update correct request method

* fixup

* frontend for cancelling subscription

* better warning message

* better function name

* update locales

* better error handling in case stripe api call fails

* rename teamId to organizationId

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-04-28 08:41:25 +01:00
Anik Dhabal Babu ef8b3e83ac fix: few details not populate (#20987) 2025-04-28 04:47:56 +00:00
devin-ai-integration[bot]andhariom@cal.com 9d228a1d96 Remove 30 seats minimum text from organization setup page (#20983)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: hariom@cal.com <hariom@cal.com>
2025-04-28 08:30:03 +05:30
Benny Joo 57df25505d perf: workflows—remove data fetching for metadata (#20734) 2025-04-26 12:21:11 +00:00
Hariom Balhara 31cbea0e23 Do not retrieve already persisted data as well (#20982) 2025-04-26 17:32:08 +05:30
Syed Ali Shahbaz edd80c056c Update sms-manager.ts (#20985) 2025-04-26 11:56:30 +00:00
Hariom Balhara 6ebf0db389 Handle acme.cal.local root path (#20981) 2025-04-26 08:35:23 -03:00
GitHub Actions 6f4e51b859 chore: release v5.2.7
Post release / release (push) Canceled after 0s
2025-04-26 01:52:58 +00:00
Omar López 4f5d0db792 docs: add JSDoc documentation to FeaturesRepository class and methods (#20980) 2025-04-26 01:44:37 +00:00
Keith Williams 8733bcaf7a perf: Add credentialId index to selectedCalendar (#20979) 2025-04-26 01:40:28 +00:00
Keith Williams 5aaca2b7aa revert: "feat: add membership creation date to Organization Member List table (CAL-5406) (#20595)" (#20978)
This reverts commit d21f7ccbc8.
2025-04-26 00:46:49 +00:00
Alex van Andel eacd05bf3b perf: Improve performance of period limits (#20974)
* perf: Improve performance of period limits

* Type fixes

* Fixed correct check for instanceof -> isDayjs
2025-04-25 21:07:18 +01:00
Hariom Balhara d51e0182b5 fix: dont persist last booking response, temporarily (#20975)
* dont persist last booking response

* remove dead code to fix ts error
2025-04-25 23:07:48 +05:30
Benny Joo 220897ba86 chore: better code - migrate getServerSideProps file directly to RSC for /apps (#20926)
* migrate getServerSideProps file directly to RSC for /apps

* fix
2025-04-25 06:41:21 -07:00
Benny Joo 78c0203a45 chore: leverage error.tsx in app router (#20955) 2025-04-25 13:55:52 +01:00
Syed Ali Shahbaz 720fd90066 fix: Don't delete Cal video room upon requesting reschedule (#20969)
* Update requestReschedule.handler.ts

* Update requestReschedule.handler.ts
2025-04-25 08:30:44 -03:00
Omar López ca8ee749a2 fix: double end-date parsing when saving availability (#20963)
* fix: double end-date parsing when saving availablity

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

* fix: cache hits in current and next month

* Update CalendarService.test.ts

---------

Signed-off-by: Omar López <zomars@me.com>
2025-04-24 19:33:55 -03:00
Anik Dhabal Babu ce2d3a4b00 fix: added order by (#20959) 2025-04-24 19:21:21 +00:00
Anik Dhabal Babu 2923684c7c fix: exclude email from replyTo (#20953)
* fix: exclude email from replyTo

* update

* Update scheduleEmailReminders.ts
2025-04-24 19:20:45 +00:00
Alex van Andel 879ae597be fix: Offset is required but not passed everywhere, default to zero. (#20917) 2025-04-24 16:46:19 +01:00
d21f7ccbc8 feat: add membership creation date to Organization Member List table (CAL-5406) (#20595)
* feat: add membership creation date to Organization Member List table (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* feat: add migration for membership creation date (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* feat: make createdAt and updatedAt nullable (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* feat: add updatedAt column to Organization Member List table (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: use type assertion to access createdAt and updatedAt fields (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: display N/A for null date values (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: use proper type assertions for createdAt and updatedAt fields (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: add createdAt and updatedAt to UserTableUser mock in test (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* feat: add PostgreSQL trigger for membership timestamps (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix: use empty string instead of N/A and add translations for column headers (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* add i18n text

* clean up type issue

* feat: add translation keys for column headers (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* disable sort

* remove duplicated i18n texts

* feat: add filters for lastActiveAt, createdAt, and updatedAt (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* support date range filter

* fix date range for end date

* hide columns by default

* revert wrong change

* add missing selects

* fix e2e test

* fix: remove PostgreSQL trigger and let application handle timestamps (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* feat: add application-level timestamp handling for Membership model (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* add more timestamp handling

* refactor: use Prisma's built-in decorators for Membership timestamps (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* refactor: remove application-level timestamp handling in favor of Prisma decorators (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* refactor: remove more application-level timestamp handling in favor of Prisma decorators (CAL-5406)

Co-Authored-By: keith@cal.com <keith@cal.com>

* fix e2e test

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keith@cal.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-04-24 10:48:06 -03:00
Lauris Skraucis 4cc1bc6746 fix: v2 sentry errors (#20902)
Post release / release (push) Canceled after 0s
* refactor: no_available_users_found_error for team event

* fix: hosts_unavailable_for_booking

* fix: Cannot read properties of undefined (reading 'replace')

* fix: Cannot read properties of undefined (reading 'phoneNumber')

* fix: No SelectedCalendar found.

* fix: Cannot read properties of undefined (reading 'length')

* refactor: add bookings errors service
2025-04-24 16:06:37 +03:00
GitHub Actions 49a2c60212 chore: release v5.2.6 2025-04-24 12:55:32 +00:00
Lauris Skraucis 7dfbb42ced docs: global platform event types and custom booking flows (#20940)
* docs: global event types

* docs: custom booking flow
2025-04-24 12:47:09 +00:00
Anik Dhabal Babu 41fafb9e5e fix: Include ordered attendees in booking select query (#20935)
* fix: correct ordering

* another
2025-04-24 12:33:20 +00:00
Benny Joo 6815f2f9c7 perf: server-side fetching for /availability (#20804)
* availability

* refactor

* add loader

* improve

* update

* fix loader

* rename

* satisfying types

* refactor

* remove log

* add CTA to skeleton

* refactor

* fix revalidations

* refactor

* update

* finish
2025-04-24 11:10:54 +02:00
Benny Joo 91ab27e070 feat: add loading component for /apps main page (#20924) 2025-04-24 10:48:03 +02:00
Lauris Skraucis 396ccb1cd2 refactor: v2 automatic team slugs (#20897)
* refactor: automatic team slugs

* refactor: automatic organizations team slugs

* refactor: automatic managed organizations slugs

* docs

* refactor: test
2025-04-24 10:00:01 +02:00
Benny Joo 4e8d5cebca perf: ssr for /settings/organizations/teams/other (#20927) 2025-04-24 09:42:09 +05:30
Benny Joo 80f2e61189 perf: remove next.redirect by going straight to event-types (#20920) 2025-04-23 20:23:50 +00:00
Benny Joo d844fd6ef7 perf: Slim down trpc queries in Settings Layout (#20918)
* slim down settings layout by removing server queries

* add check method

* fix type check

* refactor
2025-04-23 20:13:26 +00:00
Anik Dhabal Babu 513f54f4fe feat: ability to hide organizer email (#20782)
* feat: hide orgainzer email

* fix type error

* update

* update

* Update schema.prisma
2025-04-23 19:58:29 +00:00
Keith Williams 2e6d71f493 fix: Docs for deleting reserved slot (#20922) 2025-04-23 19:58:04 +00:00
GitHub Actions 55488c39f4 chore: release v5.2.5
Post release / release (push) Canceled after 0s
2025-04-23 17:22:16 +00:00
Joe Au-YeungandAnik Dhabal Babu c0e123a917 Pass customReplyTo as a managed event type prop (#20909)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-04-23 17:55:46 +01:00
Morgan ec88f16f1f fix: getBookings max 1 month in the past when no date filters specified (#20911)
* fix: getBookings max 1 month in the past

* bump platform libraries
2025-04-23 13:22:20 -03:00
Benny Joo a957c15062 perf: SSR - /availablilty/[schedule] (#20829)
* ssr for schedule page

* improve skeleton

* improve skeleton

* improve skeleton
2025-04-23 11:31:01 -03:00
Benny Joo b32b31ccdc perf: SSR for /teams (#20824)
* finish

* improve

* refactor

* add revalidations

* /settings/teams is not needed

* refactor

* rename

* redundant type casting
2025-04-23 15:09:25 +01:00
e29f6af540 feat(insights): add progress indicator to download button on /insights and /insights/routing (CAL-5408) (#20631)
* feat(insights): add progress indicator to routing form responses download button (CAL-5408)

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: address PR feedback - remove initial progress update and ensure no decimal parts in percentage

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* feat(insights): add progress indicator to Download component

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* feat: paginate rawData endpoint and implement batch fetching in Download component

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: simplify pagination logic in trpc-router.ts

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* show progress on tooltip

* fix download for /insights

* fix download when there is no data

* feat: replace tooltip progress indicator with toast notification

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: remove duplicate ProgressToast component from showToast.tsx

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: export ProgressToast component from index.ts

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: add missing translation for downloading_data

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* update progress toast implementation

* fix error

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <eunjae@cal.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-04-23 15:59:09 +02:00
Anik Dhabal BabuandJoe Au-Yeung c6cb3423fa feat: custom reply To Email (#20771)
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2025-04-23 06:44:44 -07:00
Eunjae Lee 45904c07bc fix: eventType filter on /insights (#20823)
* fix: eventType filter on /insights

* remove log
2025-04-23 13:42:39 +00:00
Omar López 0623887ff8 refactor: Returns 400 error for ZodErrors (#20831) 2025-04-23 13:47:29 +01:00
Lauris Skraucis f31c45a153 fix: platform org members cannot access their public booking page (#20821)
* fix: platform member event pages

* chore: update slots docs

* refactor: user repository call

* refactor: remove newline
2025-04-23 14:29:26 +03:00
Vijay f5217caef2 fix: caching for sprite.svg (#20832)
* update cache-control header for sprite.svg

* updated regex
2025-04-23 11:18:04 +00:00
1061 changed files with 52395 additions and 9072 deletions
+6 -2
View File
@@ -1,11 +1,15 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": "@changesets/cli/changelog",
"changelog": ["@changesets/changelog-github", { "repo": "calcom/cal.com" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
"ignore": [],
"privatePackages": {
"version": false,
"tag": false
}
}
+17 -7
View File
@@ -159,13 +159,17 @@ SENDGRID_EMAIL=
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
# Sentry
SENTRY_ORG=
SENTRY_PROJECT=
SENTRY_AUTH_TOKEN=
# Used for capturing exceptions and logging messages
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_DEBUG=
SENTRY_DISABLE_CLIENT_SOURCE_MAPS=
SENTRY_DISABLE_SERVER_SOURCE_MAPS=
SENTRY_MAX_SPANS=
SENTRY_SAMPLE_RATE=
SENTRY_TRACES_SAMPLE_RATE=
SENTRY_REPLAYS_SESSION_SAMPLE_RATE=
SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=
# Formbricks Experience Management Integration
NEXT_PUBLIC_FORMBRICKS_HOST_URL=https://app.formbricks.com
@@ -184,6 +188,10 @@ TWILIO_TOKEN=
TWILIO_MESSAGING_SID=
TWILIO_PHONE_NUMBER=
TWILIO_WHATSAPP_PHONE_NUMBER=
TWILIO_WHATSAPP_REMINDER_CONTENT_SID=
TWILIO_WHATSAPP_CANCELLED_CONTENT_SID=
TWILIO_WHATSAPP_RESCHEDULED_CONTENT_SID=
TWILIO_WHATSAPP_COMPLETED_CONTENT_SID=
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
NEXT_PUBLIC_SENDER_ID=
TWILIO_VERIFY_SID=
@@ -198,6 +206,7 @@ NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
STRIPE_TEAM_MONTHLY_PRICE_ID=
NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID=
STRIPE_TEAM_PRODUCT_ID=
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
STRIPE_ORG_MONTHLY_PRICE_ID=
@@ -363,9 +372,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
# ***********************************************************************************************************
# disable setry server source maps
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
# api v2
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
@@ -425,7 +431,7 @@ DIRECTORY_IDS_TO_LOG=
# Read more about it in the README.md
NEXT_PUBLIC_SINGLE_ORG_SLUG=
AKISMET_API_KEY=
IFFY_API_KEY=
## Env variables related to avoiding booking failures
# Request for checking reservation would be attempted to send every these seconds if the request is stale at that time
@@ -443,4 +449,8 @@ NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT=10
NEXT_PUBLIC_HEAD_SCRIPTS=
NEXT_PUBLIC_BODY_SCRIPTS=
DATABASE_CHUNK_SIZE=
DATABASE_CHUNK_SIZE=
# Service Account Encryption Key for encrypting/decrypting service account keys
# You can use: `openssl rand -base64 24` to generate one
CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY=
+5 -5
View File
@@ -1,9 +1,7 @@
/**/package.json @calcom/Foundation
/apps/api/**/* @calcom/Platform @calcom/Foundation
/apps/ui-playground/**/* @calcom/Consumer
/apps/web/components/**/* @calcom/Consumer
/apps/web/modules/**/* @calcom/Consumer
/apps/web/**/layout.tsx @calcom/Foundation
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
/apps/web/lib/**/* @calcom/Foundation
/apps/web/middleware.ts @calcom/Foundation
/deploy/**/* @calcom/Foundation
@@ -26,5 +24,7 @@
/packages/lib/slots.ts @calcom/Foundation
/packages/platform/**/* @calcom/Platform @calcom/Foundation
/packages/prisma/**/* @calcom/Foundation
/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @calcom/Foundation
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
/packages/ui/**/* @calcom/Consumer
/packages/ui/**/* @calcom/Consumer @calcom/Foundation
+37
View File
@@ -0,0 +1,37 @@
name: Create PR containing updated CHANGELOG.md and release packages to NPM once PR is merged
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions: {} # remove permissions and set explicitly under "permissions:" below
jobs:
release:
if: github.repository == 'calcom/cal.com' # prevent this action from running on forks
permissions:
id-token: write # for NPM provenance - developers can verify where and how it was built
contents: write # to create release by changesets/action below
pull-requests: write # to create pull request by changesets/action below
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup node, yarn and install dependencies
uses: ./.github/actions/yarn-install
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
version: yarn changesets-version
publish: yarn changesets-release
commit: "chore: version packages" # commit message
title: "chore: version packages" # PR title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+37
View File
@@ -0,0 +1,37 @@
on:
workflow_call:
pull_request:
types: [closed]
permissions:
contents: write
issues: write
pull-requests: write
jobs:
cleanup-report:
runs-on: ubuntu-latest
steps:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
with:
repository: calcom/test-results
ref: gh-pages
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Set Git User
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Cleanup HTML reports on PR close/merge
env:
BRANCH_REPORTS_DIR: reports/${{ github.event.pull_request.head.ref }}
run: |
if [ -d "$BRANCH_REPORTS_DIR" ]; then
rm -rf "$BRANCH_REPORTS_DIR"
git add .
git commit -m "workflow: remove all reports for branch ${{ github.event.pull_request.head.ref }} (PR closed/merged)"
git push
else
echo "No reports found for branch ${{ github.event.pull_request.head.ref }}. Nothing to clean up."
fi
+23
View File
@@ -0,0 +1,23 @@
name: Cron - checkSmsPrices
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 minute” (see https://crontab.guru)
- cron: "* * * * *"
jobs:
cron-checkSmsPrices:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_API_KEY }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/checkSmsPrices \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
+11
View File
@@ -219,6 +219,17 @@ jobs:
uses: ./.github/workflows/publish-report.yml
secrets: inherit
cleanup-report:
name: Cleanup HTML report
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && (github.event.pull_request.merged == true || github.event.pull_request.state == 'closed') }}
permissions:
contents: write
issues: write
pull-requests: write
needs: [changes, check-label, publish-report]
uses: ./.github/workflows/cleanup-report.yml
secrets: inherit
required:
needs:
[
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- calcom-monorepo
View File
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
View File
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+6
View File
@@ -0,0 +1,6 @@
undecided:
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
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"
+189
View File
@@ -0,0 +1,189 @@
# Cal.com Permission Documentation
This document maps existing role-based permission checks to the new PBAC (Permission-Based Access Control) system's permission strings in the format `resource.action`.
## Permissions by Resource
### Team Permissions
| Permission String | Description | File Path | Line |
|------------------|-------------|-----------|------|
| team.create | Create teams | [packages/trpc/server/routers/viewer/teams/create.handler.ts](packages/trpc/server/routers/viewer/teams/create.handler.ts) | 55-57 |
| team.update | Update team settings | [packages/trpc/server/routers/viewer/teams/update.handler.ts](packages/trpc/server/routers/viewer/teams/update.handler.ts) | 18 |
| team.changeMemberRole | Change role of team members | [packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts](packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts) | 18-21 |
| team.remove | Remove members from team | [packages/trpc/server/routers/viewer/teams/removeMember.handler.ts](packages/trpc/server/routers/viewer/teams/removeMember.handler.ts) | 35-36, 51-55 |
| team.invite | Invite members to team | [packages/trpc/server/routers/viewer/teams/invite.handler.ts](packages/trpc/server/routers/viewer/teams/invite.handler.ts) | - |
### Event Type Permissions
| Permission String | Description | File Path | Line |
|------------------|-------------|-----------|------|
| eventType.create | Create event types | [packages/trpc/server/routers/viewer/eventTypes/create.handler.ts](packages/trpc/server/routers/viewer/eventTypes/create.handler.ts) | - |
| eventType.update | Update event types | [packages/trpc/server/routers/viewer/eventTypes/update.handler.ts](packages/trpc/server/routers/viewer/eventTypes/update.handler.ts) | 162-164 |
| eventType.delete | Delete event types | [packages/trpc/server/routers/viewer/eventTypes/delete.handler.ts](packages/trpc/server/routers/viewer/eventTypes/delete.handler.ts) | 13-31 |
### Booking Permissions
| Permission String | Description | File Path | Line |
|------------------|-------------|-----------|------|
| booking.read | Read booking details | [packages/trpc/server/routers/viewer/bookings/get.handler.ts](packages/trpc/server/routers/viewer/bookings/get.handler.ts) | 95-107, 132-134 |
| booking.readTeamBookings | Read team bookings | [packages/trpc/server/routers/viewer/bookings/get.handler.ts](packages/trpc/server/routers/viewer/bookings/get.handler.ts) | 240-252 |
| booking.readOrgBookings | Read organization bookings | [packages/trpc/server/routers/viewer/bookings/get.handler.ts](packages/trpc/server/routers/viewer/bookings/get.handler.ts) | 240-252 |
### Organization Permissions
| Permission String | Description | File Path | Line |
|------------------|-------------|-----------|------|
| organization.read | Read organization details | [packages/trpc/server/routers/viewer/organizations/get.handler.ts](packages/trpc/server/routers/viewer/organizations/get.handler.ts) | - |
| organization.listMembers | List organization members | [packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts](packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts) | 68-76 |
| organization.create | Create organization | [packages/trpc/server/routers/viewer/organizations/create.handler.ts](packages/trpc/server/routers/viewer/organizations/create.handler.ts) | - |
### API Key Permissions
| Permission String | Description | File Path | Line |
|------------------|-------------|-----------|------|
| apiKey.create | Create API keys | [packages/trpc/server/routers/viewer/apiKeys/create.handler.ts](packages/trpc/server/routers/viewer/apiKeys/create.handler.ts) | 25-26 |
| apiKey.findKeyOfType | Find API keys by type | [packages/trpc/server/routers/viewer/apiKeys/findKeyOfType.handler.ts](packages/trpc/server/routers/viewer/apiKeys/findKeyOfType.handler.ts) | 18-19 |
## Helper Functions and Utilities
The following helper functions are commonly used for permission checks:
| Function | Description | File Path |
|----------|-------------|-----------|
| isTeamAdmin | Checks if user is admin or owner of a team | [packages/lib/server/queries/teams/index.ts](packages/lib/server/queries/teams/index.ts#L385-L405) |
| isTeamOwner | Checks if user is owner of a team | [packages/lib/server/queries/teams/index.ts](packages/lib/server/queries/teams/index.ts#L407-L416) |
| isTeamMember | Checks if user is a member of a team | [packages/lib/server/queries/teams/index.ts](packages/lib/server/queries/teams/index.ts#L418-L426) |
| canEditEntity | Checks if user can edit an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L13-L22) |
| canAccessEntity | Checks if user can access an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L24-L34) |
| getEntityPermissionLevel | Gets permission level for an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L36-L73) |
| canCreateEntity | Checks if user can create an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L102-L115) |
| withRoleCanCreateEntity | Checks if role allows entity creation | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L119-L121) |
## Permission Level Enums
The codebase uses several enums to define permission levels:
### MembershipRole Enum
```typescript
enum MembershipRole {
OWNER = "OWNER",
ADMIN = "ADMIN",
MEMBER = "MEMBER"
}
```
### Entity Permission Level Enum
```typescript
enum ENTITY_PERMISSION_LEVEL {
NONE,
USER_ONLY_WRITE,
TEAM_READ_ONLY,
TEAM_WRITE
}
```
## Common Permission Patterns
1. **Team Admin/Owner Check**: Many operations require the user to be a team admin or owner.
```typescript
if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
```
2. **Team Owner Check**: Some operations (like changing an owner's role) require the user to be a team owner.
```typescript
if (input.role === MembershipRole.OWNER && !(await isTeamOwner(ctx.user?.id, input.teamId)))
throw new TRPCError({ code: "UNAUTHORIZED" });
```
3. **Organization Admin Check**: Operations within an organization require the user to be an organization admin.
```typescript
if (user.profile?.organizationId && !user.organization.isOrgAdmin) {
throw new TRPCError({ code: "FORBIDDEN", message: "org_admins_can_create_new_teams" });
}
```
4. **Entity Permission Check**: Entity operations use permission level checks.
```typescript
const permissionLevel = await getEntityPermissionLevel(entity, userId);
return permissionLevel === ENTITY_PERMISSION_LEVEL.TEAM_WRITE ||
permissionLevel === ENTITY_PERMISSION_LEVEL.USER_ONLY_WRITE;
```
5. **Booking Access Control**: Complex permission checks for retrieving bookings based on user roles and team/organization membership.
```typescript
const membershipIdsWhereUserIsAdminOwner = (
await prisma.membership.findMany({
where: {
userId: user.id,
role: {
in: ["ADMIN", "OWNER"],
},
},
select: {
id: true,
},
})
).map((membership) => membership.id);
```
## Migration to PBAC
When migrating to the new PBAC system, these existing role-based checks should be replaced with permission string checks using the `permissionMatches` function:
```typescript
import { permissionMatches } from "@calcom/features/pbac/types/permission-registry";
// Instead of:
if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
// Use:
if (!permissionMatches("team.update", userPermissions)) throw new TRPCError({ code: "UNAUTHORIZED" });
```
## Permission String Alternatives for Helper Functions
The following table provides permission string alternatives for common helper functions:
| Helper Function | Permission String Alternative | Description |
|-----------------|-------------------------------|-------------|
| isTeamAdmin | `team.*` | Grants all team permissions |
| isTeamAdmin | `team.update` | Update team settings |
| isTeamAdmin | `team.invite` | Invite team members |
| isTeamAdmin | `team.remove` | Remove team members |
| isTeamOwner | `team.changeMemberRole` | Change role of team members |
| isTeamOwner | `team.delete` | Delete team |
| isTeamMember | `team.read` | Read-only access to team |
## Entity Permission Functions and Resources
The following functions are used to check permissions for entities with userId and teamId properties:
| Function | Description | Permission String Alternative |
|----------|-------------|-------------------------------|
| canEditEntity | Checks if user can edit an entity | `{resource}.update` |
| canAccessEntity | Checks if user can access an entity | `{resource}.read` |
| getEntityPermissionLevel | Gets permission level for an entity | N/A - Implementation detail |
| canCreateEntity | Checks if user can create an entity | `{resource}.create` |
### Resources Used with Entity Permission Functions
| Resource | canEditEntity Usage | canAccessEntity Usage | Permission String |
|----------|-------------------|---------------------|------------------|
| routingForm | [packages/app-store/routing-forms/trpc/forms.handler.ts](packages/app-store/routing-forms/trpc/forms.handler.ts#L72) | [packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts](packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts#L78) | `routingForm.update`, `routingForm.read` |
| routingForm | [packages/app-store/routing-forms/trpc/formMutation.handler.ts](packages/app-store/routing-forms/trpc/formMutation.handler.ts#L315) | | `routingForm.update` |
| routingForm | [packages/app-store/routing-forms/api/responses/[formId].ts](packages/app-store/routing-forms/api/responses/[formId].ts#L96) | | `routingForm.update` |
These functions can be used with any entity that has userId and teamId properties. When migrating to PBAC, replace these checks with appropriate permission string checks:
```typescript
// Instead of:
if (!(await canEditEntity(form, user.id))) throw new TRPCError({ code: "UNAUTHORIZED" });
// Use:
if (!permissionMatches("routingForm.update", userPermissions)) throw new TRPCError({ code: "UNAUTHORIZED" });
```
This allows for more granular permission control and the creation of custom roles with specific permissions.
+15
View File
@@ -0,0 +1,15 @@
Possible issue:
- We won't be able to use already built calendar-cache when Delegation Credential is enabled because existing CalendarCache entries don't have userId set.
## Approach of always using SelectedCalendar.credentialId for CalendarCache even for DelegationCredentials
- Delete Credentials when DelegationCredential is disabled
TO Test
- New members beyond the batch size are processed
+8
View File
@@ -646,6 +646,14 @@ following
15. Create a verify service
16. Copy Verify Service SID to your `.env` file into the `TWILIO_VERIFY_SID` field
## Changesets
We use changesets to generate changelogs and publish public packages (packages with `private: true` are ignored).
An example of good readme is [atoms readme](https://github.com/calcom/cal.com/blob/main/packages/platform/atoms/README.md). Every public package must:
1. Follow semantic versioning when using changesets.
2. Mark breaking changes using `❗️Breaking change`
<!-- LICENSE -->
## License
@@ -0,0 +1,37 @@
/**
* Constructs the WHERE clause for Prisma booking findMany operation.
*
* @param userId - The ID of the user making the request. This is used to filter bookings where the user is either the host or an attendee.
* @param attendeeEmails - An array of emails provided in the request for filtering bookings by attendee emails, used in case of Admin calls.
* @param userIds - An array of user IDs to be included in the filter. Defaults to an empty array, and an array of user IDs in case of Admin call containing it.
* @param userEmails - An array of user emails to be included in the filter if it is an Admin call and contains userId in query parameter. Defaults to an empty array.
*
* @returns An object that represents the WHERE clause for the findMany/findUnique operation.
*/
export function buildWhereClause(userId: number | null, attendeeEmails: string[], userIds: number[] = []) {
const filterByAttendeeEmails = attendeeEmails.length > 0;
const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : !!userId ? { userId } : {};
let whereClause = {};
if (filterByAttendeeEmails) {
whereClause = {
AND: [
userFilter,
{
attendees: {
some: {
email: { in: attendeeEmails },
},
},
},
],
};
} else {
whereClause = {
...userFilter,
};
}
return whereClause;
}
+1 -1
View File
@@ -30,7 +30,7 @@
"@calcom/lib": "*",
"@calcom/prisma": "*",
"@calcom/trpc": "*",
"@sentry/nextjs": "^8.8.0",
"@sentry/nextjs": "^9.15.0",
"bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0",
"next": "^13.5.9",
+79 -57
View File
@@ -1,4 +1,4 @@
import type { Prisma } from "@prisma/client";
import type { Prisma, Booking } from "@prisma/client";
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
@@ -6,6 +6,7 @@ import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { withMiddleware } from "~/lib/helpers/withMiddleware";
import { buildWhereClause } from "~/lib/utils/bookings/get/buildWhereClause";
import {
getAccessibleUsers,
retrieveOrgScopedAccessibleUsers,
@@ -151,58 +152,6 @@ type GetAdminArgsType = {
userId: number;
};
/**
* Constructs the WHERE clause for Prisma booking findMany operation.
*
* @param userId - The ID of the user making the request. This is used to filter bookings where the user is either the host or an attendee.
* @param attendeeEmails - An array of emails provided in the request for filtering bookings by attendee emails, used in case of Admin calls.
* @param userIds - An array of user IDs to be included in the filter. Defaults to an empty array, and an array of user IDs in case of Admin call containing it.
* @param userEmails - An array of user emails to be included in the filter if it is an Admin call and contains userId in query parameter. Defaults to an empty array.
*
* @returns An object that represents the WHERE clause for the findMany/findUnique operation.
*/
function buildWhereClause(
userId: number | null,
attendeeEmails: string[],
userIds: number[] = [],
userEmails: string[] = []
) {
const filterByAttendeeEmails = attendeeEmails.length > 0;
const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : !!userId ? { userId } : {};
let whereClause = {};
if (filterByAttendeeEmails) {
whereClause = {
AND: [
userFilter,
{
attendees: {
some: {
email: { in: attendeeEmails },
},
},
},
],
};
} else {
whereClause = {
OR: [
userFilter,
{
attendees: {
some: {
email: { in: userEmails },
},
},
},
],
};
}
return whereClause;
}
export async function handler(req: NextApiRequest) {
const {
userId,
@@ -238,6 +187,7 @@ export async function handler(req: NextApiRequest) {
? [queryFilterForAttendeeEmails.attendeeEmail]
: [];
const filterByAttendeeEmails = attendeeEmails.length > 0;
let userEmailsToFilterBy: string[] = [];
/** Only admins can query other users */
if (isSystemWideAdmin) {
@@ -251,7 +201,8 @@ export async function handler(req: NextApiRequest) {
userId,
};
const { userId: argUserId, userIds, userEmails } = await handleSystemWideAdminArgs(systemWideAdminArgs);
args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails);
userEmailsToFilterBy = userEmails;
args.where = buildWhereClause(argUserId, attendeeEmails, userIds);
}
} else if (isOrganizationOwnerOrAdmin) {
let requestedUserIds = [userId];
@@ -265,7 +216,8 @@ export async function handler(req: NextApiRequest) {
userId,
};
const { userId: argUserId, userIds, userEmails } = await handleOrgWideAdminArgs(orgWideAdminArgs);
args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails);
userEmailsToFilterBy = userEmails;
args.where = buildWhereClause(argUserId, attendeeEmails, userIds);
} else {
const user = await prisma.user.findUnique({
where: { id: userId },
@@ -276,7 +228,7 @@ export async function handler(req: NextApiRequest) {
if (!user) {
throw new HttpError({ message: "User not found", statusCode: 404 });
}
args.where = buildWhereClause(userId, attendeeEmails, [], []);
args.where = buildWhereClause(userId, attendeeEmails, []);
}
if (dateFrom) {
@@ -317,7 +269,77 @@ export async function handler(req: NextApiRequest) {
}
}
const data = await prisma.booking.findMany(args);
let data: Booking[] = [];
if (!filterByAttendeeEmails && userEmailsToFilterBy.length > 0) {
const queryOne = prisma.booking.findMany(args);
const whereClauseForQueryTwo: Prisma.BookingWhereInput = {
attendees: {
some: {
email: { in: userEmailsToFilterBy },
},
},
};
if (dateFrom) {
whereClauseForQueryTwo.startTime = { gte: dateFrom };
}
if (dateTo) {
whereClauseForQueryTwo.endTime = { lte: dateTo };
}
if (status === "upcoming") {
whereClauseForQueryTwo.startTime = { gte: new Date().toISOString() };
}
const argsForQueryTwo: Prisma.BookingFindManyArgs = {
...args,
where: whereClauseForQueryTwo,
};
const queryTwo = prisma.booking.findMany(argsForQueryTwo);
const [resultOne, resultTwo] = await Promise.all([queryOne, queryTwo]);
const bookingMap = new Map();
[...resultOne, ...resultTwo].forEach((booking) => {
bookingMap.set(booking.id, booking);
});
const dedupedResults = Array.from(bookingMap.values());
if (args.orderBy) {
let sortField: keyof Booking;
let sortDirection: "asc" | "desc" = "asc";
if (typeof args.orderBy === "object" && !Array.isArray(args.orderBy)) {
const orderByKey = Object.keys(args.orderBy)[0] as keyof typeof args.orderBy;
sortField = orderByKey as keyof Booking;
sortDirection = args.orderBy[orderByKey] as "asc" | "desc";
} else {
sortField = "id";
}
const sortOrder = sortDirection === "desc" ? -1 : 1;
dedupedResults.sort((a, b) => {
const aValue = a[sortField];
const bValue = b[sortField];
if (aValue < bValue) return -1 * sortOrder;
if (aValue > bValue) return 1 * sortOrder;
return 0;
});
}
if (args.take !== undefined && args.skip !== undefined) {
data = dedupedResults.slice(args.skip, args.skip + args.take);
} else {
data = dedupedResults;
}
} else {
data = await prisma.booking.findMany(args);
}
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
}
@@ -185,7 +185,12 @@ async function verifyCredentialsAndGetId({
currentCredentialId: number | null;
}) {
if (parsedBody.integration && parsedBody.externalId) {
const calendarCredentials = getCalendarCredentialsWithoutDelegation(userCredentials);
const calendarCredentials = getCalendarCredentialsWithoutDelegation(
userCredentials.map((cred) => ({
...cred,
delegationCredentialId: null,
}))
);
const { connectedCalendars } = await getConnectedCalendars(
calendarCredentials,
+6 -1
View File
@@ -24,7 +24,12 @@ import { membershipCreateBodySchema, schemaMembershipPublic } from "~/lib/valida
*/
async function postHandler(req: NextApiRequest) {
const data = membershipCreateBodySchema.parse(req.body);
const args: Prisma.MembershipCreateArgs = { data };
const args: Prisma.MembershipCreateArgs = {
data: {
createdAt: new Date(),
...data,
},
};
await checkPermissions(req);
-1
View File
@@ -1 +0,0 @@
export {};
@@ -2,6 +2,7 @@ import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, it } from "vitest";
import { ZodError } from "zod";
import prisma from "@calcom/prisma";
@@ -206,8 +207,155 @@ describe("GET /api/bookings", async () => {
console.log("bookings=>", responseData.bookings);
responseData.bookings.forEach((booking) => {
if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1");
if (booking.id === 19) expect(booking.eventType?.team).toBe(null);
if (booking.id === 19) {
// The team field can be either null or undefined due to nullish() in the schema
expect(booking.eventType?.team === null || booking.eventType?.team === undefined).toBe(true);
}
});
});
});
describe("Date filtering", () => {
it("filters bookings by dateFrom", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
dateFrom: "2023-01-01T00:00:00Z",
},
pagination: DefaultPagination,
});
req.userId = proUser.id;
const responseData = await handler(req);
responseData.bookings.forEach((booking) => {
expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(
new Date("2023-01-01T00:00:00Z").getTime()
);
});
});
it("filters bookings by dateTo", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
dateTo: "2024-12-31T23:59:59Z",
},
pagination: DefaultPagination,
});
req.userId = proUser.id;
const responseData = await handler(req);
responseData.bookings.forEach((booking) => {
expect(new Date(booking.endTime).getTime()).toBeLessThanOrEqual(
new Date("2024-12-31T23:59:59Z").getTime()
);
});
});
});
describe("Sorting and ordering", () => {
it("sorts bookings by createdAt in descending order", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
sortBy: "createdAt",
order: "desc",
},
pagination: DefaultPagination,
});
req.userId = proUser.id;
const responseData = await handler(req);
const timestamps = responseData.bookings.map((b) => new Date(b.createdAt).getTime());
expect(timestamps).toEqual([...timestamps].sort((a, b) => b - a));
});
});
describe("Multiple attendee email filtering", () => {
it("filters bookings by multiple attendee emails", async () => {
const attendeeEmails = ["test1@example.com", "test2@example.com"];
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
attendeeEmail: attendeeEmails,
},
pagination: DefaultPagination,
});
req.userId = proUser.id;
const responseData = await handler(req);
responseData.bookings.forEach((booking) => {
const bookingAttendeeEmails = booking.attendees?.map((a) => a.email);
expect(bookingAttendeeEmails?.some((email) => attendeeEmails.includes(email))).toBe(true);
});
});
});
describe("Error cases", () => {
it("throws error for invalid status parameter", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
status: "invalid_status",
},
pagination: DefaultPagination,
});
req.userId = proUser.id;
await expect(handler(req)).rejects.toThrow(ZodError);
});
});
describe("Result merging", () => {
it("does not return duplicate bookings when merging results from multiple queries", async () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
const testUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
const testUserBooking = await prisma.booking.findFirstOrThrow({
where: { userId: testUser.id },
include: { attendees: true },
});
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
userId: testUser.id, // This will make userEmailsToFilterBy contain the test user's email
},
pagination: {
take: 100,
skip: 0,
},
});
req.isSystemWideAdmin = true;
req.userId = adminUser.id;
const responseData = await handler(req);
const bookingIds = responseData.bookings.map((booking) => booking.id);
const uniqueBookingIds = new Set(bookingIds);
expect(uniqueBookingIds.size).toBe(bookingIds.length);
if (uniqueBookingIds.size !== bookingIds.length) {
const counts = bookingIds.reduce((acc, id) => {
acc[id] = (acc[id] || 0) + 1;
return acc;
}, {} as Record<number, number>);
const duplicates = Object.entries(counts)
.filter(([_, count]) => count > 1)
.map(([id]) => id);
console.log(`Found duplicate booking IDs: ${duplicates.join(", ")}`);
}
});
});
});
@@ -0,0 +1,75 @@
import { describe, it, expect } from "vitest";
import { buildWhereClause } from "~/lib/utils/bookings/get/buildWhereClause";
describe("buildWhereClause", () => {
it("should return empty object when no filters are provided", () => {
const result = buildWhereClause(null, [], []);
expect(result).toEqual({});
});
it("should filter by userId when only userId is provided", () => {
const userId = 123;
const result = buildWhereClause(userId, [], []);
expect(result).toEqual({ userId: 123 });
});
it("should filter by userIds when userIds array is provided", () => {
const userIds = [1, 2, 3];
const result = buildWhereClause(null, [], userIds);
expect(result).toEqual({ userId: { in: [1, 2, 3] } });
});
it("should filter by attendee emails when provided", () => {
const attendeeEmails = ["test@example.com", "user@example.com"];
const result = buildWhereClause(null, attendeeEmails, []);
expect(result).toEqual({
AND: [
{},
{
attendees: {
some: {
email: { in: ["test@example.com", "user@example.com"] },
},
},
},
],
});
});
it("should combine userId and attendee email filters", () => {
const userId = 123;
const attendeeEmails = ["test@example.com"];
const result = buildWhereClause(userId, attendeeEmails, []);
expect(result).toEqual({
AND: [
{ userId: 123 },
{
attendees: {
some: {
email: { in: ["test@example.com"] },
},
},
},
],
});
});
it("should combine userIds and attendee email filters", () => {
const userIds = [1, 2, 3];
const attendeeEmails = ["test@example.com"];
const result = buildWhereClause(null, attendeeEmails, userIds);
expect(result).toEqual({
AND: [
{ userId: { in: [1, 2, 3] } },
{
attendees: {
some: {
email: { in: ["test@example.com"] },
},
},
},
],
});
});
});
+2
View File
@@ -112,6 +112,8 @@ $ yarn run test:cov
### Guards
1. In case a guard would return "false" for "canActivate" instead throw ForbiddenException with an error message containing guard name and the error.
2. In case a guard would return "false" for "canActivate" DO NOT cache the result in redis, because we don't want that someone is forbidden, updates whatever was the problem, and then has to wait for cache to expire. We only cache in redis guard results where "canAccess" is "true".
3. If you use ApiAuthGuard but want that only specific auth method is allowed, for example, api key, then you also need to add `@ApiAuthGuardOnlyAllow(["API_KEY"])` under the `@UseGuards(ApiAuthGuard)`. Shortly, use `ApiAuthGuardOnlyAllow` to specify which auth methods are allowed by `ApiAuthGuard`. If `ApiAuthGuardOnlyAllow` is not used or nothing is passed to it or empty array it means that
all auth methods are allowed.
## Support
+1
View File
@@ -11,6 +11,7 @@
"^.+\\.(t|j)s$": "ts-jest"
},
"setupFiles": ["<rootDir>/test/setEnvVars.ts", "jest-date-mock"],
"setupFilesAfterEnv": ["<rootDir>/test/jest.setup-e2e.ts"],
"reporters": ["default", "jest-summarizing-reporter"],
"workerIdleMemoryLimit": "512MB",
"maxWorkers": 8
+1 -2
View File
@@ -3,7 +3,6 @@
"version": "0.0.1",
"description": "Platform API for Cal.com",
"author": "Cal.com Inc.",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "yarn dev:build && nest build",
@@ -38,7 +37,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.174",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.208",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -9,6 +9,7 @@ import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { BillingModule } from "@/modules/billing/billing.module";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { KyselyModule } from "@/modules/kysely/kysely.module";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
@@ -23,6 +24,7 @@ import { Module } from "@nestjs/common";
@Module({
imports: [
PrismaModule,
KyselyModule,
RedisModule,
TokensModule,
BillingModule,
@@ -13,6 +13,7 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { BillingService } from "@/modules/billing/services/billing.service";
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
@@ -99,6 +100,7 @@ export class BookingsController_2024_04_15 {
constructor(
private readonly oAuthFlowService: OAuthFlowService,
private readonly prismaReadService: PrismaReadService,
private readonly kyselyReadService: KyselyReadService,
private readonly oAuthClientRepository: OAuthClientRepository,
private readonly billingService: BillingService,
private readonly config: ConfigService,
@@ -129,6 +131,7 @@ export class BookingsController_2024_04_15 {
ctx: {
user: { email: user.email, id: user.id, orgId: profile?.organizationId },
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
kysely: this.kyselyReadService.kysely,
},
});
@@ -0,0 +1,27 @@
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
import type { Prisma } from "@prisma/client";
@Injectable()
export class BookingReferencesRepository_2024_08_13 {
constructor(private readonly dbRead: PrismaReadService) {}
async getBookingReferences(bookingId: number, filter?: BookingReferencesFilterInput_2024_08_13) {
const whereClause: Prisma.BookingReferenceWhereInput = { bookingId };
if (filter?.type) {
whereClause.type = filter.type;
}
return this.dbRead.prisma.bookingReference.findMany({
where: whereClause,
select: {
type: true,
uid: true,
id: true,
externalCalendarId: true,
},
});
}
}
@@ -1,7 +1,11 @@
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
@@ -16,6 +20,7 @@ import { BillingModule } from "@/modules/billing/billing.module";
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { KyselyModule } from "@/modules/kysely/kysely.module";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
@@ -35,6 +40,7 @@ import { Module } from "@nestjs/common";
@Module({
imports: [
PrismaModule,
KyselyModule,
RedisModule,
TokensModule,
BillingModule,
@@ -55,6 +61,7 @@ import { Module } from "@nestjs/common";
BookingsService_2024_08_13,
InputBookingsService_2024_08_13,
OutputBookingsService_2024_08_13,
OutputBookingReferencesService_2024_08_13,
BookingsRepository_2024_08_13,
EventTypesRepository_2024_06_14,
BookingSeatRepository,
@@ -67,6 +74,9 @@ import { Module } from "@nestjs/common";
SelectedCalendarsRepository,
OrganizationsTeamsRepository,
OrganizationsRepository,
ErrorsBookingsService_2024_08_13,
BookingReferencesService_2024_08_13,
BookingReferencesRepository_2024_08_13,
],
controllers: [BookingsController_2024_08_13],
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
@@ -1,15 +1,20 @@
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
import { BookingReferencesOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/booking-references.output";
import { CalendarLinksOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { UsersService } from "@/modules/users/services/users.service";
@@ -82,7 +87,8 @@ export class BookingsController_2024_08_13 {
constructor(
private readonly bookingsService: BookingsService_2024_08_13,
private readonly usersService: UsersService
private readonly usersService: UsersService,
private readonly bookingReferencesService: BookingReferencesService_2024_08_13
) {}
@Post("/")
@@ -176,7 +182,7 @@ export class BookingsController_2024_08_13 {
): Promise<GetBookingsOutput_2024_08_13> {
const profile = this.usersService.getUserMainProfile(user);
const bookings = await this.bookingsService.getBookings(queryParams, {
const { bookings, pagination } = await this.bookingsService.getBookings(queryParams, {
email: user.email,
id: user.id,
orgId: profile?.organizationId,
@@ -185,6 +191,7 @@ export class BookingsController_2024_08_13 {
return {
status: SUCCESS_STATUS,
data: bookings,
pagination,
};
}
@@ -386,4 +393,30 @@ export class BookingsController_2024_08_13 {
data: calendarLinks,
};
}
@Get("/:bookingUid/references")
@PlatformPlan("SCALE")
@UseGuards(ApiAuthGuard, BookingUidGuard)
@Permissions([BOOKING_READ])
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Get 'Booking References' for a booking",
})
@HttpCode(HttpStatus.OK)
async getBookingReferences(
@Param("bookingUid") bookingUid: string,
@GetUser("id") userId: number,
@Query() filter: BookingReferencesFilterInput_2024_08_13
): Promise<BookingReferencesOutput_2024_08_13> {
const bookingReferences = await this.bookingReferencesService.getBookingReferences(
bookingUid,
userId,
filter
);
return {
status: SUCCESS_STATUS,
data: bookingReferences,
};
}
}
@@ -55,6 +55,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
let eventTypeId: number;
let seatedEvent: EventType;
let eventTypeWithBookingFields: EventType;
let eventTypeWithBookingFielsCustom: 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()}`;
@@ -331,6 +332,313 @@ describe("Bookings Endpoints 2024-08-13", () => {
user.id
);
eventTypeWithBookingFielsCustom = await eventTypesRepositoryFixture.create(
{
title: "test booking field types",
slug: "test-booking-field-types",
length: 30,
bookingFields: [
{
name: "name",
type: "name",
sources: [{ id: "default", type: "default", label: "Default" }],
variant: "fullName",
editable: "system",
required: true,
defaultLabel: "your_name",
variantsConfig: {
variants: {
fullName: {
fields: [
{
name: "fullName",
type: "text",
required: true,
},
],
},
firstAndLastName: {
fields: [
{
name: "firstName",
type: "text",
label: "",
required: true,
placeholder: "",
},
{
name: "lastName",
type: "text",
label: "",
required: false,
placeholder: "",
},
],
},
},
},
},
{
name: "email",
type: "email",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system",
required: true,
defaultLabel: "email_address",
},
{
name: "location",
type: "radioInput",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system",
required: false,
defaultLabel: "location",
getOptionsAt: "locations",
optionsInputs: {
phone: { type: "phone", required: true, placeholder: "" },
somewhereElse: { type: "text", required: true, placeholder: "" },
attendeeInPerson: { type: "address", required: true, placeholder: "" },
},
hideWhenJustOneOption: true,
},
{
name: "test-phone",
type: "phone",
label: "test-phone",
hidden: false,
sources: [{ id: "user", type: "user", label: "User", fieldRequired: true }],
editable: "user",
required: false,
disableOnPrefill: false,
},
{
name: "test-address",
type: "address",
label: "test-address",
hidden: false,
sources: [{ id: "user", type: "user", label: "User", fieldRequired: true }],
editable: "user",
required: false,
disableOnPrefill: false,
},
{
name: "test-text",
type: "text",
label: "test-text",
hidden: false,
sources: [{ id: "user", type: "user", label: "User", fieldRequired: true }],
editable: "user",
required: false,
disableOnPrefill: false,
},
{
name: "test-number",
type: "number",
label: "test-number",
hidden: false,
sources: [{ id: "user", type: "user", label: "User", fieldRequired: true }],
editable: "user",
required: false,
disableOnPrefill: false,
},
{
name: "test-textarea",
type: "textarea",
label: "test-textarea",
hidden: false,
sources: [{ id: "user", type: "user", label: "User", fieldRequired: true }],
editable: "user",
required: false,
disableOnPrefill: false,
},
{
name: "test-boolean",
type: "boolean",
label: "test-boolean",
hidden: false,
sources: [{ id: "user", type: "user", label: "User", fieldRequired: true }],
editable: "user",
required: false,
labelAsSafeHtml: "<p>test-boolean</p>\n",
disableOnPrefill: false,
},
{
name: "test-url",
type: "url",
label: "test-url",
hidden: false,
sources: [{ id: "user", type: "user", label: "User", fieldRequired: true }],
editable: "user",
required: false,
placeholder: "",
labelAsSafeHtml: "<p>test-url</p>\n",
disableOnPrefill: false,
},
{
name: "test-multiemail",
type: "multiemail",
label: "test-multiemail",
hidden: false,
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: false,
disableOnPrefill: false,
},
{
name: "test-multiselect",
type: "multiselect",
label: "test-multiselect",
hidden: false,
options: [
{
label: "apple",
value: "apple",
},
{
label: "orange",
value: "orange",
},
],
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: false,
disableOnPrefill: false,
},
{
name: "test-checkbox",
type: "checkbox",
label: "test-checkbox",
hidden: false,
options: [
{
label: "blue",
value: "blue",
},
{
label: "red",
value: "red",
},
],
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: false,
disableOnPrefill: false,
},
{
name: "test-radio",
type: "radio",
label: "test-radio",
hidden: false,
options: [
{
label: "pineapple",
value: "pineapple",
},
],
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: false,
disableOnPrefill: false,
},
{
name: "test-select",
type: "select",
label: "test-select",
options: [
{
label: "water",
value: "water",
},
{
label: "juice",
value: "juice",
},
],
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: true,
placeholder: "",
disableOnPrefill: false,
},
{
name: "title",
type: "text",
hidden: true,
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: true,
defaultLabel: "what_is_this_meeting_about",
defaultPlaceholder: "",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
{
name: "guests",
type: "multiemail",
hidden: false,
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_guests",
defaultPlaceholder: "email",
},
{
name: "rescheduleReason",
type: "textarea",
views: [{ id: "reschedule", label: "Reschedule View" }],
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "reason_for_reschedule",
defaultPlaceholder: "reschedule_placeholder",
},
],
},
user.id
);
bookingWithSplitName = await bookingsRepositoryFixture.create({
user: {
connect: {
@@ -543,6 +851,318 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("Booking Field Type Validation", () => {
const basePayload = {
start: "2025-06-19T11:00:00.000Z",
attendee: {
name: "Charlie TypeTest",
email: "charlie.typetest@example.com",
timeZone: "Europe/Madrid",
language: "en",
},
};
it("should reject with 400 if 'test-phone' (phone) is not a string", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
"test-phone": 12345,
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
"Invalid type for booking field 'test-phone'. Expected type string (compatible with field type 'phone'), but received number."
);
});
it("should reject with 400 if 'test-address' (address) is not a string", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
"test-address": true,
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
"Invalid type for booking field 'test-address'. Expected type string (compatible with field type 'address'), but received boolean."
);
});
it("should reject with 400 if 'test-text' (text) is not a string", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
"test-text": 123,
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
"Invalid type for booking field 'test-text'. Expected type string (compatible with field type 'text'), but received number."
);
});
it("should reject with 400 if 'test-number' (number) is not a number", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
"test-number": "123",
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
"Invalid type for booking field 'test-number'. Expected type number (compatible with field type 'number'), but received string."
);
});
it("should reject with 400 if 'test-textarea' (textarea) is not a string", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
"test-textarea": { text: "invalid" },
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
"Invalid type for booking field 'test-textarea'. Expected type string (compatible with field type 'textarea'), but received object."
);
});
it("should reject with 400 if 'test-boolean' (boolean) is not a boolean", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
"test-boolean": "true",
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
"Invalid type for booking field 'test-boolean'. Expected type boolean (compatible with field type 'boolean'), but received string."
);
});
it("should reject with 400 if 'test-url' (url) is not a string", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
"test-url": 12345,
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
"Invalid type for booking field 'test-url'. Expected type string (compatible with field type 'url'), but received number."
);
});
it("should reject with 400 if 'test-multiemail' (multiemail) is not an array", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
"test-multiemail": "not-an-array@example.com",
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
"Invalid type for booking field 'test-multiemail'. Expected type array (compatible with field type 'multiemail'), but received string."
);
});
it("should reject with 400 if 'test-multiselect' (multiselect) is not an array", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
"test-multiselect": "not-an-array",
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
"Invalid type for booking field 'test-multiselect'. Expected type array (compatible with field type 'multiselect'), but received string."
);
});
it("should reject with 400 if 'test-checkbox' (checkbox) is not an array", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
"test-checkbox": true,
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
"Invalid type for booking field 'test-checkbox'. Expected type array (compatible with field type 'checkbox'), but received boolean."
);
});
it("should reject with 400 if 'test-radio' (radio) is not a string or number", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
"test-radio": true,
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
"Invalid type for booking field 'test-radio'. Expected type string or number (compatible with field type 'radio'), but received boolean."
);
});
it("should reject with 400 if 'test-select' (select) is not a string or number", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
"test-select": true,
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
"Invalid type for booking field 'test-select'. Expected type string or number (compatible with field type 'select'), but received boolean."
);
});
it("should throw BadRequestException for test-select field with invalid option", async () => {
const fieldName = "test-select";
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
[fieldName]: "INVALID_DRINK",
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toBe(
`Invalid option 'INVALID_DRINK' for booking field '${fieldName}'. Allowed options are: water, juice.`
);
});
it("should throw BadRequestException for test-radio field with invalid option", async () => {
const fieldName = "test-radio";
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
[fieldName]: "INVALID_FRUIT_FOR_RADIO",
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toBe(
`Invalid option 'INVALID_FRUIT_FOR_RADIO' for booking field '${fieldName}'. Allowed options are: pineapple.`
);
});
it("should throw BadRequestException for test-multiselect field with an invalid option", async () => {
const fieldName = "test-multiselect";
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
[fieldName]: ["apple", "INVALID_FRUIT"],
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toBe(
`One or more invalid options for booking field '${fieldName}'. Allowed options are: apple, orange.`
);
});
it("should throw BadRequestException for test-checkbox field with invalid options", async () => {
const fieldName = "test-checkbox";
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFielsCustom.id,
bookingFieldsResponses: {
[fieldName]: ["blue", "INVALID_COLOR"],
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toBe(
`One or more invalid options for booking field '${fieldName}'. Allowed options are: blue, red.`
);
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -385,7 +385,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -430,7 +430,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -459,7 +459,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -502,7 +502,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -14,7 +14,7 @@ import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { EventType, User } from "@prisma/client";
import { EventType, User, Workflow } from "@prisma/client";
import { advanceTo, clear } from "jest-date-mock";
import * as request from "supertest";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
@@ -22,6 +22,8 @@ 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 { WorkflowReminderRepositoryFixture } from "test/fixtures/repository/workflow-reminder.repository.fixture";
import { WorkflowRepositoryFixture } from "test/fixtures/repository/workflow.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
@@ -32,6 +34,7 @@ import {
VERSION_2024_08_13,
X_CAL_CLIENT_ID,
} from "@calcom/platform-constants";
import { AttendeeScheduledEmail, OrganizerScheduledEmail } from "@calcom/platform-libraries/emails";
import {
CreateEventTypeInput_2024_06_14,
FAILED_EVENT_TYPE_IDENTIFICATION_ERROR_MESSAGE,
@@ -60,6 +63,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let workflowReminderRepositoryFixture: WorkflowReminderRepositoryFixture;
let workflowRepositoryFixture: WorkflowRepositoryFixture;
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
@@ -68,6 +73,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
let eventTypeId: number;
let eventType: EventType;
let eventTypeWithAttendeeSmsReminder: EventType;
let workflow: Workflow;
const eventTypeSlug = `user-bookings-event-type-${randomString()}`;
let recurringEventTypeId: number;
const recurringEventTypeSlug = `user-bookings-event-type-${randomString()}`;
@@ -97,6 +104,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
workflowReminderRepositoryFixture = new WorkflowReminderRepositoryFixture(moduleRef);
workflowRepositoryFixture = new WorkflowRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({
name: `user-bookings-organization-${randomString()}`,
@@ -177,6 +186,85 @@ describe("Bookings Endpoints 2024-08-13", () => {
rating: 10,
});
workflow = await workflowRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
trigger: "BEFORE_EVENT",
time: 24,
timeUnit: "HOUR",
position: 0,
isActiveOnAll: false,
name: "Attendee SMS Reminder",
steps: {
create: {
stepNumber: 1,
action: "SMS_ATTENDEE",
sendTo: null,
reminderBody:
"Hi {ATTENDEE}, this is a reminder that your meeting ({EVENT_NAME}) with {ORGANIZER} is on {EVENT_DATE_YYYY MMM D} at {EVENT_TIME_h:mma} {TIMEZONE}.",
emailSubject: "Reminder: {EVENT_NAME} - {EVENT_DATE_ddd, MMM D, YYYY h:mma}",
template: "REMINDER",
numberRequired: true,
sender: "Cal",
numberVerificationPending: false,
includeCalendarEvent: false,
verifiedAt: new Date(),
},
},
});
eventTypeWithAttendeeSmsReminder = await eventTypesRepositoryFixture.create(
{
title: "event with attendee sms reminder",
slug: "event-with-attendee-sms-reminder",
length: 15,
bookingFields: [
{
name: "smsReminderNumber",
type: "phone",
sources: [
{
id: String(workflow.id),
type: "workflow",
label: "Workflow",
editUrl: `/workflows/${workflow.id}`,
fieldRequired: true,
},
],
editable: "system",
required: true,
defaultLabel: "number_text_notifications",
defaultPlaceholder: "enter_phone_number",
},
],
metadata: {
disableStandardEmails: {
all: {
attendee: true,
host: true,
},
confirmation: {
host: true,
attendee: true,
},
},
},
workflows: {
create: {
workflow: {
connect: {
id: workflow.id,
},
},
},
},
},
user.id
);
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
@@ -583,7 +671,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.rating).toEqual(bookingInThePast.rating);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -642,7 +730,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
await bookingsRepositoryFixture.deleteById(mockBooking.id);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -1668,7 +1756,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
describe("book by username and event type slug", () => {
it("should not create a booking by if neither event type id nor username and event type slug provided", async () => {
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
const body = {
start: new Date(Date.UTC(2036, 0, 8, 15, 0, 0)).toISOString(),
attendee: {
@@ -1677,7 +1764,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: googleMeetUrl,
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
@@ -2227,6 +2314,91 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("create booking with attendee sms reminder", () => {
it("should not be able create a booking if attendee sms reminder is missing", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAttendeeSmsReminder.id,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
};
const response = await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toEqual(
`Missing attendee phone number - it is required by the event type. Pass it as \"attendee.phoneNumber\" string in the request.`
);
});
it("should be able create a booking if attendee sms reminder is provided", async () => {
const phoneNumber = "+37122222222";
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAttendeeSmsReminder.id,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
phoneNumber,
},
};
const response = await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
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(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 15, 15, 0)).toISOString());
expect(data.duration).toEqual(15);
expect(data.eventTypeId).toEqual(eventTypeWithAttendeeSmsReminder.id);
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,
phoneNumber: body.attendee.phoneNumber,
absent: false,
});
expect(data.bookingFieldsResponses).toEqual({
name: body.attendee.name,
email: body.attendee.email,
attendeePhoneNumber: body.attendee.phoneNumber,
smsReminderNumber: body.attendee.phoneNumber,
});
const dbBooking = await bookingsRepositoryFixture.getById(data.id);
expect(dbBooking?.smsReminderNumber).toEqual(phoneNumber);
const dbWorkflowReminder = await workflowReminderRepositoryFixture.getByBookingUid(data.uid);
expect(dbWorkflowReminder?.method).toEqual("SMS");
} else {
throw new Error("Invalid response data");
}
});
});
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
return (
!Array.isArray(data) &&
@@ -0,0 +1,23 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsIn, IsOptional } from "class-validator";
export const BookingReferences = [
"google_calendar",
"office365_calendar",
"daily_video",
"google_video",
"office365_video",
"zoom_video",
] as const;
export class BookingReferencesFilterInput_2024_08_13 {
@ApiProperty({
description: "Filter booking references by type",
required: false,
enum: BookingReferences,
example: "google_calendar",
})
@IsOptional()
@IsIn(BookingReferences)
type?: (typeof BookingReferences)[number];
}
@@ -0,0 +1,46 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEnum, IsNumber, IsString } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class BookingReference {
@IsString()
@ApiProperty({
description: "The type of the booking reference",
})
type!: string;
@IsString()
@ApiProperty({
description: "The event uid of the booking",
})
eventUid!: string;
@IsString()
@ApiProperty({
description: "The id of the calendar the event is created in",
nullable: true,
})
destinationCalendarId!: string | null;
@IsNumber()
@ApiProperty({
description: "The id of the booking reference",
})
id!: number;
}
export class BookingReferencesOutput_2024_08_13 {
@ApiProperty({
description: "The status of the request, always 'success' for successful responses",
example: SUCCESS_STATUS,
})
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
description: "Booking References",
type: [BookingReference],
})
data!: BookingReference[];
}
@@ -0,0 +1,46 @@
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
@Injectable()
export class BookingReferencesService_2024_08_13 {
constructor(
private readonly bookingsRepository: BookingsRepository_2024_08_13,
private readonly bookingReferencesRepository: BookingReferencesRepository_2024_08_13,
private readonly outputBookingReferencesService: OutputBookingReferencesService_2024_08_13
) {}
async getBookingReferences(
bookingUid: string,
userId: number,
filter?: BookingReferencesFilterInput_2024_08_13
) {
const booking = await this.bookingsRepository.getByUidWithUser(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
}
if (booking.user?.id !== userId) {
throw new BadRequestException(`Booking with uid ${bookingUid} does not belong to user`);
}
const bookingReferences = await this.bookingReferencesRepository.getBookingReferences(booking.id, filter);
return this.outputBookingReferencesService.getOutputBookingReferences(bookingReferences);
}
async getOrgBookingReferences(bookingUid: string, filter?: BookingReferencesFilterInput_2024_08_13) {
const booking = await this.bookingsRepository.getByUidWithUser(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
}
const bookingReferences = await this.bookingReferencesRepository.getBookingReferences(booking.id, filter);
return this.outputBookingReferencesService.getOutputBookingReferences(bookingReferences);
}
}
@@ -1,11 +1,13 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { BillingService } from "@/modules/billing/services/billing.service";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
@@ -18,6 +20,7 @@ import { UsersRepository, UserWithProfile } from "@/modules/users/users.reposito
import { ConflictException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { BadRequestException } from "@nestjs/common";
import { Request } from "express";
import { DateTime } from "luxon";
import { z } from "zod";
import {
@@ -57,13 +60,17 @@ type CreatedBooking = {
start: string;
};
const eventTypeBookingFieldSchema = z.object({
name: z.string(),
required: z.boolean(),
editable: z.string(),
});
const eventTypeBookingFieldSchema = z
.object({
name: z.string(),
required: z.boolean(),
editable: z.string(),
type: z.string(),
options: z.array(z.object({ value: z.string() })).optional(),
})
.passthrough();
const eventTypeBookingFieldsSchema = z.array(eventTypeBookingFieldSchema);
export const eventTypeBookingFieldsSchema = z.array(eventTypeBookingFieldSchema);
export type EventTypeWithOwnerAndTeam = EventType & { owner: User | null; team: Team | null };
@@ -77,6 +84,7 @@ export class BookingsService_2024_08_13 {
private readonly bookingSeatRepository: BookingSeatRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly prismaReadService: PrismaReadService,
private readonly kyselyReadService: KyselyReadService,
private readonly billingService: BillingService,
private readonly usersService: UsersService,
private readonly usersRepository: UsersRepository,
@@ -85,34 +93,19 @@ export class BookingsService_2024_08_13 {
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
private readonly organizationsRepository: OrganizationsRepository,
private readonly teamsRepository: TeamsRepository,
private readonly teamsEventTypesRepository: TeamsEventTypesRepository
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13
) {}
async createBooking(request: Request, body: CreateBookingInput) {
let bookingTeamEventType = false;
try {
const eventType = await this.getBookedEventType(body);
if (eventType?.team) {
bookingTeamEventType = true;
}
if (!eventType) {
if (body.username && body.eventTypeSlug && !body.organizationSlug) {
throw new NotFoundException(
`Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} not found.`
);
}
if (body.username && body.eventTypeSlug && body.organizationSlug) {
throw new NotFoundException(
`Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} within organization ${body.organizationSlug} not found.`
);
}
if (body.teamSlug && body.eventTypeSlug && !body.organizationSlug) {
throw new NotFoundException(
`Event type with slug ${body.eventTypeSlug} belonging to team ${body.teamSlug} not found.`
);
}
if (body.teamSlug && body.eventTypeSlug && body.organizationSlug) {
throw new NotFoundException(
`Event type with slug ${body.eventTypeSlug} belonging to team ${body.teamSlug} within organization ${body.organizationSlug} not found.`
);
}
throw new NotFoundException(`Event type with id ${body.eventTypeId} not found.`);
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
}
body.eventTypeId = eventType.id;
@@ -138,18 +131,7 @@ export class BookingsService_2024_08_13 {
return await this.createRegularBooking(request, body, eventType);
} catch (error) {
if (error instanceof Error) {
if (error.message === "no_available_users_found_error") {
throw new BadRequestException("User either already has booking at this time or is not available");
} else if (error.message === "booking_time_out_of_bounds_error") {
throw new BadRequestException(
`The event type can't be booked at selected time ${body.start}. This could be because it's too soon (violating the minimum booking notice) or too far in the future (outside the event's scheduling window). Try fetching available slots first using the GET /v2/slots endpoint and then make a booking with "start" time equal to one of the available slots: https://cal.com/docs/api-reference/v2/slots`
);
} else if (error.message === "Attempting to book a meeting in the past.") {
throw new BadRequestException("Attempting to book a meeting in the past.");
}
}
throw error;
this.errorsBookingsService.handleBookingError(error, bookingTeamEventType);
}
}
@@ -194,36 +176,187 @@ export class BookingsService_2024_08_13 {
}
async hasRequiredBookingFieldsResponses(body: CreateBookingInput, eventType: EventType | null) {
const bookingFields = { ...body.bookingFieldsResponses, attendeePhoneNumber: body.attendee.phoneNumber };
const bookingFieldsResponses: Record<string, unknown> = {
...body.bookingFieldsResponses,
attendeePhoneNumber: body.attendee.phoneNumber,
smsReminderNumber: body.attendee.phoneNumber,
};
if (!eventType?.bookingFields) {
return true;
}
// note(Lauris): we filter out system fields, because some of them are set by default and name and email are passed in the body.attendee
// note(Lauris): we filter out system fields, because some of them are set by default and name and email are passed in the body.attendee. Only exception
// is smsReminderNumber, because if it is required and not passed sms workflow won't work.
const eventTypeBookingFields = eventTypeBookingFieldsSchema
.parse(eventType.bookingFields)
.filter((field) => !field.editable.startsWith("system"));
.filter((field) => !field.editable.startsWith("system") || field.name === "smsReminderNumber");
if (!eventTypeBookingFields.length) {
return true;
}
for (const field of eventTypeBookingFields) {
if (field.required && !(field.name in bookingFields)) {
if (field.name === "attendeePhoneNumber") {
for (const eventTypeBookingField of eventTypeBookingFields) {
if (
eventTypeBookingField.required &&
(bookingFieldsResponses[eventTypeBookingField.name] === null ||
bookingFieldsResponses[eventTypeBookingField.name] === undefined)
) {
if (
eventTypeBookingField.name === "attendeePhoneNumber" ||
eventTypeBookingField.name === "smsReminderNumber"
) {
throw new BadRequestException(
`Missing attendee phone number - it is required by the event type. Pass it as "attendee.phoneNumber" in the request.`
`Missing attendee phone number - it is required by the event type. Pass it as "attendee.phoneNumber" string in the request.`
);
}
throw new BadRequestException(
`Missing required booking field response: ${field.name} - it is required by the event type booking fields, but missing in the bookingFieldsResponses. You can fetch the event type with ID ${eventType.id} to see the required fields.`
`Missing required booking field response: ${eventTypeBookingField.name} - it is required by the event type booking fields, but missing in the bookingFieldsResponses. You can fetch the event type with ID ${eventType.id} to see the required fields.`
);
}
const bookingFieldResponseValue = bookingFieldsResponses[eventTypeBookingField.name];
if (bookingFieldResponseValue !== undefined) {
const bookingFieldResponseValueType = typeof bookingFieldResponseValue;
let expectedBookingFieldResponseValueType = "";
let isValidType = false;
const eventTypeBookingFieldType = eventTypeBookingField.type;
switch (eventTypeBookingFieldType) {
case "phone":
expectedBookingFieldResponseValueType = "string";
isValidType = bookingFieldResponseValueType === "string";
break;
case "address":
expectedBookingFieldResponseValueType = "string";
isValidType = bookingFieldResponseValueType === "string";
break;
case "text":
expectedBookingFieldResponseValueType = "string";
isValidType = bookingFieldResponseValueType === "string";
break;
case "number":
expectedBookingFieldResponseValueType = "number";
isValidType = bookingFieldResponseValueType === "number";
break;
case "textarea":
expectedBookingFieldResponseValueType = "string";
isValidType = bookingFieldResponseValueType === "string";
break;
case "multiemail":
expectedBookingFieldResponseValueType = "array";
isValidType = Array.isArray(bookingFieldResponseValue);
break;
case "boolean":
expectedBookingFieldResponseValueType = "boolean";
isValidType = bookingFieldResponseValueType === "boolean";
break;
case "select":
expectedBookingFieldResponseValueType = "string or number";
isValidType =
bookingFieldResponseValueType === "string" || bookingFieldResponseValueType === "number";
if (isValidType && Array.isArray(eventTypeBookingField.options)) {
const submittedValue = bookingFieldResponseValue as string | number;
const allowedOptionValues = eventTypeBookingField.options.map((opt) => opt.value);
if (!this.isValidSingleOptionValue(submittedValue, allowedOptionValues)) {
throw new BadRequestException(
`Invalid option '${submittedValue}' for booking field '${
eventTypeBookingField.name
}'. Allowed options are: ${allowedOptionValues.join(", ")}.`
);
}
}
break;
case "multiselect":
expectedBookingFieldResponseValueType = "array";
isValidType = Array.isArray(bookingFieldResponseValue);
if (isValidType && Array.isArray(eventTypeBookingField.options)) {
const submittedValues = bookingFieldResponseValue as (string | number)[];
const allowedOptionValues = eventTypeBookingField.options.map((opt) => opt.value);
if (!this.areValidMultipleOptionValues(submittedValues, allowedOptionValues)) {
throw new BadRequestException(
`One or more invalid options for booking field '${
eventTypeBookingField.name
}'. Allowed options are: ${allowedOptionValues.join(", ")}.`
);
}
}
break;
case "checkbox":
expectedBookingFieldResponseValueType = "array";
isValidType = Array.isArray(bookingFieldResponseValue);
if (isValidType && Array.isArray(eventTypeBookingField.options)) {
const submittedValues = bookingFieldResponseValue as (string | number)[];
const allowedOptionValues = eventTypeBookingField.options.map((opt) => opt.value);
if (!this.areValidMultipleOptionValues(submittedValues, allowedOptionValues)) {
throw new BadRequestException(
`One or more invalid options for booking field '${
eventTypeBookingField.name
}'. Allowed options are: ${allowedOptionValues.join(", ")}.`
);
}
}
break;
case "radio":
expectedBookingFieldResponseValueType = "string or number";
isValidType =
bookingFieldResponseValueType === "string" || bookingFieldResponseValueType === "number";
if (isValidType && Array.isArray(eventTypeBookingField.options)) {
const submittedValue = bookingFieldResponseValue as string | number;
const allowedOptionValues = eventTypeBookingField.options.map((opt) => opt.value);
if (!this.isValidSingleOptionValue(submittedValue, allowedOptionValues)) {
throw new BadRequestException(
`Invalid option '${submittedValue}' for booking field '${
eventTypeBookingField.name
}'. Allowed options are: ${allowedOptionValues.join(", ")}.`
);
}
}
break;
case "url":
expectedBookingFieldResponseValueType = "string";
isValidType = bookingFieldResponseValueType === "string";
break;
default:
// note(Lauris): by default pass the field if we have a missing "case" in the switch
isValidType = true;
break;
}
if (!isValidType) {
throw new BadRequestException(
`Invalid type for booking field '${eventTypeBookingField.name}'. Expected type ${expectedBookingFieldResponseValueType} (compatible with field type '${eventTypeBookingFieldType}'), but received ${bookingFieldResponseValueType}.`
);
}
}
}
return true;
}
private isValidSingleOptionValue(
bookingFieldResponseValue: string | number,
eventTypeBookingFieldOptions: string[]
): boolean {
if (eventTypeBookingFieldOptions.length === 0) {
// note(Lauris): If no options defined, cannot validate against them, so pass.
return true;
}
return eventTypeBookingFieldOptions.some((val) => val === String(bookingFieldResponseValue));
}
private areValidMultipleOptionValues(
bookingFieldResponseValues: (string | number)[],
eventTypeBookingFieldOptions: string[]
): boolean {
if (eventTypeBookingFieldOptions.length === 0) {
// note(Lauris): If no options defined, cannot validate against them, so pass.
return true;
}
return bookingFieldResponseValues.every((submittedVal) =>
eventTypeBookingFieldOptions.some((allowedVal) => allowedVal === String(submittedVal))
);
}
async createInstantBooking(
request: Request,
body: CreateInstantBookingInput_2024_08_13,
@@ -399,7 +532,7 @@ export class BookingsService_2024_08_13 {
queryParams.attendeeEmail = await this.getAttendeeEmail(queryParams.attendeeEmail, user);
}
const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({
const fetchedBookings: { bookings: { id: number }[]; totalCount: number } = await getAllUserBookings({
bookingListingByStatus: queryParams.status || [],
skip: queryParams.skip ?? 0,
take: queryParams.take ?? 100,
@@ -410,6 +543,7 @@ export class BookingsService_2024_08_13 {
ctx: {
user,
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
kysely: this.kyselyReadService.kysely,
},
sort: this.inputService.transformGetBookingsSort(queryParams),
});
@@ -454,7 +588,29 @@ export class BookingsService_2024_08_13 {
}
}
return formattedBookings;
const skip = Math.abs(queryParams?.skip ?? 0);
const take = Math.abs(queryParams?.take ?? 100);
const itemsPerPage = take;
const totalPages = itemsPerPage !== 0 ? Math.ceil(fetchedBookings.totalCount / itemsPerPage) : 0;
const currentPage = Math.floor(skip / itemsPerPage) + 1;
const hasNextPage = skip + itemsPerPage < fetchedBookings.totalCount;
const hasPreviousPage = skip > 0;
return {
bookings: formattedBookings,
pagination: {
totalItems: fetchedBookings.totalCount,
// clamp remainingItems between 0 and totalCount
remainingItems: Math.min(
Math.max(fetchedBookings.totalCount - (skip + take), 0),
fetchedBookings.totalCount
),
itemsPerPage: itemsPerPage,
currentPage: currentPage,
totalPages: totalPages,
hasNextPage: hasNextPage,
hasPreviousPage: hasPreviousPage,
},
};
}
async getAttendeeEmail(queryParamsAttendeeEmail: string, user: { id: number }) {
@@ -594,6 +750,20 @@ export class BookingsService_2024_08_13 {
async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) {
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
if (!bookingBefore) {
throw new NotFoundException(`Booking with uid=${bookingUid} not found.`);
}
const nowUtc = DateTime.utc();
const bookingStartTimeUtc = DateTime.fromJSDate(bookingBefore.startTime, { zone: "utc" });
if (nowUtc < bookingStartTimeUtc) {
throw new BadRequestException(
`Bookings can only be marked as absent after their scheduled start time. Current time in UTC+0: ${nowUtc.toISO()}, Booking start time in UTC+0: ${bookingStartTimeUtc.toISO()}`
);
}
const platformClientParams = bookingBefore?.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
: undefined;
@@ -0,0 +1,55 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { Logger } from "@nestjs/common";
import { CreateBookingInput } from "@calcom/platform-types";
@Injectable()
export class ErrorsBookingsService_2024_08_13 {
private readonly logger = new Logger("ErrorsBookingsService_2024_08_13");
handleEventTypeToBeBookedNotFound(body: CreateBookingInput): never {
if (body.username && body.eventTypeSlug && !body.organizationSlug) {
throw new NotFoundException(
`Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} not found.`
);
}
if (body.username && body.eventTypeSlug && body.organizationSlug) {
throw new NotFoundException(
`Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} within organization ${body.organizationSlug} not found.`
);
}
if (body.teamSlug && body.eventTypeSlug && !body.organizationSlug) {
throw new NotFoundException(
`Event type with slug ${body.eventTypeSlug} belonging to team ${body.teamSlug} not found.`
);
}
if (body.teamSlug && body.eventTypeSlug && body.organizationSlug) {
throw new NotFoundException(
`Event type with slug ${body.eventTypeSlug} belonging to team ${body.teamSlug} within organization ${body.organizationSlug} not found.`
);
}
throw new NotFoundException(`Event type with id ${body.eventTypeId} not found.`);
}
handleBookingError(error: unknown, bookingTeamEventType: boolean): never {
const hostsUnavaile = "One of the hosts either already has booking at this time or is not available";
if (error instanceof Error) {
if (error.message === "no_available_users_found_error") {
if (bookingTeamEventType) {
throw new BadRequestException(hostsUnavaile);
}
throw new BadRequestException("User either already has booking at this time or is not available");
} else if (error.message === "booking_time_out_of_bounds_error") {
throw new BadRequestException(
`The event type can't be booked at the "start" time provided. This could be because it's too soon (violating the minimum booking notice) or too far in the future (outside the event's scheduling window). Try fetching available slots first using the GET /v2/slots endpoint and then make a booking with "start" time equal to one of the available slots: https://cal.com/docs/api-reference/v2/slots`
);
} else if (error.message === "Attempting to book a meeting in the past.") {
throw new BadRequestException("Attempting to book a meeting in the past.");
} else if (error.message === "hosts_unavailable_for_booking") {
throw new BadRequestException(hostsUnavaile);
}
}
throw error;
}
}
@@ -1,5 +1,8 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { EventTypeWithOwnerAndTeam } from "@/ee/bookings/2024-08-13/services/bookings.service";
import {
eventTypeBookingFieldsSchema,
EventTypeWithOwnerAndTeam,
} from "@/ee/bookings/2024-08-13/services/bookings.service";
import {
bookingResponsesSchema,
seatedBookingDataSchema,
@@ -159,6 +162,12 @@ export class InputBookingsService_2024_08_13 {
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
const needsSmsReminderNumber = eventType.bookingFields
? eventTypeBookingFieldsSchema
.parse(eventType.bookingFields)
.find((field) => field.name === "smsReminderNumber")
: undefined;
return {
start: startTime.toISO(),
end: endTime.toISO(),
@@ -174,6 +183,7 @@ export class InputBookingsService_2024_08_13 {
name: inputBooking.attendee.name,
email: attendeeEmail ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
smsReminderNumber: needsSmsReminderNumber ? inputBooking.attendee.phoneNumber : undefined,
guests,
location,
},
@@ -0,0 +1,20 @@
import { Injectable } from "@nestjs/common";
interface IBookingReference {
type: string;
uid: string;
id: number;
externalCalendarId: string | null;
}
@Injectable()
export class OutputBookingReferencesService_2024_08_13 {
getOutputBookingReferences(bookingReferences: IBookingReference[]) {
return bookingReferences.map((bookingReference) => ({
type: bookingReference.type,
eventUid: bookingReference.uid,
id: bookingReference.id,
destinationCalendarId: bookingReference?.externalCalendarId,
}));
}
}
@@ -22,7 +22,7 @@ import {
RecurringBookingOutput_2024_08_13,
SeatedAttendee,
} from "@calcom/platform-types";
import { Booking, BookingSeat } from "@calcom/prisma/client";
import { Booking, BookingReference, BookingSeat } from "@calcom/prisma/client";
export const bookingResponsesSchema = z
.object({
@@ -15,6 +15,7 @@ import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service";
import { OutlookService } from "@/ee/calendars/services/outlook.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { ApiAuthGuardOnlyAllow } from "@/modules/auth/decorators/api-auth-guard-only-allow.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -51,7 +52,7 @@ import {
APPLE_CALENDAR,
CREDENTIAL_CALENDARS,
} from "@calcom/platform-constants";
import { ApiResponse, CalendarBusyTimesInput } from "@calcom/platform-types";
import { ApiResponse, CalendarBusyTimesInput, CreateCalendarCredentialsInput } from "@calcom/platform-types";
@Controller({
path: "/v2/calendars",
@@ -135,6 +136,7 @@ export class CalendarsController {
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@ApiAuthGuardOnlyAllow(["API_KEY", "ACCESS_TOKEN"])
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Get("/:calendar/connect")
@HttpCode(HttpStatus.OK)
@@ -173,7 +175,7 @@ export class CalendarsController {
@Get("/:calendar/save")
@HttpCode(HttpStatus.OK)
@Redirect(undefined, 301)
@ApiOperation({ summary: "Save an oAuth calendar credentials" })
@ApiOperation({ summary: "Save Google or Outlook calendar credentials" })
async save(
@Query("state") state: string,
@Query("code") code: string,
@@ -221,11 +223,11 @@ export class CalendarsController {
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Post("/:calendar/credentials")
@ApiOperation({ summary: "Sync credentials" })
@ApiOperation({ summary: "Save Apple calendar credentials" })
async syncCredentials(
@GetUser() user: User,
@Param("calendar") calendar: string,
@Body() body: { username: string; password: string }
@Body() body: CreateCalendarCredentialsInput
): Promise<{ status: string }> {
const { username, password } = body;
@@ -60,8 +60,9 @@ export class AppleCalendarService implements CredentialSyncCalendarApp {
}
async saveCalendarCredentials(userId: number, userEmail: string, username: string, password: string) {
if (username.length <= 1 || password.length <= 1)
if (!username || !password || username.length <= 1 || password.length <= 1) {
throw new BadRequestException(`Username or password cannot be empty`);
}
const existingAppleCalendarCredentials = await this.credentialRepository.getAllUserCredentialsByTypeAndId(
APPLE_CALENDAR_TYPE,
@@ -118,6 +119,7 @@ export class AppleCalendarService implements CredentialSyncCalendarApp {
teamId: null,
appId: APPLE_CALENDAR_ID,
invalid: false,
delegationCredentialId: null,
};
const dav = new CalendarService({
@@ -35,6 +35,7 @@ export class IcsFeedService implements ICSFeedCalendarApp {
teamId: null,
appId: ICS_CALENDAR,
invalid: false,
delegationCredentialId: null,
};
try {
@@ -1,4 +1,4 @@
import { OrganizerIntegrationLocation } from "@calcom/lib";
import { OrganizerIntegrationLocation } from "@calcom/lib/event-types/transformers/internal/locations";
type BaseEventType = {
length: number;
@@ -490,6 +490,7 @@ describe("Event types Endpoints", () => {
requiresBookerEmailVerification: false,
hideCalendarNotes: false,
hideCalendarEventDetails: false,
hideOrganizerEmail: false,
lockTimeZoneToggleOnBookingPage: true,
color: {
darkThemeHex: "#292929",
@@ -529,6 +530,7 @@ describe("Event types Endpoints", () => {
expect(createdEventType.hideCalendarNotes).toEqual(body.hideCalendarNotes);
expect(createdEventType.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
expect(createdEventType.hideOrganizerEmail).toEqual(body.hideOrganizerEmail);
expect(createdEventType.lockTimeZoneToggleOnBookingPage).toEqual(
body.lockTimeZoneToggleOnBookingPage
);
@@ -610,6 +612,7 @@ describe("Event types Endpoints", () => {
);
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
expect(fetchedEventType.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
eventType.lockTimeZoneToggleOnBookingPage
);
@@ -1009,6 +1012,7 @@ describe("Event types Endpoints", () => {
requiresBookerEmailVerification: true,
hideCalendarNotes: true,
hideCalendarEventDetails: true,
hideOrganizerEmail: true,
lockTimeZoneToggleOnBookingPage: true,
color: {
darkThemeHex: "#292929",
@@ -1073,6 +1077,7 @@ describe("Event types Endpoints", () => {
);
expect(updatedEventType.hideCalendarNotes).toEqual(body.hideCalendarNotes);
expect(updatedEventType.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
expect(updatedEventType.hideOrganizerEmail).toEqual(body.hideOrganizerEmail);
expect(updatedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
body.lockTimeZoneToggleOnBookingPage
);
@@ -1093,6 +1098,7 @@ describe("Event types Endpoints", () => {
eventType.requiresBookerEmailVerification = updatedEventType.requiresBookerEmailVerification;
eventType.hideCalendarNotes = updatedEventType.hideCalendarNotes;
eventType.hideCalendarEventDetails = updatedEventType.hideCalendarEventDetails;
eventType.hideOrganizerEmail = updatedEventType.hideOrganizerEmail;
eventType.lockTimeZoneToggleOnBookingPage = updatedEventType.lockTimeZoneToggleOnBookingPage;
eventType.color = updatedEventType.color;
eventType.bookingFields = updatedEventType.bookingFields;
@@ -1163,6 +1169,7 @@ describe("Event types Endpoints", () => {
);
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
expect(fetchedEventType.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
eventType.lockTimeZoneToggleOnBookingPage
);
@@ -1204,6 +1211,7 @@ describe("Event types Endpoints", () => {
);
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
expect(fetchedEventType.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
eventType.lockTimeZoneToggleOnBookingPage
);
@@ -1420,7 +1428,6 @@ describe("Event types Endpoints", () => {
{
name: "location",
type: "radioInput",
label: "",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system",
required: false,
@@ -1550,7 +1557,6 @@ describe("Event types Endpoints", () => {
{
name: "location",
type: "radioInput",
label: "",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system",
required: false,
@@ -1775,6 +1781,68 @@ describe("Event types Endpoints", () => {
});
});
describe("creating event type with input of another event type output", () => {
let firstCreatedEventType: EventTypeOutput_2024_06_14;
let secondCreatedEventType: EventTypeOutput_2024_06_14;
it("should create first event type", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "first created coding class",
slug: "first-created-coding-class",
lengthInMinutes: 60,
locations: [
{
type: "address",
address: "via volturno 10, Roma",
public: true,
},
],
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.locations).toEqual(body.locations);
firstCreatedEventType = responseBody.data;
});
});
it("should create second event type using first as input", async () => {
const body = {
...firstCreatedEventType,
title: "second created coding class",
slug: "second-created-coding-class",
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
secondCreatedEventType = responseBody.data;
const { id, title, slug, ...restFirst } = firstCreatedEventType;
const { id: id2, title: title2, slug: slug2, ...restSecond } = secondCreatedEventType;
expect(restFirst).toEqual(restSecond);
expect(id2).not.toEqual(id);
expect(title2).not.toEqual(title);
expect(slug2).not.toEqual(slug);
});
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -8,7 +8,6 @@ import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { ConferencingModule } from "@/modules/conferencing/conferencing.module";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
@@ -19,7 +18,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule, ConferencingModule],
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule],
providers: [
EventTypesRepository_2024_06_14,
EventTypesService_2024_06_14,
@@ -5,6 +5,9 @@ import { ConferencingService } from "@/modules/conferencing/services/conferencin
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, BadRequestException } from "@nestjs/common";
import { CONFERENCING_APPS } from "@calcom/platform-constants";
import { getUsersCredentials } from "@calcom/platform-libraries";
import { getApps } from "@calcom/platform-libraries/app-store";
import {
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
@@ -50,8 +53,7 @@ interface ValidationContext {
export class InputEventTypesService_2024_06_14 {
constructor(
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly calendarsService: CalendarsService,
private readonly conferencingService: ConferencingService
private readonly calendarsService: CalendarsService
) {}
async transformAndValidateCreateEventTypeInput(
@@ -104,13 +106,6 @@ export class InputEventTypesService_2024_06_14 {
}
transformInputCreateEventType(inputEventType: CreateEventTypeInput_2024_06_14) {
const defaultLocations: CreateEventTypeInput_2024_06_14["locations"] = [
{
type: "integration",
integration: "cal-video",
},
];
const {
lengthInMinutes,
lengthInMinutesOptions,
@@ -130,10 +125,11 @@ export class InputEventTypesService_2024_06_14 {
} = inputEventType;
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const locationsTransformed = locations?.length ? this.transformInputLocations(locations) : undefined;
const eventType = {
...rest,
length: lengthInMinutes,
locations: this.transformInputLocations(locations || defaultLocations),
locations: locationsTransformed,
bookingFields: this.transformInputBookingFields(bookingFields),
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
@@ -228,6 +224,7 @@ export class InputEventTypesService_2024_06_14 {
const systemCustomNameField = systemCustomFields?.find((field) => field.type === "name");
const systemCustomEmailField = systemCustomFields?.find((field) => field.type === "email");
const systemCustomTitleField = systemCustomFields?.find((field) => field.name === "title");
const systemCustomLocationField = systemCustomFields?.find((field) => field.name === "location");
const systemCustomNotesField = systemCustomFields?.find((field) => field.name === "notes");
const systemCustomGuestsField = systemCustomFields?.find((field) => field.name === "guests");
const systemCustomRescheduleReasonField = systemCustomFields?.find(
@@ -237,7 +234,7 @@ export class InputEventTypesService_2024_06_14 {
const defaultFieldsBefore: (SystemField | CustomField)[] = [
systemCustomNameField || systemBeforeFieldName,
systemCustomEmailField || systemBeforeFieldEmail,
systemBeforeFieldLocation,
systemCustomLocationField || systemBeforeFieldLocation,
];
const defaultFieldsAfter = [
@@ -278,7 +275,8 @@ export class InputEventTypesService_2024_06_14 {
field.name !== "title" &&
field.name !== "notes" &&
field.name !== "guests" &&
field.name !== "rescheduleReason"
field.name !== "rescheduleReason" &&
field.name !== "location"
);
}
@@ -463,10 +461,32 @@ export class InputEventTypesService_2024_06_14 {
if (location.type === "integration") {
// cal-video is global, so we can skip this check
if (location.integration !== "cal-video") {
await this.conferencingService.checkAppIsValidAndConnected(user, location.integration);
await this.checkAppIsValidAndConnected(user, location.integration);
}
}
}) ?? []
);
}
async checkAppIsValidAndConnected(user: UserWithProfile, appSlug: string) {
const conferencingApps = ["google-meet", "office365-video", "zoom"];
if (!conferencingApps.includes(appSlug)) {
throw new BadRequestException("Invalid app, available apps are: ", conferencingApps.join(", "));
}
if (appSlug === "office365-video") {
appSlug = "msteams";
}
const credentials = await getUsersCredentials(user);
const foundApp = getApps(credentials, true).filter((app) => app.slug === appSlug)[0];
const appLocation = foundApp?.appData?.location;
if (!foundApp || !appLocation) {
throw new BadRequestException(`${appSlug} not connected.`);
}
return foundApp.credential;
}
}
@@ -87,6 +87,7 @@ type Input = Pick<
| "destinationCalendar"
| "useEventTypeDestinationCalendarEmail"
| "hideCalendarEventDetails"
| "hideOrganizerEmail"
>;
@Injectable()
@@ -123,6 +124,7 @@ export class OutputEventTypesService_2024_06_14 {
seatsShowAttendees,
useEventTypeDestinationCalendarEmail,
hideCalendarEventDetails,
hideOrganizerEmail,
} = databaseEventType;
const locations = this.transformLocations(databaseEventType.locations);
@@ -197,6 +199,7 @@ export class OutputEventTypesService_2024_06_14 {
destinationCalendar,
useDestinationCalendarEmail: useEventTypeDestinationCalendarEmail,
hideCalendarEventDetails,
hideOrganizerEmail,
};
}
@@ -9,8 +9,6 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import {
BadRequestException,
Controller,
@@ -48,8 +46,6 @@ export class GcalController {
constructor(
private readonly credentialRepository: CredentialsRepository,
private readonly tokensRepository: TokensRepository,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
private readonly config: ConfigService,
private readonly gcalService: GCalService,
private readonly calendarsService: CalendarsService
+1 -1
View File
@@ -6,7 +6,7 @@ if (process.env.SENTRY_DSN) {
// Ensure to call this before requiring any other modules!
Sentry.init({
dsn: getEnv("SENTRY_DSN"),
integrations: [nodeProfilingIntegration()],
integrations: [nodeProfilingIntegration(), Sentry.prismaIntegration()],
// Performance Monitoring
tracesSampleRate: getEnv("SENTRY_TRACES_SAMPLE_RATE") ?? 1.0, // Capture 100% of the transactions
// Set sampling rate for profiling - this is relative to tracesSampleRate
@@ -0,0 +1,4 @@
import { RateLimitType } from "@/lib/throttler-guard";
import { Reflector } from "@nestjs/core";
export const Throttle = Reflector.createDecorator<RateLimitType>();
+16 -2
View File
@@ -1,5 +1,6 @@
import { getEnv } from "@/env";
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { Throttle } from "@/lib/endpoint-throttler-decorator";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis";
import { Inject, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
@@ -22,7 +23,7 @@ const rateLimitSchema = z.object({
ttl: z.number(),
blockDuration: z.number(),
});
type RateLimitType = z.infer<typeof rateLimitSchema>;
export type RateLimitType = z.infer<typeof rateLimitSchema>;
const rateLimitsSchema = z.array(rateLimitSchema);
const sixtySecondsMs = 60 * 1000;
@@ -52,7 +53,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
protected async handleRequest(requestProps: ThrottlerRequest): Promise<boolean> {
const { context } = requestProps;
const throttleOptions = this.reflector.get(Throttle, context.getHandler());
const request = context.switchToHttp().getRequest<Request>();
const IP = request?.headers?.["cf-connecting-ip"] ?? request?.headers?.["CF-Connecting-IP"] ?? request.ip;
const response = context.switchToHttp().getResponse<Response>();
@@ -63,6 +64,10 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
)}", OAuth client ID "${request.get(X_CAL_CLIENT_ID)}" and IP "${IP}"`
);
if (throttleOptions) {
return this.handleApiEndpointThrottle(tracker, throttleOptions, response);
}
if (tracker.startsWith("api_key_")) {
return this.handleApiKeyRequest(tracker, response);
} else {
@@ -70,6 +75,15 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
}
}
private async handleApiEndpointThrottle(tracker: string, options: RateLimitType, response: Response) {
const { isBlocked } = await this.incrementRateLimit(`${tracker}_${options.name}`, options, response);
if (isBlocked) {
throw new ThrottlerException("CustomThrottlerGuard - Too many requests. Please try again later.");
}
return true;
}
private async handleApiKeyRequest(tracker: string, response: Response): Promise<boolean> {
const rateLimits = await this.getRateLimitsForApiKeyTracker(tracker);
@@ -226,20 +226,26 @@ export class EventTypesAtomService {
} else {
credentials = credentials.concat(teamAppCredentials);
}
} else {
if (slug !== "stripe") {
// We only add delegationCredentials if the request for location options is for a user because DelegationCredential Credential is applicable to Users only.
const { credentials: allCredentials } =
await enrichUserWithDelegationConferencingCredentialsWithoutOrgId({
user: {
...user,
credentials,
},
});
credentials = allCredentials;
}
}
// We only add delegationCredentials if the request for location options is for a user because DelegationCredential Credential is applicable to Users only.
const { credentials: allCredentials } = await enrichUserWithDelegationConferencingCredentialsWithoutOrgId(
const enabledApps = await getEnabledAppsFromCredentials(
credentials as unknown as CredentialDataWithTeamName[],
{
user: {
...user,
credentials,
},
where: { slug },
}
);
credentials = allCredentials;
const enabledApps = await getEnabledAppsFromCredentials(allCredentials, {
where: { slug },
});
const apps = await Promise.all(
enabledApps
.filter(({ ...app }) => app.slug === slug)
@@ -0,0 +1,5 @@
import { Reflector } from "@nestjs/core";
export type AllowedAuthMethod = "OAUTH_CLIENT_CREDENTIALS" | "API_KEY" | "ACCESS_TOKEN" | "NEXT_AUTH";
export const ApiAuthGuardOnlyAllow = Reflector.createDecorator<AllowedAuthMethod[]>();
@@ -1,7 +1,20 @@
import { ExecutionContext, Inject } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { AuthGuard } from "@nestjs/passport";
import { ApiAuthGuardOnlyAllow } from "../../decorators/api-auth-guard-only-allow.decorator";
export class ApiAuthGuard extends AuthGuard("api-auth") {
constructor() {
constructor(@Inject(Reflector) private readonly reflector: Reflector) {
super();
}
getRequest(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const allowedMethods = this.reflector.get(ApiAuthGuardOnlyAllow, context.getHandler());
request.allowedAuthMethods = allowedMethods;
return request;
}
}
@@ -18,11 +18,14 @@ import { getToken } from "next-auth/jwt";
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import type { AllowedAuthMethod } from "../../decorators/api-auth-guard-only-allow.decorator";
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
export type ApiAuthGuardRequest = Request & {
authMethod: AuthMethods;
organizationId: number | null;
user: ApiAuthGuardUser;
allowedAuthMethods?: AllowedAuthMethod[];
};
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";
@@ -49,12 +52,19 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
const oAuthClientId = params.clientId || request.get(X_CAL_CLIENT_ID);
const bearerToken = request.get("Authorization")?.replace("Bearer ", "");
if (oAuthClientId && oAuthClientSecret) {
const allowedMethods = request.allowedAuthMethods;
const noSpecificAuthExpected = !allowedMethods || !allowedMethods.length;
const oAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("OAUTH_CLIENT_CREDENTIALS");
const apiKeyAllowed = noSpecificAuthExpected || allowedMethods.includes("API_KEY");
const accessTokenAllowed = noSpecificAuthExpected || allowedMethods.includes("ACCESS_TOKEN");
const nextAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("NEXT_AUTH");
if (oAuthClientId && oAuthClientSecret && oAuthAllowed) {
request.authMethod = AuthMethods["OAUTH_CLIENT"];
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret, request);
}
if (bearerToken) {
if (bearerToken && (apiKeyAllowed || accessTokenAllowed)) {
const requestOrigin = request.get("Origin");
request.authMethod = isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
? AuthMethods["API_KEY"]
@@ -64,13 +74,18 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
if (nextAuthToken) {
if (nextAuthToken && nextAuthAllowed) {
request.authMethod = AuthMethods["NEXT_AUTH"];
return await this.authenticateNextAuth(nextAuthToken, request);
}
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
const noAuthProvided = !oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken;
if (noAuthProvided) {
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
}
throw new UnauthorizedException(
`ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${allowedMethods}`
);
} catch (err) {
if (err instanceof Error) {
return this.error(err);
@@ -1,3 +1,4 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { BillingProcessor } from "@/modules/billing/billing.processor";
import { BillingRepository } from "@/modules/billing/billing.repository";
import { BillingController } from "@/modules/billing/controllers/billing.controller";
@@ -5,6 +6,7 @@ import { BillingConfigService } from "@/modules/billing/services/billing.config.
import { BillingService } from "@/modules/billing/services/billing.service";
import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.billing.service";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
@@ -33,6 +35,8 @@ import { Module } from "@nestjs/common";
BillingRepository,
BillingProcessor,
ManagedOrganizationsBillingService,
OAuthClientRepository,
BookingsRepository_2024_08_13,
],
exports: [BillingService, BillingRepository, ManagedOrganizationsBillingService],
controllers: [BillingController],
@@ -15,12 +15,21 @@ export class BillingRepository {
},
});
async getBillingForTeamBySubscriptionId(subscriptionId: string) {
return this.dbRead.prisma.platformBilling.findFirst({
where: {
subscriptionId,
},
});
}
async updateTeamBilling(
teamId: number,
billingStart: number,
billingEnd: number,
plan: PlatformPlan,
subscriptionId?: string
subscriptionId?: string,
priceId?: string
) {
return this.dbWrite.prisma.platformBilling.update({
where: {
@@ -32,6 +41,7 @@ export class BillingRepository {
subscriptionId,
plan: plan.toString(),
overdue: false,
priceId,
},
});
}
@@ -20,6 +20,7 @@ import {
HttpCode,
HttpStatus,
Logger,
Delete,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ApiExcludeController } from "@nestjs/swagger";
@@ -96,6 +97,19 @@ export class BillingController {
};
}
@Delete("/:organizationId/unsubscribe")
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
@MembershipRoles(["OWNER", "ADMIN"])
async cancelTeamSubscriptionInStripe(
@Param("organizationId") organizationId: number
): Promise<ApiResponse> {
await this.billingService.cancelTeamSubscription(organizationId);
return {
status: "success",
};
}
@Post("/webhook")
@HttpCode(HttpStatus.OK)
async stripeWebhook(
@@ -113,6 +127,9 @@ export class BillingController {
case "customer.subscription.deleted":
await this.billingService.handleStripeSubscriptionDeleted(event);
break;
case "invoice.created":
await this.billingService.handleStripeSubscriptionForActiveManagedUsers(event);
break;
case "invoice.payment_failed":
await this.billingService.handleStripePaymentFailed(event);
break;
@@ -1,12 +1,16 @@
import { AppConfig } from "@/config/type";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor";
import { BillingRepository } from "@/modules/billing/billing.repository";
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
import { PlatformPlan } from "@/modules/billing/types";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { StripeService } from "@/modules/stripe/stripe.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { InjectQueue } from "@nestjs/bull";
import {
BadRequestException,
Injectable,
InternalServerErrorException,
Logger,
@@ -29,6 +33,9 @@ export class BillingService implements OnModuleDestroy {
private readonly billingRepository: BillingRepository,
private readonly configService: ConfigService<AppConfig>,
private readonly billingConfigService: BillingConfigService,
private readonly usersRepository: UsersRepository,
private readonly oAuthClientRepository: OAuthClientRepository,
private readonly bookingsRepository: BookingsRepository_2024_08_13,
@InjectQueue(BILLING_QUEUE) private readonly billingQueue: Queue
) {
this.webAppUrl = this.configService.get("app.baseUrl", { infer: true }) ?? "https://app.cal.com";
@@ -118,7 +125,7 @@ export class BillingService implements OnModuleDestroy {
return url;
}
async setSubscriptionForTeam(teamId: number, subscriptionId: string, plan: PlatformPlan) {
async setPerBookingSubscriptionForTeam(teamId: number, subscriptionId: string, plan: PlatformPlan) {
const billingCycleStart = DateTime.now().get("day");
const billingCycleEnd = DateTime.now().plus({ month: 1 }).get("day");
@@ -131,6 +138,25 @@ export class BillingService implements OnModuleDestroy {
);
}
async setPerActiveUserSubscriptionForTeam(
teamId: number,
subscriptionId: string,
plan: PlatformPlan,
priceId: string
) {
const billingCycleStart = DateTime.now().get("day");
const billingCycleEnd = DateTime.now().plus({ month: 1 }).get("day");
return this.billingRepository.updateTeamBilling(
teamId,
billingCycleStart,
billingCycleEnd,
plan,
subscriptionId,
priceId
);
}
async handleStripeSubscriptionDeleted(event: Stripe.Event) {
const subscription = event.data.object as Stripe.Subscription;
const teamId = subscription?.metadata?.teamId;
@@ -203,9 +229,19 @@ export class BillingService implements OnModuleDestroy {
this.logger.log("Webhook received but not pertaining to Platform, discarding.");
return;
}
const isPriceIdPresent = Boolean(checkoutSession.metadata?.priceId);
if (checkoutSession.mode === "subscription") {
await this.setSubscriptionForTeam(
if (checkoutSession.mode === "subscription" && isPriceIdPresent) {
await this.setPerActiveUserSubscriptionForTeam(
teamId,
checkoutSession.subscription as string,
PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan],
checkoutSession.metadata?.priceId
);
}
if (checkoutSession.mode === "subscription" && !isPriceIdPresent) {
await this.setPerBookingSubscriptionForTeam(
teamId,
checkoutSession.subscription as string,
PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan]
@@ -219,6 +255,67 @@ export class BillingService implements OnModuleDestroy {
return;
}
async handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event) {
const invoice = event.data.object as Stripe.Invoice;
const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
if (!subscriptionId) {
throw new NotFoundException("No subscription found for team");
}
const teamWithBilling = await this.billingRepository.getBillingForTeamBySubscriptionId(subscriptionId);
if (teamWithBilling?.plan === "PER_ACTIVE_USER") {
const activeManagedUsersCount = await this.getActiveManagedUsersCount(
subscriptionId,
new Date(invoice.period_start * 1000),
new Date(invoice.period_end * 1000)
);
await this.stripeService.getStripe().subscriptions.update(subscriptionId, {
items: [
{
quantity: activeManagedUsersCount > 0 ? activeManagedUsersCount : 1,
},
],
});
}
}
async getActiveManagedUsersCount(subscriptionId: string, invoiceStart: Date, invoiceEnd: Date) {
const managedUsersEmails = await this.usersRepository.getOrgsManagedUserEmailsBySubscriptionId(
subscriptionId
);
if (!managedUsersEmails) return 0;
if (!invoiceStart || !invoiceEnd) {
this.logger.log("Invoice period start or end date is null");
return 0;
}
const activeManagedUserEmailsAsHost = await this.usersRepository.getActiveManagedUsersAsHost(
subscriptionId,
invoiceStart,
invoiceEnd
);
const activeHostEmails = activeManagedUserEmailsAsHost.map((email) => email.email);
const notActiveHostEmails = managedUsersEmails
.filter((email) => !activeHostEmails.includes(email.email))
.map((email) => email.email);
if (notActiveHostEmails.length === 0) return activeManagedUserEmailsAsHost.length;
const activeManagedUserEmailsAsAttendee = await this.usersRepository.getActiveManagedUsersAsAttendee(
notActiveHostEmails,
invoiceStart,
invoiceEnd
);
return activeManagedUserEmailsAsAttendee.length + activeManagedUserEmailsAsHost.length;
}
async updateStripeSubscriptionForTeam(teamId: number, plan: PlatformPlan) {
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
@@ -262,7 +359,7 @@ export class BillingService implements OnModuleDestroy {
proration_behavior: "create_prorations",
});
await this.setSubscriptionForTeam(
await this.setPerBookingSubscriptionForTeam(
teamId,
teamWithBilling?.platformBilling?.subscriptionId,
PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan]
@@ -313,6 +410,28 @@ export class BillingService implements OnModuleDestroy {
}
}
async cancelTeamSubscription(teamId: number) {
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
const customerId = teamWithBilling?.platformBilling?.customerId;
if (!customerId) {
throw new NotFoundException("No customer id found for team in Stripe");
}
if (!teamWithBilling?.platformBilling || !teamWithBilling?.platformBilling.subscriptionId) {
throw new NotFoundException("Team plan not found");
}
try {
await this.stripeService
.getStripe()
.subscriptions.cancel(teamWithBilling?.platformBilling?.subscriptionId);
} catch (error) {
this.logger.log(error, "error while cancelling team subscription in stripe");
throw new BadRequestException("Failed to cancel team subscription");
}
}
async onModuleDestroy() {
try {
await this.billingQueue.close();
+2 -1
View File
@@ -4,6 +4,7 @@ export enum PlatformPlan {
ESSENTIALS = "ESSENTIALS",
SCALE = "SCALE",
ENTERPRISE = "ENTERPRISE",
PER_ACTIVE_USER = "PER_ACTIVE_USER",
}
export type PlatformPlanType = "FREE" | "STARTER" | "ESSENTIALS" | "SCALE" | "ENTERPRISE";
export type PlatformPlanType = "FREE" | "STARTER" | "ESSENTIALS" | "SCALE" | "ENTERPRISE" | "PER_ACTIVE_USER";
@@ -11,6 +11,7 @@ import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/
import { RouterModule } from "@/modules/router/router.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { TimezoneModule } from "@/modules/timezones/timezones.module";
import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { Module } from "@nestjs/common";
@@ -33,6 +34,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
OrganizationsUsersBookingsModule,
OrganizationsBookingsModule,
OrganizationsRoutingFormsModule,
VerifiedResourcesModule,
RouterModule,
],
})
@@ -0,0 +1,36 @@
import type { OnModuleDestroy } from "@nestjs/common";
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { DeduplicateJoinsPlugin, Kysely, ParseJSONResultsPlugin, PostgresDialect } from "kysely";
import { Pool } from "pg";
import type { DB } from "@calcom/kysely/types";
@Injectable()
export class KyselyReadService implements OnModuleDestroy {
private logger = new Logger("KyselyReadService");
public kysely: Kysely<DB>;
constructor(readonly configService: ConfigService) {
const dbUrl = configService.get("db.readUrl", { infer: true });
const pool = new Pool({ connectionString: dbUrl });
// 3. Create the Dialect, passing the configured pool instance
const dialect = new PostgresDialect({
pool: pool, // Use the pool instance created above
});
this.kysely = new Kysely<DB>({
dialect,
plugins: [new ParseJSONResultsPlugin(), new DeduplicateJoinsPlugin()],
});
}
async onModuleDestroy() {
try {
await this.kysely.destroy();
} catch (error) {
this.logger.error(error);
}
}
}
@@ -0,0 +1,36 @@
import type { OnModuleDestroy } from "@nestjs/common";
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Kysely, ParseJSONResultsPlugin, PostgresDialect, DeduplicateJoinsPlugin } from "kysely";
import { Pool } from "pg";
import type { DB } from "@calcom/kysely/types";
@Injectable()
export class KyselyWriteService implements OnModuleDestroy {
private logger = new Logger("KyselyWriteService");
public kysely: Kysely<DB>;
constructor(readonly configService: ConfigService) {
const dbUrl = configService.get("db.writeUrl", { infer: true });
const pool = new Pool({ connectionString: dbUrl });
// 3. Create the Dialect, passing the configured pool instance
const dialect = new PostgresDialect({
pool: pool, // Use the pool instance created above
});
this.kysely = new Kysely<DB>({
dialect,
plugins: [new ParseJSONResultsPlugin(), new DeduplicateJoinsPlugin()],
});
}
async onModuleDestroy() {
try {
await this.kysely.destroy();
} catch (error) {
this.logger.error(error);
}
}
}
@@ -0,0 +1,9 @@
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
import { KyselyWriteService } from "@/modules/kysely/kysely-write.service";
import { Module } from "@nestjs/common";
@Module({
providers: [KyselyReadService, KyselyWriteService],
exports: [KyselyReadService, KyselyWriteService],
})
export class KyselyModule {}
@@ -63,6 +63,7 @@ export class MembershipsRepository {
async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) {
const membership = await this.dbRead.prisma.membership.create({
data: {
createdAt: new Date(),
role,
teamId,
userId,
@@ -177,7 +177,7 @@ export class OAuthClientUsersController {
const { id } = await this.validateManagedUserOwnership(oAuthClientId, userId);
const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } =
await this.tokensRepository.createOAuthTokens(oAuthClientId, id, true);
await this.tokensRepository.forceRefreshOAuthTokens(oAuthClientId, id);
return {
status: SUCCESS_STATUS,
@@ -45,7 +45,7 @@ export class OrganizationsBookingsController {
): Promise<GetBookingsOutput_2024_08_13> {
const { userIds, ...restParams } = queryParams;
const bookings = await this.bookingsService.getBookings(
const { bookings, pagination } = await this.bookingsService.getBookings(
{ ...restParams },
{ email: user.email, id: user.id, orgId },
userIds
@@ -54,6 +54,7 @@ export class OrganizationsBookingsController {
return {
status: SUCCESS_STATUS,
data: bookings,
pagination,
};
}
}
@@ -340,6 +340,7 @@ describe("Organizations Event Types Endpoints", () => {
requiresBookerEmailVerification: true,
hideCalendarNotes: true,
hideCalendarEventDetails: true,
hideOrganizerEmail: true,
lockTimeZoneToggleOnBookingPage: true,
color: {
darkThemeHex: "#292929",
@@ -371,6 +372,7 @@ describe("Organizations Event Types Endpoints", () => {
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail);
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
expect(data.color).toEqual(body.color);
expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234");
@@ -1099,6 +1101,7 @@ describe("Organizations Event Types Endpoints", () => {
requiresBookerEmailVerification: true,
hideCalendarNotes: true,
hideCalendarEventDetails: true,
hideOrganizerEmail: true,
lockTimeZoneToggleOnBookingPage: true,
color: {
darkThemeHex: "#292929",
@@ -1130,6 +1133,7 @@ describe("Organizations Event Types Endpoints", () => {
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail);
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
expect(data.color).toEqual(body.color);
expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234");
@@ -1,4 +1,5 @@
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
@@ -27,7 +28,8 @@ export class InputOrganizationsEventTypesService {
private readonly teamsRepository: TeamsRepository,
private readonly usersRepository: UsersRepository,
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
private readonly conferencingService: OrganizationsConferencingService
private readonly conferencingService: OrganizationsConferencingService,
private readonly conferencingRepository: ConferencingRepository
) {}
async transformAndValidateCreateTeamEventTypeInput(
userId: number,
@@ -306,6 +308,22 @@ export class InputOrganizationsEventTypesService {
}) ?? []
);
}
async checkAppIsValidAndConnected(teamId: number, app: string) {
const conferencingApps = ["google-meet", "office365-video", "zoom"];
if (!conferencingApps.includes(app)) {
throw new BadRequestException("Invalid app, available apps are: ", conferencingApps.join(", "));
}
if (app === "office365-video") {
app = "msteams";
}
const credential = await this.conferencingRepository.findTeamConferencingApp(teamId, app);
if (!credential) {
throw new BadRequestException(`${app} not connected.`);
}
return credential;
}
}
function getPriorityValue(priority: keyof typeof HostPriority): number {
@@ -70,6 +70,7 @@ type Input = Pick<
| "eventName"
| "useEventTypeDestinationCalendarEmail"
| "hideCalendarEventDetails"
| "hideOrganizerEmail"
| "team"
>;
@@ -6,6 +6,7 @@ import { CreateOrgMembershipOutput } from "@/modules/organizations/memberships/o
import { DeleteOrgMembership } from "@/modules/organizations/memberships/outputs/delete-membership.output";
import { GetAllOrgMemberships } from "@/modules/organizations/memberships/outputs/get-all-memberships.output";
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
import { OrgUserAttribute } from "@/modules/organizations/memberships/outputs/organization-membership.output";
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
@@ -14,17 +15,16 @@ import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import { Attribute, AttributeOption, User } from "@prisma/client";
import * as request from "supertest";
import { AttributeRepositoryFixture } from "test/fixtures/repository/attributes.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
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";
import { ApiSuccessResponse } from "@calcom/platform-types";
import { Membership, Team } from "@calcom/prisma/client";
describe("Organizations Memberships Endpoints", () => {
@@ -33,7 +33,8 @@ describe("Organizations Memberships Endpoints", () => {
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let membershipRepositoryFixture: MembershipRepositoryFixture;
let attributesRepositoryFixture: AttributeRepositoryFixture;
let org: Team;
let membership: Membership;
@@ -49,6 +50,17 @@ describe("Organizations Memberships Endpoints", () => {
let userToInviteViaApi: User;
let textAttribute: Attribute;
let multiSelectAttribute: Attribute;
let numberAttribute: Attribute;
let singleSelectAttribute: Attribute;
let textAttributeOption: AttributeOption;
let multiSelectAttributeOption: AttributeOption;
let multiSelectAttributeOption2: AttributeOption;
let numberAttributeOption: AttributeOption;
let singleSelectAttributeOption: AttributeOption;
const metadata = {
some: "key",
};
@@ -64,7 +76,8 @@ describe("Organizations Memberships Endpoints", () => {
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
attributesRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
@@ -91,16 +104,42 @@ describe("Organizations Memberships Endpoints", () => {
isOrganization: true,
});
membership = await membershipsRepositoryFixture.create({
await setupAttributes();
membership = await membershipRepositoryFixture.create({
role: "ADMIN",
user: { connect: { id: user.id } },
team: { connect: { id: org.id } },
});
membership2 = await membershipsRepositoryFixture.create({
membership2 = await membershipRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: user2.id } },
team: { connect: { id: org.id } },
AttributeToUser: {
create: [
{
attributeOption: { connect: { id: textAttributeOption.id } },
weight: 100,
},
{
attributeOption: { connect: { id: multiSelectAttributeOption.id } },
weight: 100,
},
{
attributeOption: { connect: { id: multiSelectAttributeOption2.id } },
weight: null,
},
{
attributeOption: { connect: { id: numberAttributeOption.id } },
weight: 100,
},
{
attributeOption: { connect: { id: singleSelectAttributeOption.id } },
weight: 100,
},
],
},
});
app = moduleRef.createNestApplication();
@@ -109,6 +148,87 @@ describe("Organizations Memberships Endpoints", () => {
await app.init();
});
async function setupAttributes() {
textAttribute = await attributesRepositoryFixture.create({
team: { connect: { id: org.id } },
type: "TEXT",
name: "team",
slug: `team-${randomString()}`,
enabled: true,
usersCanEditRelation: false,
isWeightsEnabled: false,
isLocked: false,
});
multiSelectAttribute = await attributesRepositoryFixture.create({
team: { connect: { id: org.id } },
type: "MULTI_SELECT",
name: "skills",
slug: `skills-${randomString()}`,
enabled: true,
usersCanEditRelation: false,
isWeightsEnabled: false,
isLocked: false,
});
numberAttribute = await attributesRepositoryFixture.create({
team: { connect: { id: org.id } },
type: "NUMBER",
name: "age",
slug: `age-${randomString()}`,
enabled: true,
usersCanEditRelation: false,
isWeightsEnabled: false,
isLocked: false,
});
singleSelectAttribute = await attributesRepositoryFixture.create({
team: { connect: { id: org.id } },
type: "SINGLE_SELECT",
name: "frontend",
slug: `frontend-${randomString()}`,
enabled: true,
usersCanEditRelation: false,
isWeightsEnabled: false,
isLocked: false,
});
textAttributeOption = await attributesRepositoryFixture.createOption({
attribute: { connect: { id: textAttribute.id } },
value: "coders",
slug: "coders",
isGroup: false,
});
multiSelectAttributeOption = await attributesRepositoryFixture.createOption({
attribute: { connect: { id: multiSelectAttribute.id } },
value: "javascript",
slug: "javascript",
isGroup: false,
});
multiSelectAttributeOption2 = await attributesRepositoryFixture.createOption({
attribute: { connect: { id: multiSelectAttribute.id } },
value: "typescript",
slug: "typescript",
isGroup: false,
});
numberAttributeOption = await attributesRepositoryFixture.createOption({
attribute: { connect: { id: numberAttribute.id } },
value: "18",
slug: "18",
isGroup: false,
});
singleSelectAttributeOption = await attributesRepositoryFixture.createOption({
attribute: { connect: { id: singleSelectAttribute.id } },
value: "yes",
slug: "yes",
isGroup: false,
});
}
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(organizationsRepositoryFixture).toBeDefined();
@@ -139,9 +259,54 @@ describe("Organizations Memberships Endpoints", () => {
expect(responseBody.data[1].user.email).toEqual(user2.email);
expect(responseBody.data[1].user.username).toEqual(user2.username);
expect(responseBody.data[1].teamId).toEqual(org.id);
userHasCorrectAttributes(responseBody.data[1].attributes);
});
});
function userHasCorrectAttributes(attributes: OrgUserAttribute[]) {
expect(attributes.length).toEqual(4);
const responseNumberAttribute = attributes.find((attr) => attr.type === "number");
const responseSingleSelectAttribute = attributes.find((attr) => attr.type === "singleSelect");
const responseMultiSelectAttribute = attributes.find((attr) => attr.type === "multiSelect");
const responseTextAttribute = attributes.find((attr) => attr.type === "text");
expect(responseNumberAttribute).toEqual({
id: numberAttribute.id,
name: numberAttribute.name,
optionId: numberAttributeOption.id,
option: +numberAttributeOption.value,
type: "number",
});
expect(responseSingleSelectAttribute).toEqual({
id: singleSelectAttribute.id,
name: singleSelectAttribute.name,
optionId: singleSelectAttributeOption.id,
option: singleSelectAttributeOption.value,
type: "singleSelect",
});
expect(responseMultiSelectAttribute).toEqual({
id: multiSelectAttribute.id,
name: multiSelectAttribute.name,
options: [
{
optionId: multiSelectAttributeOption2.id,
option: multiSelectAttributeOption2.value,
},
{
optionId: multiSelectAttributeOption.id,
option: multiSelectAttributeOption.value,
},
],
type: "multiSelect",
});
expect(responseTextAttribute).toEqual({
id: textAttribute.id,
name: textAttribute.name,
optionId: textAttributeOption.id,
option: textAttributeOption.value,
type: "text",
});
}
it("should get all the memberships of the org paginated", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/memberships?skip=1&take=1`)
@@ -182,6 +347,25 @@ describe("Organizations Memberships Endpoints", () => {
});
});
it("should get the membership of the org", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/memberships/${membership2.id}`)
.expect(200)
.then((response) => {
const responseBody: GetOrgMembership = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.id).toEqual(membership2.id);
expect(responseBody.data.userId).toEqual(user2.id);
expect(responseBody.data.role).toEqual("MEMBER");
expect(responseBody.data.user.bio).toEqual(bio);
expect(responseBody.data.user.metadata).toEqual(metadata);
expect(responseBody.data.user.email).toEqual(user2.email);
expect(responseBody.data.user.username).toEqual(user2.username);
expect(responseBody.data.teamId).toEqual(org.id);
userHasCorrectAttributes(responseBody.data.attributes);
});
});
it("should create the membership of the org", async () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/memberships`)
@@ -259,7 +443,8 @@ describe("Organizations Memberships Endpoints", () => {
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let membershipRepositoryFixture: MembershipRepositoryFixture;
let attributesRepositoryFixture: AttributeRepositoryFixture;
let org: Team;
let membership: Membership;
@@ -277,7 +462,8 @@ describe("Organizations Memberships Endpoints", () => {
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
attributesRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
@@ -289,7 +475,7 @@ describe("Organizations Memberships Endpoints", () => {
isOrganization: true,
});
membership = await membershipsRepositoryFixture.create({
membership = await membershipRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: user.id } },
team: { connect: { id: org.id } },
@@ -20,7 +20,6 @@ import { GetAllOrgMemberships } from "@/modules/organizations/memberships/output
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
import {
Controller,
UseGuards,
@@ -36,7 +35,6 @@ import {
HttpStatus,
} from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SkipTakePagination } from "@calcom/platform-types";
@@ -70,9 +68,7 @@ export class OrganizationsMembershipsController {
);
return {
status: SUCCESS_STATUS,
data: memberships.map((membership) =>
plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" })
),
data: memberships,
};
}
@@ -88,7 +84,7 @@ export class OrganizationsMembershipsController {
const membership = await this.organizationsMembershipService.createOrgMembership(orgId, body);
return {
status: SUCCESS_STATUS,
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
data: membership,
};
}
@@ -105,7 +101,7 @@ export class OrganizationsMembershipsController {
const membership = await this.organizationsMembershipService.getOrgMembership(orgId, membershipId);
return {
status: SUCCESS_STATUS,
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
data: membership,
};
}
@@ -122,7 +118,7 @@ export class OrganizationsMembershipsController {
const membership = await this.organizationsMembershipService.deleteOrgMembership(orgId, membershipId);
return {
status: SUCCESS_STATUS,
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
data: membership,
};
}
@@ -144,7 +140,7 @@ export class OrganizationsMembershipsController {
);
return {
status: SUCCESS_STATUS,
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
data: membership,
};
}
}
@@ -4,8 +4,31 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { MembershipUserSelect } from "@/modules/teams/memberships/teams-memberships.repository";
import { Injectable } from "@nestjs/common";
import { Prisma } from "@calcom/prisma/client";
import { UpdateOrgMembershipDto } from "./inputs/update-organization-membership.input";
export type DbOrgMembership = Awaited<ReturnType<OrganizationsMembershipRepository["findOrgMembership"]>>;
const attributeToUserSelect = {
createdByDSyncId: true,
weight: true,
attributeOption: {
select: {
id: true,
value: true,
slug: true,
attribute: {
select: {
id: true,
name: true,
type: true,
},
},
},
},
} as const satisfies Prisma.AttributeToUserSelect;
@Injectable()
export class OrganizationsMembershipRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
@@ -16,7 +39,12 @@ export class OrganizationsMembershipRepository {
id: membershipId,
teamId: organizationId,
},
include: { user: { select: MembershipUserSelect } },
include: {
user: { select: MembershipUserSelect },
AttributeToUser: {
select: attributeToUserSelect,
},
},
});
}
@@ -26,7 +54,12 @@ export class OrganizationsMembershipRepository {
teamId: organizationId,
userId,
},
include: { user: { select: MembershipUserSelect } },
include: {
user: { select: MembershipUserSelect },
AttributeToUser: {
select: attributeToUserSelect,
},
},
});
}
@@ -36,7 +69,12 @@ export class OrganizationsMembershipRepository {
id: membershipId,
teamId: organizationId,
},
include: { user: { select: MembershipUserSelect } },
include: {
user: { select: MembershipUserSelect },
AttributeToUser: {
select: attributeToUserSelect,
},
},
});
}
@@ -45,14 +83,24 @@ export class OrganizationsMembershipRepository {
create: { ...data, teamId: organizationId },
update: { role: data.role, accepted: data.accepted, disableImpersonation: data.disableImpersonation },
where: { userId_teamId: { userId: data.userId, teamId: organizationId } },
include: { user: { select: MembershipUserSelect } },
include: {
user: { select: MembershipUserSelect },
AttributeToUser: {
select: attributeToUserSelect,
},
},
});
}
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
return this.dbWrite.prisma.membership.update({
data: { ...data },
where: { id: membershipId, teamId: organizationId },
include: { user: { select: MembershipUserSelect } },
include: {
user: { select: MembershipUserSelect },
AttributeToUser: {
select: attributeToUserSelect,
},
},
});
}
@@ -61,7 +109,12 @@ export class OrganizationsMembershipRepository {
where: {
teamId: organizationId,
},
include: { user: { select: MembershipUserSelect } },
include: {
user: { select: MembershipUserSelect },
AttributeToUser: {
select: attributeToUserSelect,
},
},
skip,
take,
});
@@ -1,4 +1,4 @@
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
@@ -11,10 +11,10 @@ export class CreateOrgMembershipOutput {
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: TeamMembershipOutput,
type: OrganizationMembershipOutput,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => TeamMembershipOutput)
data!: TeamMembershipOutput;
@Type(() => OrganizationMembershipOutput)
data!: OrganizationMembershipOutput;
}
@@ -1,4 +1,4 @@
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum } from "class-validator";
@@ -9,5 +9,5 @@ export class DeleteOrgMembership {
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
data!: TeamMembershipOutput;
data!: OrganizationMembershipOutput;
}
@@ -1,4 +1,4 @@
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested, IsArray } from "class-validator";
@@ -11,11 +11,11 @@ export class GetAllOrgMemberships {
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: TeamMembershipOutput,
type: OrganizationMembershipOutput,
})
@IsNotEmptyObject()
@ValidateNested({ each: true })
@Type(() => TeamMembershipOutput)
@Type(() => OrganizationMembershipOutput)
@IsArray()
data!: TeamMembershipOutput[];
data!: OrganizationMembershipOutput[];
}
@@ -1,4 +1,4 @@
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
@@ -11,10 +11,10 @@ export class GetOrgMembership {
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: TeamMembershipOutput,
type: OrganizationMembershipOutput,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => TeamMembershipOutput)
data!: TeamMembershipOutput;
@Type(() => OrganizationMembershipOutput)
data!: OrganizationMembershipOutput;
}
@@ -0,0 +1,114 @@
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsArray, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
class BaseAttribute {
@IsString()
@Expose()
@ApiProperty()
id!: string;
@IsString()
@Expose()
@ApiProperty()
name!: string;
}
export class TextAttribute extends BaseAttribute {
@IsString()
@Expose()
@ApiProperty()
type!: "text";
@IsString()
@Expose()
@ApiProperty()
option!: string;
@IsString()
@Expose()
@ApiProperty()
optionId!: string;
}
export class NumberAttribute extends BaseAttribute {
@IsString()
@Expose()
@ApiProperty()
type!: "number";
@IsNumber()
@Expose()
@ApiProperty()
option!: number;
@IsString()
@Expose()
@ApiProperty()
optionId!: string;
}
export class SingleSelectAttribute extends BaseAttribute {
@IsString()
@IsString()
@Expose()
@ApiProperty()
type!: "singleSelect";
@IsString()
@Expose()
@ApiProperty()
option!: string;
@IsString()
@Expose()
@ApiProperty()
optionId!: string;
}
class MultiSelectAttributeOption {
@IsString()
@Expose()
@ApiProperty()
optionId!: string;
@IsString()
@Expose()
@ApiProperty()
option!: string;
}
export class MultiSelectAttribute extends BaseAttribute {
@IsString()
@Expose()
@ApiProperty()
type!: "multiSelect";
@IsArray()
@ValidateNested({ each: true })
@Expose()
@ApiProperty()
options!: MultiSelectAttributeOption[];
}
export type OrgUserAttribute = TextAttribute | NumberAttribute | SingleSelectAttribute | MultiSelectAttribute;
@ApiExtraModels(BaseAttribute, TextAttribute, NumberAttribute, SingleSelectAttribute, MultiSelectAttribute)
export class OrganizationMembershipOutput extends TeamMembershipOutput {
@IsArray()
@ValidateNested({ each: true })
@Expose()
@ApiProperty({
required: true,
oneOf: [
{ $ref: getSchemaPath(TextAttribute) },
{ $ref: getSchemaPath(NumberAttribute) },
{ $ref: getSchemaPath(SingleSelectAttribute) },
{ $ref: getSchemaPath(MultiSelectAttribute) },
],
type: "array",
})
@Type(() => Object)
attributes!: OrgUserAttribute[];
}
@@ -1,4 +1,4 @@
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
@@ -11,10 +11,10 @@ export class UpdateOrgMembership {
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: TeamMembershipOutput,
type: OrganizationMembershipOutput,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => TeamMembershipOutput)
data!: TeamMembershipOutput;
@Type(() => OrganizationMembershipOutput)
data!: OrganizationMembershipOutput;
}
@@ -0,0 +1,89 @@
import { DbOrgMembership } from "@/modules/organizations/memberships/organizations-membership.repository";
import {
MultiSelectAttribute,
NumberAttribute,
OrganizationMembershipOutput,
OrgUserAttribute,
SingleSelectAttribute,
TextAttribute,
} from "@/modules/organizations/memberships/outputs/organization-membership.output";
import { Injectable } from "@nestjs/common";
import { plainToClass } from "class-transformer";
import { groupMembershipAttributes } from "@calcom/platform-libraries";
import type { GroupedAttribute } from "@calcom/platform-libraries";
@Injectable()
export class OrganizationsMembershipOutputService {
getOrgMembershipsOutput(memberships: NonNullable<DbOrgMembership>[]) {
return memberships.map((membership) => this.getOrgMembershipOutput(membership));
}
getOrgMembershipOutput(organizationMembership: NonNullable<DbOrgMembership>) {
const { AttributeToUser, ...orgMembership } = organizationMembership;
const groupedMembershipAttributes: GroupedAttribute[] = groupMembershipAttributes(AttributeToUser);
const attributesOutput = this.getAttributesOutput(groupedMembershipAttributes);
return plainToClass(OrganizationMembershipOutput, { ...orgMembership, attributes: attributesOutput });
}
private getAttributesOutput(attributes: GroupedAttribute[]): OrgUserAttribute[] {
return attributes.map((attribute) => {
switch (attribute.type) {
case "TEXT":
return this.getTextAttributeOutput(attribute);
case "NUMBER":
return this.getNumberAttributeOutput(attribute);
case "SINGLE_SELECT":
return this.getSingleSelectAttributeOutput(attribute);
case "MULTI_SELECT":
return this.getMultiSelectAttributeOutput(attribute);
default:
return this.getTextAttributeOutput(attribute);
}
});
}
private getTextAttributeOutput(attribute: GroupedAttribute): TextAttribute {
return {
id: attribute.id,
name: attribute.name,
optionId: attribute.options[0].id,
option: attribute.options[0].value,
type: "text",
};
}
private getNumberAttributeOutput(attribute: GroupedAttribute): NumberAttribute {
return {
id: attribute.id,
name: attribute.name,
optionId: attribute.options[0].id,
option: +attribute.options[0].value,
type: "number",
};
}
private getSingleSelectAttributeOutput(attribute: GroupedAttribute): SingleSelectAttribute {
return {
id: attribute.id,
name: attribute.name,
optionId: attribute.options[0].id,
option: attribute.options[0].value,
type: "singleSelect",
};
}
private getMultiSelectAttributeOutput(attribute: GroupedAttribute): MultiSelectAttribute {
return {
id: attribute.id,
name: attribute.name,
options: attribute.options.map((option: GroupedAttribute["options"][number]) => ({
optionId: option.id,
option: option.value,
})),
type: "multiSelect",
};
}
}
@@ -1,19 +1,30 @@
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
import { Injectable } from "@nestjs/common";
import { Injectable, NotFoundException } from "@nestjs/common";
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
@Injectable()
export class OrganizationsMembershipService {
constructor(private readonly organizationsMembershipRepository: OrganizationsMembershipRepository) {}
constructor(
private readonly organizationsMembershipRepository: OrganizationsMembershipRepository,
private readonly organizationsMembershipOutputService: OrganizationsMembershipOutputService
) {}
async getOrgMembership(organizationId: number, membershipId: number) {
const membership = await this.organizationsMembershipRepository.findOrgMembership(
organizationId,
membershipId
);
return membership;
if (!membership) {
throw new NotFoundException(
`Membership with id ${membershipId} within organization id ${organizationId} not found`
);
}
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
}
async getOrgMembershipByUserId(organizationId: number, userId: number) {
@@ -21,7 +32,13 @@ export class OrganizationsMembershipService {
organizationId,
userId
);
return membership;
if (!membership) {
throw new NotFoundException(
`Membership for user with id ${userId} within organization id ${organizationId} not found`
);
}
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
}
async getPaginatedOrgMemberships(organizationId: number, skip = 0, take = 250) {
@@ -30,7 +47,7 @@ export class OrganizationsMembershipService {
skip,
take
);
return memberships;
return this.organizationsMembershipOutputService.getOrgMembershipsOutput(memberships);
}
async deleteOrgMembership(organizationId: number, membershipId: number) {
@@ -38,7 +55,7 @@ export class OrganizationsMembershipService {
organizationId,
membershipId
);
return membership;
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
}
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
@@ -47,11 +64,11 @@ export class OrganizationsMembershipService {
membershipId,
data
);
return membership;
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
}
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
const membership = await this.organizationsMembershipRepository.createOrgMembership(organizationId, data);
return membership;
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
}
}
@@ -32,6 +32,7 @@ import { OrganizationsRepository } from "@/modules/organizations/index/organizat
import { OrganizationsService } from "@/modules/organizations/index/organizations.service";
import { OrganizationsMembershipsController } from "@/modules/organizations/memberships/organizations-membership.controller";
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
import { OrganizationsMembershipOutputService } from "@/modules/organizations/memberships/services/organizations-membership-output.service";
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
@@ -98,6 +99,7 @@ import { Module } from "@nestjs/common";
EmailService,
OrganizationsMembershipRepository,
OrganizationsMembershipService,
OrganizationsMembershipOutputService,
OrganizationsEventTypesService,
InputOrganizationsEventTypesService,
OutputOrganizationsEventTypesService,
@@ -10,6 +10,16 @@ export class CreateOrganizationInput extends RefreshApiKeyInput {
@ApiProperty({ description: "Name of the organization", example: "CalTeam" })
readonly name!: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({
type: String,
description:
"Organization slug in kebab-case - if not provided will be generated automatically based on name.",
example: "cal-tel",
})
readonly slug?: string;
@ApiPropertyOptional({
type: Object,
description: METADATA_DOCS,
@@ -46,6 +46,7 @@ import {
X_CAL_SECRET_KEY,
} from "@calcom/platform-constants";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { slugify } from "@calcom/platform-libraries";
import { ApiSuccessResponse, CreateOAuthClientInput } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
@@ -176,6 +177,7 @@ describe("Organizations Organizations Endpoints", () => {
managedOrg = responseBody.data;
expect(managedOrg?.id).toBeDefined();
expect(managedOrg?.name).toEqual(body.name);
expect(managedOrg?.slug).toEqual(slugify(body.name));
expect(managedOrg?.metadata).toEqual(body.metadata);
expect(managedOrg?.apiKey).toBeDefined();

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