Compare commits

...
226 Commits
Author SHA1 Message Date
Alex van Andel aedc0f5a03 chore: release v5.9.10
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-12-07 14:25:55 +00:00
Keith Williams ff88ec0eae fix: Docker build OOM issues in v5.9.9 (#25678) 2025-12-07 14:22:24 +00:00
Kartik Labhshetwar 110b5ceaf6 style: adjust dark hover background for user page to subtle (#25662) 2025-12-06 13:24:41 +00:00
Kartik Labhshetwar 7c53d2c9ce fix: improve spacing in provide link to team member when OOO list (#25529) 2025-12-06 09:12:48 +00:00
Volnei Munhoz eccebff4c6 Companion build (#25648) 2025-12-05 21:22:31 +00:00
cal.com 0a36dd7f61 revert: perf: handleChildrenEventType update existing events without mapping 2025-12-05 22:24:15 +02:00
cal.com 5143083efb perf: handleChildrenEventType update existing events without mapping 2025-12-05 22:00:33 +02:00
AbhishekandDhairyashil Shinde b874e0d55a fix: data table toolbar styling (#25632)
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-12-05 16:01:05 +00:00
Dhairyashil Shinde 3ee24586a7 feat(api): PATCH Event Type V2 API to support all current locations (#25084)
* feat(api): PATCH Event Type V2 API to support all current locations

* docs(api): update locations documentation and add E2E tests for new integrations

- Updated locations property documentation in create-event-type.input.ts
  and update-event-type.input.ts to clarify app installation requirements
- Explained that only Google Meet, MS Teams, and Zoom can be installed via API
- Noted that Cal Video is installed by default
- Added E2E tests for creating and updating event types with newly supported
  integration locations (jitsi, zoom, google-meet, whereby, huddle, element-call)
- Regenerated openapi.json with updated API documentation

Addresses feedback from Lauris regarding platform API location support.

* fix(api): use supportedIntegrations list for app validation

Updated checkAppIsValidAndConnected to use the full supportedIntegrations
list from locations.input.ts instead of hardcoded array. This allows all
27 supported conferencing apps to be set as event type locations via API,
as long as they are already connected by the user.

* fix(api): add slug mapping for all conferencing integrations

Added comprehensive slug mapping to translate API integration names
(e.g., 'facetime-video', 'whereby-video') to actual app slugs
(e.g., 'facetime', 'whereby'). This ensures the app lookup works
correctly for all 27 supported conferencing integrations.

Addresses AI bot feedback about slug mismatches.

* fix(api): add missing huddle to huddle01 slug mapping

Added mapping for huddle -> huddle01. Other apps like tandem, jitsi,
cal-video, google-meet, and zoom don't need mapping as their API names
already match their app slugs (handled by the fallback || appSlug).

* update key

* update ket

* test(api): update E2E tests to validate newly supported integrations

Replaced end-to-end tests with validation-focused tests that follow
the existing pattern. The new test creates event types with various
newly supported integrations (jitsi, whereby-video, huddle, tandem,
element-call-video) directly in the database (bypassing app connection
checks) and verifies the API correctly returns them.

This approach tests that the input validation accepts all 27 supported
integration types without requiring actual app installations in the
test environment.

* fix(api): correct slug mappings for whatsapp, shimmer, and jelly integrations

- Fixed whatsapp-video mapping from 'whatsappvideo' to 'whatsapp'
- Fixed shimmer-video mapping from 'shimmer' to 'shimmervideo'
- Fixed jelly-conferencing mapping from 'jelly-conferencing' to 'jelly'

All slug mappings now correctly match the actual app slugs in
packages/app-store/*/config.json files. This ensures proper app
validation when users create/update event types with these locations.

Addresses feedback from @pedroccastro

* updated openapi.json file because of main branch code

* test(api): add negative test for unsupported integration locations

- Added E2E test to validate 400 error when creating event type with unsupported integration
- Test verifies exact error message listing all supported integrations
- Uses imported supportedIntegrations constant for maintainability
- Follows same pattern as booking fields validation tests

Addresses feedback from @supalarry

* test(api): add negative test for patching event type with unconnected integration

- Added E2E test to validate 400 error when user tries to PATCH event type with jitsi integration they haven't connected
- Test verifies exact error message 'jitsi not connected.'
- Follows existing test patterns with proper cleanup
2025-12-05 19:51:58 +05:30
Dhairyashil Shinde b014ded5fd feat: api v2 event types ordering - user, team, org (#25177)
* feat: api-v2-event-types-ordering

* sort team and org event types

* revert: remove accidental changes to api-auth.strategy.ts

* docs: add ordering documentation and test for event types endpoints

- Added test assertion to verify event types are returned in descending order by ID (newest first)
- Added API documentation to user event types endpoint describing default ordering behavior
- Added API documentation to team event types endpoint describing default ordering behavior
- Added API documentation to organization event types endpoints describing default ordering behavior

Addresses PR feedback to document and test the ordering behavior introduced in the API v2 event types ordering feature.

* feat: add optional sortCreatedAt parameter to event types endpoints

- Add sortCreatedAt query parameter (SortOrderType: "asc" | "desc") to all event types endpoints
- Define SortOrder enum and SortOrderType in pagination.input.ts for reusability
- When not provided, no explicit ordering is applied (backward compatible)
- Update user, team, and organization event types endpoints
- Add comprehensive e2e tests for all sorting scenarios
- Fix circular dependency in platform-types import
- Thread sortCreatedAt through all service layers
- Use spread pattern for conditional orderBy to avoid empty array issues

Addresses PR feedback to make ordering opt-in rather than changing default behavior
2025-12-05 19:51:48 +05:30
Alex van Andel 46b88a3c81 chore: release v5.9.9
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-12-05 10:55:08 +00:00
sean-brydon c97f1928bb fixes refresh issue (#25616) 2025-12-05 09:37:35 +00:00
dea6ec5003 perf: handleChildrenEventTypes transaction (#25602)
* perf: handleChildrenEventTypes transaction

* fixup! perf: handleChildrenEventTypes transaction

* fix: e2e tests

* test: update handleChildrenEventTypes tests for createMany API

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

* fix: tests

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-05 08:35:29 +00:00
Rouh 8cace7f7a0 fix (a11y): Add aria-label to progressToast close button (#25618) 2025-12-05 04:40:12 -03:00
Hariom BalharaandDevin AI b80c0692dc fix: improve Vercel domain 'forbidden' error message and add schema fields (#25520)
* fix: improve Vercel domain 'forbidden' error message

The 'forbidden' error code from Vercel is a generic permission denial,
not specifically 'domain owned by another team'. It can occur for various
reasons: wrong teamId, token lacking project access, account-level
restrictions, or domain ownership by another team.

Updated the error message to be more accurate and changed the HTTP status
code from 400 to 403 to better reflect the permission error nature.

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

* fix: add message and invalidToken fields to Vercel error response schema

This allows better logging of Vercel API error responses, especially when
tokens become invalid. The new fields help with debugging permission issues.

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

* fix: revert status code to 400 for Vercel forbidden errors

Keep the status code at 400 to avoid potential downstream side effects
while still using the improved error message and schema fields.

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

* fix: use .nullish() instead of .optional() for message and invalidToken fields

.nullish() handles both null and undefined, which is safer for API responses.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-05 05:26:42 +00:00
Kartik LabhshetwarandDhairyashil Shinde 44291be7b7 fix: show rescheduled badge on mobile view in bookings list (#25542)
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-12-05 04:00:03 +00:00
Kartik 54440cf954 fix: weekly view slots not clickable with overlay calendar (#25484) 2025-12-04 21:11:45 +00:00
Dhairyashil Shinde 43315108a2 refactor: remove cal workflow trigger message from workflow templates (#25539)
* refactor: remove cal workflow trigger message

* remove from rating template
2025-12-04 20:45:38 +00:00
Emmanuel IsenahandDhairyashil Shinde 311b6e071d feat: support hideEventTypeDetails query param on booking pages (#25565)
* fix: lint error

* feat: support `hideEventTypeDetails` via query param

Allow the event type details to be hidden based on the
hideEventTypeDetails query parameter when not in embed mode

* feat: add hideEventTypeDetails prop to EventMeta

---------

Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-12-04 20:26:08 +00:00
Kartik 308c7fa12b fix: correct typos and HTML encoding in org invite emails (#25551)
* fix: correct typos and HTML encoding in org invite emails

* removed interpolation: { escapeValue: false }

* added interpolation: { escapeValue: false }
2025-12-04 19:53:44 +00:00
Anshumancanrock 51cf73218b fix: Radio buttons not filled in (CSS background-color) (#25599)
* Radio buttons not filled in (CSS background-color)

* fix: radio buttons not filled when selected in light mode
2025-12-04 19:49:02 +00:00
Joe Au-YeungandDevin AI dc44825ce5 fix: prevent overwriting existing event type values with undefined (#25624)
When updating an event type, fields like autoTranslateDescriptionEnabled
and disableGuests were being overwritten with false/default values even
when they weren't explicitly provided in the update request.

Changes:
- autoTranslateDescriptionEnabled: Only set when explicitly provided
  (not undefined) to avoid overwriting existing true values with false
- disableGuests: Only set when bookingFields is explicitly provided,
  since it's derived from the guests field in bookingFields

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-04 19:18:45 +00:00
Kartik Labhshetwar 908128ca5d style: improve dropdown option spacing and hover separation (#25605)
* feat(ui): improve dropdown option spacing and hover separation

* style: add flex column layout and spacing to timezone select scrollbar
2025-12-05 00:47:21 +05:30
Dhairyashil ShindeandPallav b12f14c538 feat: auto scroll sidebar to active team on navigation (#25555)
* feat: auto-scroll sidebar to active team on navigation

When navigating to team settings via "Edit team" from the teams list,
the sidebar now scrolls to show the selected team and expands its section.

- Extract team ID from URL path (/settings/teams/{id}/...)
- Use existing aria-controls attribute to find team element
- Scroll with smooth behavior centered in viewport

* revert formatting changes by prettier

---------

Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-12-05 00:46:18 +05:30
Anik Dhabal Babu f03e6075a2 fix: unable to confirm/reject booking from email (#25614)
* fix: unable to confirm/reject booking

* fix lint

* Refactor booking confirmation and rejection tests
2025-12-04 23:17:17 +05:30
Peer Richelsen a8aac6dc3a added eas (#25619) 2025-12-04 14:36:38 -03:00
Anik Dhabal Babu 1594567fd5 fix: issue with workflow meetingUrl (#25620) 2025-12-04 16:46:36 +00:00
Volnei Munhoz dde6e7d140 chore: bump nextjs version (#25592)
* bump nextjs version

* bump nextjs version for api v1
2025-12-04 10:46:34 -03:00
Eesh Midha d8f36d0665 fix: enable date picker taps on iOS by setting modal={false} (#25590) 2025-12-04 13:33:43 +00:00
Abhishek 9362253bfb fix: vertical tab spacing (#25612) 2025-12-04 12:56:05 +00:00
sean-brydonandEunjae Lee c91da96956 fix: reset store on getting started (#25607)
* reset store

* Update apps/web/modules/onboarding/getting-started/onboarding-view.tsx

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

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-12-04 11:23:52 +00:00
Abhishek dbb33b9632 fix(ui): improve the alignment of api keys code (#25570)
fix: api key horizontally centered
2025-12-04 10:54:48 +00:00
Anik Dhabal Babu ead0691ea3 fix: refactored few handler and repository (#25567)
* update

* update

* refactor
2025-12-04 11:43:29 +02:00
Abir Roy e4a6b97017 fix(routing-forms): correct variable encoding and URL construction in Routing Forms Event Redirect Custom URLs (#25499)
* fix: change slugification to uri encoding

* fix: add conditional joiner symbol between event type redirect url and all other search params

* fix: update integration test to expect encoded variable

* test: add tests for query parameters joiner logic
2025-12-04 08:59:22 +00:00
sean-brydon 98b862c048 fix: form issues in QA for onboarding (#25581)
## What does this PR do?
Fixes the issue where form state wasnt corectly recorded in QA

## 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
2025-12-04 08:58:00 +00:00
Hariom BalharaandDevin AI 78c411621e feat(embed-core): add public close() API for modal-based embeds (#24746)
* feat(embed-core): add public close() API for modal-based embeds

Add a proper public API method close() to the CalApi class that allows
users to programmatically close modal-based embeds (ModalBox and FloatingButton).

This replaces the need for the undocumented hack:
  cal.instance.actionManager.fire('__closeIframe')

The new API is cleaner and more intuitive:
  cal('close')

Key features:
- Works for modal-based embeds only (ModalBox and FloatingButton)
- Throws a clear error if called on inline embeds
- Well-documented with JSDoc comments explaining usage and limitations

Example usage:
  cal('on', {
    action: 'bookingSuccessful',
    callback: () => {
      cal('close');
    }
  });

Also fixed pre-existing lint issues: unused variable in catch block and
eslint-disable comment for missing rule definition.

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

* refactor(embed-core): rename close() to closeModal() for clarity

Rename the close() method to closeModal() to make it immediately clear
from the API name itself that this method is for modal-based embeds only.

This makes the API more self-documenting:
- Before: cal('close')
- After: cal('closeModal')

Updated:
- Method name from close() to closeModal()
- JSDoc example to use cal('closeModal')
- Error message to reference closeModal()

The TypeScript types automatically recognize the new action name through
the SingleInstructionMap type system.

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

* cleanup

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-04 05:48:11 -03:00
Lauris Skraucis a1e1bf8b76 chore: add atoms changeset for tailwind v4 (#25577) 2025-12-04 08:42:58 +00:00
d00cf329e8 fix: workflow reminder send to all RR hosts. (#25597)
* fix: wrkflow reminder issue

* Update EmailWorkflowService.ts

* update

* Refactor team member email extraction for clarity

* add tests

* Undo changes in EmailWorkflowService

* Get team members assigned to the booking

* fix: Update test to match EmailWorkflowService behavior for ROUND_ROBIN

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: Update CalendarEventBuilder tests to include host emails in attendees

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Apply suggestion from @cubic-dev-ai[bot]

Filter organizer our of `hostsToInclude`

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Only include organizer and host destination calendar

* Type fix

* Type fix

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-12-04 09:21:48 +02:00
Udit Takkar 843329ccce feat: distributed tracing - 3 (#25092)
* feat: distributed tracing 2

* feat: distributed tracing 2

* refactor: feedback

* refactor: feedback

* fix: type error

* fix: trpc error

* feat: distributed tracing - 3

* chore: translation

* refactor: improvements

* fix: feedback

* chore: remove
2025-12-04 05:51:26 +00:00
741144bff2 fix: correct busy slot title for ics feed integration troubleshoot (#23929)
* fix: correct busy slot title for ics feed integration troubleshoot

* Prevent 'as' call by wrapping getFirstPropertyValue in string

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-12-04 05:49:34 +00:00
Udit TakkarandMorgan a11f7e6510 feat: add session endpoint and booking access pbac (#24637)
* feat: add session endpoint

* fix: remove participant id

* refactor; feedback

* fix: e2e test

* refactor: add pbac

* refactor: use constant

* tests: add booking tests

* fix: conflicts

* chore: test

* fix: tests

* fix: tests

* chore: remove un necessary

* chore: docs

* fix: desc

* fix: booking-pbac gurd

* fix: booking-pbac gurd

* test: add unit tests

* fix: remove legacy pbac

* fix: chore remove

* fix: update tests

* test: add more test

* test: move it to new file

* chore: comment auth

* chore: add message

* chore: add message

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-12-04 05:45:17 +00:00
Anik Dhabal Babu 3a8bf7014c fix: add all the attendees to ics (#25601) 2025-12-04 05:38:38 +00:00
6f02fbe13b fix: expand Intercom allowed URLs and prevent double-prefixing (#25181)
Co-authored-by: peer@cal.com <peer@cal.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-04 05:36:24 +00:00
af3f44a7f5 feat: add data region dropdown to signup page (#25127)
* feat: add data region dropdown to signup page

Add a dropdown to the signup page that allows users to select their data region (United States or European Union). When a region is selected, the user is navigated to the appropriate domain (app.cal.com or app.cal.eu).

- Add SelectField component to signup page below the description text
- Detect current domain and set appropriate default value
- Navigate to correct domain when selection changes
- Add translation strings for data_region, united_states, and european_union

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

* added localhost

* fix: improve data region dropdown - SSR hydration, query params, and self-hosted protection (#25463)

* fix/remove-eu-us-dropdown-from-self-hosted-signup

* fix: remove localhost bypass and fix SSR hydration for data region dropdown

- Remove localhost bypass from IS_CALCOM check to prevent self-hosted instances from seeing Cal.com-specific dropdown
- Fix SSR hydration mismatch by using WEBAPP_URL instead of window.location for region detection
- Fix hostname replacement to preserve query parameters by modifying hostname directly instead of entire URL string

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
2025-12-04 05:34:24 +00:00
Joe Au-YeungandAnik Dhabal Babu 0605572969 fix: Log detailed error in getConnectedCalendar (#25338)
* Log error

* Reverse sent error message changes

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-12-03 19:51:51 +00:00
AbhishekandAnik Dhabal Babu 5ddfbcf225 fix: date picker content render issue (#25584)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-12-03 19:46:28 +00:00
Volnei Munhoz 2fc1078158 fix companion oidc url (#25572) 2025-12-03 19:38:41 +00:00
sean-brydon ad416ac9df fix error spam (#25591) 2025-12-03 19:36:05 +00:00
Anik Dhabal Babu 2a7439b58d fix: e2e flake (#25578) 2025-12-03 16:07:27 -03:00
Anik Dhabal Babu 8aaa84142b fix: unable to edit member as a team owner/admin (#25587)
* fix: unaable to edit member as owner/admin

* Clean up comments in EditMemberSheet.test.tsx

Removed comments explaining the fix for permissions prop.

* revert
2025-12-03 18:14:35 +00:00
Udit Takkar 059ca2503d fix: prevent clicking outside (#25588) 2025-12-03 17:57:23 +00:00
Udit Takkar 35d6c41fff feat: Disable booking emails to guests (#25217)
* feat: disable SMS org setting

* chore: undo

* fix: cal evnet

* feat: add more settings

* tests: add email manager unit tests

* fix: update

* fix: type error

* fix: test

* refactor: UI

* refactor: UI

* perf: fetch only once

* perf: fetch only once

* chore: use org

* fix: test

* test: add unit tests

* refactor: email manager

* fix: add confirmation dialog for individual checkbox

* fix: sms

* chore: common.json
2025-12-03 17:21:31 +00:00
Eunjae LeeandDevin AI 1c3ced5b70 fix: simplify date range picker to Airbnb-style selection (#25386)
* feat: simplify date range picker to Airbnb-style selection

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

* feat: apply Airbnb-style selection to allowPastDates branch

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

* refactor: extract date range selection logic and add unit tests

- Extract date range selection logic into pure function (dateRangeLogic.ts)
- Remove unused allowPastDates parameter from selection logic
- Add comprehensive unit tests (11 test cases covering all scenarios)
- Simplify DateRangePicker component (30+ lines -> 5 lines)
- Improve separation of concerns: allowPastDates only controls calendar date restrictions

* style update

* feat: add hover highlighting for date range selection

When a start date is selected and user hovers over other dates,
the potential range between start and hovered date now shows
bg-emphasis background for better visual feedback.

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

* memoize hovering range and update styles

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-03 14:52:25 +00:00
88ce7868e8 feat: companion expo and chrome (#25022)
* feat: companion expo and chrome

* fix: api-v2-fetch-bookings (#25023)

* migrated to app expo router

* feat(companion): added availability screen and fix event type fetch (#25025)

* fix(companion): add host check in bookings (#25036)

* using liquid glass bottom nav experimental

* added action sheet

* added icons to event type tabs

* added action bar for event-type details

* long press for edit event types

* added limits

* added more limits

* added more limits

* added username to event types

* feat(companion): config to set and show all the available meeting options a user has stalled (#25081)

* feat: Add tailwind support using nativewind. (#25028)

* init nativewind

* replace styles with tailwind classes

* changes

* feat(companion): all locations options, icons, and apply nativewind to remaining files (#25143)

* feat: all locations options and icons and apply nativwind to reamaining files

* icon bug fix

* cal video icon

* other location frontend

* other location api

* address cubics comment about correctly detects SVGs

* feat(companion): edit availability (#25149)

* feat: companion-edit-availability

* date override and use correct config for patch api endpoint

* Android Alert supports a maximum of 3 buttons

* feat: companion-ui-release-1 (#25150)

* feat(companion): Add event type & schedule creation flows with API expansion (#25187)

* feat(companion): event schedule creation and apis

* profile section

* limits, advanced, recurring tab

* Clean Folder Structure

* addressed cubics comments

* fix cubiics comments

* replaced text with cal.com svg logo

* added correct more button for event-types

* added copy link button

* added extension popup

* added chrome extension sidebar

* achieved liquid glass buttons

* nit

* more progress

* fixed ios bug, added icon

* added cal icon to gmail

* nit

* nit

* added dialog for one-off meeting

* added one-time dialog

* feat(companion): gmail plugin (#25327)

* Update Cal.com Companion to version 1.7.0 with enhanced background and content scripts. The background script now handles fetching event types from the Cal.com API and improved error handling. The content script has been updated to manage sidebar visibility and respond to messages more effectively. Removed deprecated dev background script and updated manifest permissions for better functionality.

* chore: update companion .gitignore to exclude build outputs and env files

* feat: insert Cal.com event link directly at cursor position in Gmail compose

- Add insertTextAtCursor function to insert booking link where user is typing
- Automatically positions cursor after inserted link for seamless typing
- Fallback to clipboard copy if compose field not found
- Improved UX: no need to manually paste the link

* feat(companion): improve notification UI and add event types caching

- Update notification to Cal.com brand style (black bg, check icon, smooth fade)
- Change notification text to 'Link inserted' for subtlety
- Move notification to bottom-right position (matching Cal.com toasts)
- Add event types caching (5min duration) to reduce API calls
- Cache automatically refreshes on page reload
- Fix TypeScript error with composeBody.focus() type assertion

* Notification toast  border

* conditional description

* conditional description

* feat(companion): Enhance UI with action buttons, tooltips, and Gmail extension improvements (#25353)

* feat: companion-and-gmail-ui-upgrade

* feat: companion-and-gmail-ui-upgrade

* feat: companion-and-gmail-ui-upgrade

* feat: companion-and-gmail-ui-upgrade

* feat(companion): one-off links (#25355)

* added one off prototype

* resolve merge conflicts

---------

Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>

* fixed sidebar iframe

* fix: enable pointer events on sidebar while maintaining click-through on transparent area

- Added pointerEvents: 'auto' to sidebar View component in _layout.tsx
- Changed iframe width from 100% to 400px to match sidebar width
- Changed iframe pointerEvents from 'none' to 'auto' to enable clicking
- This allows sidebar items to be clickable while transparent area remains click-through

* feat(companion): center all modals on full screen in browser extension (#25380)

* fix: companion-full-width-iframe

* center the popups on extension view

* feat(companion): center all modals on full screen in browser extension

- Created FullScreenModal component that expands iframe to full width
- Updated content script to handle instant iframe expansion/collapse
- Converted 17 modals across 6 files to use FullScreenModal
- All modals now appear centered on full browser viewport
- Removed animations for instant modal appearance
- Mobile app behavior remains unchanged

Files updated:
- companion/components/FullScreenModal.tsx (new)
- companion/extension/entrypoints/content.ts
- companion/app/(tabs)/event-types.tsx (5 modals)
- companion/app/(tabs)/bookings.tsx (2 modals)
- companion/app/(tabs)/availability.tsx (3 modals)
- companion/app/booking-detail.tsx (2 modals)
- companion/app/availability-detail.tsx (4 modals)
- companion/components/Header.tsx (1 modal)

* security(companion): validate postMessage origin in content script

Add origin and source validation to postMessage listener to prevent
malicious scripts on host pages from manipulating the companion iframe.

- Verify event.source matches iframe.contentWindow
- Verify event.origin matches iframe origin (localhost:8081)
- Reject all unauthorized messages before processing

This prevents external scripts from:
- Forcing full-screen overlays
- Intercepting pointer events
- Manipulating the companion UI

* fix(companion): center 'Add new event type' modal on full screen

Replace KeyboardAvoidingView with TouchableOpacity pattern in the
create event type modal to ensure it appears centered on the full
screen like all other modals.

This was the last remaining modal that wasn't properly centered.

* fix(companion): resolve modal transition timing issue

Fixed the 'Add new event type' modal not centering properly by:

- Added 100ms delay when transitioning between modals
- Prevents conflicting expand/collapse messages
- Removed redundant postMessage calls from modal handlers
- Added max-height constraint to modal content
- Cleaned up debug console logs

The issue was that when clicking 'New Event Type' from the 'New' menu,
both modals were transitioning simultaneously, causing the create modal
to collapse immediately. The delay ensures proper sequencing.

All 17 modals now work correctly and appear centered on full screen.

* added working date range

* removed all of one-off event code

* nit

* feat(companion): Suggest `Cal.com` links and Embed using Gemini Ai in Gmail (#25406)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* add companion build on CI

* feat: oauth companion (#25526)

* first implemenation of oauth

* fix logout for web

* revert app name

* remove isUsingOAuth

* clean up log out button

* remove logs

* clean up authcontext

* simplify code generation

* code clean up

* clean up oauthService.ts

* oauth browser extension

* add redirect uri to .env.example

* remove api key support

* clean up _layout file

* add identity

* fix + clean up

* improvements to oauth flow

* code clean up

* remove comments

* clean up comments

* add log out dialog

* add confirm modal

---------

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

---------

Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
Co-authored-by: Abhijeet Singh <asingh9829@gmail.com>
Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-12-03 08:46:04 -03:00
ba3838ec0f chore: docs for BookerEmbed atom (#25541)
* chore: docs for BookerEmbed atom

* chore: PR feedback

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Update docs/platform/atoms/booker-embed.mdx

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Volnei Munhoz <volnei@cal.com>
2025-12-03 11:39:09 +00:00
sean-brydon f87d4de546 fix: UI nits in QA verison of onboarding v3 (#25532)
* fix create button

* fix subdomain prefix in orgs details view

* remove redundant text

* fix prefix on link fields

* brand mode colours

* Fix banner positioning

* remove app descrtipion plus add mr to account for button

* fix gaps

* improve free plan welcome modal
2025-12-03 09:57:01 +00:00
Alex van Andel fd6c6e6419 chore: release v5.9.8
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-12-03 09:25:02 +00:00
Alex van AndelandDevin AI 63d1361a7a fix: simplify credentials provider authorization flow (#25563)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-03 09:21:20 +00:00
Udit Takkar cbcd769f88 docs: add automatic no show updated docs (#25507) 2025-12-03 06:35:54 +00:00
Anik Dhabal Babu a2185d9ee0 fix: sms and whatsapp reminder issue with seated event (#25294)
* fix: sms reminder issue with seated event

* fix seated event and workflow issue

* fix whatsapp reminder

* Refactor imports in reminderScheduler.ts

* use repository
2025-12-03 04:12:38 +00:00
Anik Dhabal Babu 7a66add217 Prevent navigation for disabled booking actions (#25543) 2025-12-03 04:10:31 +00:00
Morgan 68b93385eb refactor: platform billing controller auth guards (#25559)
* refactor: platform billing controller auth guards

* fixup! refactor: platform billing controller auth guards
2025-12-02 20:38:29 +00:00
Alex van Andel e2aa6361d5 fix: config is statically analyzed, failing on build (#25558) 2025-12-02 18:38:29 +00:00
d15281160f fix: preserve seatsPerTimeSlot during partial event type updates (#25450)
* fix: preserve seatsPerTimeSlot during partial event type updates

When doing a partial update on the PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} endpoint, providing a partial body would reset seatsPerTimeSlot back to null because the transformSeatsApiToInternal function was always called even when seats was undefined.

This fix ensures that the seat options are only transformed when the seats field is explicitly provided in the update request, preserving existing values during partial updates.

Also adds e2e test to verify partial updates preserve seatsPerTimeSlot.

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

* fix: handle TypeScript union type narrowing for seatsPerTimeSlot

When seats is not provided in a partial update, the transformed body
may not have seatsPerTimeSlot property. This fix uses 'in' operator
to safely check for the property before accessing it.

Also fixes the e2e test to properly narrow the union type for seats
using type guards.

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

* refactor: simplify seatsPerTimeSlot handling per review feedback

Per supalarry's suggestion, instead of using 'in' operator checks in
multiple validation calls, we now return {seatsPerTimeSlot: undefined}
when seats is not provided. This keeps the validation code unchanged
and only requires modifying one line in the transformation.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-02 18:38:01 +00:00
Volnei Munhoz 8b6c6273ee Fix api v1 block paths (#25554) 2025-12-02 16:53:17 +00:00
Rajiv Sahal 957d19740e feat: CalendarView atom v2 (#24896)
* refactor: make individual component for generic and event type specific calendar views

* fixup

* update examples app

* fix: show busy times from calendars in week view

* fix: use calendar busy times not working

* chore: add changesets

* fixup

* chore: implement PR feedback

* fix: merge conflicts

* chore: implement PR feedback

* fixup

* chore: implement PR feedback
2025-12-02 21:44:30 +05:30
Hariom Balhara 150e93dc6e fix: Add /router to isBookingPages to prevent 500 error on customPageMessage redirect (#25522)
* Add /router to isBookingPages

* Remove Dynamic Posthog Provider from AppProvider.tsx because that is only used by the pages router, and the pages router is only in use by the /router endpoint, which is a booking page and we havent implemented corresponding GeoProvider support for it
2025-12-02 13:51:04 +00:00
Anik Dhabal Babu 94c7bfea4d fix: display message in the response (#25102) 2025-12-02 13:46:59 +00:00
Kartik Labhshetwar dbef7644d4 fix: improve availability working hours layout spacing and editable heading icon gap (#25525)
* fix: improve availability working hours layout spacing and editable heading icon gap

* refactor: wrap DateOverride in styled container in AvailabilitySettings
2025-12-02 13:20:14 +00:00
abhix4 a843a38e67 fix: styling override at availability settings (#25521) 2025-12-02 12:41:49 +00:00
Udit Takkar 73ffd9c7b5 chore: update next (#25485) 2025-12-02 12:05:42 +00:00
2c8ff89fac perf: Calendar Cache Improvements (#25502)
* wip

* Filter only selectedCalendars where ff is enabled

* test: fix and add tests for calendar subscription cache feature

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* test: fix SelectedCalendarRepository tests for new teamIds parameter

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-02 08:11:52 -03:00
Benny JooandDevin AI 3bf2a8fa6c refactor: replace TRPCError with ErrorWithCode in packages/features (#25482)
* refactor: replace TRPCError with ErrorWithCode in packages/features

This refactor moves error handling from throwing TRPCError directly in
packages/features to throwing ErrorWithCode instead. The conversion to
TRPCError now happens at the TRPC layer.

Changes:
- Add generic ErrorCode values (Unauthorized, Forbidden, NotFound,
  BadRequest, InternalServerError) to errorCodes.ts
- Update getServerErrorFromUnknown to map new ErrorCodes to proper
  HTTP status codes
- Create toTRPCError helper in packages/trpc/server/lib
- Create errorMappingMiddleware in packages/trpc/server/middlewares
- Migrate TRPCError throws in packages/features to ErrorWithCode:
  - teamService.ts
  - getEventTypeById.ts
  - eventTypeRepository.ts
  - OrganizationPermissionService.ts
  - OrganizationPaymentService.ts
  - sso.ts
  - handleCreatePhoneCall.ts
  - userCanCreateTeamGroupMapping.ts

This improves separation of concerns by making packages/features
transport-agnostic, allowing the same feature code to be reused from
tRPC, API routes, workers, etc.

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

* fix: remove isTrpcCall parameter and fix lint warning

- Remove isTrpcCall parameter from get.handler.ts call since the
  feature layer no longer needs to know about tRPC
- Fix unsafe optional chaining lint warning in getEventTypesByViewer.ts
  by precomputing usersSource variable
- Complete migration of getEventTypesByViewer.ts to ErrorWithCode

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

* revert

* add eslint rule

* add comment

* fix: add isTrpcCall back to getEventTypeById interface

The user reverted the removal of isTrpcCall parameter from the handler,
so we need to add it back to the interface to fix the type error.

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

* test: update teamService tests to expect ErrorWithCode instead of TRPCError

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

* refactor

* wip

* feat: integrate errorMappingMiddleware into base TRPC procedure

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

* connect middlewares

* revert

* revert

* refactor

* rename

* fix: handle ErrorWithCode in teams server-page error handling

The error handling was checking for TRPCError, but teamService now throws
ErrorWithCode. This caused the 'This invitation is not for your account'
error message to not be displayed when a wrong user tries to use an
invitation link.

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

* fix

* fix

* fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-02 08:11:17 -03:00
Hariom Balhara 16a4c75e1e Update yarn.lock (#25524) 2025-12-02 09:58:57 +00:00
sean-brydon f9db1d6111 fix: getTeamIdsWithPermission (#25387) 2025-12-02 06:11:56 -03:00
Anik Dhabal Babu 1d9aee7154 fix: Use booker locale for email verification email (#25420) 2025-12-02 08:25:39 +00:00
abhix4andDhairyashil Shinde 56e3a314a8 fix(ui): add missing checked state styling for event type checkbox (#25478)
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-12-02 06:47:30 +00:00
Alex van Andel 8167fe0d05 chore: release v5.9.7
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-12-02 04:22:35 +00:00
KartikandPallav c23c2d128a fix: allow false for custom field 'fieldRequired' by using z.boolean() (#25322)
* fix: allow false for custom field 'fieldRequired' by using z.boolean()

* updated to z.boolean().optional()

---------

Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-12-02 02:46:29 +00:00
65893ae943 fix: [object object] problem (#24643)
* fix: object problem

* revert and improve logic

* revert

* fix

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

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-12-02 02:32:51 +00:00
f33edb2b99 feat: Generate email workflow payload at time of sending (#25446)
* wip

* wip

* feature: Booking Tasker without DI yet

* feature: Booking Tasker with DI

* fix type check 1

* fix type check 2

* fix

* comment booking tasker for now

* fix: DI regularBookingService api v2

* fix: convert trigger.dev SDK imports to dynamic imports to fix unit tests

The unit tests were failing because BookingEmailAndSmsTriggerTasker.ts had static imports of trigger files that depend on @trigger.dev/sdk. This caused Vitest to try to resolve the SDK at module load time, even though it should be optional.

Changed all imports in BookingEmailAndSmsTriggerTasker.ts from static to dynamic (using await import()) so the trigger files are only loaded when the tasker methods are actually called, not at module load time during tests.

This fixes the 'Failed to load url @trigger.dev/sdk' errors that were causing 28+ test failures.

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

* fix unit tests

* keep inline smsAndEmailHandler.send calls

* chore: add team feature flag

* add satisfies ModuleLoader

* fix type check app flags

* move trigger in feature

* fix: add trigger.dev prisma  generator

* fix: email app statuses

* fix: CalEvtBuilder unit test

* chore: improvements, schema, config, retry

* fixup! chore: improvements, schema, config, retry

* chore: cleanup code

* chore: cleanup code

* chore: clean code and give full payload

* remove log

* add booking notifications queue

* add attendee phone number for sms

* bump trigger to 4.1.0

* add missing booking seat data in attendee

* update config

* fix logger regular booking service

* fix: prisma as external deps of trigger

* fix yarn.lock

* revert change to example app booking page

* fix: resolve circular dependencies and improve cold start performance in trigger tasks

- Convert BookingRepository import to type-only in CalendarEventBuilder.ts to eliminate circular dependency risk
- Convert EventNameObjectType, CalendarEvent, and JsonObject imports to type-only in BookingEmailAndSmsTaskService.ts
- Use dynamic imports in all trigger notification tasks (confirm, request, reschedule, rr-reschedule) to reduce cold start time
- Move heavy imports (BookingEmailSmsHandler, BookingRepository, prisma, TriggerDevLogger, BookingEmailAndSmsTaskService) inside run functions
- Eliminates module-level prisma import which violates repo guidelines and adds cold start overhead
- Reduces initial module dependency graph by deferring heavy imports (email templates, workflows, large repositories) until task execution

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

* fix: improve cold start performance in reminderScheduler with dynamic imports

- Remove module-level prisma import (violates 'No prisma outside repositories' guideline)
- Use dynamic imports for UserRepository (1,168 lines) - only loaded when needed in EMAIL_ATTENDEE action
- Use dynamic imports for twilio provider (386 lines) - only loaded in cancelScheduledMessagesAndScheduleEmails
- Use dynamic imports for all manager functions by action type:
  - scheduleSMSReminder (387 lines) - loaded only for SMS actions
  - scheduleEmailReminder (459 lines) - loaded only for Email actions
  - scheduleWhatsappReminder (266 lines) - loaded only for WhatsApp actions
  - scheduleAIPhoneCall (478 lines) - loaded only for AI phone call actions
- Use dynamic imports for sendOrScheduleWorkflowEmails in cancelScheduledMessagesAndScheduleEmails
- Significantly reduces cold start time by deferring heavy module loading until execution paths need them
- Eliminates module-level prisma import that violated repository pattern guidelines

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

* fix: improve cold start performance in BookingEmailSmsHandler with dynamic imports

- Remove module-level imports of all email-manager functions (653 LOC + 30+ email templates)
- Add dynamic imports in each method (_handleRescheduled, _handleRoundRobinRescheduled, _handleConfirmed, _handleRequested, handleAddGuests)
- Defer heavy email-manager loading until method execution
- Verified no circular dependencies between email-manager and bookings
- Significantly reduces cold start time for RegularBookingService and BookingEmailAndSmsTaskService

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

* fix: use dynamic imports

* update yarn lock

* code review

* trigger config project ref in env

* update yarn lock

* add .env.example trigger variables

* add .env.example trigger variables

* fix: cleanup error handling and loggin

* fix: trigger config from env

* fix: small typo fix

* fix: ai review comments

* fix: ai review comments

* ai review

* Add `create` on `WorkflowReminderRepository`

* `sendWorkflowEmails` tasker to accept lazy payload

* Add `scheduleLazyEmailWorkflow` to `WorkflowService

* Process scheduled date in `scheduleLazyEmailWorkflow`

* Type fixes

* Use `WorkflowService` to schedule

* Refactor `scheduleEmailReminderForEvt` to use
`WorkflowService.processWorkflowScheduledDate`

* Pass seat reference to lazy scheduled workflow reminder

* Refactor `WorkflowReminderRepository` to accept prisma as constructor

* Abstract `FormSubmissionData` type

* Abstract select statement and add get by uid to `BookingRepository`

* Add `FormSubmissionData` type

* Add `findByIdIncludeStepAndWorkflow` to `WorkflowReminderRepository`

* Tasker payload to accept `workflowReminderId`

* Create `BookingSeatRepository`

* Write `workflowReminderId` to tasker payload

* Add `generateCommonScheduleFunctionParams` to `WorkflowService`

* Init

* Use services in tasker

* Abstract types

* In reminderScheduler use workflowService to generate common params

* Type fix

* Return params from emailWorkflowService.generateParametersToBuildEmailWorkflowContent

* Use emailWorkflowService to generate params

* Abstract types

* Generate email content and send in EmailWorkflowService

* Move check to caller

* Use EmailWorkflowService to generate email payload in
emailReminderManager

* Fix initalizing repository

* Use evt.videoCallData first before the booking metadata

* Only get non-deleted references to build calendar event

* Remove check for videoCallData.id

* Dynamic import credit service

* BookingRepository.getByUid to only return what we need from attendee

* Type fixes

* test: Add comprehensive tests for lazy email workflow generation and fix broken tests

- Fix prisma mocks in sms-manager.test.ts and outOfOfficeCreateOrUpdate.handler.test.ts
  to export both 'default' and named 'prisma' exports
- Add EmailWorkflowService.test.ts with 4 tests for error handling paths
- Add sendWorkflowEmails.test.ts with 7 tests for schema validation and email sending
- Add tests to WorkflowService.test.ts for scheduleLazyEmailWorkflow,
  processWorkflowScheduledDate, and generateCommonScheduleFunctionParams methods

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Write `seatReferenceUid`

* Return promise

* Fix log

* Change to `Promise.allSettled`

* Type fix

* Fix failing test

* fix: reorder workflow step checks to fix test failure

The test 'should throw error if workflow step not found on reminder' was failing because the code checked workflowStep.verifiedAt before checking if workflowStep exists. When workflowStep is null, this caused the error message to include 'undefined' instead of the expected workflow step id.

Fixed by reordering the checks:
1. First check if workflowStep exists
2. Then check if workflowStep.verifiedAt exists

Also updated the test expectation to match the correct error message.

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

---------

Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: hbjORbj <sldisek783@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-12-02 02:25:07 +00:00
5d8c4ad13d fix: Allow i18n of form builder max/min character count. Earlier strings missed due to variable. (#22367)
* added german translation

* fixed count issue

* fixed count issue

* resolved type safety errors

* fix

* Update getBookingResponsesSchema.ts

* fixes

* removed 1 character case

* fixes

* fix

* chore

* fix

* type-check

* type-fix

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-12-02 02:24:54 +00:00
f359d37799 feat(apps): add databuddy analytics (#24689)
* feat(apps): add databuddy analytics

* Update packages/app-store/databuddy/DESCRIPTION.md

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-12-02 02:18:25 +00:00
Anik Dhabal Babu b98bb8b852 chore: profile repository refactor (#25328)
* chore: profile repository refactor

* remove comment

* remove comment

* cleaning

* review addressed

* rename the method name
2025-12-01 22:47:37 -03:00
abhix4 7ca2082c62 fix: availability cta hover state (#25504) 2025-12-01 23:50:35 +00:00
Anshumancanrock c11865fba9 fix: ProgressBar styling is hardcoded in dark mode (#25516) 2025-12-01 23:18:53 +00:00
Tamal ChakrabortyandPallav a3c0d06c13 fix: encode JWT in 2FA redirect URL to prevent invalid header error (#25331)
Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-12-01 17:53:39 +00:00
cd74f292ee fix: keyPress behaviour for Add Variable dropdown (#23405)
* Fixed variable dropdown

* Radix fix

* addressed coderabits comments

* Adjust styles and structure in AddVariablesDropdown

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-12-01 17:34:52 +00:00
Alex van AndelandAnik Dhabal Babu 2c52e31c76 chore: Pin lingo.dev version to 0.117.6 (#25475)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-12-01 16:59:16 +00:00
Eunjae LeeandDevin AI 6479adf14e fix: integer to text comparison in routing insights query (#25019)
* fix: integer to text comparison in routing insights query

Add explicit integer array cast to prevent PostgreSQL type mismatch error when comparing bookingUserId (integer) with user_id array values in getRoutedToPerPeriodData query

* add e2e tests

* refactor: remove LoadingInsight component and handle loading in ChartCard

- Enhanced ChartCard to render default loading UI when isPending is true
- Replaced all LoadingInsight usages with ChartCard that accepts isPending/isError props
- Removed LoadingInsight component and updated exports
- Updated 16 chart components to use the new pattern
- ChartCard now shows spinner and skeleton title during loading state

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

* fix: make children prop optional in ChartCard when isPending is true

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

* refactor: remove unused loadingState prop from ChartCard

- Remove loadingState prop and ChartLoadingState type export
- Simplify computedLoadingState to derive state only from isPending/isError
- No functional changes - loadingState was not being used by any components
- data-loading-state attribute behavior remains unchanged for E2E tests

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

* refactor: remove duplicate isPending early returns from chart components

- Update all 16 chart components to use single ChartCard return pattern
- Gate children rendering with !isPending && isSuccess && data checks
- Prevents data processing code from executing during loading state
- Improves code consistency and maintainability across all charts

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

* refactor: remove redundant !isPending check from chart conditionals

- Simplify conditional rendering to use just 'isSuccess && data' or 'isSuccess'
- In TanStack Query, isSuccess and isPending are mutually exclusive
- The !isPending check was redundant since isSuccess already implies !isPending
- Applied to all 16 chart components for consistency
- Components with safe defaults (data ?? []) use just 'isSuccess'
- Components requiring data check use 'isSuccess && data'

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

* revert the mistake

* apply feedback

* apply feedback

* fix e2e

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-01 15:07:57 +01:00
Rajiv Sahal 3c46c35b69 init: doc to setup platform billing locally via stripe (#25490) 2025-12-01 13:36:25 +00:00
Eunjae LeeandDevin AI 41ad0f718d refactor: use Button component instead of plain button in CopyButton (#25489)
* refactor: use Button component instead of plain button in CopyButton

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

* add shrink-0 to StartIcon and EndIcon of Button

* remove unused class names

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-01 13:13:01 +00:00
Syed Ali Shahbaz 0103321b71 feat: managed event reassignment (#24809)
* init

* --

* update dialog

* reassignment

* further changes

* add unit test

* fix

* type fix??

* fix type??

* emails

* workflows

* reason recorder

* refactor reassigned email

* fix reassigned reason

* fix type

* improve dialog

* add integration tests

* -

* remove unnecessary comments --1

* removed unnecessary comments

* fix type?

* address cubic

* address feedback

* --

* fix type?

* address cubic

* type-fix?

* fix type

* further fixes

* fix location update

* type fix

* propagate error to top

* fix mocking

* fix reported bugs

* fix

* fix success page video URL

* remove PII from logs

* persist video URL

* better audit trail

* revert email function name change

* fix test

* fix flake

* di

* fixes

* extract logic and other repo access from BookingRepository

* fixes

* (☞゚∀゚)☞ udit

* integration test fixes

* mroe fixes

* extract to repo

* extract to repo --2

* fix type

* cubic

* address feedback --1

* --2

* wip

* addressed feedback

* type fixes

* type fixes

* fix issues

* feedback --1

* feedback --2

* BookingAccessService DI

* fix

* break down function into small functions

* fixes --1

* fixes

* typefix

* addressing feedback

* fix merge conflict lost change
2025-12-01 14:04:24 +04:00
Anik Dhabal Babu 38890dad30 fix: org admin/owner can access routng form (#25412) 2025-12-01 09:46:50 +00:00
spandevandAnik Dhabal Babu 0776657faf fix: made playground page use same layout as other pages (#25216)
* fix: made playground page use same layout as other pages

* update: added feature key

* Change link display from table to list format

Refactor page layout to use a list instead of a table for links.

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-12-01 09:33:46 +00:00
Dhairyashil ShindeandKeith Williams 3cf7266048 refactor: hide duplicate translations warning (#25472)
* refactor: hide duplicate translations warning

* Apply suggestion from @keithwillcode

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-12-01 09:28:30 +00:00
Eunjae Lee 86b9aa5f23 fix: exclude AWAITING_HOST from seed-insights random status selection (#25414)
## What does this PR do?

Fixes a bug in the seed-insights script where `AWAITING_HOST` status was randomly assigned to bookings along with a `userId`, which violates the correct behavior of instant meetings.

**Problem:**
- The seed script randomly picked from all 5 booking statuses (including `AWAITING_HOST`)
- When `AWAITING_HOST` was selected, the script still assigned a `userId`
- Real instant meetings with `AWAITING_HOST` status should have `userId = NULL` until a host joins
- This created incorrect test data that doesn't match production behavior

**Solution:**
- Filter out `AWAITING_HOST` from `BookingStatus` values using `Object.values(BookingStatus).filter()`
- This approach is more maintainable than listing valid statuses explicitly - if new statuses are added in the future, they will automatically be included
- Moved `validStatusesForSeed` to module level so it's calculated once at load time instead of on every shuffle call
- Added comments explaining why this status requires special handling

## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [x] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). N/A - seed script only.
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works. N/A - this is a seed script for test data generation.

## How should this be tested?

1. Run the seed script: `yarn seed-insights`
2. Verify no `AWAITING_HOST` bookings are created with a `userId`:
```sql
SELECT COUNT(*) FROM "Booking" 
WHERE status = 'awaiting_host' AND "userId" IS NOT NULL 
AND "createdAt" > NOW() - INTERVAL '1 day';
-- Should return 0 for newly seeded data
```
3. Verify other statuses are still generated:
```sql
SELECT status, COUNT(*) FROM "Booking" 
WHERE "createdAt" > NOW() - INTERVAL '1 day' 
GROUP BY status;
-- Should show ACCEPTED, PENDING, CANCELLED, REJECTED (but not AWAITING_HOST)
```

## Checklist

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

---

> **Link to Devin run:** https://app.devin.ai/sessions/08f634e7210d415982d71c8f687b4468
> **Requested by:** eunjae@cal.com (@eunjae-lee)
2025-12-01 09:24:22 +00:00
Dhairyashil Shinde b94f107493 fix: dark-mode-multi-location-toggle-booking-page (#25469) 2025-12-01 09:21:40 +00:00
Benny Joo 4a451b411c chore: make app-store eslint config more strict to prevent circular deps (#25483) 2025-12-01 09:14:05 +00:00
sean-brydon b64cfc9e66 feat: remove min seat usage from org creation (#25448)
* remove min seat usage

* update constants and also turbo json

* Remove comments

* remove redundant tests as no min exists
2025-12-01 07:55:44 +00:00
KartikandPallav ce279da7c4 fix: stabilize date range calculation in column view (#25384)
* fix: stabilize date range calculation in column view

* fix: restore eslint-disable for scrollIntoViewSmooth

---------

Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-11-30 03:36:13 +05:30
Anshumancanrockandsean-brydon 7dbc888848 fix: End Impersonation Without Logging Out Admin (#25395)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-11-29 21:38:31 +00:00
Rajiv Sahal 8fcbf2c167 fix: Connect atoms not working inside iframe (#25418)
* fix: google connect broken for iframe
* chore: add changesets
* chore: update atoms exports
* chore: update atoms exports
2025-11-28 11:44:20 -03:00
a050ccb4ee feat: Booking EmailAndSms Notifications Tasker (#24944)
* wip

* wip

* feature: Booking Tasker without DI yet

* feature: Booking Tasker with DI

* fix type check 1

* fix type check 2

* fix

* comment booking tasker for now

* fix: DI regularBookingService api v2

* fix: convert trigger.dev SDK imports to dynamic imports to fix unit tests

The unit tests were failing because BookingEmailAndSmsTriggerTasker.ts had static imports of trigger files that depend on @trigger.dev/sdk. This caused Vitest to try to resolve the SDK at module load time, even though it should be optional.

Changed all imports in BookingEmailAndSmsTriggerTasker.ts from static to dynamic (using await import()) so the trigger files are only loaded when the tasker methods are actually called, not at module load time during tests.

This fixes the 'Failed to load url @trigger.dev/sdk' errors that were causing 28+ test failures.

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

* fix unit tests

* keep inline smsAndEmailHandler.send calls

* chore: add team feature flag

* add satisfies ModuleLoader

* fix type check app flags

* move trigger in feature

* fix: add trigger.dev prisma  generator

* fix: email app statuses

* fix: CalEvtBuilder unit test

* chore: improvements, schema, config, retry

* fixup! chore: improvements, schema, config, retry

* chore: cleanup code

* chore: cleanup code

* chore: clean code and give full payload

* remove log

* add booking notifications queue

* add attendee phone number for sms

* bump trigger to 4.1.0

* add missing booking seat data in attendee

* update config

* fix logger regular booking service

* fix: prisma as external deps of trigger

* fix yarn.lock

* revert change to example app booking page

* fix: resolve circular dependencies and improve cold start performance in trigger tasks

- Convert BookingRepository import to type-only in CalendarEventBuilder.ts to eliminate circular dependency risk
- Convert EventNameObjectType, CalendarEvent, and JsonObject imports to type-only in BookingEmailAndSmsTaskService.ts
- Use dynamic imports in all trigger notification tasks (confirm, request, reschedule, rr-reschedule) to reduce cold start time
- Move heavy imports (BookingEmailSmsHandler, BookingRepository, prisma, TriggerDevLogger, BookingEmailAndSmsTaskService) inside run functions
- Eliminates module-level prisma import which violates repo guidelines and adds cold start overhead
- Reduces initial module dependency graph by deferring heavy imports (email templates, workflows, large repositories) until task execution

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

* fix: improve cold start performance in reminderScheduler with dynamic imports

- Remove module-level prisma import (violates 'No prisma outside repositories' guideline)
- Use dynamic imports for UserRepository (1,168 lines) - only loaded when needed in EMAIL_ATTENDEE action
- Use dynamic imports for twilio provider (386 lines) - only loaded in cancelScheduledMessagesAndScheduleEmails
- Use dynamic imports for all manager functions by action type:
  - scheduleSMSReminder (387 lines) - loaded only for SMS actions
  - scheduleEmailReminder (459 lines) - loaded only for Email actions
  - scheduleWhatsappReminder (266 lines) - loaded only for WhatsApp actions
  - scheduleAIPhoneCall (478 lines) - loaded only for AI phone call actions
- Use dynamic imports for sendOrScheduleWorkflowEmails in cancelScheduledMessagesAndScheduleEmails
- Significantly reduces cold start time by deferring heavy module loading until execution paths need them
- Eliminates module-level prisma import that violated repository pattern guidelines

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

* fix: improve cold start performance in BookingEmailSmsHandler with dynamic imports

- Remove module-level imports of all email-manager functions (653 LOC + 30+ email templates)
- Add dynamic imports in each method (_handleRescheduled, _handleRoundRobinRescheduled, _handleConfirmed, _handleRequested, handleAddGuests)
- Defer heavy email-manager loading until method execution
- Verified no circular dependencies between email-manager and bookings
- Significantly reduces cold start time for RegularBookingService and BookingEmailAndSmsTaskService

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

* fix: use dynamic imports

* update yarn lock

* code review

* trigger config project ref in env

* update yarn lock

* add .env.example trigger variables

* add .env.example trigger variables

* fix: cleanup error handling and loggin

* fix: trigger config from env

* fix: small typo fix

* fix: ai review comments

* fix: ai review comments

* ai review

* prettier

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-28 14:00:04 +00:00
Pallav 2522638414 fixed description for disable rescheduling and cancelling (#25447) 2025-11-28 13:13:40 +00:00
Pasquale Vitiello 0ddb456a91 fix: video page dialog bg in light mode (#25455) 2025-11-28 12:57:33 +00:00
9048d053ac feat: posthog version upgrade and added trackings (#24401)
* posthog version upgrade and calai banner tracking

* disable posthog for EU

* bunch more posthog tracking

* Revert yarn.lock changes

* add posthog package yarn changes

* fix: add missing posthog import and fix lint warning in MemberInvitationModal

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* fix: type check

* cubic fixes

* refactor

* remove ui playground

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-28 12:43:37 +00:00
Hariom Balhara cbc8d3d7a8 Add DI for BookingAudit (#25123) 2025-11-28 09:14:48 +00:00
e6ba89961c refactor: consolidate error handlers to use getServerErrorFromUnknown (#25114)
* refactor: consolidate error handlers to use getServerErrorFromUnknown

- Migrate server-only code to use getServerErrorFromUnknown for better error handling
- Add JSDoc documentation to both getErrorFromUnknown and getServerErrorFromUnknown
- Update webhook handlers, payment services, email service, and booking service
- Keep getErrorFromUnknown for client-side and isomorphic code
- Improve error message extraction by using err.cause?.stack instead of err.stack
- Fix ESLint warnings: replace 'any' with 'unknown' types, fix hasOwnProperty usage

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

* Update packages/app-store/paypal/api/webhook.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* refactor

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-11-28 09:14:06 +01:00
Dhairyashil Shinde 724204843a fix: Add PBAC permission checks for insights access (#25381)
* fix: Add PBAC permission checks for insights access

- Add checkInsightsPermission() helper that properly checks insights.read permission with PBAC support
- Update userBelongsToTeamProcedure to use PBAC-aware permission check for org-level access
- Update teamListForUser query to filter teams based on insights.read permission instead of only checking base ADMIN/OWNER roles
- Maintain backward compatibility with fallback to traditional role checks (ADMIN/OWNER) when PBAC is disabled
- Org admins (base role ADMIN/OWNER) continue to have automatic insights access as a privileged position
- Team-level admins with custom roles now properly checked for insights.read permission

Fixes issue where users with custom PBAC roles couldn't access insights even if they had insights.read permission.

Related: CAL-XXXX

* perf: Optimize team permission checks to avoid N+1 queries

Replace individual permission checks per team with bulk query using getTeamIdsWithPermission().
This reduces database queries from N (one per team) to a single optimized query.

- Use PermissionCheckService.getTeamIdsWithPermission() for bulk permission checking
- Filter teams based on returned team IDs instead of individual checks
- Maintains same functionality with significantly better performance for users with many teams

* perf: Fetch only teams with insights access instead of filtering after

Move permission check before team query to filter at database level.
Previously fetched all teams then filtered in JavaScript.
Now only fetches teams the user has insights access to.

- Check permissions first using getTeamIdsWithPermission()
- Add teamId filter to membership query (teamId: { in: teamIdsWithAccess })
- Remove JavaScript filter step (done at DB level)
- Reduces data transfer and improves query efficiency
2025-11-28 02:26:04 +05:30
Udit Takkar f136b7145f chore: update transcript endpoint description (#25385)
* chore: update transcript endpoint description

* chore: update transcript endpoint description
2025-11-27 20:17:40 +00:00
Udit Takkar 179ace23e1 fix: sign upsert error (#25437) 2025-11-27 12:33:29 -05:00
Anik Dhabal Babu 3af6fee05d fix: eventypes description overflow issue (#25436)
* fix: add overflow-y-auto to prevent description text from spilling over

* Rename setter for seatedEventData in EventMeta
2025-11-27 16:29:08 +00:00
Udit Takkar 0f84cedad2 fix: signup username collision (#25435)
* fix: signup username collision

* tests: add unit tests
2025-11-27 15:41:38 +00:00
Pasquale Vitiello a23400777a fix: prevent buttons from looking active when interacting with parent divs (#25431) 2025-11-27 14:18:35 +00:00
Dhairyashil Shinde 33c3bf3558 fix: allow-reschedule-on-prevent-impersonation (#25427)
* fix: allow-reschedule-on-prevent-impersonation

* remove selfexplanatory comment
2025-11-27 11:39:40 +00:00
emrysal 2701b4de79 chore: release v5.9.6
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-27 11:05:14 +00:00
Pasquale Vitiello a9f83ac5e2 fix: minor styling issues (#25421)
* style: remove unwanted bg

* fix: adjust FAB positioning

* style: skeleton improvements

* fix: reset password button

* fix: remove left border from remove icon

* style: improve fields

* style: wrap base form styles into layer base

* style: remove email-specific focus styles from TextField component
2025-11-27 11:00:44 +00:00
sean-brydonandcubic-dev-ai[bot] ff1533d344 chore: UI Nits onboarding v3 (#25409)
* Fix UI nits

* fix bio + team name on invite orgs

* use bg-cal-muted

* Fix error from mass edit

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

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix org labels

* Fix i18n

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-11-27 08:18:43 +00:00
Benny JooandDevin AI d9cddd85ff fix: break circular dependency by passing creditCheckFn in messageDispatcher (#25343)
* fix: break circular dependency in messageDispatcher via dependency injection

Break the 4-file circular dependency chain:
credit-service → reminderScheduler → smsReminderManager → messageDispatcher → credit-service

Solution:
- Add optional creditCheckFn parameter to messageDispatcher functions
- Thread creditCheckFn through the call chain: scheduleWorkflowReminders → scheduleSMSReminder/scheduleWhatsappReminder → messageDispatcher
- When creditCheckFn is provided, use it; otherwise fall back to dynamic CreditService import for backward compatibility
- This breaks the workflows → billing import while preserving immediate fallback behavior

Changes:
- messageDispatcher: Accept optional creditCheckFn parameter, use it if provided
- smsReminderManager: Thread creditCheckFn through scheduleSMSReminder
- whatsappReminderManager: Thread creditCheckFn through scheduleWhatsappReminder
- reminderScheduler: Add creditCheckFn to ScheduleWorkflowRemindersArgs and pass through processWorkflowStep

All type checks, lint checks, and unit tests pass.

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

* feat: wire creditCheckFn from all callers to complete circular dependency fix

- Add creditCheckFn parameter to WorkflowService.scheduleFormWorkflows
- Wire creditCheckFn from all 10 entry points that call workflow scheduling:
  * formSubmissionUtils.ts (form submissions)
  * roundRobinManualReassignment.ts (round-robin reassignment)
  * triggerFormSubmittedNoEventWorkflow.ts (form workflow trigger)
  * handleBookingRequested.ts (booking requests)
  * RegularBookingService.ts (2 calls - payment initiated & new bookings)
  * handleSeats.ts (seated bookings)
  * handleConfirmation.ts (2 calls - confirmation & payment)
  * handleMarkNoShow.ts (no-show updates)
  * confirm.handler.ts (booking rejection)
- Update test expectations to use expect.objectContaining()
- Fix pre-existing lint warning in handleMarkNoShow.ts (any type)
- This completes the messageDispatcher circular dependency fix by ensuring
  creditCheckFn is actually passed through the call chain, breaking the
  4-file circular dependency at runtime

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

* fix: use generic type with type guard in logFailedResults to fix type check error

- Replace constrained type with generic type parameter
- Add proper type guard for rejected promises
- Fixes CI type check failure in handleMarkNoShow.ts:385
- Avoids 'any' type while accepting any fulfilled value shape

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

* wip

* wip

* wip

* revert

* revert

* feat: wire creditCheckFn from all remaining callers to eliminate fallbacks

- Wire creditCheckFn in packages/sms/sms-manager.ts (can safely import CreditService)
- Create makeHandler factory pattern for CRON endpoints (scheduleSMSReminders.ts, scheduleWhatsappReminders.ts)
- Wire creditCheckFn from apps/web CRON routes to factories
- Add warning log in messageDispatcher when fallback is used
- Complete creditCheckFn wiring from all direct callers (activateEventType.handler.ts, util.ts)

This eliminates all fallbacks to dynamic import except as a safety net for unforeseen call sites.
The circular dependency (workflows ↔ billing) remains acceptable as discussed with user (Option C).

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

* test: update formSubmissionUtils tests to expect creditCheckFn parameter

The scheduleFormWorkflows function now receives creditCheckFn as a parameter.
Updated test assertions to use expect.objectContaining() with creditCheckFn: expect.any(Function)
to account for the new dependency injection parameter.

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

* test: update sms-manager test to expect creditCheckFn parameter

The sendSmsOrFallbackEmail function now receives creditCheckFn as a parameter.
Updated test assertion to use expect.objectContaining() with creditCheckFn: expect.any(Function)
to account for the new dependency injection parameter. Also removed teamId: undefined
assertion as the key may be omitted entirely from the actual call.

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

* feat: make creditCheckFn required to fully break circular dependency

This commit completes the circular dependency fix by making creditCheckFn
required throughout the call chain, eliminating the dynamic import fallback
entirely.

Changes:
- Make creditCheckFn required in messageDispatcher functions (sendSmsOrFallbackEmail, scheduleSmsOrFallbackEmail)
- Remove dynamic import fallback and warning log from messageDispatcher
- Make creditCheckFn required in ScheduleTextReminderArgs (smsReminderManager)
- Make creditCheckFn required in processWorkflowStep and ScheduleWorkflowRemindersArgs (reminderScheduler)
- Add creditCheckFn to SendCancelledRemindersArgs and wire from handleCancelBooking

The circular dependency is now fully broken - no more dynamic imports of
CreditService from within the workflows package. All callers must explicitly
provide creditCheckFn via dependency injection.

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

* fix: make creditCheckFn required in WorkflowService.scheduleFormWorkflows

This commit fixes the CI type check error by making creditCheckFn required
in WorkflowService.scheduleFormWorkflows. Previously, creditCheckFn was
optional in scheduleFormWorkflows but required in scheduleWorkflowReminders,
causing a type mismatch.

Changes:
- Make creditCheckFn required in scheduleFormWorkflows signature
- Update WorkflowService.test.ts to pass mock creditCheckFn in all test cases
- Add responseId and routedEventTypeId to test calls for completeness

All callers of scheduleFormWorkflows already pass creditCheckFn, so this
change is safe and completes the circular dependency fix.

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

* remove

* fix

* refactor

* refactor

* refactor

* wip

* fix

* fix

* rm

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-27 16:31:26 +09:00
Alex van Andel 1ab4b9dfab chore: Fix circular dependency in tanstack-table.d.ts (#25411)
* chore: Fix circular dependency in tanstack-table.d.ts

* fix: Re-export TextFilterOperator

* Unable to export non-type values from @calcom/types

* Refactor data-table types in a way that ensures type-safety as before

* Add FilterPopover and further fixups

* Fix further type errors missed earlier

* More hidden type errors

* Type error in useFilterValue
2025-11-26 14:05:55 -03:00
Anik Dhabal Babu f3d0475840 fix: role isn't updating properly (#25415) 2025-11-26 16:13:59 +00:00
Lauris Skraucis 061283bab9 fix: api v2 cancellation reason and by not being returned (#25413)
* fix: api v2 cancellation reason and by not being returned

* fix: e2e
2025-11-26 17:09:18 +01:00
4e0798577a feat: OAuth PKCE (#25313)
* add public client

* implement PKCE

* pass codeChallenge and codeChallengeMethod to handler

* fixes for secure oauth flow

* fix type error

* clean up refresh token endpoint

* only support S256

* fix type error

* remove comment

* add tests

* fix type errors in route.test.ts

* add missing support for refresh token

* add e2e test for public client refresh tokens

* allow pkce for confidential clients

* fix type error

* fix e2e

* fix option pkce for confidential clients

* e2e test improvements

* fix test

* remove only

* add delay

* fix e2e tests

* remove only

* don't skip pkce if codeChallenge is set

* add service functions for token endpoint

* use service function in refreshToken endpoint

* use repository

* remove return types

* e2e test fixes

* fix e2e test

* remove .only in e2e test

* remove pause

* fix error responses in token endpoints

* adjust tests to new error responses

* fix error responses

* e2e improvements

* redirect on error

* adjust tests

* Update apps/web/modules/auth/oauth2/authorize-view.tsx

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-11-26 17:02:42 +01:00
Anik Dhabal Babu c95b0833c1 fix: oauth screen go back button not functional (#25401)
* fix: oauth screen back

* update
2025-11-26 15:18:53 +00:00
sean-brydon 2a7c6590a3 feat: add permission for editUsers + implement UI (#25402)
## What does this PR do?
This PR implements attributes PBAC - router checks + UI

## 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?
Enable PBAC on an org 
Create a custom role -> advanced -> organizations -> "editUser" 
Assign it to a user
impersonate user
test they have access to all things attributes
remove permissions
check they dont have permissions. 

## 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










<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds a new PBAC “editUsers” permission and read gating for Attributes, updating the UI and backend so users can view and edit attributes only when allowed.

- **New Features**
  - Added CustomAction.EditUsers to the permission registry for organization-scoped attribute editing.
  - Settings computes canViewAttributes and shows the Attributes tab only when allowed.
  - Members page fetches Attributes permissions and exposes canViewAttributes and canEditAttributesForUser to the UI.
  - User Edit Sheet hides attributes without read permission and shows attribute editing and the bulk “Mass Assign Attributes” action only with editUsers; other user edits depend on changeMemberRole.
  - Attributes TRPC router gates create/edit/delete/toggle via PBAC and requires organization.attributes.editUsers for assign/bulk-assign; added a helper to create PBAC-aware procedures.

- **Migration**
  - Run the new Prisma migration to seed the admin role with editUsers.
  - If using custom roles, grant Edit Users under Attributes as needed.

<sup>Written for commit 856aa2e8e1521fc22cd71cb0fa6d720036efe8a2. Summary will update automatically on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2025-11-26 13:58:29 +00:00
Anik Dhabal Babu 7d2a06ddb6 fix: hide userresponses (#25370) 2025-11-26 13:27:13 +00:00
Lauris Skraucis d34ad2b0f6 fix: v2 api rescheduledByEmail not returned (#25103)
* fix: output

* fix: creation

* test: e2e rescheduling

* refactor: remove explicit return type

* fix: e2e

* fix: e2e

* fix: e2e

* fix: e2e tests

* fix: e2e tests

* fix: e2e

* logs: add debug logs for e2e

* debug e2e

* Revert "debug e2e"

This reverts commit 3d92d2bfbc351021ab06492b7279ed95843bf36a.

* Revert "logs: add debug logs for e2e"

This reverts commit f3b321fd96853bc2cf408c7f3667ad38d0931435.

* try to fix e2e

* extract e2e in separate file

* fix: e2e
2025-11-26 14:58:45 +02:00
Anik Dhabal Babu e06249d2df fix: wrong credentials (#25394) 2025-11-26 11:23:58 +00:00
sean-brydon a09dfe37a1 fix: redirect when creating orgs onboarding v3 (#25390)
* Fix redirect

* fix redirect
2025-11-26 09:54:16 +00:00
71e7f16b29 fix: prevent calendar credentials from leaking into video adapter calls (#25200)
* fix: prevent calendar credentials from leaking into video adapter calls

Split getCredentialAndWarnIfNotFound into two category-specific functions:
- getVideoCredentialAndWarnIfNotFound: only searches video credentials
- getCalendarCredentialAndWarnIfNotFound: only searches calendar credentials

This prevents the bug where calendar credentials (like google_calendar) were
being passed to getVideoAdapters() during booking deletion, causing
'Couldn't get adapter for googlecalendar' errors.

The root cause was the fallback logic that searched both videoCredentials
and calendarCredentials when a credential wasn't found by ID. Now each
function only searches within its own credential category and validates
the returned credential has the expected type suffix (_video/_conferencing
or _calendar).

Also fixed pre-existing ESLint warnings in EventManager.ts:
- Prefixed unused delegatedCredentialLast with underscore
- Replaced 'any' types with 'unknown' for better type safety
- Fixed unused variable warning in updateAllCalendarEvents error handler

Fixes the issue where deleteVideoEventForBookingReference could receive
a calendar credential when the video credential was missing, leading to
errors in getVideoAdapters.

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

* refactor: remove shared helper and implement logic directly in each function

- Removed getCredentialInternal shared helper
- Implemented logic directly in getVideoCredentialAndWarnIfNotFound
- Implemented logic directly in getCalendarCredentialAndWarnIfNotFound
- Changed fallback to explicitly use this.videoCredentials and this.calendarCredentials

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

* remove duplicate

* revert

* revert

* revert

* refactor

* add tests

* address

* clean up

* simplify

* simplify

* rename

* rename

* rename

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-11-26 00:04:21 +00:00
d6546c3107 feat: upgrade tailwind v4 (#24598)
* chore: refactor config files to prevent migration tool errors

* refactor: upgrade with the tailwind migration tool

* chore: restore pre-commit command + mc

* refactor(wip): update dependencies and migrate to Tailwind CSS v4 (mainly web)

* chore: resolve Tailwind v4 migration conflicts from merging main

* chore: remove unused Tailwind packages from config and update dependencies for v4 migration

* chore: uncomment Tailwind CSS utility classes in globals.css

* fix: resolve token conflicts between calcom and coss ui

* fix: textarea scrollbar

* Fix CUI-16

* fix: added @tailwindcss/forms plugin and cleaned up CSS classes in various components to remove unnecessary dark mode styles

* fix: selects and inputs of different sizes

* fix: remove unnecessary leading-20 class from modal titles in various components

* fix: update Checkbox component styles to remove unnecessary border on checked state

* fix: clean up styles in RequiresConfirmationController, Checkbox, and Radio components to enhance consistency

* fix: update button and filter component styles to remove unnecessary rounded classes for consistency

* fix: calendar

* fix: update KBarSearch

* fix: refine styles in Empty and Checkbox components for improved consistency

* Fix focus state email input

* fix: update button hover and active states to use 'not-disabled' instead of 'enabled'

* fix: line-height issues

* fix: update class name for muted background in BookingListItem component

* fix: sidebar spacing

* chore: update class names to use new Tailwind CSS color utilities

* fix embed

* chore: upgrade Tailwind CSS to version 4.1.16 and update related dependencies

* Map css variables and add a playground test for heavy css customization

* suggestion for coss-ui

* refactor: update CSS variable usage and clean up styles

- Replace instances of `--cal-brand-color` with `--cal-brand` in embed-related HTML files.
- Remove the now-unnecessary `addAppCssVars` function from the embed core.
- Import theme tokens in the embed core styles for better consistency.
- Clean up whitespace and formatting in CSS files for improved readability.
- Add a comment in `tokens.css` regarding its usage in both embed and webapp contexts.

* Handle within tokens.css instead of fixing coss-ui

* Remove initial, not needed. Also, remove tailwind.config.js as tailwidn scans the html automaically

* fix: examples app breaking

* fix: modal not resizing correctly

* feat: upgrade atoms to tailwind v4

* fix: atoms build breaking

* fix: atoms build breaking

* chore: upgrate examples/base to tailwind 4

* chore: update globals.css

* fix: add missing scheduler css variables

* fix: PlatformAdditionalCalendarSelector

* chore: update global styles

* chore: update tailwindcss and postcss dependencies to stable versions

* chore: remove unneeded class

* fix: dialog and toast animation

* fix: replace flex-shrink-0 with shrink-0 for consistent styling in various components

* fix: dialog modal for Apple connect

* add margin in SaveFilterSegmentButton

* Fix radix button nested states

* add cursor pointer to buttons but keep dsabled state

* Fix commandK selectors and adds cursor pointer

* Fix teams filter

* fix - round checkboxes

* fix filter checkbox

* fix select indicator's margin

* command group font size

* style: fix badge and tooltip radius

* chore: remove unneeded files

* Delete PR_REVIEW_MANAGED_EVENT_REASSIGNMENT.md

* remove ui-playground leftover

* fix: add missing react phone input styles in atoms

* Delete managed-event-reassignment-flow-and-architecture.mermaid

* fix: inter font not loading

* Add theme to skeleton container so that it can support dark mode

* fix: create custom stack-y-* utilities post tw4 upgrade

* fix: typo

* fix: atoms stack class + remove unused css file

* fix default radius valiue

* fix space-y in embed

* fix skeleton background

* Hardcode radius values to match production

* fix border in embed

* add missing externalThemeClass

* feat: create a custom stack-y-* utility

* fix: add stack utility to atom global css

* fix: Skeleton loader class modalbox

* Add stack-y utility in embed

* fix: add missing stack utilities in atoms globals.css

* update yarn.lock

* add popover portla

* update

---------

Co-authored-by: Sean Brydon <sean@cal.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Anik Dhabal Babu <adhabal2002@gmail.com>
2025-11-25 17:32:28 -03:00
625c260e40 fix: OOO days not correctly blocked (#25259)
* fix: OOO check in buildSlotsWithDateRanges

* added testcase

---------

Co-authored-by: chauhan_s <somaychauhan98@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-25 20:14:05 +00:00
Amit Sharma 81224f324a feat: google ads conversion tracking (#25198)
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
* feat: google ads conversion tracking

* gaClientid

* store gclid in stripe metadata

* tracking only in the us

* track google campaign id as well

* rename gclid -> google ads

* fix: build

* fix

* refactor

* fix: type check

* fix: type check

* fix: type check

* fix: type check

* fix: store it in cookie

* refactor

* fix

* cleanup

* linked ads tracking

* refactor checkout session tracking
2025-11-25 12:58:10 +00:00
emrysal 700d8e7f30 chore: release v5.9.5 2025-11-25 12:55:45 +00:00
Anik Dhabal Babu 301376e33b fix: set new icalUid when host reassign (#25365)
* fix seated event and workflow issue

* update

* revert

* Update smsReminderNumber assignment in handleSeats
2025-11-25 11:46:35 +00:00
Anik Dhabal Babu 57476ea392 chore: team update handler refactor (#25332)
* chore: team update handler refactor

* add tests

* fix
2025-11-25 11:27:26 +00:00
Anik Dhabal Babu 9c2d7f99c2 fix: google meet link is disappear (#25368) 2025-11-25 16:25:12 +05:30
sean-brydon ef3281f06f Remove square enforcing in resizeBase64Image (#25389) 2025-11-25 16:15:57 +05:30
sean-brydon d03f45dd0f fix: Onboarding v3 - create team then invite. (#25364)
## What does this PR do?
This PR changes the order of creating the team -> paying -> invite users.

The old approach was create team -> invite -> pay. But we are matching the current implementation of how billing works with usage based billing

## How should this be tested?

Enable onboarding-v3 flag on your instance
Ensure you have stripe enabled + stripe:listen running
login as onboarding@example.com, onboarding
Go through the team setup flow
Pay
Invite
Ensure uers were added to team

Create a new account
disable billing
Test again 

## 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
2025-11-25 09:14:59 +00:00
2b2bf36703 fix: Grab booking organizer credentials when team admins request reschedule (#24645)
* Change arg name from `bookingUId` to `bookingUid`

* Lint fix

* Use `BookingRepository` to find booking to reschedule

* Move early return further up if no booking is found

* Use `PermissionCheckService` if request rescheduling a team booking

* Remove redundent check

* Remove redundent eventType query

* Using `BookingRepository` to update the booking to rescheduled

* Update type in `getUsersCredentialsIncludeServiceAccountKey` to only
require params that are required

* Get booking organizer credentials

* Type fixes

* test: Add tests for team admin request reschedule with organizer credentials

- Add test for team admin requesting reschedule with proper permissions
- Add test verifying organizer's credentials are used (not requester's)
- Add test for team member without permissions (should fail)

These tests cover the fix in PR #24645 which ensures that when a team admin
requests a reschedule, the booking organizer's credentials are used to delete
calendar events instead of the requester's credentials.

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: Address code review comments for request reschedule

- Change user: true to user: { select: { id, email } } to only fetch required fields
- Change eventType include to select with explicit fields including teamId
- Remove sensitive information (user object, cancellationReason) from debug log
- All integration tests passing locally

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Type fix

* Remove businesss logic references from repository methods.

* Move business logic to handler

* Type fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-25 07:27:07 +00:00
Benny Joo 272d97c0a3 fix: prevent 500 errors in round-robin scheduling from OOO calibration for single host (#25369)
* wip

* add unit test
2025-11-25 09:13:04 +02:00
emrysal f2b2b6e491 chore: release v5.9.4
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-25 01:39:25 +00:00
15ef547e41 fix: enable block calendar slots by default for required confirmations (#25239)
* fix: enable block calendar slots by default for required confirmations

* remove unnecessary wasenabled

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-11-25 00:23:40 +05:30
Eunjae Lee 251d29f65b fix: improve overlapping events with dynamic offsets and widths (#25310)
## What does this PR do?

Improves the visual presentation of overlapping calendar events in the weekly view with two key enhancements:

- **Dynamic startHour per scenario**: Each playground scenario now displays the calendar starting at an appropriate hour based
on its earliest event time, rather than always starting at 6am
- **Full width for non-overlapping events**: Single events and non-overlapping events now display at 100% width (previously
80%) for maximum visibility

## Key Changes

### Overlapping Event Layout Algorithm

Replaces the previous uniform-width, fixed-offset layout with an intelligent spread algorithm:

**Previous behavior:**
- All overlapping events: 80% width with 8% offset steps
- Events clustered on the left side

**New behavior:**
- **2 overlapping events**: 80% and 50% widths
- **3 overlapping events**: 55%, ~42%, and 33% widths
- **4+ overlapping events**: Progressive narrowing from 40% down to minimum 25%
- **Spread algorithm**: Events distribute across the full width with the last event aligned to the right edge
- **Right edge distribution**: `ri = Rmin + (Rmax - Rmin) × i/(n-1)` for even spacing

### Visual Improvements

- Single/non-overlapping events: **100% width** (was 80%)
- Overlapping events: **Variable widths** based on cascade position (leftmost events wider, rightmost narrower)
- Last overlapping event: **Aligned to right border** for maximum scatter
- Minimum width: **25%** maintained for readability

**Devin session:** https://app.devin.ai/sessions/168d2227f5304c49ae4d34d17da5b025  
**Requested by:** eunjae@cal.com (@eunjae-lee)

## Visual Demo


https://github.com/user-attachments/assets/693546fa-448d-470a-b041-c08f4697c177



## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code
- [x] I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. **N/A** - playground-only changes
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.

## How should this be tested?

1. Navigate to `/settings/admin/playground/weekly-calendar`
2. Verify each scenario:
   - **Non-Overlapping Events**: Both events should be 100% width, no offset
   - **Touching Events**: Both events should be 100% width, no offset
   - **Two Overlapping Events**: First event 80% width, second 50% width aligned to right
   - **Three Overlapping Events**: Progressive narrowing with spread across full width
   - **Four Overlapping Events**: Four events spread across full width
3. Verify startHour values:
   - Most scenarios should start at 9am (events start at 10am)
   - Dense day scenario should start at 8am (events start at 9am)
   - Mixed statuses scenario should start at 1pm (events start at 2pm)
4. Test with real calendar data to ensure overlapping events look visually distinct

**Environment variables:** Standard Cal.com development setup  
**Test data:** Use playground scenarios or create overlapping events in your calendar

## Human Review Checklist

**⚠️ CRITICAL ITEMS:**

1. **Visual verification in playground** (MOST IMPORTANT):
   - Open `/settings/admin/playground/weekly-calendar`
   - Verify non-overlapping events are 100% width (not 80%)
   - Verify overlapping events spread properly across full width
   - Verify last overlapping event aligns to right edge
   - Verify each scenario starts at appropriate hour

2. **Algorithm correctness**:
   - Single events: 100% width (was 80%)
   - Two overlapping: 80%, 50% widths with last aligned to right
   - Three overlapping: 55%, ~42%, 33% widths spread across full width
   - Right-edge distribution: `ri = Rmin + (Rmax - Rmin) * i/(n-1)`

3. **Edge cases**:
   - Test with 10+ overlapping events to ensure no overflow
   - Verify minimum width (25%) is respected
   - Verify backward compatibility: custom `baseWidthPercent`/`offsetStepPercent` should use legacy behavior

4. **Type safety**:
   - `startHour` parameter now properly typed as `Hours` (union of 0-23)
   - All scenarios use valid `Hours` values

**Known limitations:**
- Local visual testing was not completed due to environment issues
- Easing curve parameters (curveExponent: 1.3) were chosen based on examples but may need visual tuning
- No E2E tests for visual appearance (only unit tests for layout calculations)

## Checklist

- [x] I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- [x] My code follows the style guidelines of this project
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have checked if my changes generate no new warnings
2025-11-24 12:01:05 +00:00
Benny JooandDevin AI 8b652613dd fix: break circular dependency between reminderScheduler and credit-service (#25312)
* fix: break circular dependency between reminderScheduler and credit-service

- Created WorkflowReminderRepository to handle workflow reminder queries
- Refactored cancelScheduledMessagesAndScheduleEmails to accept userIdsWithoutCredits parameter
- Moved credit-checking logic from workflows to CreditService
- Changed dynamic import to static import in CreditService
- Updated tests to pass userIdsWithoutCredits parameter
- Removed unused imports (prisma, WorkflowMethods)

This creates a one-way dependency (billing -> workflows) and follows the repository pattern by removing direct Prisma usage from reminderScheduler.

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

* fix: resolve type check errors in circular dependency fix

- Use MembershipRepository.listAcceptedTeamMemberIds instead of findAllAcceptedPublishedTeamMemberships
- Re-add prisma import to reminderScheduler.ts for UserRepository
- Update WorkflowReminderRepository to use WorkflowActions enum instead of string

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

* refactor

* refactor

* wip

* wip

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-24 10:00:57 +02:00
8e1a0999d2 fix: added pattern checking for urls while creating a team (#24777)
* fix: added pattern checking for urls while creating a team

* update: used slugify and replace logic

* update

* Fix formatting of rules in CreateANewTeamForm

---------

Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-11-22 15:49:28 +00:00
e29aa2097e feat: Ensure teams with conflicting slugs owned by the user are migrated(handled in backend, frontend already had this restriction) (#25291)
* feat: add backend validation for conflicting team slugs during org onboarding

- Added findOwnedTeamsByUserId method to TeamRepository
- Created buildTeamsAndInvites method in BaseOnboardingService that automatically:
  - Detects teams with same slug as organization
  - Marks conflicting teams for migration (isBeingMigrated: true)
  - Filters empty team names and invite emails
- Updated BillingEnabledOrgOnboardingService to use new method
- Updated SelfHostedOnboardingService to use new method
- Added comprehensive tests for slug conflict scenarios

This ensures backend validation even if frontend is bypassed, preventing
slug conflicts during organization creation. All inheriting classes
automatically get this validation without code changes.

* refactor: use TeamRepository in listOwnedTeamsHandler

Refactored listOwnedTeamsHandler to use TeamRepository.findOwnedTeamsByUserId
instead of direct Prisma queries. This:
- Reduces code duplication
- Ensures consistency across the codebase
- Follows repository pattern
- Makes the handler more maintainable

* fix: update tests to use renamed buildTeamsAndInvites method

- Renamed testFilterTeamsAndInvites to testBuildTeamsAndInvites
- Made test wrapper method async to match the async buildTeamsAndInvites
- Added orgSlug parameter to all test calls
- Updated all 9 test cases to use await with the new method signature
- Fixed lint warnings by using proper types instead of 'any'
- Imported OnboardingIntentResult and User types
- Used Pick<User> for mockUser type
- Removed all 'as any' type casts

Fixes test failures where filterTeamsAndInvites was renamed to buildTeamsAndInvites in the base service.

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

* fix: mock TeamRepository in tests to prevent database calls

Added vi.mock for TeamRepository to avoid database calls in unit tests.
The buildTeamsAndInvites method now calls ensureConflictingSlugTeamIsMigrated
which uses TeamRepository.findOwnedTeamsByUserId(). Mocking this prevents
Prisma errors in CI while keeping the tests focused on filtering logic.

The mock returns an empty array so no teams are found for migration,
allowing the tests to verify the filtering behavior without database access.

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

* refactor: improve readability of ensureConflictingSlugTeamIsMigrated

Refactored the conditional logic in ensureConflictingSlugTeamIsMigrated
for better readability while preserving exact behavior. Changed from
manual array manipulation to using .map() for updating team migration
status. This is a cosmetic change with no functional differences.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-22 09:07:28 +00:00
emrysal 1578dee622 chore: release v5.9.3
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-21 19:59:15 +00:00
Joe Au-Yeung e5ebf7feb4 fix: signup (#25334)
* Handle Stripe logic in `paymentCallback`

* Remove endpoint

* Do not send email verification email if premium username

* Remove logic from verify-view

* Callback send verification email

* Add `create` method to `VerificationToken` repository

* Create `VerificationTokenService`

* Early return if payment failed

* Refactor token generation

* Add tests

* Type fixes

* Type fixes
2025-11-21 19:51:19 +00:00
Rajiv Sahal 06348982a3 fix: make sure only admins and owners are authorized for team (#25333) 2025-11-21 18:44:51 +00:00
Syed Ali ShahbazandKeith Williams a7d87fc5be fix: Proxy Mintlify traffic through a Next.js API route (#25320)
* hotfix

* type fix and test fix

* update env example

* improvements

* more fix

* tada

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-11-21 18:43:09 +00:00
sean-brydonandUdit Takkar 0d77fc8474 Fix filter on invites (#25330)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-21 18:34:05 +00:00
Rajiv Sahal 0af77be127 chore: improve membership check (#25326)
* chore: improve membership check

* fix: update change role validation

* chore: add tests

* chore: update error message
2025-11-21 14:34:15 -03:00
Eunjae Lee afbfa11ab5 chore: update seed.ts (#25316)
* chore: update seed.ts

* Revert "chore: update seed.ts"

This reverts commit 2bd0c5a6dc57142632f8fbd267aa6b5eba6d1cea.

* use env vars instead
2025-11-21 16:37:57 +00:00
a7aabc8b4f fix: toggleEnabled handler (#25325)
* fix toggle enabled handler

* add tests

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-11-21 13:15:04 -03:00
sean-brydon c941192979 fix: access service class for editLocationHandler (#25315)
* fix access service class

* Move out of trpc
2025-11-21 13:13:06 -03:00
sean-brydon 44311f51ba refactor handler (#25323) 2025-11-21 15:51:23 +00:00
Morgan 0af6354954 fix: meeting ended page server component dto (#25318)
* fix: meeting ended page server component dto

* fix: repository function fetching too much
2025-11-21 15:29:53 +00:00
Joe Au-Yeung dd7c553cc3 fix: Remove hosts - verify event type belongs to event type (#25321)
* Add check that event type belongs to team

* Add `findAcceptedMembershipsByUserIdsInTeam` to `MembershipRepository`

* Validate that passed `userIds` belong to a team

* Add tests

* Typo fix
2025-11-21 10:26:22 -05:00
5cb1d4aad0 fix: Improve add users to org (#25314)
* Improve add users to org

* Improve add users to org

* test: Add comprehensive tests for handleUserEvents cross-tenant hijack prevention

- Add tests for cross-tenant hijack prevention security fix
- Test that users belonging to different organizations are blocked
- Test that users with null organizationId are blocked (prevents legacy user hijacking)
- Test that users belonging to correct organization succeed
- Test new user creation flow
- Test user activation/deactivation flows
- Test custom attribute syncing
- All 8 tests passing locally with proper type safety (no 'as any' usage)

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-21 12:18:41 -03:00
Eunjae Lee 701490a493 docs: update the app store doc regarding seed (#25317) 2025-11-21 14:45:29 +00:00
Morgan 6499243a42 fix: enable api v2 sms (#25311) 2025-11-21 13:40:27 +00:00
sean-brydonandDevin AI 3c8215add3 feat: filter out platform organizations from admin organization list (#24926)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-21 12:03:06 +00:00
mintlify[bot]andVolnei Munhoz 2bd7549f20 Add Cal.com API V1 to V2 Migration Guide (#25196)
* Update docs/api-reference/v2/migration-guide.mdx

* Update docs/api-reference/v2/v1-v2-differences.mdx

* Update docs/api-reference/v2/v1-v2-differences.mdx

* Update docs/api-reference/v2/migration-guide.mdx

---------

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Co-authored-by: Volnei Munhoz <volnei@cal.com>
2025-11-21 11:00:00 +00:00
mintlify[bot] 629371a5f1 Documentation edits made through Mintlify web editor (#25275)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2025-11-21 06:09:35 +00:00
dc3df122e2 feat: Add configurable trial days to org subscriptions + wizard warning (#25229)
* feat: Add trial days to organization subscriptions and workspace warning

- Add ORGANIZATION_TRIAL_DAYS environment variable for configurable trial periods
- Implement trial days in Stripe checkout session (only when env var is set)
- Add warning message to organization setup page about workspace structure
- Add translation string for organization trial workspace warning
- Add ORGANIZATION_TRIAL_DAYS to turbo.json env vars
- Fix pre-existing linting warnings in CreateANewOrganizationForm.tsx
- Add ESLint disable comments for turbo/no-undeclared-env-vars warnings

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Rename variable

* Add trial days to `purchaseTeamOrOrgSubscription`

* fix: Move organization trial warning to spot 3 below form card

- Add optional footer prop to WizardLayout component
- Use footer prop in create-new-view.tsx to render warning at spot 3
- Remove warning from inside CreateANewOrganizationForm component
- Warning now appears below the form card as requested

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* feat: Add organization trial warning to all wizard steps

- Add warning footer to step 2 (about-view.tsx)
- Add warning footer to step 3 (add-teams-view.tsx)
- Add warning footer to step 4 (onboard-members-view.tsx)
- Add warning footer to step 5 (payment-status-view.tsx)
- Add warning footer to resume-view.tsx (step 1 resume page)
- Warning now appears on all steps of the organization creation wizard

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: Correct environment variable name to STRIPE_ORG_TRIAL_DAYS

- Changed ORGANIZATION_TRIAL_DAYS to STRIPE_ORG_TRIAL_DAYS in turbo.json
- This matches the actual usage in OrganizationPaymentService.ts and payments.ts
- Ensures the environment variable is properly recognized by the build system

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* refactor: Create shared OrganizationWizardLayout component

- Create OrganizationWizardLayout component that wraps WizardLayout
- Centralizes the trial warning footer logic in one place
- Update all 6 wizard pages to use the shared component:
  - create-new-view.tsx
  - about-view.tsx
  - add-teams-view.tsx
  - onboard-members-view.tsx
  - payment-status-view.tsx
  - resume-view.tsx
- Remove duplicate useLocale/Alert imports and footer props from pages
- Simplifies maintenance by having warning logic in a single location

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Fix lint error

* Add `ORG_TRIAL_DAYS` as constant

* Add comment

* Ensure no negative number is passed

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-20 15:02:01 -03:00
176fb777fc fix: One flaky E2E tests (#24815)
* fix: exclude booking responses from calendar/CRM integrations for seated events

For seated events (events with seatsPerTimeSlot), booking responses are now excluded from being sent to calendar integrations (Google Calendar, etc.), CRM integrations (HubSpot, Salesforce, etc.), and other integrations.

This applies to both creating new seated bookings and rescheduling seated bookings.

The calendar event and CRM records are still created, but without the custom booking field responses included in the description/notes.

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* test

* Refactor Office 365 Calendar OAuth redirect URI

Removed debug logging for redirect URI and client ID.

* test

* check

* Update Prisma connection logic for integration tests

Refactor database connection handling for integration tests.

* update

* Refactor conditionals and simplify role handling

* Add test comment in index.ts

* Remove integration test flag from index.ts

Remove integration test flag from database connection logic.

* update

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-20 22:10:02 +05:30
Alex van Andel ae7fd0cae2 refactor: Remove all code related to the old cache system (#25284)
* chore: Remove all code related to the old cache system

* Removed some redundant tests, some type fixes

* Further type fixes

* More type fixes re. tests

* Next iteration, couple of fixes remaining

* Remove cache from CredentialActionsDropdown

* Fix tests by mocking credential, instead of db queries

* Remove Cache DI wiring from v2

* Make sure apiv2 build passes

* Remove another cache cron

* Remove old tokens for calendar-cache v1
2025-11-20 18:02:18 +02:00
Piyanshu Saini d1184f65e2 fix(platform): show loading spinner only for selected plan, not all plans (#25290) (#25292)
## Problem
When users clicked on a plan card in the Cal.com Platform pricing UI, the loading
spinner would appear on ALL plan cards simultaneously instead of just the selected one.
This gave a confusing UX impression that all plans were being processed at the same time.

## Solution
Track the currently loading plan using a `loadingPlan` state variable that stores
which plan the user clicked on, rather than relying solely on the pending state from
the mutation hook.

## Changes
- Added `loadingPlan` state to track which specific plan is loading
- Update `loadingPlan` when a plan subscription is initiated
- Reset `loadingPlan` on error to ensure UI returns to normal state
- Modified `isLoading` prop condition to check if the current plan matches
  the `loadingPlan` before showing the spinner

This ensures that only the plan card the user clicked on displays the loading indicator,
while other plans remain in their normal state.
2025-11-20 15:25:12 +00:00
MorganandDevin AI 342e5baca2 feat: add hashedLink to BOOKING_REQUESTED webhook payload (#25274)
- Add hashedLink field to CalendarEvent type definition
- Add withHashedLink method to CalendarEventBuilder
- Pass hashedLink from booking request to CalendarEvent in RegularBookingService
- hashedLink will now be included in webhook payloads when booking via private API links

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-20 08:24:21 -03:00
chauhan_sandMorgan fbfa27735b feat: add validation for null values in bookingFieldsResponses (#25272)
* feat: add validation for null values in bookingFieldsResponses

- Add test case to verify 400 error when bookingFieldsResponses contains null values
- Create ValidateBookingFieldsResponses decorator to reject null values in booking field responses
- Apply validator to CreateBookingInput_2024_08_13.bookingFieldsResponses property
- Ensure all booking field response values are non-null strings

* feat: transform null values to empty strings in bookingFieldsResponses

- Remove ValidateBookingFieldsResponses validator that rejected null values
- Add Transform decorator to convert null values to empty strings in bookingFieldsResponses
- Update test to verify null values are transformed to empty strings instead of returning 400 error

* remove extra spaces

* test: add rescheduleReason null value test case

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-11-20 10:45:22 +00:00
emrysal 37c26793f5 chore: release v5.9.2
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-20 10:33:06 +00:00
sean-brydon 956f81fab3 chore: tidy up onboarding with new animations and illustrations (#25124)
<!-- This is an auto-generated description by cubic. -->
## Summary by cubic
Revamps onboarding with subtle route transitions, new org/teams illustrations, and clearer invite flows with real-time previews. Adds “Skip for now” on invites and guardrails to avoid incomplete team creation.

- **New Features**
  - Added email invite pages for organizations and teams with shared EmailInviteForm and RoleSelector.
  - Built a live invite browser preview that renders a 3×3 grid and updates as you type; works for orgs and teams.
  - Applied route-keyed framer-motion transitions to layout, cards, and browser views for smoother page changes.
  - Continuation prompt now detects saved org or team and routes to the right next step; localized copy.
  - Added loading state to plan selection to prevent double navigation.
  - Refreshed Organizations welcome modal with a new animated ring illustration and better scrolling.
  - Added “Skip for now” to org and team invite-by-email steps to proceed without invites.
  - Calendar browser preview now focuses on a time window around “now” for a more realistic demo.
  - Added an optional floating footer to keep actions visible on long, scrollable lists.

- **Refactors and Fixes**
  - Unified invite browser and replaced the org preview on the org email step.
  - Split and simplified legacy invite views; team and org routes are cleaner and easier to extend.
  - Streamlined the team invite step by removing a redundant action; non-admins are redirected to the email invite page.
  - Prevented creating teams with empty details by redirecting back to team details.
  - Minor UI cleanup: tighter borders, padding, and mobile max-heights across onboarding screens.
  - Hide team select on org invites when no teams exist.

<sup>Written for commit ae7f5277ab998560ae6e2f3f9144852a7b4959a7. Summary will update automatically on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2025-11-20 10:20:38 +00:00
1dcd54aab7 feat: add avatarUrl and bio fields to /me endpoint response (#25224)
* feat: add avatarUrl to /v2/me endpoint response

- Add avatarUrl field to userSchemaResponse schema in packages/platform/types/me.ts
- Update e2e tests to verify avatarUrl is returned in GET and PATCH /v2/me responses
- Field is nullable to match User model in Prisma schema
- Fix pre-existing lint warnings by removing 'as any' type assertions in test file

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* feat: add avatarUrl to MeOutput DTO for OpenAPI docs

- Add avatarUrl field to MeOutput class in apps/api/v2/src/ee/me/outputs/me.output.ts
- Field is nullable to match the Zod schema and Prisma model
- This ensures OpenAPI documentation will include avatarUrl when regenerated

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* feat: add bio field to /v2/me endpoint response

- Add bio field to userSchemaResponse Zod schema in packages/platform/types/me.ts
- Add bio field to MeOutput NestJS DTO in apps/api/v2/src/ee/me/outputs/me.output.ts
- Update e2e tests to verify bio is returned in both GET and PATCH responses
- Field is nullable to match the User model in Prisma schema

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: chauhan_s <somaychauhan98@gmail.com>
2025-11-19 23:24:31 +00:00
68d3d9d964 docs: add Stripe Connect atom documentation (#24791)
* docs: add Stripe Connect atom documentation

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* docs: restructure Stripe Connect docs with Steps component

- Add Steps component explaining two ways to connect Stripe
- Step 1: Direct Stripe Connect atom usage
- Step 2: Event Type Settings payment tab usage
- Include code examples for both methods
- Add image placeholders for visual demonstrations
- Move advanced usage examples under separate section

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* chore: add link to stripe connect atom

* chore: update docs

* chore: implement feedback

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 23:12:05 +00:00
717a26f223 fix: prevent bulk update of locked locations in child managed event types (#24978)
* fix: prevent bulk update of locked locations in child managed event types

- Filter out child managed event types with locked locations in getBulkUserEventTypes
- Add validation in bulkUpdateEventsToDefaultLocation to prevent updating locked fields
- Implements defense in depth with validation at multiple layers

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Abstract filtering logic

* test: add comprehensive tests for bulk location update filtering

- Add unit tests for filterEventTypesWhereLocationUpdateIsAllowed
- Add unit tests for bulkUpdateEventsToDefaultLocation
- Add integration tests for getBulkUserEventTypes
- Fix bug: change unlockedFields?.locations check from !== undefined to === true
  This ensures that locations: false is properly treated as locked, addressing
  the security issue identified in PR review comments

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: filter locked managed event types on app installation page

- Add parentId to eventTypeSelect in getEventTypes function
- Apply filterEventTypesWhereLocationUpdateIsAllowed to both team and user event types
- Only filter when isConferencing is true to avoid affecting other app types
- Fixes issue where locked managed event types were showing in the event type selection list on /apps/installation/event-types page

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix(embed-react): remove obsolete availabilityLoaded event listener

The availabilityLoaded event does not exist in the EventDataMap type system
in embed-core. This code was causing 5 TypeScript errors in CI:
- Type 'availabilityLoaded' does not satisfy constraint 'keyof EventDataMap'
- 'data' is of type 'unknown' (2 occurrences)
- Type 'availabilityLoaded' is not assignable to action union (2 occurrences)

Since this is an example file and the event is not defined in the type system,
removing this obsolete code resolves the type errors.

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: correct Prisma type for metadata in test helper function

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: use flexible PrismaLike type for better test compatibility

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: properly type mock Prisma objects in test files

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: properly mock Prisma methods in test file

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Filter out metadata

* Undo change in embed file

* Address feedback

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 15:46:05 -05:00
Alex van Andel 6818806c5e chore: No more clientside markdown when importing Checkbox (#25278) 2025-11-19 15:40:37 -03:00
Eunjae Lee dd7f108f08 fix: put booking details and calendar behind feature flag (#25175)
* Revert "fix: revert bookings redesign (#25172)"

This reverts commit 1f102bf3b4.

* add bookings-v3 feature flag

* put things behind a feature flag

* remove no longer needed test

* revert e2e tests

* put back description

* revert AvatarGroup

* apply feedback

* remove "view" booking action

* remove Alert (When the bookings query errors, this branch now renders only the alert and skips the data-table filter/segment controls. Those controls moved into BookingsList, so in error states users can no longer clear or tweak filters to recover from the failure, effectively trapping them behind the alert.)

* address feedback

* revert useMediaQuery
2025-11-19 16:27:13 +01:00
7e4d9e26c6 refactor: implement DI in team billing service and team billing data repository factory (#24803)
* Move TeamBillingRepositories

* WIP refactor team internal billing service

* Remove duplicate billing repository files

* Remove logic check in repository for billing is enabled

* Rename repository to `TeamBillingData`

* Use repository factory in main service

* Fix new import paths

* Rename  to

* Ensure `IS_TEAM_BILLING_ENABLED` is of type boolean

* Rename classes to TeamBillingService and TeamBillingServiceFactory

* Implement DI in `BookingServiceFactory`

* `TeamBillingService` use repository in `getOrgIfNeeded`

* DI `isTeamBillingEnabled` to `TeamBillingServiceFactory`

* Rename files for consistency

* Return stub BillingRepository if billing is not enabled

* Move Stripe billing service to service folder

* Rename file

* `StripeBillingService.getSubscriptionStatus` return `SubscriptionStatus`

* Type fices in StripeBillingService

* Type fix in `stubTeamBillingService`

* DI the `BillingProviderService` into the `TeamBillingService`

* Implement DI in `skipTeamTrials.handler`

* Implement DI for team billing in `inviteMember.handler`

* `skipTeamTrials.handler` use `team.isOrganization`

* Implement DI for billing in `hasActiveTeamPlan.handler`

* Type fixes

* Implement DI in `bulkDeleteUsers.handler`

* Implement `BillingProviderServiceFactory` in `updateProfile.handler`

* Implment `BillingProviderServiceFactory` in `buyCredits.handler`

* Fix import in `stripeCustomer.handler`

* Add a constructor to `teamBillingServiceFactory`

* Add DI to `PrismaTeamBillingRepository`

* Add DI to `StripeBillingService`

* Implement singleton in `BillingProviderServiceFactory`

* Add DI folder and contents to billing folder

* Use `getTeamBillingServiceFactory` in `inviteMember.handler`

* Add `saveTeamBilling` method to `ITeamBillingService`

* Implement DI in new team route

* Implement DI in `teamService`

* Implement DI in `OrganizationPaymentService`

* Implement DI in `credit-service`

* In `StripeBillingService` remove `static` from status methods

* Implemnt DI in `_invoice.paid.org`

* Refactor `hasActiveTeamPlan` to use `getTeamBillingFactory`

* Refactor `skipTeamTrials` to use `getTeamBillingFactory`

* Refactor `skipTeamTrials` to use `getTeamBillingServiceFactory`

* `stripeCustomer.handler` to use `getBillingProviderService`

* Remove old factories

* Type fix

* Remove unused factory

* Refactor `updateProfile.handler` to use `getBillingProviderService`

* Change name to `TeamBillingDataRepositoryFactory`

* Type Prisma return in `prisma.module`

* Type fix

* Refactor `buyCredits.handler` to use `getBillingProviderService`

* Refactor `credit-service` to use billing DI containers

* Type fix

* Add `getTeamBillingDataRepository`

* Refactor `_invoice.paid.org` to use DI container

* Refactor `_customer.subscription.deleted.team-plan` to use DI container

* Refactor `calcomHandler` to use DI container

* Refactor `getCustomerAndCheckoutSession` to use DI container

* Refactor `verify-email` to use DI containers

* Refactor `api/create/route` to use DI container

* Refactor downgradeUsers to use DI container

* Type fix

* Clean up console.logs

* Add await to `this.billingRepository.create` in `saveTeamBilling`

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Fix type errors

* Address comments

* fix: update tests to work with new DI pattern

- Update teamBillingService.test.ts to properly inject DI dependencies
- Remove unused billingModule import and mock
- Fix import naming in teamService.integration-test.ts (remove unused rename)
- Fix import path for TeamBillingPublishResponseStatus

All tests now properly mock IBillingProviderService, ITeamBillingDataRepository,
and IBillingRepository instead of using the old BillingRepositoryFactory pattern.

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: add compatibility layer and env setup for unit tests

- Add STRIPE_PRIVATE_KEY dummy value to vitest.config.ts to prevent DI module errors
- Fix import paths in credit-service.test.ts (StripeBillingService, TeamBillingService)
- Create compatibility barrel at packages/features/ee/billing/teams/index.ts for test mocking

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: update unit tests to mock DI container properly

- Update teamService.test.ts to mock getTeamBillingServiceFactory() instead of TeamBilling.findAndInit
- Update teamService.alternative.test.ts to mock DI container
- Update credit-service.test.ts to mock getBillingProviderService() and use SubscriptionStatus enum values
- Update OrganizationPaymentService.test.ts to mock DI container instead of direct StripeBillingService import
- Remove all 'as any' type casting to comply with Cal.com coding standards
- Fix unused variable warnings by prefixing with underscore

All 53 tests now passing (16 + 1 + 30 + 6)

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: update remaining unit tests to use DI pattern

- Fix StripeBillingService.test.ts to inject mock Stripe client directly
- Fix teamBillingFactory.test.ts to mock getTeamBillingServiceFactory() from DI container
- Fix skipTeamTrials.test.ts to mock DI container and use SubscriptionStatus enum

All 11 previously failing tests now pass (5 + 5 + 1)

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Undo changes made to Prisma module

* fix: address test-related PR comments

- Fix OrganizationPaymentService.test.ts mock path from @calcom/ee to @calcom/features/ee
- Refactor teamBillingFactory.test.ts to test real factory logic instead of mocking container
- Remove duplicate teamBillingService.test..ts file with incorrect double-dot filename

All three test files now pass successfully with proper DI patterns.

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Address feedback

* fix: update teamService integration test to mock new DI factory pattern

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* refactor: remove duplicate imports in credit-service.test.ts

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Remove unused index file

* `getBySubscriptionId` to return team or null

* Address feedback

* Merge fix

* Refactor file names

* fix: correct mockStripe variable name to stripeMock in StripeBillingService.test.ts

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* refactor: update internal-team-billing.test.ts to use new DI structure with TeamBillingService

- Replace InternalTeamBilling with TeamBillingService
- Use constructor injection with mock dependencies instead of factory pattern
- Remove BillingRepositoryFactory mock and import
- Update all test cases to use mockBillingProviderService, mockTeamBillingDataRepository, and mockBillingRepository
- Simplify saveTeamBilling tests to focus on repository.create calls
- All 11 tests now pass with the new DI structure

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: update createWithPaymentIntent.handler.test.ts to mock DI container's getBillingProviderService

- OrganizationPaymentService now uses getBillingProviderService() from DI container
- Test was mocking @calcom/features/ee/payments/server/stripe directly, which no longer works
- Added mock for @calcom/features/ee/billing/di/containers/Billing module
- Mock returns fake billing provider that delegates to mockSharedStripe
- Preserves all existing test assertions and helpers
- Fixed lint error by prefixing unused lastCreatedSessionId with underscore
- All 11 tests now pass (1 skipped as expected)

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2025-11-19 10:21:22 -05:00
sean-brydonandDevin AI a7352cf587 chore: remove ui-playground app (#25266)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 14:48:47 +00:00
Peer RichelsenandEunjae Lee 5f8db0557c chore: improve floating schedule button (#23873)
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-11-19 14:16:32 +00:00
emrysal 88be45457f chore: release v5.9.1
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-19 14:04:07 +00:00
Alex van Andel 90a42c37fd chore: Remove next-collect package (#25146) 2025-11-19 13:54:26 +00:00
cf7430d531 fix: convert booking field label error to log.error to prevent booking failures (#25062)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-19 13:30:46 +00:00
Hariom BalharaandDevin AI 105a82f97d feat: add uuidv7 and @db.Uuid to AuditActor and BookingAudit models (#25269)
- Update AuditActor.id to use uuid(7) with @db.Uuid
- Add @db.Uuid to BookingAudit.bookingUid and actorId fields
- Create migration to convert existing TEXT columns to UUID type

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 13:17:02 +00:00
Yash fdd3d11111 Fix dynamic group meeting slots not loading (#25101) 2025-11-19 12:56:47 +00:00
emrysal e7ea1884b7 chore: release v5.9.1 2025-11-19 11:43:50 +00:00
Hariom Balhara c04402e7ee feat: [Booking Audit Stack - 1] Add Booking Audit System foundation (database schema and repositories) (#24838)
* feat: Implement Booking Audit System with database architecture and repository interfaces

- Added `ARCHITECTURE.md` detailing the design and structure of the Booking Audit System, including core tables `AuditActor` and `BookingAudit`.
- Created repository interfaces `IAuditActorRepository` and `IBookingAuditRepository` for managing audit actor and booking audit records.
- Implemented `PrismaAuditActorRepository` and `PrismaBookingAuditRepository` for database interactions.
- Defined enums for `BookingAuditType`, `BookingAuditAction`, and `AuditActorType` in the Prisma schema.
- Added migration scripts to create necessary database tables and enums for the audit system.

This commit establishes a robust framework for tracking booking-related actions, ensuring compliance and data integrity.

* feat(audit): add system actor migration
2025-11-19 08:21:34 -03:00
075d209b6f feat: remove OAuth clients link from settings navigation (#25206)
* feat: remove OAuth clients link from settings navigation

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

* fix: resolve infinite loop in sidebar close effect

Use functional state update to avoid reading sideContainerOpen in dependency array, which was causing infinite re-renders when navigating between settings pages.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 11:09:07 +00:00
chauhan_s 26e8582302 fix: use UTC parsing for recurring booking dates to prevent timezone conversion issues (#25235)
Replace dayjs.tz() with dayjs.utc() when parsing recurring booking date strings to ensure dates are interpreted as UTC timestamps rather than being converted to the specified timezone. This prevents incorrect date/time display when the dateStr is already in UTC format.
2025-11-19 11:03:14 +00:00
Anik Dhabal Babu 9f277c5179 fix: archive meeting during host-change reschedules (#25098) 2025-11-19 10:24:50 +00:00
21411b8f93 docs: update embed-events.mdx with all currently emitted embed events (#25099)
* docs: update embed-events.mdx with all currently emitted embed events

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* docs: remove V2 suffix from event names in documentation

Per feedback, the documentation should not expose internal versioning details.
Event names are now presented as clean, non-versioned names:
- bookingSuccessful (instead of bookingSuccessfulV2)
- rescheduleBookingSuccessful (instead of rescheduleBookingSuccessfulV2)
- dryRunBookingSuccessful (instead of dryRunBookingSuccessfulV2)
- dryRunRescheduleBookingSuccessful (instead of dryRunRescheduleBookingSuccessfulV2)

Added note about using wildcard listener for future-proofing.

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* docs: use V2 suffix for embed event names to match code (#25263)

* docs: update embed events to use V2 event names

- Document bookingSuccessfulV2 instead of deprecated bookingSuccessful
- Document rescheduleBookingSuccessfulV2 instead of deprecated rescheduleBookingSuccessful
- Keep dryRunBookingSuccessful and dryRunRescheduleBookingSuccessful without V2 suffix (only V2 versions exist)
- Add comprehensive event documentation with all public and internal events
- Organize events into Public Events and Internal Events sections

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

* fix: use V2 suffix for dry-run event names

- Change dryRunBookingSuccessful to dryRunBookingSuccessfulV2
- Change dryRunRescheduleBookingSuccessful to dryRunRescheduleBookingSuccessfulV2
- These match the actual event names emitted in the code (only V2 versions exist)

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

---------

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-11-19 08:54:12 +00:00
Anik Dhabal Babu 0c160de404 fix: addtinonal notes isn't visisble (#25163) 2025-11-19 08:26:40 +00:00
a0eca13c18 feat: add organization-level autofill disable setting (#23504)
* feat: add organization-level autofill disable setting

- Create DisableAutofillOnBookingPageSwitch component following existing patterns
- Add toggle to organization general page alongside other settings
- Update tRPC organizations update handler to support new field
- Add organization-level check to useShouldBeDisabledDueToPrefill hook
- Add translation keys for new autofill disable setting
- Include database migration for disableAutofillOnBookingPage field
- Maintain backward compatibility with individual field settings

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* feat: complete autofill disable implementation

- Add disableAutofillOnBookingPage to orgSettings type definition
- Update Prisma schema with new organization setting field
- Clean up test file formatting

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* fix: resolve tRPC mocking issues in tests and add missing disableAutofillOnBookingPage to organization repository

- Fix tRPC module mocking in useShouldBeDisabledDueToPrefill tests
- Add disableAutofillOnBookingPage to organization repository select and return statements
- All form builder tests now pass (24/24)
- Organization-level autofill disable tests working correctly

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* test: simplify autofill disable test to single focused test

- Replace multiple tests with one test that verifies org setting blocks autocomplete
- Test includes searchParams with prefill data to verify blocking behavior
- Removes unnecessary test complexity as requested

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* fix: add missing disableAutofillOnBookingPage to organizationSettings select statements

- Add disableAutofillOnBookingPage to both parent and main organizationSettings select statements in getTeamWithMembers
- Resolves TypeScript error in getServerSideProps.tsx where MinimumOrganizationSettings type requires this property
- Ensures organization settings type compatibility across the codebase

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* Remove disableAutofillOnBookingPage setting

Removed 'disableAutofillOnBookingPage' setting from organization configuration.

* update

* Remove duplicate settings in common.json

Removed duplicate entries for automatic transcription and autofill settings.

* Fix syntax error in common.json

* update

* add tests

* Remove comments for autofill disabled check

Removed comments explaining scenarios for autofill check.

* addressed review

* fix

* change

* Add handling for disableAutofillOnBookingPage input

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 08:25:46 +00:00
Anik Dhabal Babu 5cda123a4c fix test flakes (#25240) 2025-11-19 09:57:10 +02:00
Udit Takkar 6169b92e98 fix: dialog click propagation (#25154) 2025-11-19 11:34:34 +05:30
cc1efe1298 fix: Conditional fetch cache (#24816)
* Fix cache to fetch only when it's available

* Non hierarchical feature check

* Fix tests: Add missing mock method and comprehensive CalendarCacheWrapper tests

- Add checkIfUserHasFeatureNonHierarchical to features.repository mock to fix failing GoogleCalendar tests
- Add comprehensive unit tests for CalendarCacheWrapper covering:
  - Calendars with sync only (cache-only path)
  - Calendars without sync only (original calendar path)
  - Mixed calendars (both cache and original)
  - Timezone handling with UTC defaults
  - Edge cases (empty arrays, undefined methods, null ids)
- Use proper types instead of 'as any' to satisfy lint rules

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Fix: Sanitize logging to avoid exposing PII

- Replace logging full selectedCalendars objects with only calendar IDs and count
- Prevents exposure of email fields and other sensitive information in logs
- Addresses AI code reviewer feedback

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Apply suggestion from @volnei

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-19 01:11:52 +00:00
Joe Au-YeungandDevin AI 7635d338c0 fix: increase dropdown z-index to appear above logo (#24057)
- Change dropdown z-index from z-10 to z-50 to ensure dropdowns appear above navigation elements
- TopNav uses z-40, so dropdowns need higher z-index to be properly visible
- Fixes issue where Cal.com logo was appearing above dropdown menus

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-18 20:36:03 +00:00
Udit Takkar 03ebabd6b4 fix: transcript endpoint (#25136) 2025-11-18 21:00:38 +02:00
Anik Dhabal Babu 0ab1bd0b6c fix: correct assignment reason badge mapping (#25255) 2025-11-18 17:16:04 +00:00
Anik Dhabal Babuandcubic-dev-ai[bot] 908e5e05f2 fix: routing-form org redirects when profile slug equals legacy slug (#25003)
* fix: routing-form org redirects when profile slug equals legacy slug

* update

* Update packages/app-store/routing-forms/getEventTypeRedirectUrl.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix type error

* addressed review

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-11-18 09:12:58 +00:00
Peer Richelsen 3490272815 feat: normalize routing form identifier field with URL-safe format (#25128)
## What does this PR do?

- Fixes #25116
- Fixes [CAL-6741](https://linear.app/calcom/issue/CAL-6741/make-routing-edit-question-form-less-intrusive-on-how-it-handles-the)

https://github.com/user-attachments/assets/d78c4c62-2af5-44d8-b25c-e938cc8ddf6f


Improves the routing form field identifier auto-fill behavior to generate URL-safe identifiers from labels. Previously, the identifier field simply duplicated the label text. Now it normalizes the input to be URL-friendly.

**Changes:**
- Converts labels to lowercase with hyphens (e.g., "What is your name?" → "what-is-your-name")
- Limits identifiers to the first 5 words (e.g., "What do you work on and how can we help?" → "what-do-you-work-on")
- Updates the Identifier label to clarify it's a URL parameter with an example
- Preserves manual identifier edits when the label changes

**Link to Devin run:** https://app.devin.ai/sessions/f569297990fe4436bab1fec89d8b71ac
**Requested by:** @PeerRich (peer@cal.com)

## Key Implementation Details

The `normalizeIdentifier` function:
- Strips special characters and replaces them with spaces
- Converts to lowercase
- Splits on whitespace and takes first 5 words
- Joins with hyphens

The auto-fill logic only updates the identifier if:
1. The identifier field is empty, OR
2. The identifier matches the normalized version of the previous label

This preserves manual edits while still providing helpful auto-fill for new fields.

## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [x] 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. **N/A** - This is a UI behavior change that doesn't require documentation updates.
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works. **Note:** No automated tests were added for this change. Manual testing is recommended.

## How should this be tested?

**Test Cases:**

1. **Basic normalization:**
   - Create a new routing form field
   - Enter label: "What is your name?"
   - Verify identifier auto-fills to: "what-is-your-name"

2. **5-word limit:**
   - Enter label: "What do you work on and how can we help?"
   - Verify identifier becomes: "what-do-you-work-on"

3. **Special characters:**
   - Enter label: "Email (required)!"
   - Verify identifier becomes: "email-required"

4. **Manual edit preservation:**
   - Enter label: "Test Field"
   - Manually change identifier to: "custom-id"
   - Change label to: "New Test Field"
   - Verify identifier stays: "custom-id" (not auto-updated)

5. **Updated label text:**
   - Verify the Identifier field label shows: "Identifier (URL Parameter, i.e.: &what-is-your-name=john-doe)"

**Environment:**
- No special environment variables needed
- Test in the routing forms builder at `/apps/routing-forms/[formId]`

## Important Review Points

⚠️ **No automated tests**: The normalization function and auto-fill behavior lack test coverage. Consider adding unit tests for edge cases.

🔍 **Auto-fill logic**: The identifier only auto-updates if it's empty or matches the previous normalized label. This preserves manual edits but may be surprising to users in some scenarios.

🌍 **Unicode handling**: Non-ASCII characters are replaced with spaces. This may not be ideal for international users with non-English labels.

## Checklist

- [x] I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- [x] My code follows the style guidelines of this project
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have checked if my changes generate no new warnings
2025-11-18 11:24:33 +05:30
Mauryan Kansara 02dab51543 fix: Prevent silent loss of changes in routing form when only one field exists (#25197) 2025-11-18 02:02:57 +00:00
Anik Dhabal Babu 358b873aa7 fix: unable to update username (#25221)
* unable to update username

* update

* Add import for useEffect and useState hooks
2025-11-18 01:55:12 +00:00
Anik Dhabal Babu 44aa26292d fix: flaky integrations tests (#25218)
* Revert "fix: resolve flaky integration tests (#25030)"

This reverts commit 4e5d4f67d5.

* update

* test

* Remove connection pool setup in Prisma index

Set the connection pool to undefined, removing conditional pooling logic.

* update
2025-11-18 00:04:24 +00:00
b8530e2d78 fix: delegation credential error webhooks + refactor repeated code (#25232)
* fix: delegation credential error webhooks

* refactor: extract repeated delegation credential error webhook logic into helper methods

- Added private triggerDelegationCredentialError method in Office365CalendarService class
- Added triggerDelegationCredentialError helper function in TeamsVideoApiAdapter
- Replaced all 4 instances in Office365CalendarService with helper method call
- Replaced all 4 instances in TeamsVideoApiAdapter with helper function call
- Keeps code DRY by eliminating repeated if statement and webhook trigger logic

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-18 00:00:42 +02:00
Dhairyashil ShindeandPallav 7f1b94ad19 style: hide icons from booking actions dialog in mobile view (#25147)
* style: hide icons from booking actions dialog in mobile view

* apply correct padding in mobile view

---------

Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-11-17 20:50:41 +00:00
Dhairyashil ShindeandMorgan 801ba50221 fix: empty event type array if no username (#25178)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-11-18 01:39:03 +05:30
spandevandDhairyashil Shinde 74af23136c fix: empty screen added in routing position page when no options are present (#25170)
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-11-17 20:06:39 +00:00
Anik Dhabal Babu da9ee5c186 chore: Update test results repository to test-results-2 (#25199) 2025-11-17 23:06:03 +05:30
037cb8ee2a feat: add defaultPhoneCountry prop with ISO 3166-1 alpha-2 type safety (#25204)
* feat: add defaultPhoneCountry prop to BookerPlatformWrapper

- Add defaultPhoneCountry to BookerStore type and implementation
- Add defaultPhoneCountry prop to BookerPlatformWrapper types
- Pass defaultPhoneCountry through store initialization
- Update PhoneInput to use defaultPhoneCountry from store
- Support default phone country extension for phone inputs in booker form

* feat: add strict typing for defaultPhoneCountry with ISO 3166-1 alpha-2 codes

- Define CountryCode type using ISO 3166-1 alpha-2 country codes
- Update defaultPhoneCountry prop type in BookerPlatformWrapper to use CountryCode
- Update defaultPhoneCountry type in BookerStore to use CountryCode
- Ensures type safety by only allowing valid country codes like 'us', 'gb', 'ee', etc.
- Fix lint warnings by prefixing type-only constants with underscore

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

* refactor: export CountryCode from store to avoid duplication

- Export CountryCode type from packages/features/bookings/Booker/store.ts
- Import CountryCode in packages/platform/atoms/booker/types.ts from store
- Remove duplicate CountryCode definition from types.ts
- Maintains single source of truth for country code type definition

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

* fix: add type-safe casts for CountryCode in PhoneInput

- Import CountryCode type from store
- Add explicit type annotation to useState<CountryCode>
- Add safe type casts with isSupportedCountry validation
- Validate navigator.language country code before using it
- Fixes CI type error: string not assignable to CountryCode

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

* docs: add defaultPhoneCountry prop documentation and changeset

- Add defaultPhoneCountry prop to booker.mdx documentation
- Add changeset for minor version bump
- Document ISO 3166-1 alpha-2 country code support

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2025-11-17 15:56:10 +00:00
sean-brydon f0e10129a1 test: Improve create teams handler tests (#25213)
I am refactoring create teams handler to a service in another PR - thought we should improve the test in the mean time.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Expand and restructure tests for createTeamsHandler to cover auth, org/slug validation, team creation and migration, redirects, credits, invites, and subscriptions. This improves reliability ahead of the service refactor.

- **Refactors**
  - Rewrote tests into scenario-based suites for Authorization, Organization Validation, Slug Validation, Team Creation/Migration, Redirects, and Edge Cases.
  - Added helpers (createTestProfile, flexible createScenario/createTestMembership) and organization/profile support to model slug collisions.
  - Assert TRPCError codes/messages and redirect records; cover platform orgs, unpublished/non-existent teams, and empty names.
  - Mocked Stripe subscription cancel and inviteMembersWithNoInviterPermissionCheck; verified credit transfer and owner memberships.

<sup>Written for commit 8c216c670faa85bf9a06a8895030f40808fdda0e. Summary will update automatically on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2025-11-17 11:08:33 +00:00
Dhairyashil ShindeandPallav 7c5731e893 fix: german-translation-link-meeting (#25210)
Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-11-17 10:57:58 +00:00
spandevandPallav 1012f2f153 fix: made show all columns work correctly (#25106)
* fix: made show all columns work correctly

* test: added test for show all columns in ColumnVisibilityButton

---------

Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-11-15 22:01:01 +00:00
Emmanuel IsenahandDhairyashil Shinde d6f6be7d50 fix: correct square image validation logic in resizeBase64Image (#25130)
The validation was comparing image height to itself instead of comparing height to width, causing the square check to always pass even for non-square images.

Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-11-16 01:50:50 +05:30
emrysal 9e10dfd77b chore: release v5.9.0
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-15 14:39:17 +00:00
spandevandEunjae Lee 82782fcf77 fix: added timezonebadge to insights/routing which appears on timezone mismatch (#25138)
* fix: added timezonebadge to insights/routing which appears on timezone mismatch

* update: removed user repository and directly used prisma

* clean up code

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-11-14 18:15:34 +00:00
emrysal 2477220d2d chore: release v5.8.14
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-14 17:45:48 +00:00
Anik Dhabal Babu 89fe869766 fix: Improve MS Teams app guidance in App Store (#25173) 2025-11-14 17:39:32 +00:00
Eunjae Lee 1f102bf3b4 fix: revert bookings redesign (#25172)
* Revert "refactor: extract logic as bookingDetailsSheetStore (#25129)"

This reverts commit 7a0b8a9eba.

* Revert "feat: bookings page redesign v3 with calendar view (#24664)"

This reverts commit 89230474a5.
2025-11-14 17:24:31 +00:00
Eunjae Lee 7a0b8a9eba refactor: extract logic as bookingDetailsSheetStore (#25129)
* refactor: extract logic as bookingDetailsSheetStore

* clean up

* revert something

* provide store from context
2025-11-14 14:50:20 +00:00
Anik Dhabal Babu 8043505f3a fix: prevent modal clicks from closing when used in booking list (#25153) 2025-11-14 11:47:34 +00:00
Eunjae Lee 919fc3cdd5 chore: fix seed-insights.ts (#25018) 2025-11-14 10:50:30 +01:00
Eunjae Lee d7e6334ccb refactor: remove useInsightsParameters() (#25020)
* refactor: remove useInsightsParameters (no longer needed)

* remove log
2025-11-14 10:50:22 +01:00
Hariom BalharaandDevin AI 06e5c9803a fix: allow whitelisted paths like onboarding as team/user slugs on org domains (#24984)
* fix: allow whitelisted paths like onboarding as team/user slugs on org domains

- Add whitelistedPaths array to pagesAndRewritePaths.js with 'onboarding'
- Update getRegExpMatchingAllReservedRoutes to accept exclusions parameter
- Modify org route patterns to exclude whitelisted paths from reserved routes
- Add tests to verify onboarding can be used as a slug on org domains
- Fixes issue where acme.cal.com/onboarding would 404

This allows teams/users on org domains to use 'onboarding' as their slug
while still preserving the /onboarding app route on non-org domains.

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

* Remove accidental change by AI

* Add special character handling test

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-14 15:10:59 +09:00
1448 changed files with 87346 additions and 50757 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": minor
---
This PR introduces two variants for the calendar view atom, a generic calendar view and an event type specific view containing bookings data and busy times from connected calendars.
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": minor
---
Add defaultPhoneCountry prop to BookerPlatformWrapper with ISO 3166-1 alpha-2 type safety
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": patch
---
This PR fixes the issue of connect atoms not working inside iframes. It also updates the atoms exports to include the `useAvailableSlots` hook.
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": major
---
feat: use tailwind v4 (breaking change)
+14 -1
View File
@@ -214,6 +214,8 @@ STRIPE_TEAM_PRODUCT_ID=
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
STRIPE_ORG_MONTHLY_PRICE_ID=
STRIPE_ORG_PRODUCT_ID=
# Used to pass trial days for orgs during the Stripe checkout session
STRIPE_ORG_TRIAL_DAYS=
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET_APPS=
@@ -423,7 +425,8 @@ VAPID_PRIVATE_KEY=
# Mintlify chat api
# Power AI chat in for docs
NEXT_PUBLIC_MINTLIFY_CHAT_API_KEY=
MINTLIFY_CHAT_API_KEY=
NEXT_PUBLIC_ENABLE_MINTLIFY_CHAT=
NEXT_PUBLIC_CHAT_API_URL=
NEXT_PUBLIC_DOCS_URL=
@@ -489,3 +492,13 @@ SALESFORCE_GRAPHQL_MAX_RETRIES=3
# Force Node.js to use UTC as process timezone
TZ=UTC
# test oauth client for `packages/platform/examples/base` (optional)
SEED_PLATFORM_OAUTH_CLIENT_ID=
SEED_PLATFORM_OAUTH_CLIENT_SECRET=
# Trigger.dev
ENABLE_ASYNC_TASKER="false" # set to "true" to enable
TRIGGER_SECRET_KEY=
TRIGGER_API_URL=https://api.trigger.dev
TRIGGER_DEV_PROJECT_REF=
-1
View File
@@ -1,6 +1,5 @@
/**/package.json @calcom/Foundation
/apps/api/**/* @calcom/API
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
/apps/web/lib/csp.ts @calcom/Foundation
/apps/web/lib/buildNonce.ts @calcom/Foundation
+1 -1
View File
@@ -26,7 +26,7 @@ runs:
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
- run: USE_POOL=true yarn db-seed
- run: yarn db-seed
if: steps.cache-db.outputs.cache-hit != 'true'
shell: bash
- name: Postgres Dump Backup
+2
View File
@@ -1,2 +1,4 @@
GET /v2/event-types/{eventTypeId} added to the 'data' response property 'oneOf' list for the response status '200'
GET /v2/event-types/{eventTypeId} added '#/components/schemas/EventTypeOutput_2024_06_14, #/components/schemas/TeamEventTypeOutput_2024_06_14' to the 'data' response property 'oneOf' list for the response status '200'
GET /v2/bookings/{bookingUid}/recordings added the new required 'header' request parameter 'Authorization'
GET /v2/bookings/{bookingUid}/transcripts added the new required 'header' request parameter 'Authorization'
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
with:
repository: calcom/test-results
repository: calcom/test-results-2
ref: gh-pages
token: ${{ secrets.GH_ACCESS_TOKEN }}
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v2
with:
repository: calcom/test-results
repository: calcom/test-results-2
ref: gh-pages
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Set Git User
+37
View File
@@ -0,0 +1,37 @@
name: Companion Builds
on:
workflow_call:
permissions:
contents: read
env:
EXPO_PUBLIC_CAL_API_KEY: ${{ secrets.CI_EXPO_PUBLIC_CAL_API_KEY || vars.CI_EXPO_PUBLIC_CAL_API_KEY || vars.EXPO_PUBLIC_CAL_API_KEY }}
jobs:
build:
name: Build Companion App & Extension
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: companion/package-lock.json
- name: Install dependencies
working-directory: companion
run: npm ci
- name: Build Expo web app
working-directory: companion
env:
EXPO_NO_TELEMETRY: 1
CI: true
run: npx expo export --platform web
- name: Build browser extension
working-directory: companion
run: npm run ext:build
+1
View File
@@ -5,6 +5,7 @@ permissions:
contents: read
env:
NODE_OPTIONS: --max-old-space-size=4096
INTEGRATION_TESTS: "true"
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
+8
View File
@@ -146,6 +146,13 @@ jobs:
uses: ./.github/workflows/docs-build.yml
secrets: inherit
build-companion:
name: Companion builds
needs: [changes, check-label, deps]
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/companion-build.yml
secrets: inherit
build:
name: Production builds
needs: [changes, check-label, deps]
@@ -244,6 +251,7 @@ jobs:
build-api-v2,
build-atoms,
build-docs,
build-companion,
e2e,
e2e-api-v2,
e2e-embed,
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
with:
repository: calcom/test-results
repository: calcom/test-results-2
ref: gh-pages
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Set Git User
@@ -67,7 +67,7 @@ jobs:
- name: Output Report URL as Workflow Annotation
id: url
run: |
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results/$HTML_REPORT_URL_PATH
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results-2/$HTML_REPORT_URL_PATH
echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
- name: Find Comment
+3
View File
@@ -105,3 +105,6 @@ packages/**/.yarn/ci-cache/
# AI
.claude
# trigger.dev
.trigger
-1
View File
@@ -4,7 +4,6 @@
"yzhang.markdown-all-in-one", // nicer markdown support
"esbenp.prettier-vscode", // prettier plugin
"dbaeumer.vscode-eslint", // eslint plugin
"bradlc.vscode-tailwindcss", // hinting / autocompletion for tailwind
"ban.spellright", // Spell check for docs
"stripe.vscode-stripe", // stripe VSCode extension
"Prisma.prisma", // syntax|format|completion for prisma
+1 -1
View File
@@ -8,5 +8,5 @@
"typescript.preferences.importModuleSpecifier": "non-relative",
"spellright.language": ["en"],
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
"tailwindCSS.classFunctions": ["cva"]
}
View File
View File
View File
View File
View File
+2
View File
@@ -0,0 +1,2 @@
undecided:
- calcom-monorepo
View File
View File
View File
+1 -1
View File
@@ -10,7 +10,7 @@ ARG CALCOM_TELEMETRY_DISABLED
ARG DATABASE_URL
ARG NEXTAUTH_SECRET=secret
ARG CALENDSO_ENCRYPTION_KEY=secret
ARG MAX_OLD_SPACE_SIZE=4096
ARG MAX_OLD_SPACE_SIZE=6144
ARG NEXT_PUBLIC_API_V2_URL
ARG CSP_POLICY
-15
View File
@@ -1,15 +0,0 @@
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
+4 -4
View File
@@ -16,15 +16,15 @@ export const isAdminGuard = async (req: NextApiRequest) => {
where: {
userId: userId,
accepted: true,
role: {
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
},
team: {
isOrganization: true,
organizationSettings: {
is: {
isAdminAPIEnabled: true,
},
isAdminAPIEnabled: true,
},
},
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
},
select: {
team: {
+6
View File
@@ -113,6 +113,12 @@ export const schemaBookingReadPublic = Booking.extend({
)
.optional(),
responses: z.record(z.any()).nullable(),
// Override metadata to handle reassignment objects from Round Robin/Managed Events
// Safe to use z.any() here because:
// 1. API v1 POST only accepts z.record(z.string()) for metadata (user input restricted)
// 2. API v1 PATCH does not accept metadata changes at all
// 3. Complex metadata (objects) are only set by trusted internal features
metadata: z.record(z.any()).nullable(),
}).pick({
id: true,
userId: true,
+19
View File
@@ -0,0 +1,19 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
export function middleware(_request: NextRequest) {
// Matcher guarantees the last segment is one of the blocked segments, so we can
// immediately return a 403 without further path checks.
return NextResponse.json({ message: "Forbidden" }, { status: 403 });
}
export const config = {
// The blocked segment is always the last part of the path, so we only need this matcher.
matcher: [
"/:path*/_get",
"/:path*/_post",
"/:path*/_patch",
"/:path*/_delete",
"/:path*/_auth-middleware",
],
};
+1 -1
View File
@@ -35,7 +35,7 @@
"@sentry/nextjs": "^9.15.0",
"bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0",
"next": "^15.4.5",
"next": "^15.4.8",
"next-api-middleware": "^1.0.1",
"next-axiom": "^0.17.0",
"next-swagger-doc": "^0.3.6",
@@ -1,7 +1,7 @@
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, it, expect } from "vitest";
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import prisma from "@calcom/prisma";
@@ -11,45 +11,114 @@ type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
describe("PATCH /api/bookings", () => {
let member1Booking: Awaited<ReturnType<typeof prisma.booking.create>>;
let member0Booking: Awaited<ReturnType<typeof prisma.booking.create>>;
const createdBookingIds: number[] = [];
let testAdminUserId: number | null = null;
beforeAll(async () => {
const member1 = await prisma.user.findFirstOrThrow({
where: { email: "member1-acme@example.com" },
});
const member0 = await prisma.user.findFirstOrThrow({
where: { email: "member0-acme@example.com" },
});
// Create bookings for testing
member1Booking = await prisma.booking.create({
data: {
uid: `test-member1-booking-${Date.now()}`,
title: "Member 1 Test Booking",
startTime: new Date(Date.now() + 86400000), // Tomorrow
endTime: new Date(Date.now() + 90000000), // Tomorrow + 1 hour
userId: member1.id,
status: "ACCEPTED",
},
});
createdBookingIds.push(member1Booking.id);
member0Booking = await prisma.booking.create({
data: {
uid: `test-member0-booking-${Date.now()}`,
title: "Member 0 Test Booking",
startTime: new Date(Date.now() + 172800000), // Day after tomorrow
endTime: new Date(Date.now() + 176400000), // Day after tomorrow + 1 hour
userId: member0.id,
status: "ACCEPTED",
},
});
createdBookingIds.push(member0Booking.id);
});
afterAll(async () => {
if (createdBookingIds.length > 0) {
await prisma.booking.deleteMany({
where: { id: { in: createdBookingIds } },
});
}
// Clean up test admin user if created
if (testAdminUserId) {
await prisma.user.delete({
where: { id: testAdminUserId },
});
}
});
it("Returns 403 when user has no permission to the booking", async () => {
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
// Member2 tries to access Member0's booking - should fail
const member2 = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "PATCH",
body: {
title: booking.title,
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
userId: memberUser.id,
title: member0Booking.title,
startTime: member0Booking.startTime.toISOString(),
endTime: member0Booking.endTime.toISOString(),
userId: member2.id,
},
query: {
id: booking.id,
id: member0Booking.id,
},
});
req.userId = memberUser.id;
req.userId = member2.id;
await handler(req, res);
expect(res.statusCode).toBe(403);
});
it("Allows PATCH when user is system-wide admin", async () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "admin@example.com" } });
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
// Check if admin user already exists before upserting
const existingAdmin = await prisma.user.findUnique({ where: { email: "test-admin@example.com" } });
// Create a system-wide admin user for this test
const adminUser = await prisma.user.upsert({
where: { email: "test-admin@example.com" },
update: { role: "ADMIN" },
create: {
email: "test-admin@example.com",
username: "test-admin",
name: "Test Admin",
role: "ADMIN",
},
});
// Only track for cleanup if we created it (not if it already existed)
if (!existingAdmin) {
testAdminUserId = adminUser.id;
}
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "PATCH",
body: {
title: booking.title,
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
userId: proUser.id,
title: member0Booking.title,
startTime: member0Booking.startTime.toISOString(),
endTime: member0Booking.endTime.toISOString(),
userId: member0Booking.userId,
},
query: {
id: booking.id,
id: member0Booking.id,
},
});
@@ -62,19 +131,17 @@ describe("PATCH /api/bookings", () => {
it("Allows PATCH when user is org-wide admin", async () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member1-acme@example.com" } });
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: memberUser.id } });
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "PATCH",
body: {
title: booking.title,
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
userId: memberUser.id,
title: member1Booking.title,
startTime: member1Booking.startTime.toISOString(),
endTime: member1Booking.endTime.toISOString(),
userId: member1Booking.userId,
},
query: {
id: booking.id,
id: member1Booking.id,
},
});
@@ -1,7 +1,7 @@
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, it, beforeAll } from "vitest";
import { describe, expect, it, beforeAll, afterAll } from "vitest";
import { ZodError } from "zod";
import { prisma } from "@calcom/prisma";
@@ -19,29 +19,64 @@ const DefaultPagination = {
describe("GET /api/bookings", () => {
let proUser: Awaited<ReturnType<typeof prisma.user.findFirstOrThrow>>;
let proUserBooking: Awaited<ReturnType<typeof prisma.booking.findFirstOrThrow>>;
let memberUser: Awaited<ReturnType<typeof prisma.user.findFirstOrThrow>>;
let memberUserBooking: Awaited<ReturnType<typeof prisma.booking.create>>;
beforeAll(async () => {
proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
// Find an event type for memberUser or use a simple booking
const memberEventType = await prisma.eventType.findFirst({
where: {
OR: [
{ userId: memberUser.id },
{ team: { members: { some: { userId: memberUser.id } } } }
]
}
});
memberUserBooking = await prisma.booking.create({
data: {
uid: `test-member-booking-${Date.now()}`,
title: "Member Test Booking",
startTime: new Date(Date.now() + 86400000), // Tomorrow
endTime: new Date(Date.now() + 90000000), // Tomorrow + 1 hour
userId: memberUser.id,
eventTypeId: memberEventType?.id,
status: "ACCEPTED",
},
});
});
afterAll(async () => {
// Clean up the test booking created in beforeAll
if (memberUserBooking?.id) {
await prisma.booking.delete({
where: { id: memberUserBooking.id },
});
}
});
it("Does not return bookings of other users when user has no permission", async () => {
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
userId: proUser.id,
userId: proUser.id, // Try to access proUser's bookings
},
pagination: DefaultPagination,
});
req.userId = memberUser.id;
req.userId = memberUser.id; // But request is from memberUser
const responseData = await handler(req);
const groupedUsers = new Set(responseData.bookings.map((b) => b.userId));
// Should only return memberUser's own bookings, not proUser's
expect(responseData.bookings.find((b) => b.userId === memberUser.id)).toBeDefined();
expect(responseData.bookings.find((b) => b.id === memberUserBooking.id)).toBeDefined();
expect(groupedUsers.size).toBe(1);
const firstEntry = groupedUsers.entries().next().value;
expect(firstEntry?.[0]).toBe(memberUser.id);
@@ -1,7 +1,7 @@
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, it, expect } from "vitest";
import { describe, it, expect, beforeAll } from "vitest";
import prisma from "@calcom/prisma";
@@ -12,6 +12,53 @@ type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
describe("isAdmin guard", () => {
beforeAll(async () => {
const acmeOrg = await prisma.team.findFirst({
where: {
slug: "acme",
isOrganization: true,
},
});
if (acmeOrg) {
await prisma.organizationSettings.upsert({
where: {
organizationId: acmeOrg.id,
},
update: {
isAdminAPIEnabled: true,
},
create: {
organizationId: acmeOrg.id,
orgAutoAcceptEmail: "acme.com",
isAdminAPIEnabled: true,
},
});
}
const dunderOrg = await prisma.team.findFirst({
where: {
slug: "dunder-mifflin",
isOrganization: true,
},
});
if (dunderOrg) {
await prisma.organizationSettings.upsert({
where: {
organizationId: dunderOrg.id,
},
update: {
isAdminAPIEnabled: false,
},
create: {
organizationId: dunderOrg.id,
orgAutoAcceptEmail: "dunder-mifflin.com",
isAdminAPIEnabled: false,
},
});
}
});
it("Returns false when user does not exist in the system", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
@@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, beforeAll } from "vitest";
import prisma from "@calcom/prisma";
@@ -8,6 +8,30 @@ import {
} from "../../../lib/utils/retrieveScopedAccessibleUsers";
describe("retrieveScopedAccessibleUsers tests", () => {
beforeAll(async () => {
const acmeOrg = await prisma.team.findFirst({
where: {
slug: "acme",
isOrganization: true,
},
});
if (acmeOrg) {
await prisma.organizationSettings.upsert({
where: {
organizationId: acmeOrg.id,
},
update: {
isAdminAPIEnabled: true,
},
create: {
organizationId: acmeOrg.id,
orgAutoAcceptEmail: "acme.com",
isAdminAPIEnabled: true,
},
});
}
});
describe("getAccessibleUsers", () => {
it("Does not return members when only admin user ID is supplied", async () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
+8 -1
View File
@@ -41,4 +41,11 @@ LOGGER_BRIDGE_LOG_LEVEL="1"
# 1: Rewrite /api/v2 to /v2
# 0: Don't rewrite
REWRITE_API_V2_PREFIX="1"
REWRITE_API_V2_PREFIX="1"
# Trigger.dev
ENABLE_ASYNC_TASKER="false" # set to "true" to enable
TRIGGER_SECRET_KEY=
TRIGGER_API_URL=https://api.trigger.dev
TRIGGER_DEV_PROJECT_REF=
@@ -1,10 +1,12 @@
import { BookingGuestsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-guests.controller";
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service";
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 { CalVideoOutputService } from "@/ee/bookings/2024-08-13/services/cal-video.output.service";
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.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";
@@ -96,6 +98,8 @@ import { Module } from "@nestjs/common";
BookingReferencesService_2024_08_13,
BookingReferencesRepository_2024_08_13,
CalVideoService,
CalVideoOutputService,
BookingPbacGuard,
],
controllers: [BookingsController_2024_08_13, BookingGuestsController_2024_08_13],
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
@@ -1,3 +1,4 @@
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
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";
@@ -23,6 +24,7 @@ import {
GetOptionalUser,
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Pbac } from "@/modules/auth/decorators/pbac/pbac.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
@@ -66,6 +68,7 @@ import {
RescheduleSeatedBookingInput_2024_08_13,
GetBookingRecordingsOutput,
GetBookingTranscriptsOutput,
GetBookingVideoSessionsOutput,
} from "@calcom/platform-types";
import {
CreateBookingInputPipe,
@@ -189,7 +192,7 @@ export class BookingsController_2024_08_13 {
2. uid of one of the recurring booking recurrences
3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).
If you are fetching a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
@@ -209,12 +212,16 @@ export class BookingsController_2024_08_13 {
}
@Get("/:bookingUid/recordings")
// @Pbac(["booking.readRecordings"])
@Permissions([BOOKING_READ])
@UseGuards(BookingUidGuard)
// @UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Get all the recordings for the booking",
description: `Fetches all the recordings for the booking \`:bookingUid\`
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
description: `Fetches all the recordings for the booking \`:bookingUid\`. Requires authentication and proper authorization. Access is granted if you are the booking organizer, team admin or org admin/owner.
<Note>cal-api-version: \`2024-08-13\` is required in the request header.</Note>
`,
})
async getBookingRecordings(@Param("bookingUid") bookingUid: string): Promise<GetBookingRecordingsOutput> {
@@ -223,16 +230,26 @@ export class BookingsController_2024_08_13 {
return {
status: SUCCESS_STATUS,
data: recordings,
message:
"This endpoint will require authentication in a future release. Please update your integration to include valid credentials. See https://cal.com/docs/api-reference/v2/introduction#authentication for details.",
};
}
@Get("/:bookingUid/transcripts")
// @Pbac(["booking.readRecordings"])
@Permissions([BOOKING_READ])
@UseGuards(BookingUidGuard)
// @UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Get all the transcripts download links for the booking",
description: `Fetches all the transcripts download links for the booking \`:bookingUid\`
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
summary: "Get Cal Video real time transcript download links for the booking",
description: `Fetches all the transcript download links for the booking \`:bookingUid\`
<Note>
Transcripts are generated when clicking "Transcribe" during a Cal Video meeting. Download links are valid for 1 hour only - make a new request to generate fresh links after expiration.
Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.
</Note>
`,
})
async getBookingTranscripts(@Param("bookingUid") bookingUid: string): Promise<GetBookingTranscriptsOutput> {
@@ -241,6 +258,8 @@ export class BookingsController_2024_08_13 {
return {
status: SUCCESS_STATUS,
data: transcripts ?? [],
message:
"This endpoint will require authentication in a future release. Please update your integration to include valid credentials. See https://cal.com/docs/api-reference/v2/introduction#authentication for details.",
};
}
@@ -279,7 +298,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Reschedule a booking",
description: `Reschedule a booking or seated booking
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -321,7 +340,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Cancel a booking",
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
\nCancelling normal bookings:
If the booking is not seated and not recurring, simply pass :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and optionally cancellationReason in the request body \`{"cancellationReason": "Will travel"}\`.
@@ -329,11 +348,11 @@ export class BookingsController_2024_08_13 {
It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.
\n1. As an attendee - provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and seatUid in the request body \`{"seatUid": "123-123-123"}\` . This will remove this particular attendance from the booking.
\n2. As the host or org admin of host - host can cancel booking for all attendees aka for every seat, this also applies to org admins. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.
\nCancelling recurring seated bookings:
For recurring seated bookings it is not possible to cancel all of them with 1 call
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.
If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
@@ -374,7 +393,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Mark a booking absence",
description: `The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -399,7 +418,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Reassign a booking to auto-selected host",
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -423,7 +442,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Reassign a booking to a specific host",
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -454,7 +473,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Confirm a booking",
description: `The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -478,8 +497,8 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Decline a booking",
description: `The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
async declineBooking(
@@ -502,8 +521,8 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Get 'Add to Calendar' links for a booking",
description: `Retrieve calendar links for a booking that can be used to add the event to various calendar services. Returns links for Google Calendar, Microsoft Office, Microsoft Outlook, and a downloadable ICS file.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@HttpCode(HttpStatus.OK)
@@ -542,4 +561,25 @@ export class BookingsController_2024_08_13 {
data: bookingReferences,
};
}
@Get("/:bookingUid/conferencing-sessions")
@HttpCode(HttpStatus.OK)
@Pbac(["booking.readRecordings"])
@Permissions([BOOKING_READ])
@UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Get Video Meeting Sessions. Only supported for Cal Video",
description: `Requires authentication and proper authorization. Access is granted if you are the booking organizer, team admin or org admin/owner.
<Note>cal-api-version: \`2024-08-13\` is required in the request header.</Note>`,
})
async getVideoSessions(@Param("bookingUid") bookingUid: string): Promise<GetBookingVideoSessionsOutput> {
const sessions = await this.calVideoService.getVideoSessions(bookingUid);
return {
status: SUCCESS_STATUS,
data: sessions,
};
}
}
@@ -0,0 +1,245 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.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 { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type { Booking, User, PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Booking access authorization", () => {
let app: INestApplication;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let oAuthClient: PlatformOAuthClient;
const ownerEmail = `booking-access-auth-owner-${randomString()}@api.com`;
const unauthorizedEmail = `booking-access-auth-unauthorized-${randomString()}@api.com`;
let ownerUser: User;
let unauthorizedUser: User;
let ownerApiKey: string;
let unauthorizedApiKey: string;
let eventTypeId: number;
let testBooking: Booking;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await organizationsRepositoryFixture.create({
name: `booking-access-auth-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
ownerUser = await userRepositoryFixture.create({
email: ownerEmail,
locale: "en",
name: `booking-access-auth-owner-${randomString()}`,
});
unauthorizedUser = await userRepositoryFixture.create({
email: unauthorizedEmail,
locale: "en",
name: `booking-access-auth-unauthorized-${randomString()}`,
});
const { keyString: ownerKeyString } = await apiKeysRepositoryFixture.createApiKey(ownerUser.id, null);
ownerApiKey = `cal_test_${ownerKeyString}`;
const { keyString: unauthorizedKeyString } = await apiKeysRepositoryFixture.createApiKey(
unauthorizedUser.id,
null
);
unauthorizedApiKey = `cal_test_${unauthorizedKeyString}`;
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: `booking-access-auth-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(ownerUser.id, userSchedule);
const eventType = await eventTypesRepositoryFixture.create(
{
title: `booking-access-auth-event-type-${randomString()}`,
slug: `booking-access-auth-event-type-${randomString()}`,
length: 60,
},
ownerUser.id
);
eventTypeId = eventType.id;
testBooking = await bookingsRepositoryFixture.create({
uid: `booking-access-auth-booking-${randomString()}`,
title: "Test Booking for Access Auth",
description: "",
startTime: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)),
endTime: new Date(Date.UTC(2030, 0, 8, 11, 0, 0)),
eventType: {
connect: {
id: eventTypeId,
},
},
user: {
connect: {
id: ownerUser.id,
},
},
metadata: {},
responses: {
name: "Test Attendee",
email: "attendee@example.com",
},
references: {
create: [
{
type: "daily_video",
uid: `daily-room-${randomString()}`,
meetingId: `daily-room-${randomString()}`,
meetingPassword: "test-password",
meetingUrl: `https://daily.co/test-room-${randomString()}`,
},
],
},
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
describe("GET /v2/bookings/:bookingUid/conferencing-sessions - Authorization", () => {
it("should allow booking organizer to access conferencing sessions", async () => {
const calVideoService = app.get(CalVideoService);
jest.spyOn(calVideoService, "getVideoSessions").mockResolvedValue([]);
const response = await request(app.getHttpServer())
.get(`/v2/bookings/${testBooking.uid}/conferencing-sessions`)
.set("Authorization", `Bearer ${ownerApiKey}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200);
expect(response.body.status).toEqual(SUCCESS_STATUS);
});
it("should return 403 when unauthorized user tries to access conferencing sessions", async () => {
await request(app.getHttpServer())
.get(`/v2/bookings/${testBooking.uid}/conferencing-sessions`)
.set("Authorization", `Bearer ${unauthorizedApiKey}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(403);
});
});
// describe("GET /v2/bookings/:bookingUid/recordings - Authorization", () => {
// it("should allow booking organizer to access recordings", async () => {
// const calVideoService = app.get(CalVideoService);
// jest.spyOn(calVideoService, "getRecordings").mockResolvedValue([]);
// const response = await request(app.getHttpServer())
// .get(`/v2/bookings/${testBooking.uid}/recordings`)
// .set("Authorization", `Bearer ${ownerApiKey}`)
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
// .expect(200);
// expect(response.body.status).toEqual(SUCCESS_STATUS);
// });
// it("should return 403 when unauthorized user tries to access recordings", async () => {
// await request(app.getHttpServer())
// .get(`/v2/bookings/${testBooking.uid}/recordings`)
// .set("Authorization", `Bearer ${unauthorizedApiKey}`)
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
// .expect(403);
// });
// });
// describe("GET /v2/bookings/:bookingUid/transcripts - Authorization", () => {
// it("should allow booking organizer to access transcripts", async () => {
// const calVideoService = app.get(CalVideoService);
// jest.spyOn(calVideoService, "getTranscripts").mockResolvedValue([]);
// const response = await request(app.getHttpServer())
// .get(`/v2/bookings/${testBooking.uid}/transcripts`)
// .set("Authorization", `Bearer ${ownerApiKey}`)
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
// .expect(200);
// expect(response.body.status).toEqual(SUCCESS_STATUS);
// });
// it("should return 403 when unauthorized user tries to access transcripts", async () => {
// await request(app.getHttpServer())
// .get(`/v2/bookings/${testBooking.uid}/transcripts`)
// .set("Authorization", `Bearer ${unauthorizedApiKey}`)
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
// .expect(403);
// });
// });
afterAll(async () => {
await bookingsRepositoryFixture.deleteById(testBooking.id);
await eventTypesRepositoryFixture.delete(eventTypeId);
await userRepositoryFixture.deleteByEmail(ownerEmail);
await userRepositoryFixture.deleteByEmail(unauthorizedEmail);
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await app.close();
});
});
});
@@ -20,11 +20,7 @@ import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
CreateBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { CreateBookingInput_2024_08_13, GetBookingOutput_2024_08_13, GetSeatedBookingOutput_2024_08_13 } from "@calcom/platform-types";
import { BookingOutput_2024_08_13 } from "@calcom/platform-types";
import type { Booking, PlatformOAuthClient, Team, User, EventType } from "@calcom/prisma/client";
@@ -854,7 +850,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
describe("Booking Field Type Validation", () => {
const basePayload = {
start: "2025-06-19T11:00:00.000Z",
start: new Date(Date.UTC(2030, 5, 19, 11, 0, 0)).toISOString(),
attendee: {
name: "Charlie TypeTest",
email: "charlie.typetest@example.com",
@@ -1162,6 +1158,31 @@ describe("Bookings Endpoints 2024-08-13", () => {
`One or more invalid options for booking field '${fieldName}'. Allowed options are: blue, red.`
);
});
it("should transform null values to empty strings in bookingFieldsResponses", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFields.id,
bookingFieldsResponses: {
"favorite-movie": "The Matrix",
rescheduleReason: null,
notes: null,
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(201);
expect(response.body.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(response.body.data)) {
const data: BookingOutput_2024_08_13 = response.body.data;
expect(data.bookingFieldsResponses.notes).toBe("");
expect(data.bookingFieldsResponses.rescheduleReason).toBe("");
expect(data.bookingFieldsResponses["favorite-movie"]).toBe("The Matrix");
}
});
});
afterAll(async () => {
@@ -1176,4 +1197,4 @@ describe("Bookings Endpoints 2024-08-13", () => {
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
});
});
@@ -0,0 +1,230 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import type { EventType, User } from "@calcom/prisma/client";
describe("Reschedule bookings 2024-08-13", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let normalBookingUser: User;
let normalBookingEventType: EventType;
let normalBookingEventTypeId: number;
const normalBookingUserEmail = `user-bookings-normal-user-${randomString(5)}@api.com`;
const normalBookingEventTypeSlug = `user-bookings-normal-event-type-${randomString(5)}`;
let oldNormalBooking: BookingOutput_2024_08_13;
let newNormalBooking: BookingOutput_2024_08_13;
const RESCHEDULE_REASON = "Flying to venus that day";
const RESCHEDULED_BY = `user-venus-bookings-rescheduler-${randomString(5)}@api.com`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
normalBookingUserEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
)
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
// initialize normal booking user, schedule and event type
normalBookingUser = await userRepositoryFixture.create({
email: normalBookingUserEmail,
username: normalBookingUserEmail,
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: `user-bookings-normal-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(normalBookingUser.id, userSchedule);
normalBookingEventType = await eventTypesRepositoryFixture.create(
{
title: `user-bookings-normal-event-type-${randomString()}`,
slug: normalBookingEventTypeSlug,
length: 60,
},
normalBookingUser.id
);
normalBookingEventTypeId = normalBookingEventType.id;
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
describe("reschedule normal booking", () => {
it("should create a booking to be rescheduled", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 1, 9, 10, 0, 0)).toISOString(),
eventTypeId: normalBookingEventTypeId,
attendee: {
name: `user-venus-bookings-attendee-${randomString(10)}`,
email: `user-venus-bookings-attendee-${randomString(10)}@gmail.com`,
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/oj12u83128u9312ou3",
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
expect(bookingResponseBody.data).toBeDefined();
expect(responseDataIsBooking(bookingResponseBody.data)).toBe(true);
if (responseDataIsBooking(bookingResponseBody.data)) {
const created: BookingOutput_2024_08_13 = bookingResponseBody.data;
oldNormalBooking = created;
expect(oldNormalBooking).toBeDefined();
expect(oldNormalBooking.uid).toBeDefined();
} else {
throw new Error("should create a booking to be rescheduled - Invalid response data");
}
});
it("should reschedule", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 1, 9, 12, 0, 0)).toISOString(),
reschedulingReason: RESCHEDULE_REASON,
rescheduledBy: RESCHEDULED_BY,
};
const beforeCreate = new Date();
return request(app.getHttpServer())
.post(`/v2/bookings/${oldNormalBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const afterCreate = new Date();
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: BookingOutput_2024_08_13 = responseBody.data as BookingOutput_2024_08_13;
expect(data.reschedulingReason).toEqual(RESCHEDULE_REASON);
expect(data.rescheduledFromUid).toEqual(oldNormalBooking.uid);
expect(data.rescheduledByEmail).toEqual(RESCHEDULED_BY);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2040, 1, 9, 13, 0, 0)).toISOString());
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(normalBookingUser.id);
expect(data.hosts[0].username).toEqual(normalBookingUser.username);
expect(data.hosts[0].email).toEqual(normalBookingUser.email);
expect(data.status).toEqual(oldNormalBooking.status);
expect(data.duration).toEqual(oldNormalBooking.duration);
expect(data.eventTypeId).toEqual(oldNormalBooking.eventTypeId);
expect(data.attendees[0]).toEqual(oldNormalBooking.attendees[0]);
expect(data.location).toEqual(oldNormalBooking.location);
expect(data.absentHost).toEqual(oldNormalBooking.absentHost);
expect(data.metadata).toEqual(oldNormalBooking.metadata);
newNormalBooking = data;
expect(newNormalBooking).toBeDefined();
const createdAtDate = new Date(data.createdAt);
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
});
});
it("should fetch old booking and verify rescheduledToUid and rescheduledByEmail and status cancelled", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${oldNormalBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.rescheduledToUid).toEqual(newNormalBooking.uid);
expect(data.rescheduledByEmail).toEqual(RESCHEDULED_BY);
expect(data.status).toEqual("cancelled");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
it("should fetch new booking and verify rescheduledFromUid, rescheduledByEmail and reschedulingReason", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${newNormalBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.rescheduledFromUid).toEqual(oldNormalBooking.uid);
expect(data.rescheduledByEmail).toEqual(RESCHEDULED_BY);
expect(data.reschedulingReason).toEqual(RESCHEDULE_REASON);
expect(data.status).toEqual("accepted");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(normalBookingUser.email);
});
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data !== null && data && "id" in data;
}
});
@@ -4,6 +4,7 @@ import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
@@ -79,7 +80,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
const eventTypeRequiresConfirmationSlug = `user-bookings-event-type-requires-confirmation-${randomString()}`;
let createdBooking: BookingOutput_2024_08_13;
let rescheduledBooking: BookingOutput_2024_08_13;
let createdRecurringBooking: RecurringBookingOutput_2024_08_13[];
let bookingInThePast: Booking;
@@ -1360,139 +1360,217 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("reschedule bookings", () => {
it("should reschedule normal booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
const beforeCreate = new Date();
return request(app.getHttpServer())
.post(`/v2/bookings/${createdBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const afterCreate = new Date();
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 15, 0, 0)).toISOString());
expect(data.rescheduledFromUid).toEqual(createdBooking.uid);
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual(createdBooking.status);
expect(data.duration).toEqual(createdBooking.duration);
expect(data.eventTypeId).toEqual(createdBooking.eventTypeId);
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
expect(data.location).toEqual(createdBooking.location);
expect(data.absentHost).toEqual(createdBooking.absentHost);
expect(data.metadata).toEqual(createdBooking.metadata);
// When a booking is rescheduled, a new booking is created and the old booking is cancelled.
// We want to make sure the createdAt date of the new booking is between the beforeCreate and afterCreate dates.
const createdAtDate = new Date(data.createdAt);
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
rescheduledBooking = data;
describe("rescheduling bookings", () => {
describe("cant't reschedule cancelled or rescheduled booking booking", () => {
it("should not be able to reschedule cancelled booking", async () => {
const cancelledBooking = await bookingsRepositoryFixture.create({
status: "CANCELLED",
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `cancelled-booking-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${cancelledBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toEqual(
`Can't reschedule booking with uid=${cancelledBooking.uid} because it has been cancelled. Please provide uid of a booking that is not cancelled.`
);
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
});
it("should not be able to reschedule rescheduled booking", async () => {
const rescheduledBooking = await bookingsRepositoryFixture.create({
status: "CANCELLED",
rescheduled: true,
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `cancelled-booking-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const newBooking = await bookingsRepositoryFixture.create({
status: "ACCEPTED",
user: {
connect: {
id: user.id,
},
},
fromReschedule: rescheduledBooking.uid,
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `new-booking-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${rescheduledBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toEqual(
`Can't reschedule booking with uid=${rescheduledBooking.uid} because it has been cancelled and rescheduled already to booking with uid=${newBooking.uid}. You probably want to reschedule ${newBooking.uid} instead by passing it within the request URL.`
);
await bookingsRepositoryFixture.deleteById(rescheduledBooking.id);
await bookingsRepositoryFixture.deleteById(newBooking.id);
});
});
it("should set rescheduled booking status to cancelled", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${createdBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.status).toEqual("cancelled");
expect(data.rescheduledToUid).toEqual(rescheduledBooking.uid);
});
});
describe("recurring booking", () => {
it("should reschedule recurrence of a recurring booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars again",
};
it("should reschedule recurrence of a recurring booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars again",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${createdRecurringBooking[0].uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: RecurringBookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual(createdRecurringBooking[0].status);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 9, 15, 0, 0)).toISOString());
expect(data.duration).toEqual(createdRecurringBooking[0].duration);
expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid);
expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
expect(data.location).toEqual(createdRecurringBooking[0].location);
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
expect(data.metadata).toEqual(createdRecurringBooking[0].metadata);
return request(app.getHttpServer())
.post(`/v2/bookings/${createdRecurringBooking[0].uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: RecurringBookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual(createdRecurringBooking[0].status);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 9, 15, 0, 0)).toISOString());
expect(data.duration).toEqual(createdRecurringBooking[0].duration);
expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid);
expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
expect(data.location).toEqual(createdRecurringBooking[0].location);
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
expect(data.metadata).toEqual(createdRecurringBooking[0].metadata);
const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid);
expect(oldBooking).toBeDefined();
expect(oldBooking?.status).toEqual("CANCELLED");
});
});
const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid);
expect(oldBooking).toBeDefined();
expect(oldBooking?.status).toEqual("CANCELLED");
});
});
it("should get recurring booking recurrences after rescheduling one", async () => {
const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid;
return request(app.getHttpServer())
.get(`/v2/bookings/${recurringBookingUid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
it("should get recurring booking recurrences after rescheduling one", async () => {
const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid;
return request(app.getHttpServer())
.get(`/v2/bookings/${recurringBookingUid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(4);
const cancelled = data.find((booking) => booking.status === "cancelled");
expect(cancelled).toBeDefined();
const rescheduledNew = data.find(
(booking) => booking.start === new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString()
);
expect(rescheduledNew).toBeDefined();
createdRecurringBooking = data;
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
if (responseDataIsRecurringBooking(responseBody.data)) {
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
expect(data.length).toEqual(4);
const cancelled = data.find((booking) => booking.status === "cancelled");
expect(cancelled).toBeDefined();
const rescheduledNew = data.find(
(booking) => booking.start === new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString()
);
expect(rescheduledNew).toBeDefined();
createdRecurringBooking = data;
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
});
});
@@ -2385,95 +2463,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("rescheduling booking with invalid booking responses", () => {
it("should reschedule", async () => {
const email = `user-bookings-attendee-${randomString(10)}@gmail.com`;
const username = `user-bookings-attendee-${randomString(10)}`;
const locale = "it";
const timeZone = "Europe/Rome";
const attendee = await userRepositoryFixture.create({
email,
username,
locale,
timeZone,
});
const booking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${randomString(10)}`,
title: "booking title",
startTime: "2060-09-05T11:00:00.000Z",
endTime: "2060-09-05T12:00:00.000Z",
eventType: {
connect: {
id: eventTypeId,
},
},
status: "ACCEPTED",
metadata: {},
responses: "null",
user: {
connect: {
id: user.id,
},
},
attendees: {
create: {
email: email,
name: username,
timeZone,
locale,
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2060, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${booking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2060, 0, 9, 15, 0, 0)).toISOString());
expect(data.rescheduledFromUid).toEqual(booking.uid);
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual("accepted");
expect(data.eventTypeId).toEqual(booking.eventTypeId);
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: attendee.username,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: false,
});
expect(data.bookingFieldsResponses).toEqual({
name: attendee.username,
email: attendee.email,
rescheduledReason: body.reschedulingReason,
guests: [],
});
await bookingsRepositoryFixture.deleteById(data.id);
await userRepositoryFixture.delete(attendee.id);
});
});
});
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 = {
@@ -2559,146 +2548,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("cant't reschedule cancelled booking", () => {
it("should not be able to reschedule cancelled booking", async () => {
const cancelledBooking = await bookingsRepositoryFixture.create({
status: "CANCELLED",
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `cancelled-booking-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${cancelledBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toEqual(
`Can't reschedule booking with uid=${cancelledBooking.uid} because it has been cancelled. Please provide uid of a booking that is not cancelled.`
);
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
});
it("should not be able to reschedule rescheduled booking", async () => {
const rescheduledBooking = await bookingsRepositoryFixture.create({
status: "CANCELLED",
rescheduled: true,
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `cancelled-booking-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const newBooking = await bookingsRepositoryFixture.create({
status: "ACCEPTED",
user: {
connect: {
id: user.id,
},
},
fromReschedule: rescheduledBooking.uid,
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: `new-booking-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
responses: {
name: "Oldie",
email: "oldie@gmail.com",
},
attendees: {
create: {
email: "oldie@gmail.com",
name: "Oldie",
locale: "lv",
timeZone: "Europe/Rome",
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${rescheduledBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
expect(response.body.error.message).toEqual(
`Can't reschedule booking with uid=${rescheduledBooking.uid} because it has been cancelled and rescheduled already to booking with uid=${newBooking.uid}. You probably want to reschedule ${newBooking.uid} instead by passing it within the request URL.`
);
await bookingsRepositoryFixture.deleteById(rescheduledBooking.id);
await bookingsRepositoryFixture.deleteById(newBooking.id);
});
});
describe("cant't cancel already cancelled booking", () => {
it("should not be able to cancel alraedy cancelled booking", async () => {
const cancelledBooking = await bookingsRepositoryFixture.create({
@@ -2752,6 +2601,56 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("cancellation reason and cancelled by returned if null in database", () => {
it("should return cancellation reason and cancelled by if null in database", async () => {
const dbBooking = await bookingsRepositoryFixture.create({
uid: randomString(10),
title: "test",
startTime: new Date(Date.UTC(2040, 0, 13, 16, 0, 0)),
endTime: new Date(Date.UTC(2040, 0, 13, 17, 0, 0)),
eventType: {
connect: {
id: eventTypeId,
},
},
user: {
connect: {
id: user.id,
},
},
metadata: {},
responses: {
name: "tester",
email: "tester@example.com",
guests: [],
},
cancellationReason: null,
cancelledBy: null,
});
const response = await request(app.getHttpServer())
.get(`/v2/bookings/${dbBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200);
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.cancellationReason).toBe("");
expect(data.cancelledByEmail).toBe("");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
describe("calendar events", () => {
beforeEach(() => {
jest.restoreAllMocks();
@@ -3180,6 +3079,102 @@ describe("Bookings Endpoints 2024-08-13", () => {
);
}
describe("Meeting sessions", () => {
it("should get cal video sessions for a booking", async () => {
const mockSessions = [
{
id: "session-123",
room: "daily-room-123",
start_time: 1678901234,
duration: 3600,
ongoing: false,
max_participants: 10,
participants: [
{
user_id: "user-1",
participant_id: "participant-1",
user_name: "John Doe",
join_time: 1678901234,
duration: 3600,
},
],
},
];
const booking = await bookingsRepositoryFixture.create({
uid: `test-video-session-${randomString()}`,
title: "Test Video Session Booking",
description: "",
startTime: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)),
endTime: new Date(Date.UTC(2030, 0, 8, 11, 0, 0)),
eventType: {
connect: {
id: eventTypeId,
},
},
user: {
connect: {
id: user.id,
},
},
references: {
create: [
{
type: "daily_video",
uid: `daily-room-123-${randomString()}`,
meetingId: "daily-room-123",
meetingPassword: "test-password",
meetingUrl: "https://daily.co/daily-room-123",
},
],
},
});
const calVideoService = app.get(CalVideoService);
jest
.spyOn(calVideoService, "getVideoSessions")
.mockResolvedValue([
{
id: mockSessions[0].id,
room: mockSessions[0].room,
startTime: mockSessions[0].start_time,
duration: mockSessions[0].duration,
ongoing: mockSessions[0].ongoing,
maxParticipants: mockSessions[0].max_participants,
participants: mockSessions[0].participants.map((p) => ({
userId: p.user_id,
userName: p.user_name,
joinTime: p.join_time,
duration: p.duration,
})),
},
]);
const response = await request(app.getHttpServer())
.get(`/v2/bookings/${booking.uid}/conferencing-sessions`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200);
expect(response.body.status).toEqual(SUCCESS_STATUS);
expect(response.body.data).toBeDefined();
expect(Array.isArray(response.body.data)).toBe(true);
expect(response.body.data.length).toBe(1);
expect(response.body.data[0].id).toEqual(mockSessions[0].id);
expect(response.body.data[0].room).toEqual(mockSessions[0].room);
expect(response.body.data[0].startTime).toEqual(mockSessions[0].start_time);
expect(response.body.data[0].duration).toEqual(mockSessions[0].duration);
expect(response.body.data[0].ongoing).toEqual(mockSessions[0].ongoing);
expect(response.body.data[0].maxParticipants).toEqual(mockSessions[0].max_participants);
expect(response.body.data[0].participants.length).toBe(1);
expect(response.body.data[0].participants[0].userId).toEqual(mockSessions[0].participants[0].user_id);
expect(response.body.data[0].participants[0].userName).toEqual(
mockSessions[0].participants[0].user_name
);
await bookingsRepositoryFixture.deleteById(booking.id);
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -0,0 +1,52 @@
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
UnauthorizedException,
BadRequestException,
} from "@nestjs/common";
import { Request } from "express";
import { BookingAccessService } from "@calcom/platform-libraries";
@Injectable()
export class BookingPbacGuard implements CanActivate {
private bookingAccessService: BookingAccessService;
constructor(private readonly prismaReadService: PrismaReadService) {
this.bookingAccessService = new BookingAccessService(this.prismaReadService.prisma);
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context
.switchToHttp()
.getRequest<Request & { user?: ApiAuthGuardUser; pbacAuthorizedRequest?: boolean }>();
const user = request.user;
const bookingUid = request.params.bookingUid;
if (!user) {
throw new UnauthorizedException();
}
if (!bookingUid) {
throw new BadRequestException("BookingPbacGuard - bookingUid is required");
}
const hasAccess = await this.bookingAccessService.doesUserIdHaveAccessToBooking({
userId: user.id,
bookingUid,
});
if (!hasAccess) {
throw new ForbiddenException(
`BookingPbacGuard - user with id=${user.id} does not have access to booking with uid=${bookingUid}`
);
}
request.pbacAuthorizedRequest = true;
return true;
}
}
@@ -0,0 +1,23 @@
import { Injectable } from "@nestjs/common";
import type { CalMeetingSession } from "@calcom/platform-libraries/conferencing";
@Injectable()
export class CalVideoOutputService {
getOutputVideoSessions(sessions: CalMeetingSession[]) {
return sessions.map((session) => ({
id: session.id,
room: session.room,
startTime: session.start_time,
duration: session.duration,
ongoing: session.ongoing,
maxParticipants: session.max_participants,
participants: session.participants.map((participant) => ({
userId: participant.user_id,
userName: participant.user_name,
joinTime: participant.join_time,
duration: participant.duration,
})),
}));
}
}
@@ -1,11 +1,13 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { CalVideoOutputService } from "@/ee/bookings/2024-08-13/services/cal-video.output.service";
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { CAL_VIDEO_TYPE } from "@calcom/platform-constants";
import {
getRecordingsOfCalVideoByRoomName,
getAllTranscriptsAccessLinkFromRoomName,
getDownloadLinkOfCalVideoByRecordingId,
getCalVideoMeetingSessionsByRoomName,
} from "@calcom/platform-libraries/conferencing";
@Injectable()
@@ -13,18 +15,23 @@ export class CalVideoService {
private readonly logger = new Logger("CalVideoService");
constructor(
private readonly bookingsRepository: BookingsRepository_2024_08_13,
private readonly outputService: OutputBookingsService_2024_08_13
private readonly calVideoOutputService: CalVideoOutputService
) {}
private getVideoSessionsRoomName(references?: Array<{ type: string; meetingId?: string | null }>) {
return (
references?.filter((reference) => reference.type === CAL_VIDEO_TYPE)?.pop()?.meetingId ??
undefined
);
}
async getRecordings(bookingUid: string) {
const booking = await this.bookingsRepository.getByUidWithBookingReference(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const roomName =
booking?.references?.filter((reference) => reference.type === "daily_video")?.pop()?.meetingId ??
undefined;
const roomName = this.getVideoSessionsRoomName(booking.references);
if (!roomName) {
throw new NotFoundException(`No Cal Video reference found with booking uid ${bookingUid}`);
}
@@ -68,10 +75,7 @@ export class CalVideoService {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const roomName =
booking?.references?.filter((reference) => reference.type === "daily_video")?.pop()?.meetingId ??
undefined;
const roomName = this.getVideoSessionsRoomName(booking.references);
if (!roomName) {
throw new NotFoundException(`No Cal Video reference found with booking uid ${bookingUid}`);
}
@@ -80,4 +84,19 @@ export class CalVideoService {
return transcripts;
}
async getVideoSessions(bookingUid: string) {
const booking = await this.bookingsRepository.getByUidWithBookingReference(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const roomName = this.getVideoSessionsRoomName(booking.references);
if (!roomName) {
throw new NotFoundException(`No Cal Video reference found with booking uid ${bookingUid}`);
}
const sessions = await getCalVideoMeetingSessionsByRoomName(roomName);
return this.calVideoOutputService.getOutputVideoSessions(sessions.data);
}
}
@@ -544,12 +544,18 @@ export class InputBookingsService_2024_08_13 {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
newRequest.body = {
...bodyTransformed,
rescheduledBy: request.body.rescheduledBy,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
newRequest.body = {
...bodyTransformed,
noEmail: false,
creationSource: CreationSource.API_V2,
rescheduledBy: request.body.rescheduledBy,
};
}
return newRequest as unknown as BookingRequest;
@@ -25,7 +25,6 @@ import type {
ReassignBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import type { Booking, BookingSeat } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
export const bookingResponsesSchema = z
.object({
@@ -105,14 +104,8 @@ export class OutputBookingsService_2024_08_13 {
);
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const rescheduledToInfo = databaseBooking.rescheduled
? await this.getRescheduledToInfo(databaseBooking.uid)
: undefined;
const rescheduledToUid = rescheduledToInfo?.uid;
const rescheduledByEmail = databaseBooking.rescheduled
? rescheduledToInfo?.rescheduledBy
: databaseBooking.rescheduledBy;
const rescheduledToUid = await this.getRescheduledToUid(databaseBooking);
const rescheduledByEmail = await this.getRescheduledByEmail(databaseBooking);
const booking = {
id: databaseBooking.id,
@@ -121,9 +114,8 @@ export class OutputBookingsService_2024_08_13 {
description: databaseBooking.description,
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason:
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
cancellationReason: databaseBooking.cancellationReason || "",
cancelledByEmail: databaseBooking.cancelledBy || "",
reschedulingReason: bookingResponses?.rescheduledReason,
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
@@ -168,6 +160,23 @@ export class OutputBookingsService_2024_08_13 {
};
}
private async getRescheduledToUid(databaseBooking: DatabaseBooking) {
if (!databaseBooking.rescheduled) return undefined;
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(databaseBooking.uid);
return rescheduledTo?.uid;
}
private async getRescheduledByEmail(databaseBooking: DatabaseBooking) {
if (databaseBooking.rescheduled) {
return databaseBooking.rescheduledBy;
}
if (databaseBooking.fromReschedule) {
const previousBooking = await this.bookingsRepository.getByUid(databaseBooking.fromReschedule);
return previousBooking?.rescheduledBy ?? databaseBooking.rescheduledBy;
}
return databaseBooking.rescheduledBy;
}
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
if (databaseMetadata === null) return {};
@@ -227,10 +236,8 @@ export class OutputBookingsService_2024_08_13 {
description: databaseBooking.description,
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason:
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancellationReason : undefined,
cancelledByEmail:
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancelledBy : undefined,
cancellationReason: databaseBooking.cancellationReason || "",
cancelledByEmail: databaseBooking.cancelledBy || "",
reschedulingReason: bookingResponses?.rescheduledReason,
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
@@ -284,14 +291,8 @@ export class OutputBookingsService_2024_08_13 {
const duration = dateEnd.diff(dateStart, "minutes").minutes;
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
const location = metadata?.videoCallUrl || databaseBooking.location;
const rescheduledToInfo = databaseBooking.rescheduled
? await this.getRescheduledToInfo(databaseBooking.uid)
: undefined;
const rescheduledToUid = rescheduledToInfo?.uid;
const rescheduledByEmail = databaseBooking.rescheduled
? rescheduledToInfo?.rescheduledBy
: databaseBooking.rescheduledBy;
const rescheduledToUid = await this.getRescheduledToUid(databaseBooking);
const rescheduledByEmail = await this.getRescheduledByEmail(databaseBooking);
const booking = {
id: databaseBooking.id,
@@ -300,8 +301,7 @@ export class OutputBookingsService_2024_08_13 {
description: databaseBooking.description,
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason:
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancellationReason : undefined,
cancellationReason: databaseBooking.cancellationReason || "",
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
end: databaseBooking.endTime,
@@ -428,8 +428,7 @@ export class OutputBookingsService_2024_08_13 {
description: databaseBooking.description,
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason:
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancellationReason : undefined,
cancellationReason: databaseBooking.cancellationReason || "",
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
end: databaseBooking.endTime,
@@ -34,6 +34,7 @@ import { SchedulingType } from "@calcom/platform-libraries";
import {
BaseConfirmationPolicy_2024_06_14,
TeamEventTypeOutput_2024_06_14,
supportedIntegrations,
type ApiSuccessResponse,
type CreateEventTypeInput_2024_06_14,
type EventTypeOutput_2024_06_14,
@@ -640,9 +641,9 @@ describe("Event types Endpoints", () => {
hiddenEventType = responseBody.data;
});
it(`/GET/event-types by username`, async () => {
it(`/GET/event-types by username with sortCreatedAt=desc`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}`)
.get(`/api/v2/event-types?username=${user.username}&sortCreatedAt=desc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
@@ -653,6 +654,12 @@ describe("Event types Endpoints", () => {
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(2);
// Verify ordering: event types are returned newest to oldest when sortCreatedAt=desc
// hiddenEventType was created after eventType, so it should have a higher ID and appear first
expect(responseBody.data[0].id).toBeGreaterThan(responseBody.data[1].id);
expect(responseBody.data[0].id).toEqual(hiddenEventType.id);
expect(responseBody.data[1].id).toEqual(eventType.id);
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
const fetchedHiddenEventType = responseBody.data?.find((et) => et.id === hiddenEventType.id);
@@ -711,6 +718,46 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType?.hidden).toEqual(false);
});
it(`/GET/event-types by username with sortCreatedAt=asc`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}&sortCreatedAt=asc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(2);
// Verify ordering: event types are returned oldest to newest when sortCreatedAt=asc
// eventType was created before hiddenEventType, so it should have a lower ID and appear first
expect(responseBody.data[0].id).toBeLessThan(responseBody.data[1].id);
expect(responseBody.data[0].id).toEqual(eventType.id);
expect(responseBody.data[1].id).toEqual(hiddenEventType.id);
});
it(`/GET/event-types by username without sortCreatedAt parameter`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(2);
// Without sortCreatedAt, no specific order is guaranteed
// Just verify both event types are present
const ids = responseBody.data.map((et) => et.id);
expect(ids).toContain(eventType.id);
expect(ids).toContain(hiddenEventType.id);
});
it(`/GET/event-types by username should not return hidden event type if no auth provided`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}`)
@@ -2855,6 +2902,119 @@ describe("Event types Endpoints", () => {
});
});
it("should accept newly supported integration locations in input validation", async () => {
// Test that the API accepts various newly supported integrations by creating event types
// with these location types directly in the database and then retrieving them
// Note: Database uses underscores in integration types, API returns hyphens
const integrationTests = [
{ internal: "integrations:jitsi", api: "jitsi" },
{ internal: "integrations:whereby_video", api: "whereby-video" },
{ internal: "integrations:huddle01", api: "huddle" },
{ internal: "integrations:tandem", api: "tandem" },
{ internal: "integrations:element-call_video", api: "element-call-video" },
];
for (const { internal, api } of integrationTests) {
const eventTypeInput = {
title: `event type ${api}`,
description: "event type description",
length: 30,
hidden: false,
slug: `event-type-${api}`,
locations: [
{
type: internal,
link: `https://example.com/${api}/meeting`,
},
],
schedulingType: SchedulingType.ROUND_ROBIN,
bookingFields: [],
};
const legacyEventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${legacyEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
// Verify the integration location is returned correctly
expect(fetchedEventType.locations).toHaveLength(1);
expect(fetchedEventType.locations[0]).toMatchObject({
type: "integration",
integration: api,
});
// Clean up
await eventTypesRepositoryFixture.delete(legacyEventType.id);
}
});
it("should reject with 400 if creating event type with unsupported integration location", async () => {
const createPayload = {
title: "Event with unsupported integration",
slug: "event-with-unsupported-integration",
lengthInMinutes: 30,
locations: [
{
type: "integration",
integration: "unsupported-integration",
},
],
};
const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(createPayload);
expect(response.status).toBe(400);
expect(response.body.error.message).toBe(
`Validation failed for integration location: integration must be one of the following values: ${supportedIntegrations.join(
", "
)}`
);
});
it("should reject with 400 if patching event type with integration that user has not connected", async () => {
const createPayload = {
title: "Event for patch test",
slug: "event-patch-test-unconnected-integration",
lengthInMinutes: 30,
};
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(createPayload)
.expect(201);
const createdEventType = createResponse.body.data;
const patchPayload = {
locations: [
{
type: "integration",
integration: "jitsi",
},
],
};
const patchResponse = await request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(patchPayload);
expect(patchResponse.status).toBe(400);
expect(patchResponse.body.error.message).toBe("jitsi not connected.");
// Cleanup
await eventTypesRepositoryFixture.delete(createdEventType.id);
});
describe("EventType Hidden Property", () => {
let createdEventTypeId: number;
@@ -155,6 +155,8 @@ export class EventTypesController_2024_06_14 {
summary: "Get all event types",
description: `Hidden event types are returned only if authentication is provided and it belongs to the event type owner.
Use the optional \`sortCreatedAt\` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -3,6 +3,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import type { SortOrderType } from "@calcom/platform-types";
import type { Prisma } from "@calcom/prisma/client";
@Injectable()
@@ -67,21 +68,23 @@ export class EventTypesRepository_2024_06_14 {
});
}
async getUserEventTypes(userId: number) {
async getUserEventTypes(userId: number, sortCreatedAt?: SortOrderType) {
return this.dbRead.prisma.eventType.findMany({
where: {
userId,
},
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
include: { users: true, schedule: true, destinationCalendar: true },
});
}
async getUserEventTypesPublic(userId: number) {
async getUserEventTypesPublic(userId: number, sortCreatedAt?: SortOrderType) {
return this.dbRead.prisma.eventType.findMany({
where: {
userId,
hidden: false,
},
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
include: { users: true, schedule: true, destinationCalendar: true },
});
}
@@ -22,7 +22,7 @@ import {
getEventTypesPublic,
EventTypesPublic,
} from "@calcom/platform-libraries/event-types";
import type { GetEventTypesQuery_2024_06_14 } from "@calcom/platform-types";
import type { GetEventTypesQuery_2024_06_14, SortOrderType } from "@calcom/platform-types";
import type { EventType } from "@calcom/prisma/client";
@Injectable()
@@ -160,15 +160,16 @@ export class EventTypesService_2024_06_14 {
orgSlug?: string;
orgId?: number;
authUser?: AuthOptionalUser;
sortCreatedAt?: SortOrderType;
}) {
const user = await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId);
if (!user) {
return [];
}
if (params.authUser?.id !== user.id) {
return await this.getUserEventTypesPublic(user.id);
return await this.getUserEventTypesPublic(user.id, params.sortCreatedAt);
}
return await this.getUserEventTypes(user.id);
return await this.getUserEventTypes(user.id, params.sortCreatedAt);
}
async getUserToCreateEvent(user: UserWithProfile) {
@@ -211,16 +212,16 @@ export class EventTypesService_2024_06_14 {
};
}
async getUserEventTypes(userId: number) {
const eventTypes = await this.eventTypesRepository.getUserEventTypes(userId);
async getUserEventTypes(userId: number, sortCreatedAt?: SortOrderType) {
const eventTypes = await this.eventTypesRepository.getUserEventTypes(userId, sortCreatedAt);
return eventTypes.map((eventType) => {
return { ownerId: userId, ...eventType };
});
}
async getUserEventTypesPublic(userId: number) {
const eventTypes = await this.eventTypesRepository.getUserEventTypesPublic(userId);
async getUserEventTypesPublic(userId: number, sortCreatedAt?: SortOrderType) {
const eventTypes = await this.eventTypesRepository.getUserEventTypesPublic(userId, sortCreatedAt);
return eventTypes.map((eventType) => {
return { ownerId: userId, ...eventType };
@@ -237,7 +238,7 @@ export class EventTypesService_2024_06_14 {
}
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14, authUser?: AuthOptionalUser) {
const { username, eventSlug, usernames, orgSlug, orgId } = queryParams;
const { username, eventSlug, usernames, orgSlug, orgId, sortCreatedAt } = queryParams;
if (username && eventSlug) {
const eventType = await this.getEventTypeByUsernameAndSlug({
username,
@@ -255,6 +256,7 @@ export class EventTypesService_2024_06_14 {
orgSlug,
orgId,
authUser,
sortCreatedAt,
});
}
@@ -263,6 +265,10 @@ export class EventTypesService_2024_06_14 {
return [dynamicEventType];
}
if (authUser?.id) {
return await this.getUserEventTypes(authUser.id, sortCreatedAt);
}
return [];
}
@@ -39,6 +39,7 @@ import {
InputBookingField_2024_06_14,
OutputUnknownLocation_2024_06_14,
UpdateEventTypeInput_2024_06_14,
supportedIntegrations,
} from "@calcom/platform-types";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
@@ -252,7 +253,7 @@ export class InputEventTypesService_2024_06_14 {
requiresConfirmationWillBlockSlot:
confirmationPolicyTransformed?.requiresConfirmationWillBlockSlot ?? undefined,
eventTypeColor: this.transformInputEventTypeColor(color),
...this.transformInputSeatOptions(seats),
...(seats ? this.transformInputSeatOptions(seats) : { seatsPerTimeSlot: undefined }),
eventName: customName,
useEventTypeDestinationCalendarEmail: useDestinationCalendarEmail,
...maxActiveBookingsPerBooker,
@@ -542,14 +543,40 @@ export class InputEventTypesService_2024_06_14 {
}
async checkAppIsValidAndConnected(user: UserWithProfile, appSlug: string) {
const conferencingApps = ["google-meet", "office365-video", "zoom"];
const conferencingApps = supportedIntegrations as readonly string[];
if (!conferencingApps.includes(appSlug)) {
throw new BadRequestException("Invalid app, available apps are: ", conferencingApps.join(", "));
}
if (appSlug === "office365-video") {
appSlug = "msteams";
}
// Map API integration names to actual app slugs
const slugMap: Record<string, string> = {
"office365-video": "msteams",
"facetime-video": "facetime",
"whereby-video": "whereby",
"whatsapp-video": "whatsapp",
"webex-video": "webex",
"telegram-video": "telegram",
"sylaps-video": "sylapsvideo",
"skype-video": "skype",
"sirius-video": "sirius_video",
"signal-video": "signal",
"shimmer-video": "shimmervideo",
"salesroom-video": "salesroom",
"roam-video": "roam",
"riverside-video": "riverside",
"ping-video": "ping",
"mirotalk-video": "mirotalk",
"jelly-video": "jelly",
"jelly-conferencing": "jelly",
"huddle": "huddle01",
"element-call-video": "element-call",
"eightxeight-video": "eightxeight",
"discord-video": "discord",
"demodesk-video": "demodesk",
"campfire-video": "campfire",
};
appSlug = slugMap[appSlug] || appSlug;
const credentials = await getUsersCredentialsIncludeServiceAccountKey(user);
@@ -318,7 +318,7 @@ const CustomFieldsSchema = z.object({
id: z.literal("user"),
type: z.literal("user"),
label: z.literal("User"),
fieldRequired: z.literal(true),
fieldRequired: z.boolean().optional(),
})
),
editable: z.enum(["user", "user-readonly"]),
@@ -95,6 +95,8 @@ describe("Me Endpoints", () => {
expect(responseBody.data.id).toEqual(user.id);
expect(responseBody.data.email).toEqual(user.email);
expect(responseBody.data.name).toEqual(user.name);
expect(responseBody.data.avatarUrl).toEqual(user.avatarUrl);
expect(responseBody.data.bio).toEqual(user.bio);
expect(responseBody.data.timeFormat).toEqual(user.timeFormat);
expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId);
expect(responseBody.data.weekStart).toEqual(user.weekStart);
@@ -117,6 +119,8 @@ describe("Me Endpoints", () => {
expect(responseBody.data.id).toEqual(user.id);
expect(responseBody.data.email).toEqual(user.email);
expect(responseBody.data.avatarUrl).toEqual(user.avatarUrl);
expect(responseBody.data.bio).toEqual(user.bio);
expect(responseBody.data.timeFormat).toEqual(user.timeFormat);
expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId);
expect(responseBody.data.weekStart).toEqual(user.weekStart);
@@ -151,13 +155,13 @@ describe("Me Endpoints", () => {
});
it("should not update user associated with access token given invalid time format", async () => {
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 as any };
const bodyWithIncorrectTimeFormat = { timeFormat: 100 };
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
});
it("should not update user associated with access token given invalid week start", async () => {
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" as any };
const bodyWithIncorrectWeekStart = { weekStart: "waba luba dub dub" };
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
});
@@ -27,6 +27,14 @@ export class MeOutput {
@DocsProperty({ type: String, nullable: true })
name!: string | null;
@IsString()
@DocsProperty({ type: String, nullable: true })
avatarUrl!: string | null;
@IsString()
@DocsProperty({ type: String, nullable: true })
bio!: string | null;
@IsInt()
@DocsProperty()
timeFormat!: number;
+43 -21
View File
@@ -1,11 +1,12 @@
import { Injectable, Logger as NestLogger, Scope } from "@nestjs/common";
import type { Logger as TsLogger } from "tslog";
// 1. Define an interface for the settings
interface IMyLoggerSettings {
minLevel: number; // Example: 0=debug, 1=info, 2=warn, 3=error
displayTimestamp: boolean;
logFormat: "pretty" | "json" | "simple";
// Add any other settings you need, mimicking tslog or your own requirements
// Add unknown other settings you need, mimicking tslog or your own requirements
// e.g., name?: string; displayFunctionName?: boolean; etc.
}
@@ -23,7 +24,10 @@ const LogLevel = {
* (e.g., sending to Axiom) and adds an optional prefix for context.
*/
@Injectable({ scope: Scope.TRANSIENT }) // TRANSIENT ensures getSubLogger provides truly independent instances if needed elsewhere
export class Logger {
export class Logger
implements
Pick<TsLogger<unknown>, "log" | "silly" | "trace" | "debug" | "info" | "warn" | "error" | "getSubLogger">
{
// Use NestLogger for the actual logging output
private readonly nestLogger = new NestLogger("LoggerBridge");
// Prefix to add to messages for this instance
@@ -57,50 +61,68 @@ export class Logger {
* @param options.prefix - An array of strings to join as the log prefix.
* @returns A new LoggerBridge instance with the specified prefix.
*/
getSubLogger(options: { prefix?: string[] }): Logger {
getSubLogger(options: { prefix?: string[] }): TsLogger<unknown> {
const newLogger = new Logger();
// Set the prefix for the *new* instance
newLogger.prefix = options?.prefix?.join(" ") ?? "";
return newLogger;
return newLogger as unknown as TsLogger<unknown>;
}
// --- Public logging methods ---
info(...args: any[]) {
this.settings.minLevel <= 1 && this.logInternal("log", ...args);
log(...args: unknown[]): undefined {
if (this.settings.minLevel <= LogLevel.INFO) this.logInternal("log", ...args);
}
warn(...args: any[]) {
this.settings.minLevel <= 2 && this.logInternal("warn", ...args);
info(...args: unknown[]): undefined {
if (this.settings.minLevel <= 1) {
this.logInternal("log", ...args);
}
}
error(...args: any[]) {
this.settings.minLevel <= 3 && this.logInternal("error", ...args);
warn(...args: unknown[]): undefined {
if (this.settings.minLevel <= 2) {
this.logInternal("warn", ...args);
}
}
debug(...args: any[]) {
this.settings.minLevel === 0 && this.logInternal("debug", ...args);
error(...args: unknown[]): undefined {
if (this.settings.minLevel <= 3) {
this.logInternal("error", ...args);
}
}
trace(...args: any[]) {
this.settings.minLevel === 0 && this.logInternal("verbose", ...args);
debug(...args: unknown[]): undefined {
if (this.settings.minLevel === 0) {
this.logInternal("debug", ...args);
}
}
fatal(...args: any[]) {
trace(...args: unknown[]): undefined {
if (this.settings.minLevel === 0) {
this.logInternal("verbose", ...args);
}
}
fatal(...args: unknown[]): undefined {
// Prepend FATAL: to the message and log as error
const fatalMessage = `fatal: ${this.formatArgsAsString(args)}`;
this.settings.minLevel <= 3 && this.logInternal("error", fatalMessage);
if (this.settings.minLevel <= 3) {
this.logInternal("error", fatalMessage);
}
}
silly(...args: any[]) {
silly(...args: unknown[]): undefined {
const sillyMessage = `silly: ${this.formatArgsAsString(args)}`;
this.settings.minLevel === 0 && this.logInternal("verbose", sillyMessage);
if (this.settings.minLevel === 0) {
this.logInternal("verbose", sillyMessage);
}
}
// --- Internal logging implementation ---
/** Formats arguments into a single string */
private formatArgsAsString(args: any[]): string {
private formatArgsAsString(args: unknown[]): string {
return args
.map((arg) => {
if (typeof arg === "string") {
@@ -109,7 +131,7 @@ export class Logger {
// Attempt to stringify non-string arguments
try {
return JSON.stringify(arg);
} catch (e) {
} catch {
return "[Unserializable Object]";
}
})
@@ -117,7 +139,7 @@ export class Logger {
}
/** Central method to forward logs to NestLogger */
private logInternal(level: "log" | "warn" | "error" | "debug" | "verbose", ...args: any[]) {
private logInternal(level: "log" | "warn" | "error" | "debug" | "verbose", ...args: unknown[]) {
try {
// Format message from potentially multiple arguments
const formattedMessage = this.formatArgsAsString(args);
@@ -10,7 +10,6 @@ import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository"
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
import { BusyTimesService } from "@/lib/services/busy-times.service";
import { CacheService } from "@/lib/services/cache.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
@@ -35,7 +34,6 @@ import { Module } from "@nestjs/common";
PrismaFeaturesRepository,
PrismaMembershipRepository,
CheckBookingLimitsService,
CacheService,
AvailableSlotsService,
UserAvailabilityService,
BusyTimesService,
@@ -1,17 +1,23 @@
import { Logger } from "@/lib/logger.bridge";
import { PrismaAttributeRepository } from "@/lib/repositories/prisma-attribute.repository";
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository";
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { CacheService } from "@/lib/services/cache.service";
import { BookingEmailSmsService } from "@/lib/services/booking-emails-sms-service";
import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service";
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { HashedLinkService } from "@/lib/services/hashed-link.service";
import { LuckyUserService } from "@/lib/services/lucky-user.service";
import { RegularBookingService } from "@/lib/services/regular-booking.service";
import { BookingEmailAndSmsSyncTaskerService } from "@/lib/services/tasker/booking-emails-sms-sync-tasker.service";
import { BookingEmailAndSmsTaskService } from "@/lib/services/tasker/booking-emails-sms-task.service";
import { BookingEmailAndSmsTasker } from "@/lib/services/tasker/booking-emails-sms-tasker.service";
import { BookingEmailAndSmsTriggerTaskerService } from "@/lib/services/tasker/booking-emails-sms-trigger-tasker.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module } from "@nestjs/common";
import { Module, Scope } from "@nestjs/common";
@Module({
imports: [PrismaModule],
@@ -22,13 +28,25 @@ import { Module } from "@nestjs/common";
PrismaHostRepository,
PrismaOOORepository,
PrismaUserRepository,
CacheService,
{
provide: Logger,
useFactory: () => {
return new Logger();
},
scope: Scope.TRANSIENT,
},
BookingEventHandlerService,
CheckBookingAndDurationLimitsService,
CheckBookingLimitsService,
HashedLinkService,
LuckyUserService,
BookingEmailSmsService,
BookingEmailAndSmsTaskService,
BookingEmailAndSmsSyncTaskerService,
BookingEmailAndSmsTriggerTaskerService,
BookingEmailAndSmsTasker,
RegularBookingService,
],
exports: [RegularBookingService],
})
export class RegularBookingModule { }
export class RegularBookingModule {}
@@ -8,7 +8,6 @@ import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { BusyTimesService } from "@/lib/services/busy-times.service";
import { CacheService } from "@/lib/services/cache.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
@@ -34,7 +33,6 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
featuresRepository: PrismaFeaturesRepository,
qualifiedHostsService: QualifiedHostsService,
checkBookingLimitsService: CheckBookingLimitsService,
cacheService: CacheService,
userAvailabilityService: UserAvailabilityService,
busyTimesService: BusyTimesService,
noSlotsNotificationService: NoSlotsNotificationService
@@ -50,7 +48,6 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
userRepo: userRepository,
redisClient: redisService,
checkBookingLimitsService,
cacheService,
userAvailabilityService,
busyTimesService,
qualifiedHostsService,
@@ -1,7 +1,7 @@
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { BookingCancelService as BaseBookingCancelService } from "@calcom/features/bookings/lib/handleCancelBooking";
import { BookingCancelService as BaseBookingCancelService } from "@calcom/platform-libraries/bookings";
@Injectable()
export class BookingCancelService extends BaseBookingCancelService {
@@ -0,0 +1,13 @@
import { Logger } from "@/lib/logger.bridge";
import { Injectable } from "@nestjs/common";
import { BookingEmailSmsHandler } from "@calcom/platform-libraries/bookings";
@Injectable()
export class BookingEmailSmsService extends BookingEmailSmsHandler {
constructor(logger: Logger) {
super({
logger,
});
}
}
@@ -0,0 +1,18 @@
import { Injectable } from "@nestjs/common";
import { BookingEventHandlerService as BaseBookingEventHandlerService } from "@calcom/platform-libraries/bookings";
import { Logger } from "@/lib/logger.bridge";
import { HashedLinkService } from "./hashed-link.service";
@Injectable()
export class BookingEventHandlerService extends BaseBookingEventHandlerService {
constructor(hashedLinkService: HashedLinkService, bridgeLogger: Logger) {
super({
log: bridgeLogger,
hashedLinkService,
});
}
}
@@ -1,11 +0,0 @@
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { Injectable } from "@nestjs/common";
import { CacheService as BaseCacheService } from "@calcom/platform-libraries";
@Injectable()
export class CacheService extends BaseCacheService {
constructor(featuresRepository: PrismaFeaturesRepository) {
super({ featuresRepository });
}
}
@@ -1,9 +1,11 @@
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { CacheService } from "@/lib/services/cache.service";
import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service";
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
import { HashedLinkService } from "@/lib/services/hashed-link.service";
import { LuckyUserService } from "@/lib/services/lucky-user.service";
import { BookingEmailAndSmsTasker } from "@/lib/services/tasker/booking-emails-sms-tasker.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
@@ -13,22 +15,26 @@ import type { PrismaClient } from "@calcom/prisma";
@Injectable()
export class RegularBookingService extends BaseRegularBookingService {
constructor(
cacheService: CacheService,
checkBookingAndDurationLimitsService: CheckBookingAndDurationLimitsService,
prismaWriteService: PrismaWriteService,
bookingRepository: PrismaBookingRepository,
hashedLinkService: HashedLinkService,
luckyUserService: LuckyUserService,
userRepository: PrismaUserRepository
userRepository: PrismaUserRepository,
bookingEmailAndSmsTasker: BookingEmailAndSmsTasker,
featuresRepository: PrismaFeaturesRepository,
bookingEventHandler: BookingEventHandlerService
) {
super({
cacheService,
checkBookingAndDurationLimitsService,
prismaClient: prismaWriteService.prisma as unknown as PrismaClient,
bookingRepository,
hashedLinkService,
luckyUserService,
userRepository,
bookingEmailAndSmsTasker,
featuresRepository,
bookingEventHandler,
});
}
}
@@ -0,0 +1,15 @@
import { Logger } from "@/lib/logger.bridge";
import { BookingEmailAndSmsTaskService } from "@/lib/services/tasker/booking-emails-sms-task.service";
import { Injectable } from "@nestjs/common";
import { BookingEmailAndSmsSyncTasker as BaseBookingEmailAndSmsSyncTaskerService } from "@calcom/platform-libraries/bookings";
@Injectable()
export class BookingEmailAndSmsSyncTaskerService extends BaseBookingEmailAndSmsSyncTaskerService {
constructor(bookingTaskService: BookingEmailAndSmsTaskService, logger: Logger) {
super({
logger,
bookingTaskService,
});
}
}
@@ -0,0 +1,21 @@
import { Logger } from "@/lib/logger.bridge";
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { BookingEmailSmsService } from "@/lib/services/booking-emails-sms-service";
import { Injectable } from "@nestjs/common";
import { BookingEmailAndSmsTaskService as BaseBookingEmailAndSmsTaskService } from "@calcom/platform-libraries/bookings";
@Injectable()
export class BookingEmailAndSmsTaskService extends BaseBookingEmailAndSmsTaskService {
constructor(
bookingEmailSmsService: BookingEmailSmsService,
bookingRepository: PrismaBookingRepository,
logger: Logger
) {
super({
logger,
emailsAndSmsHandler: bookingEmailSmsService,
bookingRepository,
});
}
}
@@ -0,0 +1,21 @@
import { Logger } from "@/lib/logger.bridge";
import { BookingEmailAndSmsSyncTaskerService } from "@/lib/services/tasker/booking-emails-sms-sync-tasker.service";
import { BookingEmailAndSmsTriggerTaskerService } from "@/lib/services/tasker/booking-emails-sms-trigger-tasker.service";
import { Injectable } from "@nestjs/common";
import { BookingEmailAndSmsTasker as BaseBookingEmailAndSmsTasker } from "@calcom/platform-libraries/bookings";
@Injectable()
export class BookingEmailAndSmsTasker extends BaseBookingEmailAndSmsTasker {
constructor(
syncTasker: BookingEmailAndSmsSyncTaskerService,
asyncTasker: BookingEmailAndSmsTriggerTaskerService,
logger: Logger
) {
super({
logger,
asyncTasker: asyncTasker,
syncTasker: syncTasker,
});
}
}
@@ -0,0 +1,13 @@
import { Logger } from "@/lib/logger.bridge";
import { Injectable } from "@nestjs/common";
import { BookingEmailAndSmsTriggerDevTasker as BaseBookingEmailAndSmsTriggerDevTasker } from "@calcom/platform-libraries/bookings";
@Injectable()
export class BookingEmailAndSmsTriggerTaskerService extends BaseBookingEmailAndSmsTriggerDevTasker {
constructor(logger: Logger) {
super({
logger,
});
}
}
@@ -2,6 +2,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/reposito
import { BillingProcessor } from "@/modules/billing/billing.processor";
import { BillingRepository } from "@/modules/billing/billing.repository";
import { BillingController } from "@/modules/billing/controllers/billing.controller";
import { IsUserInBillingOrg } from "@/modules/billing/guards/is-user-in-billing-org";
import { BillingServiceCachingProxy } from "@/modules/billing/services/billing-service-caching-proxy";
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
import { BillingService } from "@/modules/billing/services/billing.service";
@@ -44,6 +45,7 @@ import { Module } from "@nestjs/common";
ManagedOrganizationsBillingService,
OAuthClientRepository,
BookingsRepository_2024_08_13,
IsUserInBillingOrg,
],
exports: [BillingService, BillingRepository, ManagedOrganizationsBillingService],
controllers: [BillingController],
@@ -17,7 +17,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withNextAuth } from "test/utils/withNextAuth";
import { withApiAuth } from "test/utils/withApiAuth";
import type { Team, PlatformBilling } from "@calcom/prisma/client";
@@ -32,8 +32,10 @@ describe("Platform Billing Controller (e2e)", () => {
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
let organization: Team;
let organization2: Team;
beforeAll(async () => {
const moduleRef = await withNextAuth(
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
@@ -49,6 +51,10 @@ describe("Platform Billing Controller (e2e)", () => {
name: `billing-organization-${randomString()}`,
isPlatform: true,
});
organization2 = await organizationsRepositoryFixture.create({
name: `billing-organization-2-${randomString()}`,
isPlatform: true,
});
user = await userRepositoryFixture.create({
email: userEmail,
@@ -133,7 +139,7 @@ describe("Platform Billing Controller (e2e)", () => {
});
});
it("/billing/webhook (GET) should get billing plan for org", () => {
it("/billing/:orgId/check (GET) should check billing plan for org", () => {
return request(app.getHttpServer())
.get(`/v2/billing/${organization.id}/check`)
.expect(200)
@@ -143,6 +149,10 @@ describe("Platform Billing Controller (e2e)", () => {
});
});
it("/billing/:organizationId/check (GET) should not be able to check other org plan", () => {
return request(app.getHttpServer()).get(`/v2/billing/${organization2.id}/check`).expect(403);
});
it("/billing/webhook (POST) failed payment should set billing free plan to overdue", () => {
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
() =>
@@ -1,11 +1,13 @@
import { AppConfig } from "@/config/type";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { ApiAuthGuardOnlyAllow } from "@/modules/auth/decorators/api-auth-guard-only-allow.decorator";
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
import { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subscribe-to-plan.input";
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
import { SubscribeTeamToBillingResponseDto } from "@/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto";
import { IsUserInBillingOrg } from "@/modules/billing/guards/is-user-in-billing-org";
import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface";
import { StripeService } from "@/modules/stripe/stripe.service";
import {
@@ -49,8 +51,9 @@ export class BillingController {
}
@Get("/:teamId/check")
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg)
@MembershipRoles(["OWNER", "ADMIN", "MEMBER"])
@ApiAuthGuardOnlyAllow(["NEXT_AUTH"])
async checkTeamBilling(
@Param("teamId", ParseIntPipe) teamId: number
): Promise<ApiResponse<CheckPlatformBillingResponseDto>> {
@@ -66,8 +69,9 @@ export class BillingController {
}
@Post("/:teamId/subscribe")
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg)
@MembershipRoles(["OWNER", "ADMIN"])
@ApiAuthGuardOnlyAllow(["NEXT_AUTH"])
async subscribeTeamToStripe(
@Param("teamId") teamId: number,
@Body() input: SubscribeToPlanInput
@@ -84,8 +88,9 @@ export class BillingController {
}
@Post("/:teamId/upgrade")
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg)
@MembershipRoles(["OWNER", "ADMIN"])
@ApiAuthGuardOnlyAllow(["NEXT_AUTH"])
async upgradeTeamBillingInStripe(
@Param("teamId") teamId: number,
@Body() input: SubscribeToPlanInput
@@ -100,13 +105,12 @@ export class BillingController {
};
}
@Delete("/:organizationId/unsubscribe")
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
@Delete("/:teamId/unsubscribe")
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg)
@MembershipRoles(["OWNER", "ADMIN"])
async cancelTeamSubscriptionInStripe(
@Param("organizationId") organizationId: number
): Promise<ApiResponse> {
await this.billingService.cancelTeamSubscription(organizationId);
@ApiAuthGuardOnlyAllow(["NEXT_AUTH"])
async cancelTeamSubscriptionInStripe(@Param("teamId") teamId: number): Promise<ApiResponse> {
await this.billingService.cancelTeamSubscription(teamId);
return {
status: "success",
@@ -0,0 +1,35 @@
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";
import type { Team } from "@calcom/prisma/client";
@Injectable()
export class IsUserInBillingOrg implements CanActivate {
constructor(private organizationsRepository: OrganizationsRepository) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request & { team: Team }>();
const orgId: string = request.params.teamId;
const userId = (request.user as ApiAuthGuardUser).id;
if (!userId) {
throw new ForbiddenException("IsUserInBillingOrg - No user id found.");
}
if (!orgId) {
throw new ForbiddenException("IsUserInBillingOrg - No org id found in request params.");
}
const user = await this.organizationsRepository.findOrgUser(Number(orgId), Number(userId));
if (!user) {
throw new ForbiddenException(
`IsUserInBillingOrg - user with id=${userId} is not part of the organization with id=${orgId}.`
);
}
return true;
}
}
@@ -47,8 +47,8 @@ import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
import { handleCreatePhoneCall } from "@calcom/platform-libraries";
import {
CreateTeamEventTypeInput_2024_06_14,
GetOrganizationEventTypesQuery_2024_06_14,
GetTeamEventTypesQuery_2024_06_14,
SkipTakePagination,
TeamEventTypeOutput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
@@ -159,12 +159,16 @@ export class OrganizationsEventTypesController {
@UseGuards(IsOrgGuard, IsTeamInOrg, IsAdminAPIEnabledGuard)
@Get("/teams/:teamId/event-types")
@ApiOperation({ summary: "Get team event types" })
@ApiOperation({
summary: "Get team event types",
description:
'Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.',
})
async getTeamEventTypes(
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
): Promise<GetTeamEventTypesOutput> {
const { eventSlug, hostsLimit } = queryParams;
const { eventSlug, hostsLimit, sortCreatedAt } = queryParams;
if (eventSlug) {
const eventType = await this.organizationsEventTypesService.getTeamEventTypeBySlug(
@@ -179,7 +183,7 @@ export class OrganizationsEventTypesController {
};
}
const eventTypes = await this.organizationsEventTypesService.getTeamEventTypes(teamId);
const eventTypes = await this.organizationsEventTypesService.getTeamEventTypes(teamId, sortCreatedAt);
return {
status: SUCCESS_STATUS,
@@ -191,16 +195,21 @@ export class OrganizationsEventTypesController {
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@Get("/teams/event-types")
@ApiOperation({ summary: "Get all team event types" })
@ApiOperation({
summary: "Get all team event types",
description:
'Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.',
})
async getTeamsEventTypes(
@Param("orgId", ParseIntPipe) orgId: number,
@Query() queryParams: SkipTakePagination
@Query() queryParams: GetOrganizationEventTypesQuery_2024_06_14
): Promise<GetTeamEventTypesOutput> {
const { skip, take } = queryParams;
const { skip, take, sortCreatedAt } = queryParams;
const eventTypes = await this.organizationsEventTypesService.getOrganizationsTeamsEventTypes(
orgId,
skip,
take
take,
sortCreatedAt
);
return {
@@ -1,16 +1,24 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
import type { SortOrderType } from "@calcom/platform-types";
@Injectable()
export class OrganizationsEventTypesRepository {
constructor(private readonly dbRead: PrismaReadService) {}
async getOrganizationTeamsEventTypes(orgId: number, skip: number, take: number) {
async getOrganizationTeamsEventTypes(
orgId: number,
skip: number,
take: number,
sortCreatedAt?: SortOrderType
) {
return this.dbRead.prisma.eventType.findMany({
where: {
team: {
parentId: orgId,
},
},
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
skip,
take,
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
@@ -1230,6 +1230,70 @@ describe("Organizations Event Types Endpoints", () => {
});
});
it("should preserve seatsPerTimeSlot when doing partial update", async () => {
const createBody: CreateTeamEventTypeInput_2024_06_14 = {
title: "Coding consultation with seats",
slug: `organizations-event-types-seats-${randomString()}`,
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
schedulingType: "COLLECTIVE",
hosts: [
{
userId: teammate1.id,
},
],
seats: {
seatsPerTimeSlot: 5,
showAttendeeInfo: true,
showAvailabilityCount: true,
},
};
const createResponse = await request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`)
.send(createBody)
.expect(201);
const createdEventType: TeamEventTypeOutput_2024_06_14 = createResponse.body.data;
const createdSeats = createdEventType.seats;
expect(createdSeats).toBeDefined();
expect(createdSeats && "seatsPerTimeSlot" in createdSeats).toBe(true);
if (createdSeats && "seatsPerTimeSlot" in createdSeats) {
expect(createdSeats.seatsPerTimeSlot).toEqual(5);
expect(createdSeats.showAttendeeInfo).toEqual(true);
expect(createdSeats.showAvailabilityCount).toEqual(true);
}
// Now do a partial update that only changes a different field (not seats)
const updateBody: UpdateTeamEventTypeInput_2024_06_14 = {
bookingRequiresAuthentication: false,
};
const updateResponse = await request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${createdEventType.id}`)
.send(updateBody)
.expect(200);
const updatedEventType: TeamEventTypeOutput_2024_06_14 = updateResponse.body.data;
// Verify that seatsPerTimeSlot is preserved and not reset to null
const updatedSeats = updatedEventType.seats;
expect(updatedSeats).toBeDefined();
expect(updatedSeats && "seatsPerTimeSlot" in updatedSeats).toBe(true);
if (updatedSeats && "seatsPerTimeSlot" in updatedSeats) {
expect(updatedSeats.seatsPerTimeSlot).toEqual(5);
expect(updatedSeats.showAttendeeInfo).toEqual(true);
expect(updatedSeats.showAvailabilityCount).toEqual(true);
}
expect(updatedEventType.bookingRequiresAuthentication).toEqual(false);
});
function evaluateHost(expected: Host, received: Host | undefined) {
expect(expected.userId).toEqual(received?.userId);
expect(expected.mandatory).toEqual(received?.mandatory);
@@ -1,4 +1,6 @@
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import type { SortOrderType } from "@calcom/platform-types";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
import {
TransformedCreateTeamEventTypeInput,
@@ -83,16 +85,22 @@ export class OrganizationsEventTypesService {
return this.teamsEventTypesService.getTeamEventTypeBySlug(teamId, eventTypeSlug, hostsLimit);
}
async getTeamEventTypes(teamId: number): Promise<DatabaseTeamEventType[]> {
return await this.teamsEventTypesService.getTeamEventTypes(teamId);
async getTeamEventTypes(teamId: number, sortCreatedAt?: SortOrderType): Promise<DatabaseTeamEventType[]> {
return await this.teamsEventTypesService.getTeamEventTypes(teamId, sortCreatedAt);
}
async getOrganizationsTeamsEventTypes(
orgId: number,
skip = 0,
take = 250
take = 250,
sortCreatedAt?: SortOrderType
): Promise<DatabaseTeamEventType[]> {
return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(orgId, skip, take);
return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(
orgId,
skip,
take,
sortCreatedAt
);
}
async updateOrganizationTeamEventType(
@@ -188,7 +188,8 @@ export class SlotsController_2024_09_04 {
@DocsResponse({
status: 200,
description: `A map of available slots indexed by date, where each date is associated with an array of time slots. If format=range is specified, each slot will be an object with start and end properties denoting start and end of the slot.
For seated slots each object will have attendeesCount and bookingUid properties.`,
For seated slots each object will have attendeesCount and bookingUid properties.
If no slots are available, the data field will be an empty object {}.`,
schema: {
oneOf: [
{
@@ -136,12 +136,16 @@ export class TeamsEventTypesController {
}
@Get("/")
@ApiOperation({ summary: "Get a team event type" })
@ApiOperation({
summary: "Get team event types",
description:
'Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.',
})
async getTeamEventTypes(
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
): Promise<GetTeamEventTypesOutput> {
const { eventSlug, hostsLimit } = queryParams;
const { eventSlug, hostsLimit, sortCreatedAt } = queryParams;
if (eventSlug) {
const eventType = await this.teamsEventTypesService.getTeamEventTypeBySlug(
@@ -156,7 +160,7 @@ export class TeamsEventTypesController {
};
}
const eventTypes = await this.teamsEventTypesService.getTeamEventTypes(teamId);
const eventTypes = await this.teamsEventTypesService.getTeamEventTypes(teamId, sortCreatedAt);
return {
status: SUCCESS_STATUS,
@@ -11,6 +11,8 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException, Logger } from "@nestjs/common";
import type { SortOrderType } from "@calcom/platform-types";
import { createEventType, updateEventType } from "@calcom/platform-libraries/event-types";
@Injectable()
@@ -101,8 +103,8 @@ export class TeamsEventTypesService {
return eventType;
}
async getTeamEventTypes(teamId: number): Promise<DatabaseTeamEventType[]> {
return await this.teamsEventTypesRepository.getTeamEventTypes(teamId);
async getTeamEventTypes(teamId: number, sortCreatedAt?: SortOrderType): Promise<DatabaseTeamEventType[]> {
return await this.teamsEventTypesRepository.getTeamEventTypes(teamId, sortCreatedAt);
}
async updateTeamEventType(
@@ -2,6 +2,8 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import type { SortOrderType } from "@calcom/platform-types";
@Injectable()
export class TeamsEventTypesRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
@@ -79,11 +81,12 @@ export class TeamsEventTypesRepository {
});
}
async getTeamEventTypes(teamId: number) {
async getTeamEventTypes(teamId: number, sortCreatedAt?: SortOrderType) {
return this.dbRead.prisma.eventType.findMany({
where: {
teamId,
},
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
include: {
users: true,
schedule: true,
-44
View File
@@ -1,44 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# Source maps
*.source/*
-36
View File
@@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
-9
View File
@@ -1,9 +0,0 @@
import { HomeLayout } from "fumadocs-ui/layouts/home";
import type { ReactNode } from "react";
import { baseOptions } from "../layout.config";
export default function Layout({ children }: { children: ReactNode }) {
// @ts-expect-error weird type check
return <HomeLayout {...baseOptions}>{children}</HomeLayout>;
}
-12
View File
@@ -1,12 +0,0 @@
export default function Page() {
return (
<div className="font-cal -mt-16 flex min-h-screen w-screen items-center justify-center border pt-16 text-center ">
<div>
<h1 className="text-7xl leading-none md:text-8xl">
Open UI for absolutely
<br /> everyone
</h1>
</div>
</div>
);
}
@@ -1,16 +0,0 @@
import { componentSource } from "@/app/source";
import { createSearchAPI } from "fumadocs-core/search/server";
const indexes = [componentSource].flatMap((src) =>
src.getPages().map((page) => ({
title: page.data.title,
description: page.data.description,
structuredData: page.data.structuredData,
id: page.url,
url: page.url,
}))
);
export const { GET } = createSearchAPI("advanced", {
indexes,
});
@@ -1,21 +0,0 @@
import type { PropsWithChildren } from "react";
type Props = {
name: string;
};
export const Icon: React.FC<PropsWithChildren<Props>> = (props) => {
return (
<div className="text-gray-12 flex flex-col items-center justify-center gap-4">
<div className="size-12 border-gray-5 bg-gray-3 flex aspect-square items-center justify-center rounded-lg border ">
{props.children}
</div>
<span className="font-mono text-sm">
{"<"}
{props.name}
{"/>"}
</span>
</div>
);
};
Icon.displayName = "Icon";
@@ -1,57 +0,0 @@
"use client";
import { type PropsWithChildren, useState } from "react";
import className from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
type Props = {
customCodeSnippet?: string;
className?: string;
};
export const RenderComponentWithSnippet: React.FC<PropsWithChildren<Props>> = (props) => {
const [open, setOpen] = useState(false);
// const snippet =
// props.customCodeSnippet ??
// reactElementToJSXString(props.children, {
// showFunctions: true,
// useBooleanShorthandSyntax: true,
// displayName: (node) => {
// // @ts-ignore
// return node?.type.displayName ?? node?.type ?? "Unknown";
// },
// });
return (
<div className="border-gray-6 bg-default rounded-lg border">
<div className={className("not-prose p-8 xl:p-12", props.className)}>{props.children}</div>
<div className="bg-gray-3 border-gray-6 flex items-center justify-start border-b border-t p-2">
<Button
color="minimal"
onClick={() => {
setOpen(!open);
}}>
Code (WIP)
</Button>
</div>
<div
className={className(
"bg-gray-2 max-h-96 w-full overflow-y-scroll transition-all",
open ? "block" : "hidden"
)}>
<div className="flex items-start">
{/* <pre className="py-0 text-gray-8 text-right">
{snippet
.split("\n")
.map((_line, i) => i + 1)
.join("\n")}
</pre>
<pre className="py-0 text-gray-12 w-full">{snippet}</pre> */}
</div>
</div>
</div>
);
};
@@ -1,7 +0,0 @@
import type { PropsWithChildren } from "react";
export const Row: React.FC<PropsWithChildren> = (props) => {
return <div className="flex w-full items-center justify-between gap-8">{props.children}</div>;
};
Row.displayName = "Row";
@@ -1,56 +0,0 @@
import { componentSource } from "@/app/source";
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { createTypeTable } from "fumadocs-typescript/ui";
import defaultMdxComponents from "fumadocs-ui/mdx";
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/page";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { Toaster } from "react-hot-toast";
export default async function Page(props: { params: Promise<{ slug?: string[] }> }) {
const params = await props.params;
const page = componentSource.getPage(params.slug);
if (!page) {
notFound();
}
const { AutoTypeTable } = createTypeTable();
const MDX = page.data.body;
return (
<>
{/* @ts-expect-error idk why fumadocs doersnt like this */}
<DocsPage toc={page.data.toc} full={page.data.full}>
<Toaster />
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<TooltipProvider>
{/* @ts-expect-error idk why fumadocs doersnt like this */}
<MDX components={{ ...defaultMdxComponents, AutoTypeTable }} />
</TooltipProvider>
</DocsBody>
</DocsPage>
</>
);
}
export async function generateStaticParams() {
return componentSource.generateParams();
}
export async function generateMetadata({ params }: { params: Promise<{ slug?: string[] }> }) {
const page = componentSource.getPage((await params).slug);
if (!page) {
notFound();
}
return {
title: page.data.title,
description: page.data.description,
} satisfies Metadata;
}
-17
View File
@@ -1,17 +0,0 @@
import { componentSource } from "@/app/source";
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { DocsLayout } from "fumadocs-ui/layouts/notebook";
import type { ReactNode } from "react";
import { IconSprites } from "@calcom/ui/components/icon";
import { baseOptions } from "../layout.config";
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout tree={componentSource.pageTree} {...baseOptions}>
<TooltipProvider>{children}</TooltipProvider>
<IconSprites />
</DocsLayout>
);
}
File diff suppressed because it is too large Load Diff
-26
View File
@@ -1,26 +0,0 @@
import type { HomeLayoutProps } from "fumadocs-ui/layouts/home";
/**
* Shared layout configurations
*
* you can configure layouts individually from:
* Home Layout: app/(home)/layout.tsx
* Docs Layout: app/docs/layout.tsx
*/
export const baseOptions: HomeLayoutProps = {
nav: {
title: "Cal.com",
},
githubUrl: "https://github.com/calcom/cal.com",
links: [
{
text: "Design",
url: "/design",
active: "nested-url",
},
{
text: "GitHub",
url: "https://github.com/calcom/cal.com",
},
],
};
-36
View File
@@ -1,36 +0,0 @@
import { RootProvider } from "fumadocs-ui/provider";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./global.css";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<style>{`
:root {
--font-inter: ${inter.style.fontFamily.replace(/\'/g, "")};
}
`}</style>
</head>
<body className={`${inter.variable} antialiased`}>
<RootProvider>{children}</RootProvider>
</body>
</html>
);
}
-13
View File
@@ -1,13 +0,0 @@
import { components, docs, meta } from "@/.source";
import { loader } from "fumadocs-core/source";
import { createMDXSource } from "fumadocs-mdx";
export const source = loader({
baseUrl: "/docs",
source: createMDXSource(docs, meta),
});
export const componentSource = loader({
baseUrl: "/design",
source: createMDXSource(components, []),
});
@@ -1,19 +0,0 @@
import { Toaster } from "react-hot-toast";
import { Status } from "./Status";
export function MDXLayout({ children, frontmatter }: { children: React.ReactNode; frontmatter: any }) {
return (
<div className="prose max-w-none">
<Toaster />
{frontmatter.airtableId && (
<Status
airtableId={frontmatter.airtableId}
designStatus={frontmatter.designStatus}
devStatus={frontmatter.devStatus}
/>
)}
{children}
</div>
);
}
@@ -1,65 +0,0 @@
"use client";
import React from "react";
import { Toaster } from "react-hot-toast";
import { showToast } from "@calcom/ui/components/toast";
interface BorderRadiusToken {
name: string;
value: string;
className: string;
}
interface BorderRadiusTableProps {
tokens: BorderRadiusToken[];
}
const tokens: BorderRadiusToken[] = [
{ name: "None", value: "0px", className: "rounded-none" },
{ name: "Small", value: "0.125rem", className: "rounded-sm" },
{ name: "Default", value: "0.25rem", className: "rounded" },
{ name: "Medium", value: "0.375rem", className: "rounded-md" },
{ name: "Large", value: "0.5rem", className: "rounded-lg" },
{ name: "XLarge", value: "0.75rem", className: "rounded-xl" },
{ name: "2XLarge", value: "1rem", className: "rounded-2xl" },
{ name: "3XLarge", value: "1.5rem", className: "rounded-3xl" },
{ name: "Full", value: "9999px", className: "rounded-full" },
];
export const BorderRadiusTable: React.FC<BorderRadiusTableProps> = () => {
const handleCopy = async (value: string) => {
try {
await navigator.clipboard.writeText(value);
showToast(`Copied ${value}`, "success");
} catch (error) {
console.error("Failed to copy:", error);
showToast("Failed to copy", "error");
}
};
return (
<>
<Toaster position="bottom-right" />
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{tokens.map((token) => (
<div
key={token.name}
onClick={() => handleCopy(token.className)}
className="border-subtle bg-default hover:bg-subtle group relative cursor-pointer overflow-hidden rounded-lg border p-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<div>
<p className="text-emphasis text-sm font-medium">{token.name}</p>
<p className="text-subtle mt-1 text-xs">{token.className}</p>
</div>
<p className="text-subtle text-sm">{token.value}</p>
</div>
<div className={`bg-emphasis h-16 w-full ${token.className}`} />
</div>
</div>
))}
</div>
</>
);
};
@@ -1,372 +0,0 @@
"use client";
import React, { useState, useMemo } from "react";
import { Toaster } from "react-hot-toast";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
type IconName =
| "activity"
| "arrow-down"
| "arrow-left"
| "arrow-right"
| "arrow-up-right"
| "arrow-up"
| "asterisk"
| "at-sign"
| "atom"
| "badge-check"
| "ban"
| "bell"
| "binary"
| "blocks"
| "bold"
| "book"
| "book-open-check"
| "book-open"
| "book-user"
| "building"
| "calendar-check-2"
| "calendar-days"
| "calendar-heart"
| "calendar-range"
| "calendar-search"
| "calendar-x-2"
| "calendar"
| "chart-bar"
| "chart-line"
| "check-check"
| "check"
| "chevron-down"
| "chevron-left"
| "chevron-right"
| "chevron-up"
| "chevrons-down-up"
| "chevrons-left"
| "chevrons-right"
| "chevrons-up-down"
| "circle-alert"
| "circle-arrow-up"
| "circle-check-big"
| "circle-check"
| "circle-help"
| "circle-x"
| "circle"
| "clipboard-check"
| "clipboard"
| "clock"
| "code"
| "columns-3"
| "command"
| "contact"
| "copy"
| "corner-down-left"
| "corner-down-right"
| "credit-card"
| "disc"
| "dot"
| "download"
| "ellipsis-vertical"
| "ellipsis"
| "external-link"
| "eye-off"
| "eye"
| "file-down"
| "file-text"
| "file"
| "filter"
| "fingerprint"
| "flag"
| "folder"
| "gift"
| "git-merge"
| "github"
| "globe"
| "grid-3x3"
| "handshake"
| "info"
| "italic"
| "key"
| "layers"
| "layout-dashboard"
| "link-2"
| "link"
| "loader"
| "lock-open"
| "lock"
| "log-out"
| "mail-open"
| "mail"
| "map-pin"
| "map"
| "menu"
| "message-circle"
| "messages-square"
| "moon"
| "paintbrush"
| "paperclip"
| "pencil"
| "phone-call"
| "phone"
| "plus"
| "refresh-ccw"
| "refresh-cw"
| "repeat"
| "rocket"
| "rotate-ccw"
| "rotate-cw"
| "search"
| "send"
| "settings"
| "share-2"
| "shield-check"
| "shield"
| "shuffle"
| "sliders-horizontal"
| "sliders-vertical"
| "smartphone"
| "sparkles"
| "square-check"
| "star"
| "sun"
| "sunrise"
| "sunset"
| "tags"
| "terminal"
| "trash-2"
| "trash"
| "trello"
| "triangle-alert"
| "upload"
| "user-check"
| "user-plus"
| "user-x"
| "user"
| "users"
| "venetian-mask"
| "video"
| "waypoints"
| "webhook"
| "x"
| "zap";
interface IconGridProps {
className?: string;
}
interface CopyMenuProps {
name: IconName;
onCopy: (format: string) => void;
}
const CopyMenu: React.FC<CopyMenuProps> = ({ name, onCopy }) => {
return (
<div className="absolute inset-2 flex items-center justify-center bg-black/10 opacity-0 transition-opacity group-hover:opacity-100">
<div className="bg-default space-y-1 rounded-md p-2 shadow-lg">
<button
onClick={() => onCopy(`<Icon name="${name}" className="h-4 w-4" />`)}
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
Copy Component
</button>
</div>
</div>
);
};
export const IconGrid: React.FC<IconGridProps> = ({ className }) => {
const [searchQuery, setSearchQuery] = useState("");
const handleCopy = async (value: string) => {
try {
await navigator.clipboard.writeText(value);
showToast(`Copied ${value}`, "success");
} catch (error) {
console.error("Failed to copy:", error);
showToast("Failed to copy", "error");
}
};
const icons: IconName[] = [
"activity",
"arrow-down",
"arrow-left",
"arrow-right",
"arrow-up-right",
"arrow-up",
"asterisk",
"at-sign",
"atom",
"badge-check",
"ban",
"bell",
"binary",
"blocks",
"bold",
"book-open-check",
"book-open",
"book-user",
"building",
"calendar-check-2",
"calendar-days",
"calendar-heart",
"calendar-range",
"calendar-search",
"calendar-x-2",
"calendar",
"chart-bar",
"chart-line",
"check-check",
"check",
"chevron-down",
"chevron-left",
"chevron-right",
"chevron-up",
"chevrons-down-up",
"chevrons-left",
"chevrons-right",
"chevrons-up-down",
"circle-alert",
"circle-arrow-up",
"circle-check-big",
"circle-check",
"circle-help",
"circle-x",
"circle",
"clipboard-check",
"clipboard",
"clock",
"code",
"columns-3",
"command",
"contact",
"copy",
"corner-down-left",
"corner-down-right",
"credit-card",
"disc",
"dot",
"download",
"ellipsis-vertical",
"ellipsis",
"external-link",
"eye-off",
"eye",
"file-down",
"file-text",
"file",
"filter",
"fingerprint",
"flag",
"folder",
"gift",
"git-merge",
"github",
"globe",
"grid-3x3",
"handshake",
"info",
"italic",
"key",
"layers",
"layout-dashboard",
"link-2",
"link",
"loader",
"lock-open",
"lock",
"log-out",
"mail-open",
"mail",
"map-pin",
"map",
"menu",
"message-circle",
"messages-square",
"moon",
"paintbrush",
"paperclip",
"pencil",
"phone-call",
"phone",
"plus",
"refresh-ccw",
"refresh-cw",
"repeat",
"rocket",
"rotate-ccw",
"rotate-cw",
"search",
"send",
"settings",
"share-2",
"shield-check",
"shield",
"shuffle",
"sliders-horizontal",
"sliders-vertical",
"smartphone",
"sparkles",
"square-check",
"star",
"sun",
"sunrise",
"sunset",
"tags",
"terminal",
"trash-2",
"trash",
"trello",
"triangle-alert",
"upload",
"user-check",
"user-plus",
"user-x",
"user",
"users",
"venetian-mask",
"video",
"waypoints",
"webhook",
"x",
"zap",
];
const filteredIcons = useMemo(() => {
if (!searchQuery) return icons;
const query = searchQuery.toLowerCase();
return icons.filter((icon) => icon.toLowerCase().includes(query));
}, [searchQuery]);
return (
<>
<Toaster position="bottom-right" />
<div className={className}>
<div className="mb-8">
<input
type="text"
placeholder="Search icons..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="border-subtle bg-default text-emphasis focus:ring-emphasis placeholder:text-subtle w-full rounded-md border px-4 py-2 focus:outline-none focus:ring-2"
/>
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
{filteredIcons.map((iconName) => (
<div
key={iconName}
className="border-subtle bg-default group relative overflow-hidden rounded-lg border p-4">
<div className="flex flex-col items-center space-y-2">
<div className="bg-subtle flex h-12 w-12 items-center justify-center rounded-md">
<Icon name={iconName} className="text-emphasis h-6 w-6" />
</div>
<p className="text-emphasis text-center text-sm">{iconName}</p>
</div>
<CopyMenu name={iconName} onCopy={handleCopy} />
</div>
))}
</div>
</div>
</>
);
};
@@ -1,126 +0,0 @@
"use client";
import React from "react";
import { Toaster } from "react-hot-toast";
import { showToast } from "@calcom/ui/components/toast";
interface ShadowToken {
name: string;
className: string;
description: string;
}
interface ShadowTableProps {
tokens: ShadowToken[];
}
const tokens: ShadowToken[] = [
{
name: "Dropdown",
className: "shadow-dropdown",
description: "Used for dropdown menus and popovers",
},
{
name: "Solid Gray Rested",
className: "shadow-solid-gray-rested",
description: "Default state for solid gray buttons",
},
{
name: "Solid Gray Hover",
className: "shadow-solid-gray-hover",
description: "Hover state for solid gray buttons",
},
{
name: "Solid Gray Active",
className: "shadow-solid-gray-active",
description: "Active/pressed state for solid gray buttons",
},
{
name: "Outline Gray Rested",
className: "shadow-outline-gray-rested",
description: "Default state for outline gray buttons",
},
{
name: "Outline Gray Hover",
className: "shadow-outline-gray-hover",
description: "Hover state for outline gray buttons",
},
{
name: "Outline Gray Active",
className: "shadow-outline-gray-active",
description: "Active/pressed state for outline gray buttons",
},
{
name: "Outline Red Rested",
className: "shadow-outline-red-rested",
description: "Default state for outline red buttons",
},
{
name: "Outline Red Hover",
className: "shadow-outline-red-hover",
description: "Hover state for outline red buttons",
},
{
name: "Outline Red Active",
className: "shadow-outline-red-active",
description: "Active/pressed state for outline red buttons",
},
{
name: "Elevation Low",
className: "shadow-elevation-low",
description: "Subtle elevation for UI elements",
},
{
name: "Button Solid Brand Default",
className: "shadow-button-solid-brand-default",
description: "Default state for solid brand buttons",
},
{
name: "Button Solid Brand Hover",
className: "shadow-button-solid-brand-hover",
description: "Hover state for solid brand buttons",
},
{
name: "Button Solid Brand Active",
className: "shadow-button-solid-brand-active",
description: "Active/pressed state for solid brand buttons",
},
];
export const ShadowTable: React.FC<ShadowTableProps> = () => {
const handleCopy = async (value: string) => {
try {
await navigator.clipboard.writeText(value);
showToast(`Copied ${value}`, "success");
} catch (error) {
console.error("Failed to copy:", error);
showToast("Failed to copy", "error");
}
};
return (
<>
<Toaster position="bottom-right" />
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
{tokens.map((token) => (
<div
key={token.name}
onClick={() => handleCopy(token.className)}
className="group relative cursor-pointer">
<div
className={`border-subtle bg-default hover:bg-subtle relative overflow-hidden rounded-lg border p-4 transition-colors ${token.className}`}>
<div className="space-y-2">
<div>
<p className="text-emphasis text-sm font-medium">{token.name}</p>
<p className="text-subtle mt-1 text-xs">{token.className}</p>
</div>
<p className="text-subtle text-sm">{token.description}</p>
</div>
</div>
</div>
))}
</div>
</>
);
};
@@ -1,45 +0,0 @@
"use client";
import React from "react";
import { Toaster } from "react-hot-toast";
interface SpacingToken {
name: string;
value: string;
pixels: string;
}
interface SpacingTableProps {
tokens: SpacingToken[];
}
export const SpacingTable: React.FC<SpacingTableProps> = ({ tokens }) => {
return (
<>
<Toaster position="bottom-right" />
<div className="space-y-6">
<div className="grid grid-cols-3 gap-4">
{tokens.map((token, index) => (
<div
key={index}
className="border-subtle bg-default group relative overflow-hidden rounded-lg border">
<div className="flex items-center gap-4 p-4">
<div className="w-[100px] space-y-1">
<p className="text-emphasis text-sm font-medium">
{token.name === "px" ? "px" : `${token.name} (${token.pixels})`}
</p>
</div>
<div className="bg-subtle relative h-8 rounded">
<div
className="absolute left-0 top-1/2 h-2 -translate-y-1/2 bg-red-500"
style={{ width: token.pixels }}
/>
</div>
</div>
</div>
))}
</div>
</div>
</>
);
};

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