Compare commits

..
Author SHA1 Message Date
Lauris Skraucis 31da30b982 fix: specify atoms package.json repository (#24828) 2025-10-31 14:25:26 +00:00
Lauris Skraucis d149f8385e fix: run changesets on github hosted runner (#24825)
* fix: run changesets on github hosted runner

* refactor: run on ubuntu latest
2025-10-31 13:02:09 +00:00
Hariom BalharaandDevin AI ea5a0bd5f0 fix: Add Web Crypto feature detection for BotID in React Native WebView (#24790)
* fix: Add Web Crypto feature detection for BotID in React Native WebView

Fixes #24360

- Add feature detection to check for window.crypto availability before initializing BotID
- Skip BotID initialization in React Native WebView environments
- Prevents 'undefined is not an object (evaluating crypto)' error during booking
- BotID requires Web Crypto API which is not available in React Native WebView

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

* refactor: Use minimal feature detection for BotID initialization

- Remove React Native WebView-specific check
- Use pure feature detection for Web Crypto API
- Check for window.crypto.getRandomValues availability
- Add SSR guard with typeof window check

This approach is more robust and environment-agnostic, working across
all environments that lack Web Crypto support, not just React Native.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-31 12:42:30 +00:00
github-actions[bot] 67c9fd92e1 chore: version packages (#24818)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-31 13:07:10 +01:00
50be997753 fix: Allow all team members to view hidden fields and UTM tracking data (#24619)
* Allow team admins to see hidden fields and UTM tracking data

- Add team admin permission check for booking data visibility
- Team admins can now view hidden booking field answers
- Team admins can now view UTM tracking parameters
- Support for managed events (parent team admins can view child event data)
- Maintain backward compatibility with existing host permissions

Fixes team admin access to booking details as requested in issue.

* feat: allow all team members to view hidden fields and UTM data

Previously only team admins could view hidden booking field answers and UTM tracking parameters. Now all team members can see this data, providing better transparency within teams.

- Changed from isTeamAdmin to isTeamMember check
- Team members (not just admins) can now view hidden booking fields
- Team members can now view UTM tracking parameters
- Maintains host permission (organizers can still see everything)
- Supports managed events (parent team members can view child event data)

* Address reviewer feedback: use existing membership pattern

- Remove custom checkIfUserIsTeamAdmin function
- Use existing userId_teamId pattern with prisma.membership.findUnique
- Revert PrismaPlugin and variable name changes
- Maintain same functionality with cleaner implementation

* fix: use existing isTeamAdmin function and remove unnecessary UI changes

* fix: Allow all team members to see hidden fields instead of just admins

- Replace isTeamAdmin with isTeamMember in booking view permissions
- Rename isLoggedInUserTeamAdmin to isLoggedInUserTeamMember for clarity
- All team members can now view hidden fields and UTM tracking data

Addresses feedback from @hariombalhara to allow all team members (not just admins) to see hidden booking data.

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

* fix: Correct import path for isTeamMember function

- Change import from non-existent @calcom/lib/server/queries/teams
- Use correct path @calcom/features/ee/teams/lib/queries
- Fixes type check error: Cannot find module

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

* refactor: Optimize isTeamMember check to call function only once

- Extract teamId using nullish coalescing operator
- Single isTeamMember call instead of two
- Cleaner and more efficient logic

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

* simplify

---------

Co-authored-by: naaa760 <neh6a683@gmail.com>
Co-authored-by: neha <neha@posthog.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-31 12:05:02 +00:00
Eunjae LeeandDevin AI f00c14d0c1 feat: implement booking calendar view with weekly layout (#24563)
* feat: implement booking calendar view with weekly layout

- Create reusable WeekCalendarView component that displays bookings in a weekly calendar format
- Replace EmptyScreen in BookingsCalendar with the new calendar view
- Calendar view includes:
  - Week navigation with Today, Previous, and Next buttons
  - 7-day week view with time slots from 12 AM to 11 PM
  - Bookings displayed as colored blocks positioned by time
  - Support for event type colors and status-based colors
  - Responsive design that fills the viewport
  - Hover tooltips showing booking details
- Filters remain functional at the top of the view

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

* refactor: use existing Calendar component from weeklyview

- Replace custom calendar implementation with the existing Calendar component
- Use parseEventTypeColor to properly handle event type colors
- Simplify implementation by leveraging existing calendar infrastructure
- Maintain week navigation and filtering functionality

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

* fix imports

* fix: replace isSameOrAfter with isAfter || isSame

- isSameOrAfter method does not exist in dayjs
- Use combination of isAfter and isSame instead

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

* remove useBookerTime dependency from weekly calendar view

* modify date range filters

* initial callback

* sort events

* clean up FilterBar

* add showBackgroundPattern

* update styles

* update style

* update styles

* fix type error

* fix error

* update styles

* update styles

* update event colors

* rename component

* persist weekStart on the url

* use FilterBar

* apply feedback

* extract BorderColor type

* use client

* clean up

* adjust styles

* color-code events

* rename borderColor to color

* restore class name

* add feature flag

* update class name

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-31 11:13:41 +00:00
Anik Dhabal Babu 8d4dd0241a fix: disable request reschedule for seated event (#24793) 2025-10-31 10:43:06 +00:00
chauhan_s 90f97c3c92 fix: preserve attendee names during round robin host reassignment when reassignmentReason is required (#24771) 2025-10-31 10:41:35 +00:00
Alex van Andel 1407886612 chore: Set shouldServeCache to falsy, unless specified input (#24778) 2025-10-31 10:34:31 +00:00
Eunjae LeeandDevin AI 139e47fe3b feat: include EventType.seatsPerTimeSlot in bookings get handler response (#24813)
- Add seatsPerTimeSlot field to EventType selection in bookings query
- Refactor conditional array pushes to use if statements instead of && operator to fix lint warnings

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-31 07:33:17 -03:00
Lauris Skraucis 9d4ecf2baa fix: changesets node version (#24812) 2025-10-31 10:27:55 +00:00
Dhairyashil ShindeandUdit Takkar 9d1cf3e70e feat: link advanced event type settings to docs (#24629)
* feat: link advanced event type settings to docs

* Limits and Assignment tab settings

* remove testing code

* add noopener noreferrer rel to links

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-10-31 15:47:11 +05:30
sean-brydon 6beb279a27 chore: add missing metadata i18n strings (#24814)
## What does this PR do?

- Adds new localization strings for various onboarding sections including team and organization settings

## Visual Demo (For contributors especially)

N/A - This PR adds a new component for animations and localization strings that will be used in the onboarding flow.

## Mandatory Tasks (DO NOT REMOVE)

- [ ] I have self-reviewed the code.
- [ ] I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. 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?

- Navigate through the onboarding flow to verify that page transitions are smooth with the new animation component
- Check that the new localization strings appear correctly in the UI for team and organization setup screens
- Verify that the `AnimatePresence` component properly handles exit animations when moving between onboarding steps

## Checklist

- I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code follows the style guidelines of this project
- I have commented my code, particularly in hard-to-understand areas
- I have checked if my changes generate no new warnings
2025-10-31 10:05:22 +00:00
sean-brydon 69e4b12f52 fix: Use index DB instead of local storage for onboarding (#24798)
## What does this PR do?

Implements an IndexedDB storage adapter for the onboarding store to replace localStorage. This allows storing much larger data (up to 50% of available disk space) compared to localStorage's limited quota, making it suitable for storing base64-encoded images like logos and banners during onboarding. The implementation includes a fallback to localStorage if IndexedDB is unavailable.

Sometimes when you uploaded a large image or banner you get this error. Even if its within the bounds of 10mb (which is what we allow)
![image.png](https://app.graphite.dev/user-attachments/assets/f9fad1b6-5c8a-438b-8948-ea299c2ce832.png)

## Visual Demo (For contributors especially)

N/A - This is an infrastructure change with no visual component.

## 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
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.

## How should this be tested?

- Test the onboarding flow and verify that data persists between page refreshes
- Test uploading large images during onboarding to ensure they're properly stored
- Test in browsers with IndexedDB disabled to verify the localStorage fallback works correctly
- Verify that the onboarding state is properly maintained when navigating through the onboarding steps

## Checklist

- I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code follows the style guidelines of this project
- I have commented my code, particularly in hard-to-understand areas
- I have checked if my changes generate no new warnings
2025-10-31 09:53:14 +00:00
Anik Dhabal Babu b8098ed3ab test(e2e): optimize email verification timeout to 3 seconds (#24799) 2025-10-31 15:14:44 +05:30
sean-brydon db55b11034 chore(onboarding): redirect if part of orgs or not company email (#24800) 2025-10-31 08:13:23 +00:00
sean-brydon 2273b7ea24 chore: add signout button on onboarding v3 layout (#24801)
<img width="2778" height="1633" alt="CleanShot 2025-10-30 at 16 42 23" src="https://github.com/user-attachments/assets/45d84980-26c9-4cb4-ac85-21dc174dba35" />



<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Added a sign-out button to the onboarding v3 layout and refactored Getting Started to use the shared OnboardingLayout for a consistent header and structure.

- **New Features**
  - Sign-out button in the onboarding footer using next-auth (redirects to /auth/logout).
  - Localized button label via useLocale.

- **Refactors**
  - Getting Started view now wraps content with OnboardingLayout.
  - Removed duplicated header and progress UI from the view.
  - Simplified content container and spacing; layout handles max width and flex.

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

<!-- End of auto-generated description by cubic. -->
2025-10-31 08:07:37 +00:00
0a90212980 chore: CalendarEventBuilder - generate calendarEvent without PII using booking (#24678)
* chore: BookingHydrationService - generate calendarEvent without PII

* chore: wip add build calendarEvent from booking in CalEventBuilder

* chore: build calendar event from booking

* remove hydration service

* test: add comprehensive test for CalendarEventBuilder.fromBooking with all properties

- Add vi.mock() calls for external dependencies (getBookerBaseUrl, getTranslation, getCalEventResponses)
- Add comprehensive test 'should create a complete calendar event with all properties using a booking'
- Test covers team scheduling, video calls, seats/webinar, recurring events, custom fields, and all configuration flags
- Fix import paths in CalendarEventBuilder.ts to use @calcom/features prefix
- Fix timestamp format issues in existing tests to use toISOString()
- Fix destinationCalendar length expectations to account for user calendar
- Fix responses format in seats tests to match bookingResponsesSchema

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-31 07:31:50 +00:00
Alex van AndelandDevin AI 2efcfab715 Fix isEventTypeLoggingEnabled bug where empty string matches eventTypeId 0 (#24804)
When BOOKING_LOGGING_EVENT_IDS is set to an empty string, the current
implementation splits it into [''] and Number('') returns 0, causing
any event type with ID 0 to incorrectly trigger debug logging.

This fix:
- Filters out empty/invalid event IDs (NaN, <=0)
- Filters out empty usernames
- Only matches valid, positive event type IDs

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-30 19:32:17 +00:00
Anik Dhabal Babu 3c1dfe8b74 fix: return the actual error message for hitpay (#24792) 2025-10-30 17:58:04 +00:00
Rajiv Sahal e47ddf9835 chore: add handleCreateRecurringBooking prop to booker atom (#24786)
* chore: update Booker platform wrapper props

* fix: pass handle create recurring prop to useHandleBookEvent if prop present

* chore: add changesets
2025-10-30 15:50:17 +00:00
08c62b554e chore: update Booker atom docs (#24755)
* chore: update booker atom docs

* chore: implement PR feedback

* Update docs/platform/atoms/booker.mdx

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

---------

Co-authored-by: chauhan_s <somaychauhan98@gmail.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-10-30 15:46:45 +01:00
b0b94b91fe fix: allow org admin to cancel and reschedule seated bookings (#24640)
* refactor: move org admin related logic to org server

* fix: update cancellation logic to make sure org admin can cancel seated bookings of a team user

* fix: import path

* update bookings repository

* fix: update reschedule endpoint logic to let org admin reschedule bookings for a user

* refactor: make logic more simple

* chore: update platform libraries

* more refactors

* fix: add check to make sure org admin can reschedule booking

* chore: remove unused comments

* test: add e2e tests for org admin reschedule and cancel seated bookings

- Add seated event type creation for testing
- Add test for org admin rescheduling a seated booking for a managed user
- Add test for org admin canceling a full seated booking for a managed user
- Add test for org admin canceling a specific seat in a seated booking

These tests verify the functionality added in PR #24640 which allows
org admins to reschedule and cancel seated bookings for users in their
organization.

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

* chore: add cubic feedback

* fix: tests for seated booking management by org admin

* chore: implement PR feedback

* fixup

* chore: update docs

* fixup: get optional user from request and then pass it down to getBookingForReschedule

* fix: validate seatUid before checking booking cancellation status

Move canRescheduleBooking call to happen after input validation
(including seatUid validation for seated bookings) but before the
actual booking creation. This ensures that when trying to reschedule
a seated booking without providing seatUid, users get the proper
'seatUid required' error instead of 'booking has been cancelled' error.

Fixes failing e2e test: 'should not be able to reschedule seated
booking if seatUid is not provided'

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-30 16:43:05 +02:00
Eunjae LeeandDevin AI af61b6d341 feat: move bookings tabs to sidebar navigation (#24605)
* feat: move bookings tabs to sidebar navigation

- Add bookings sub-items (Upcoming, Unconfirmed, Recurring, Past, Cancelled) to sidebar navigation
- Remove HorizontalTabs component from bookings view
- Add redirect from /bookings to /bookings/upcoming for default navigation
- Parent Bookings menu item now automatically navigates to Upcoming sub-item

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

* clean up

* add e2e tests

* address feedback

* add horizontal tab back

* useBuildHref

* update preserveQueryParams logic

* put back lint rule

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-30 15:35:26 +01:00
Udit Takkar 28693aba96 feat: distributed tracking (#24717) 2025-10-30 14:12:22 +00:00
2dc0e2a6f2 fix: use highest role for org admins accessing sub team event types (#24748)
* fix: use highest role for org admins accessing sub team event types

When an org admin who is also a member of a sub team (but only as MEMBER role) tries to update a sub team event type, they were being denied access. This was because the permission check logic was using the team MEMBER role instead of checking both team and org roles and using the highest one.

This fix updates PermissionCheckService.checkPermission() and checkPermissions() to use the highest role between team membership and org membership, similar to how getEventTypePermissions() already works.

Changes:
- Added getHighestRole() helper method to determine the highest role between two roles
- Updated checkPermission() to check both team and org membership and use the highest role
- Updated checkPermissions() to use the same logic
- Added test case for org admin who is team member updating sub team event type

Fixes the issue where org admins get 'permission required eventType.update' errors when updating sub team event types.

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

* test: add PBAC-enabled test case for org admin accessing sub team event types

Adds a test case to verify that when PBAC is enabled, org admins who are also members of a sub team (but only with MEMBER role) can still access sub team event types through their org-level ADMIN permissions.

This complements the existing fallback (PBAC disabled) test case and ensures both code paths handle org admin permissions correctly.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-30 13:59:48 +00:00
sean-brydon 6cbe1c1cf1 chore: Implement onboarding redirect logic in event-types and main page components (#24785)
## What does this PR do?

Currently we load /event-types for a few seconds while the onboarding hook catches up. This adds that logic to the serverside and not just client side to ensure onboarding is triggered

Adds server-side onboarding redirect checks to prevent users from accessing event types pages before completing onboarding. This implementation:

- Creates a new `checkOnboardingRedirect` utility function in a dedicated file
- Applies the redirect check on both the root page and event-types page
- Optimizes performance by using organizationId from session when available
- Handles email verification requirements before redirecting to onboarding
- Supports both legacy and v3 onboarding paths based on feature flags

## How should this be tested?

- Create a new user account that hasn't completed onboarding
- Attempt to access the root page or event-types page directly
- Verify you're redirected to the appropriate onboarding flow
- Test with email verification feature flag enabled/disabled
- Test with onboarding-v3 feature flag enabled/disabled
- Verify users who have completed onboarding can access event-types normally
- Verify organization users aren't redirected to onboarding

## Video Demo

Before:

[CleanShot 2025-10-30 at 10.54.41.mp4 <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/user-attachments/thumbnails/8282decc-a00d-4bc8-9215-fe1c4809fd8f.mp4" />](https://app.graphite.dev/user-attachments/video/8282decc-a00d-4bc8-9215-fe1c4809fd8f.mp4)

After

## [CleanShot 2025-10-30 at 10.54.06.mp4 <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/user-attachments/thumbnails/2acc7601-6c8c-496a-af4d-08dadef9aa2d.mp4" />](https://app.graphite.dev/user-attachments/video/2acc7601-6c8c-496a-af4d-08dadef9aa2d.mp4)

## Checklist

- [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
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works
2025-10-30 13:44:11 +00:00
Rodrigo Ehlers 1c2da58d2a feat: add separator support to data table (#24752)
* feat: add separator support to data table

* chore: remove unnecessary comments

* chore: move files to more appropriate places

* fix: border bottom
2025-10-30 14:41:21 +01:00
Lauris Skraucis 7bfdaa3d64 fix: changesets release to npm (#24788)
* fix: changesets release to npm

* chore: resolve merge conflict
2025-10-30 13:33:56 +00:00
c38d2afcf5 fix: Optimized Slots setting not updating on children event types (#24208)
* fixed : Optimized Slots setting not updating on children event types

* updated tests to expect showOptimizedSlots sync

* fixed : Optimized Slots setting not updating on children event types

---------

Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-10-30 12:34:48 +00:00
chauhan_s 807848cce2 fix: require notice threshold when type time (#24784)
* feat: enhance validation for confirmation policy notice threshold

* feat: add confirmation policy validation tests for event types
2025-10-30 14:20:56 +02:00
chauhan_s e9d409c0d5 fix: Modified teamName to be null when host is not fixed (#24728)
* fix: Modified teamName to be null when host is not fixed

* feat: Added test cases for round robin bookings reassignment with fixed and non-fixed hosts

* feat: improve team name handling in round robin reassignment

* refactor: improve booking title validation in reassignment tests
2025-10-30 14:00:25 +02:00
github-actions[bot] 0c43d60039 chore: version packages (#24729)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-30 11:53:22 +01:00
sean-brydon 1173822344 chore: invites to submit hook instead of relying on state (#24744)
## What does this PR do?
Pass invites to submit hook instead of relying on state. State can be out of sync on last step as its not commited onChange but onsubmit. 

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Pass the invite list directly to the onboarding submit hook to avoid stale state and ensure the final step submits the correct invites. Fixes missed or outdated invites during onboarding.

- **Bug Fixes**
  - submitOnboarding now accepts invites as a parameter and uses them for submission.
  - OrganizationInviteView passes invitesData to submitOnboarding instead of relying on store.invites.

<!-- End of auto-generated description by cubic. -->
2025-10-30 09:16:14 +00:00
Anik Dhabal Babu a934cec51a fix: exclude attendee reponses from calendar (#24739) 2025-10-30 07:06:42 +00:00
Hariom Balhara 257acc3f71 fix: Auto Assignment of team member to event when that member is (#24731)
auto-accepted during direct invite to sub-team
2025-10-30 09:42:41 +05:30
Alex van Andel 91063c41f8 chore: Determine to serve cache high level (#24756)
* chore: Determine to serve cache high level

* Fix invalid import

* fix: Cache test
2025-10-29 22:08:23 +00:00
Keith Williams 1e1298fdbc chore: Removed unneeded yarn reinstall API v2 Docker (#24772) 2025-10-29 19:19:52 +02:00
95120e1ff4 feat: add bookingRequiresAuthentication validation to 2024-04-15 booking controller (#24735)
* feat: add bookingRequiresAuthentication validation to 2024-04-15 booking controller

- Add checkBookingRequiresAuthentication method to validate authentication requirements
- Check if user is event type owner, host, team admin/owner, or org admin/owner
- Add comprehensive e2e tests for bookingRequiresAuthentication feature
- Ensure parity with 2024-08-13 controller implementation
- Fix type issue in setPlatformAttendeesEmails method

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

* refactor: move Prisma calls to repository pattern

- Add findByIdIncludeHostsAndTeamMembers method to EventTypeRepository
- Inject PrismaEventTypeRepository and PrismaTeamRepository into controller
- Replace direct Prisma calls with repository methods in checkBookingRequiresAuthentication
- Use getTeamByIdIfUserIsAdmin for org admin/owner check
- Add repositories to BookingsModule_2024_04_15 providers

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

* handle httpException in handleBookingErrors

* test: add test case for authenticated but unauthorized user booking

- Create second user who is not authorized to book the event type
- Verify that authenticated user without proper permissions receives 403 Forbidden
- Test validates that bookingRequiresAuthentication properly checks authorization levels
- Cleanup unauthorized user in afterAll hook

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

* fix: add accepted filter to team members and handle org-owned event types

Addresses PR comments from cubic-dev-ai and @ThyMinimalDev:

1. Add accepted: true filter to team.members query
   - Prevents pending team invitations from being treated as authorized
   - Also filter by role to only fetch ADMIN and OWNER roles
   - Reduces payload size and improves query performance

2. Add isOrganization field to team select
   - Enables proper handling of org-owned event types

3. Update authorization logic for org-owned event types
   - Handle case where team.isOrganization is true with no parent
   - Ensure org admins/owners are properly authorized for org-owned events
   - Matches behavior of 2024-08-13 controller

Changes:
- packages/features/eventtypes/repositories/eventTypeRepository.ts:
  * Add where clause to members query with accepted: true and role filter
  * Add isOrganization: true to team select
- apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts:
  * Update authorization logic to handle org-owned event types
  * Check if team.isOrganization is true when no parentId exists

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-29 16:48:16 +00:00
Keith WilliamsandDevin AI ee00f0d52c feat: Add official docker support (#24672)
* feat: Add official Docker support

* Adding scarf data support

* Comment out pushing the image for now

* Getting env vars ported

* Renamed the job to Release instead of Remote Release

* Move the Dockerfile and docker-compose files to monorepo root

* Remove Slack notifications for failures for now

* Show database container status

* Setting env directly for testing

* Removing env var

* Adding container logs

* Change the volume

* fixing file paths

* Double-quotes wrecking things

* Fixing /calcom paths

* Update permission for scripts

* Fixed the Slack notification

* Updated Slack notification emojis

* Checking the workflow_dispatch input for checkout

* Commenting out the tag checkout for now since our new Docker files are not in main

* Added .dockerignore

* Remove the scarf data export

* Removed extra empty line

* refactor: Create reusable Docker build action for AMD64 and ARM support

- Extract common Docker build logic into reusable composite action
- Create separate workflows for AMD64 and ARM builds that run in parallel
- Both workflows use the same reusable action with platform-specific parameters
- ARM builds use ubuntu-24.04-arm runner and add -arm suffix to tags
- AMD64 builds use buildjet-4vcpu-ubuntu-2204 runner
- Remove old monolithic release-docker.yaml workflow

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

* Revert "refactor: Create reusable Docker build action for AMD64 and ARM support"

This reverts commit 66d2c1741e094e8d39b928b109edaf67b6a1cc8e.

* refactor: Add parallel AMD64 and ARM Docker builds using reusable action

- Create reusable composite action in .github/actions/docker-build-and-test
- Extract common Docker build, test, and push logic into the action
- Update release-docker.yaml to have two parallel jobs:
  - release-amd64: Builds for linux/amd64 on buildjet-4vcpu-ubuntu-2204
  - release-arm: Builds for arm64 on ubuntu-24.04-arm with -arm suffix
- Both jobs use the same reusable action with platform-specific parameters
- Maintains existing functionality while enabling parallel builds

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

* Update the ARM action to run on buildjet 4vCPU ARM

* Move the Dockerfile to apps/web

* Revert "Move the Dockerfile to apps/web"

This reverts commit fd91ebe5b4285cfa3416e6f869f567329ece8b23.

* Revert the arm machine back off build jet

* Use node 20

* Set push to true

* Remove Dockerfile.render

* Removed commented Docker lines

* Fixed read me

* Updated README for Docker support

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-29 15:31:02 +00:00
Udit TakkarandKeith Williams 246abe0dd9 perf: add docker ignore in root path (#24585)
* chore: add docker ignore in root path

* chore: add docker ignore in root path

* chore: remove

* chore: remove some more

* chore: remove some more

* Removed unnecessary whitespace

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-10-29 14:16:57 +00:00
Benny Joo 14e845ea1f perf: og image caching v2 (#23189) 2025-10-29 10:36:17 -03:00
8ba65c4450 fix: prevent base64 logo/banner storage in organization onboarding (#24761)
* fix: prevent base64 logo/banner storage in organization onboarding

- Add processOnboardingBrandAssets helper method to BaseOnboardingService
- Process base64 images and upload them before storing in database
- Use uploadAvatar with userId to avoid foreign key issues before Team exists
- Handle both create and update/resume flows
- Ensure OrganizationOnboarding and Team records store regular URLs not base64
- Fixes header size issues when logoUrl is added to session cookies

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

* fix: use processed URLs from OrganizationOnboarding record

- Update SelfHostedOnboardingService to use organizationOnboarding.logo/bannerUrl instead of raw input
- Update BillingEnabledOrgOnboardingService payment intent to use processed URLs
- Ensures Team records receive processed URLs, not base64 data

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

* refactor: remove unused variables and imports

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

* refactor: extract image processing to helper method and add tests

- Created private processImageField() method to eliminate code duplication
- Uses regex for more robust data URI and URL detection
- Processes logo and bannerUrl in parallel with Promise.all
- Added 2 important tests for base64 image processing:
  1. Verifies base64 conversion with correct resize options (bannerUrl uses maxSize: 1500)
  2. Validates undefined vs null semantics (no-op vs explicit clear)
- Fixed pre-existing lint warnings by replacing 'any' types with proper types

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

* Improve code

* Fixup organizationId

* simplify

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-10-29 18:39:56 +05:30
sean-brydon aff494f21c chore: refactor onboarding v3 to use reusable components (#24711)
\## What does this PR do?

Refactors the onboarding UI components to use shared layout components across personal and organization onboarding flows. This change:

- Creates reusable `OnboardingCard` and `OnboardingLayout` components
- Implements these components across all onboarding views
- Maintains consistent UI and reduces code duplication
- Improves maintainability by centralizing layout logic

## Visual Demo (For contributors especially)

#### Image Demo:

Before this change, each onboarding view had its own layout implementation with duplicated header, footer, and card structure. After this change, the shared components provide consistent styling and structure.

## 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. If N/A, write N/A here and check the checkbox.
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.

## How should this be tested?

- Navigate through the onboarding flow for both personal and organization accounts
- Verify that all screens maintain consistent layout and styling
- Check that progress indicators correctly show the current step
- Ensure all functionality (form submissions, navigation between steps) works as expected

## Checklist

- I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code follows the style guidelines of this project
- I have commented my code, particularly in hard-to-understand areas
- I have checked if my changes generate no new warnings
2025-10-29 12:51:21 +00:00
Lauris Skraucisandchauhan_s 18ad7e348e chore: make atoms release depend on trusted publisher (#24769)
Co-authored-by: chauhan_s <somaychauhan98@gmail.com>
2025-10-29 12:26:33 +00:00
emrysal 78f3d76ae2 chore: release v5.8.7
Post release / release (push) Canceled after 0s
2025-10-29 11:51:10 +00:00
Anik Dhabal Babu 6f9e04c6d9 fix: hosts group missmatch issue (#24757) 2025-10-29 10:46:16 +00:00
Benny JooandAlex van Andel fdb0e5d004 fix: Use isValidPhoneNumber from libphonenumber-js/max (#23353)
* import from max

* fix

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-10-29 08:13:41 +00:00
Rajiv Sahal 268a142370 fix: date overrides missing minutes (#24753) 2025-10-28 22:51:15 +00:00
Alex van AndelandMorgan 29a3892669 chore: add indexes for user delete (#24740)
* chore: Adding indexes for user delete cascade

* Add comments to indicate a partial index has been added on a field

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-10-28 22:28:40 +00:00
Rajiv Sahal 0223027958 chore: update docs to clarify api v2 supports sms workdlows (#24704)
* chore: update docs to clarify api v2 supports sms workdlows

* chore: implement feedback

* fix: cleanup
2025-10-28 16:03:11 +00:00
d7df154059 fix: Add critical logger for booking created log to prevent stdout buffering issues (#24570)
* Add critical logger for booking created log to prevent stdout buffering issues

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

* Add comment

* Separate critialLogger into it's own file

---------

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-10-28 12:59:44 -03:00
sean-brydon 51d78e6f9d fix(onboarding): adderss nits for invite link / spacing (#24742)
## What does this PR do?

- Redirects users to the personal settings page after organization creation instead of the getting-started page
- Adds a new `skipToPersonal` function in `useSubmitOnboarding` hook to handle this redirection
- Creates a new `getGettingStartedPathWhenInvited` method in `OnboardingPathService` to provide the correct path for invited users
- Updates all invitation-related code to use the new path method
- Improves UI spacing in the organization invite view

## Visual Demo (For contributors especially)

#### Image Demo (if applicable):
- Before: Users were redirected to `/onboarding/getting-started` or `/getting-started` after organization creation
- After: Users are now redirected to `/onboarding/personal/settings` or `/getting-started` based on feature flag

## Mandatory Tasks (DO NOT REMOVE)

- [ ] I have self-reviewed the code.
- [ ] I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. 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?

1. Create a new organization through the onboarding flow
2. Verify you are redirected to the personal settings page instead of getting-started
3. Accept an invitation to an organization as a new user
4. Verify you are directed to the personal settings page after signup
5. Check that the UI spacing in the organization invite view looks correct

## Checklist

- I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code follows the style guidelines of this project
- I have commented my code, particularly in hard-to-understand areas
- I have checked if my changes generate no new warnings
2025-10-28 14:35:57 +00:00
d5894d9fbc fix: update booking view UI for awaiting payment state (#24577)
* fix: update booking view UI for awaiting payment state

- Show calendar icon instead of green check for awaiting payment
- Update title to show 'Your meeting is awaiting payment' or 'Your meeting is awaiting a payment method'
- Update subtitle to show 'Complete your booking: {title} on {date}'
- Handle both HOLD payment option and regular payment pending states
- Fix unused variable warning

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

* fix: correct formatToLocalizedDate parameters

Use proper dateStyle parameter instead of time format number

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-28 14:29:27 +00:00
8b0539028e chore: Adds a users.uuid column (#24659)
* chore: Adds a users.uuid column

* Add uuid backfill

* Remove backfill due to possible locking issues

* feat: Add batched UUID backfill to prevent database locking

Implements a batched approach for backfilling UUIDs on existing users:
- Processes 1000 rows at a time to avoid table locks
- Uses FOR UPDATE SKIP LOCKED to prevent blocking other transactions
- Includes small delays between batches (10ms) to allow other operations
- Logs progress every 10,000 rows for monitoring

This approach is safe for production databases with millions of users.

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-10-28 09:17:54 -03:00
Syed Ali Shahbaz c7600d8ead fix: Make no-show dialog scrollable (#24734)
* fix

* --
2025-10-28 12:15:57 +00:00
6f5b8ead75 feat: update Office365 getMainTimeZone to handle new Graph API format and convert Windows timezones to IANA (#23763)
* feat: update Office365 getMainTimeZone to handle new Graph API format and convert Windows timezones to IANA

- Handle new Graph API response format with 'value' field containing Windows timezone names
- Add windows-iana dependency for timezone conversion from Windows to IANA format
- Maintain backward compatibility with legacy direct string response format
- Add proper error handling with fallback to original timezone name if conversion fails
- Log conversion details for debugging and monitoring

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

* fix: example app select default value

* fixup! fix: example app select default value

* fix: preserve legacy string timezone behavior, only convert new object format

- Address GitHub comments from supalarry about behavior change concerns
- Preserve original behavior: return legacy string responses as-is without conversion
- Only convert Windows timezones from new Graph API object format
- Add explanatory comments about API format evolution
- Restructure conditional logic to handle formats independently

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

* trigger CI with ready-for-e2e label

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: morgan@cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-10-28 12:15:34 +00:00
fc159fb9ea fix: description label is not focusing on new event-type create modal (#23603)
* fix: description label is not focusing

* changes

---------

Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-10-28 12:02:04 +00:00
b3297f2843 fix(event-type): remove spinner flash before skeleton loads (#23678) (#23679)
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
2025-10-28 11:48:38 +00:00
Rajiv Sahal 1dade81299 refactor: team and org billing portal service (#24497)
* refactor: team and org billing portal service

* fix: implement coderabbit feedback

* fix: move core logic to base service

* remove unused comments
2025-10-28 11:42:22 +00:00
spandevandPallav b2839830c8 update: added empty screen component to empty call history page (#24721)
Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-10-28 11:40:47 +00:00
sean-brydon fa884e8516 chore: Onboarding path service plus redirects (#24679)
* i18n

* WIP icon stuff

* More icon work

* More gradient tuning

* Mvoe plan-icon to planicon.tsx

* Fix cubic suggestion

* Fix darkmode icon and gradients

* Fix type error

* Onboarding path service

* Update usages of onboarding path and getting started hook
2025-10-28 11:10:25 +00:00
3418a25a85 fix: seed script command (#24668)
* feat: add seed-all command to run all seed scripts

- Add seed-all command to packages/prisma/package.json
- Create scripts/seed-all.ts to run seed.ts, seed-insights.ts, and seed-pbac-organization.ts in sequence
- Update Prisma configuration to use seed-all as the default seed command
- Add GitHub workflow to test seed-all command with a real PostgreSQL database

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

* fix: use correct path resolution in seed-all script

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

* fix: simplify database verification step in test workflow

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

* refactor: use seed-all in cache-db action instead of separate workflow

- Update cache-db action to use seed-all command
- Remove test-seed-all.yml workflow as requested

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

* fix error

* change command name

* fix integration tests

* run basic seed by default

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-10-28 10:56:52 +00:00
89cc175d73 feat: add no-show hosts and csat graphs on Insights (#24556)
* feat: Add No-Show Hosts and CSAT insights with beautiful card design

- Add No-Show Hosts Over Time chart with line visualization
- Add CSAT Over Time chart with percentage tracking
- Add Recent No-Show Guests list with email copy functionality
- Redesign all KPI cards as individual separated cards with curved edges
- Add proper TRPC endpoints and database queries for new insights
- Follow Cal.com design patterns with consistent styling
- All charts now have individual cards with rounded-2xl borders and shadow-sm elevation
- Fix all linting issues with unused imports and variables

* coderabbit fixes

* fix(insights): address review feedback on charts

* fix(insights): correct dayjs timezone usage for type safety

* Update packages/features/insights/services/InsightsBookingBaseService.ts

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

* Fix InfoBadge import path

* Wrap InfoBadge in span for styling

* Add insights charts and i18n keys

* Fix ChartCard usage and double card wrapping issues

* Remove double card wrapping for chart components

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-10-28 10:39:39 +00:00
Eunjae Lee c7c8564b63 fix(insights): convert string filter values to numbers for integer columns (#24730)
## What does this PR do?

Fixes PostgreSQL error "operator does not exist: integer = text" when filtering
by `bookingStatusOrder` and `bookingUserId` fields. Frontend sends filter values as
strings, but these database columns are integers requiring type conversion.

Changes:
- Convert `bookingStatusOrder` filter values from strings to numbers
- Convert `bookingUserId` filter values from strings to numbers
- Update tests to use numeric values for `bookingStatusOrder` filters
- Add test for string-to-number conversion in `bookingUserId` filter
- Fix `Prisma.Sql` composition in 3 integration tests

## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [x] N/A - 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.
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Converts string filter values to numbers for integer columns in Insights routing to prevent PostgreSQL type errors. Fixes filtering by bookingStatusOrder and bookingUserId when the frontend sends strings.

- **Bug Fixes**
  - Cast bookingStatusOrder and bookingUserId filter values to numbers and use = ANY([...]).
  - Update tests to expect numeric arrays; add test for string-to-number conversion on bookingUserId.
  - Correct Prisma.sql composition in three integration tests before calling $queryRaw.

<!-- End of auto-generated description by cubic. -->
2025-10-28 10:34:56 +00:00
b620581d7a feat: Add endpoint to add attendees to existing bookings (#24414)
* feat: add endpoint to add attendees to existing bookings

- Created POST /v2/bookings/:bookingUid/attendees endpoint
- Added AddAttendeesInput_2024_08_13 for input validation
- Added AddAttendeesOutput_2024_08_13 for response format
- Created BookingAttendeesService_2024_08_13 for business logic
- Created BookingAttendeesController_2024_08_13 for API endpoint
- Added validation to check for duplicate attendee emails
- Integrated with existing booking and event type repositories
- Added validateAndTransformAddAttendeesInput method to InputBookingsService
- Fixed pre-existing ESLint no-prototype-builtins warnings
- Left placeholder for custom booking field validation logic

Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com>

* refactor: move booking attendee operations to dedicated repository

* refactor: move repository files into dedicated repositories directory

* feat: validate guests field availability before adding attendees to booking

* feat: migrate addAttendees API to use existing addGuests handler

* refactor: remove unused validateAndTransformAddAttendeesInput method from InputBookingsService

* refactor: rename attendees to guests in booking API endpoints and types

* refactor: rename booking-attendees to booking-guests for consistency

* WIP: add e2e tests for add booking guests endpoint

* faet: improve guest booking tests

* refactor: extract getHtml method in email templates

* feat: add email toggle support for guest invites based on OAuth client settings

* refactor: addGuests handler

* feat: add SMS notifications when adding guests to existing bookings

* refactor: rename add-attendees to add-guests for consistent terminology

* refactor: added repository pattern in addGuests.handler

* test: add attendee scheduled email spy to booking guests tests

* fix: use event type team ID instead of user org ID for booking permission check

* Update BookingEmailSmsHandler.ts

* Remove comments

* refactor: rename booking guests to booking attendees

* refactor: rename guest-related methods to use attendees terminology for consistency

* update api docs

* refactor: restructure addGuests handler to top

* refactor: update guest email format to use object structure in booking tests

* docs: clarify API version header requirement for booking attendees endpoint

* docs: add email notification details to booking attendees API documentation

* refactor: rename booking attendees to guests for consistency

* refactor: rename attendees to guests in booking API endpoints

* feat: add email validation for guest invites

* feat: improve error handling for guest booking failures

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-10-28 14:16:07 +05:30
chauhan_s c0530cc91c chore: add changeset for new atoms release (#24709) 2025-10-28 14:15:34 +05:30
emrysal f738b3bc0a chore: release v5.8.6
Post release / release (push) Canceled after 0s
2025-10-28 02:30:12 +00:00
Morgan 0853b4ca35 chore: add missing parseIntPipe on oauth client user endpoints (#24714) 2025-10-28 01:58:37 +00:00
Rodrigo Ehlers 8be05b1dc5 chore: explicitly set timezone for local (#24719) 2025-10-27 20:29:16 +00:00
Keith Williams 71515ad17c refactor: remove tRPC references from packages/lib (#24553) 2025-10-27 18:13:25 +00:00
sean-brydon 66234ffba9 chore: illustrations to getting-started page in onboarding v3 (#24673)
* i18n

* WIP icon stuff

* More icon work

* More gradient tuning

* Mvoe plan-icon to planicon.tsx

* Fix cubic suggestion

* Fix darkmode icon and gradients

* Fix type error
2025-10-27 17:32:06 +00:00
Rodrigo Ehlers 66c5f810ea fix: remove singular query that was using slow OR query (#24715)
## What does this PR do?

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

Removes and replaces the singular query that was still using the slow OR query from prisma. It was mainly used on the public booking page and triggered after someone entered their email (600ms debounce delay).

## Visual Demo (For contributors especially)

Internal change only nothing changes on the UI or business logic.

#### Video Demo (if applicable):

N/A

#### Image Demo (if applicable):

N/A

## 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.
- [x] 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 -->

Basically the same as https://github.com/calcom/cal.com/pull/24298 since it touches relevant code.

1. Have a user that has the setting to prevent impersonation on.
2. Go to another users public booking page of any meeting.
3. Try to enter the email of the first user into the booking for.
4. Wait 1 second and make sure the button shows "Verify email".

## 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-10-27 17:11:05 +00:00
81d9d84b4a fix: make next environment variables optional, externalize react runtime (#24613)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-10-27 15:16:05 +00:00
sean-brydon 6ddc666827 feat: onboarding v3 - app install flow personal (#24710)
## What does this PR do?

Refactors the personal onboarding flow by creating reusable components for better code organization and maintainability. The changes include:

- Creates shared components for the onboarding experience: `OnboardingLayout`, `OnboardingCard`, `InstallableAppCard`, and `SkipButton`
- Implements a custom hook `useAppInstallation` to manage app installation state and callbacks
- Enhances the video app connection step to automatically set the first connected video app as default
- Improves the calendar connection flow with proper installation handling
- Adds visual indicators for connected apps

## Visual Demo (For contributors especially)

The refactored components maintain the same visual appearance while improving code structure and reusability.

## Mandatory Tasks

- [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
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.

## How should this be tested?

- Go through the personal onboarding flow
- Test connecting calendar apps in step 3
- Test connecting video apps in step 4
- Verify that the first connected video app is automatically set as default
- Check that connected apps show the "connected" indicator
- Verify that skipping steps works correctly

## Checklist

- I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code follows the style guidelines of this project
- I have commented my code, particularly in hard-to-understand areas
- I have checked if my changes generate no new warnings
2025-10-27 15:01:09 +00:00
sean-brydonandcubic-dev-ai[bot] 01346abb85 feat: Add personal v3 onboarding flow (#24681)
* WIP personal onbaording flow

* Add i18n

* Fix overflow clip blockin timezone select

* Remove duplicate label

* Move to two column approach

* Restore lock fle

* Remove duplicate key in common json

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

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

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

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

* Update personal-settings-view.tsx

* Fix endpoint

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-10-27 13:36:46 +00:00
Lauris Skraucisandrez1coder 3d9650c841 docs: cancelling normal booking (#24703)
* docs: cancelling normal booking

* Update apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts

Co-authored-by: rez1coder <66778119+rez1coder@users.noreply.github.com>

---------

Co-authored-by: rez1coder <66778119+rez1coder@users.noreply.github.com>
2025-10-27 12:39:11 +00:00
Anik Dhabal Babu f8ede75dd8 fix: few flaky e2e tests (#24690)
* fix: flaky e2e tests

* fix one more flakes

* update
2025-10-27 08:38:59 -03:00
sean-brydon 435587f043 feat: onboarding v3 teams (#24573)
## What does this PR do?
This PR is stacked on https://github.com/calcom/cal.com/pull/24299 this adds the v3 flow for onboarding with teams.

    
<!-- This is an auto-generated description by cubic. -->
---

## Summary by cubic
Adds the v3 team onboarding flow with Details, Brand, and Invite steps, including slug validation, branding, and member invites. Updates routing to support the Team plan and creates teams with Stripe checkout when needed.

- **New Features**
  - Team Details: name and slug with async availability check and URL preview.
  - Team Brand: hex color picker and logo upload with live preview.
  - Team Invite: add/remove emails, invite role toggle (Member/Admin), and form validation.
  - State: adds teamDetails, teamBrand, and teamInvites to the onboarding store with actions.
  - Creation: new useCreateTeam hook; redirects to Stripe if checkout URL is returned, or to Getting Started on success.
  - Routing/Auth: protected team pages (details, brand, invite) and updates plan selection to route to /onboarding/teams.

<!-- End of auto-generated description by cubic. -->
2025-10-27 11:00:11 +00:00
Anik Dhabal BabuandDevin AI e5abe93940 fix: Add beforeAll hooks to ensure organization settings are properly configured in integration tests (#24674)
The flaky test failures were caused by the tests depending on the database being properly seeded with the isAdminAPIEnabled flag set to true for the Acme organization. The tests would fail randomly when the database wasn't properly seeded or when the organization settings weren't configured correctly.

This fix adds beforeAll hooks to the failing integration tests to ensure that:
1. The Acme organization has isAdminAPIEnabled set to true
2. The Dunder Mifflin organization has isAdminAPIEnabled set to false

This ensures consistent test behavior regardless of the database state and prevents the flaky failures.

Fixes the following failing tests:
- isAdmin.integration-test.ts: Returns org-wide admin when user is set as such & admin API access is granted
- retrieveScopedAccessibleUsers.integration-test.ts: Returns members when admin user ID is supplied and members IDs are supplied
- retrieveScopedAccessibleUsers.integration-test.ts: Returns members when admin user ID is an admin of an org
- _get.integration-test.ts: Returns bookings for org users when accessed by org admin
- _patch.integration-test.ts: Allows PATCH when user is org-wide admin

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-27 05:32:05 -03:00
c3a5158a53 feat: add availabilityLoaded embed event (#21350)
* feat: add availabiityLoaded embed event

* chore

* fix

---------

Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
2025-10-27 06:22:09 +00:00
Sagnik PalandDhairyashil Shinde a9e14b9816 fix/23800-capitalize-button-lable-consistency (#23822)
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-10-27 11:23:03 +05:30
Dhairyashil Shinde e207f40611 refactor: update the config to auto close stale pr (#24701) 2025-10-26 23:28:38 +00:00
Shrey-Sutariya 4ca2076c72 fix: add spacing between form and submit button in TeamEventTypeForm (#24680)
* fix: add spacing between form and submit button in TeamEventTypeForm

* fix: address PR feedback - add bottom padding to form div
2025-10-26 12:46:19 +00:00
Akshat Bajetha 959b803d84 fix: event-title-overflow (#24693)
* fix: event-title-overflow

* fix: title overflow in multiple components

* fix: title overflow in readonly component
2025-10-26 12:14:21 +00:00
Albin_Baby 7f6136004c fix: enable overflow (#24560) 2025-10-25 16:18:59 +00:00
spandevandVolnei Munhoz bb79368b2d fix: empty event types page doesn't show no more results anymore (#24655)
* fix: empty event types page doesn't show no more results anymore

* update: conditional rendering of results button

---------

Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
2025-10-25 15:32:44 +00:00
chauhan_sandDevin AI 70825dd138 fix: define process.env variables in atoms vite config to prevent 'process is undefined' errors (#24665)
- Add all process.env variables used by Next.js code to the define section
- This ensures they are replaced at build time and don't cause runtime errors in Vite projects
- Fixes #24213

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-25 20:38:48 +05:30
Volnei Munhoz 05c399db9c lint stage not loading config from file - using root instead (#24671) 2025-10-25 10:51:18 +00:00
Morgan 57776b829b fix: old rec booking endpoint api v2 (#24669)
* fix: old rec booking endpoint api v2

* fixup! fix: old rec booking endpoint api v2

* fixup! fixup! fix: old rec booking endpoint api v2
2025-10-25 07:24:48 -03:00
Kartik 7fd28c39f3 fix: improve mobile positioning for link dialog in form builder checkbox editor (#24268)
* Updated positionEditorElement function to respect viewport boundaries

* cleanup
2025-10-25 10:03:22 +00:00
Pallav ff17aa79fc updated animation for drop down menu (#23650) 2025-10-25 09:23:50 +00:00
emrysal 578be32936 chore: release v5.8.5
Post release / release (push) Canceled after 0s
2025-10-25 00:25:46 +00:00
Dhairyashil ShindeandAnik Dhabal Babu b183239ff5 chore: add autocomplete for inputs - name, phone, location (#24422)
* add autocomplete for inputs - name, phone, location

* Addressed coderabits comments

* autocomplete for email and url

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-10-24 21:36:41 +00:00
Pallav 013e614397 fix: handle empty location in event types (#24342) 2025-10-24 17:58:34 -03:00
sean-brydonandAnik Dhabal Babu 6843ced3db Use correct memebrships for permission calculations (#24653)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-10-24 07:44:47 +01:00
Anik Dhabal Babu 7318015a6e fix: Delete all workflow reminders when workflow is deleted (#24615) 2025-10-24 02:54:08 +00:00
emrysal fc4d0a6aa3 chore: release v5.8.4
Post release / release (push) Canceled after 0s
2025-10-24 00:27:10 +00:00
sean-brydon 8e0cdf3671 feat: Onboarding V3 (#24299)
* feat: redirect to new onboarding flow

* Getting started

* Brand details

* Preview organization brands

* Orgs team pages

* Invite team steps

* Move to global zustand store

* Few darkmdoe fixes

* Wip onboarding + stripe flow

* Default plan state

Server Action for gettting slug satus of org

* Remove onboardingId

* Confirmation prompt

* Update old onboarding flow handlers to handle new fields

* update onboarding hook

* Filter out organization section for none -company emails

* Match placeholders to users domain

* Drop migration

* Wip new onboarding intent

* WIP flow for self-hosted. Same service call just split logic

* WIP

* Add TODO

* Use onboarding user type instead of trpc session

* WIP

* WIP

* pass role and team name from onboarding to save in schema

* Add test to ensure role + name + team are persisted into onboarding table

* migrate roles to enum values

* Update ENUM

* Fix type error

* Redirect if flag is disabled

* Revert packages

* Revert all packages/* changes to original branch point

* Layout fixes + design

* Add slugify

* Support saving brand,logo and banner

* Cleanup

* iMobile fixes

* More mobile and darkmdoe fixes

* Add I18n

* Fix lock file

* Fix types

* Fix types errors

* Copy changes
2025-10-23 20:01:54 +01:00
Eunjae LeeandDevin AI fcd9480f9f fix: update seed-insights.ts Prisma connection handling for v6.16 (#24660)
* fix: update seed-insights.ts to use correct Prisma import and connection handling

- Change prisma import from named export to default export to match Prisma v6.16 changes
- Update promise chain from .then().catch() to .catch().finally() for proper connection cleanup
- Comment out duplicate runSeed() call that was causing the script to execute twice
- Fixes TypeError: Cannot read properties of undefined (reading '$connect')

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

* fix implementation

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-23 15:59:05 +00:00
Eunjae LeeandDevin AI 09ddbe886f refactor: make DataTableProvider framework-agnostic by requiring tableIdentifier (#24513)
* refactor: make DataTableProvider framework-agnostic by requiring tableIdentifier

- Remove Next.js usePathname dependency from DataTableProvider
- Make tableIdentifier a required prop instead of optional
- Update all usages to provide explicit tableIdentifier values
- This makes DataTableProvider usable in non-Next.js contexts

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

* refactor: use usePathname at usage sites instead of hardcoding tableIdentifier

- Add validation in DataTableProvider for empty/nullish tableIdentifier
- Use usePathname() in Next.js apps to pass pathname as tableIdentifier
- Use descriptive identifiers for non-Next.js package components
- This keeps DataTableProvider framework-agnostic while allowing Next.js apps to use pathname

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

* use pathname instead of hard-coded identifiers

* change type of tableIdentifier

* simplify

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-23 15:32:16 +02:00
Eunjae LeeandDevin AI 4ac8a51bc2 feat: migrate nuqs from v1.20.0 to v2.7.2 (#24514)
* feat: migrate nuqs from v1.20.0 to v2.7.2

- Update nuqs package version in apps/web/package.json
- Add NuqsAdapter wrapper in root layout for App Router compatibility
- Server-side imports already using correct 'nuqs/server' path
- All existing parsers and usage patterns remain compatible
- No breaking changes to application functionality
- Fix unnecessary escape characters in fontFamily regex

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

* move adapter

* revert

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-23 10:24:22 -03:00
82951cd04e chore: [Booking Flow Refactor - 5] Move post booking things to separate service BookingEventHandlerService starting with HashedLink usage handling (#24025)
* chore: Move post booking stuff to separate service

* refactor: improve ISP compliance in BookingEventHandlerService

- Refactor updatePrivateLinkUsage to only accept hashedLink parameter instead of full payload
- Remove shouldProcess function and inline isDryRun check for better clarity
- Addresses feedback from PR #24025

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

* define only what is used

* Define hashed-link-service as well in api-v2

* fix: export HashedLinkService through platform-libraries

- Add HashedLinkService to platform-libraries/private-links.ts
- Update API V2 to import from platform library instead of direct path
- Fixes MODULE_NOT_FOUND error in API V2 build

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: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-10-23 15:05:55 +03:00
ae87fdcc9f perf: rename eventTypes.heavy to eventTypesHeavy (#24630)
Post release / release (push) Canceled after 0s
* Rename eventTypes.heavy to eventTypesHeavy

* Update eventTypes lib types

* Remove trpc and trpc client heavy resolve endpoint

* Update shared file

* Fix comments

* Rename path

* Fix wait for URL path

* Fix API endpoint in event-types.e2e.ts test

* fix: Import WorkflowType from enums instead of client

The WorkflowType enum should be imported from @calcom/prisma/enums
rather than @calcom/prisma/client. This was causing integration tests
to fail with 'Cannot read properties of undefined (reading EVENT_TYPE)'
because the enum wasn't being properly exported from the client.

This fixes the remaining integration test failures after the Prisma 6.16
upgrade.

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

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: anik@cal.com <adhabal2002@gmail.com>
2025-10-23 07:32:06 -03:00
emrysal 35e29c4bee chore: release v5.8.3 2025-10-23 09:21:19 +00:00
emrysal 387a1704f3 chore: release v5.8.3 2025-10-23 01:36:11 +00:00
Dhairyashil Shinde 3fb8c5bbc5 fix: impersonate correct user using email (#24648) 2025-10-23 06:42:20 +05:30
Anik Dhabal BabuandDevin AI 626585f5d0 fix: Handle empty array in Prisma OR conditions for Prisma 6.16 (#24638)
After upgrading to Prisma 6.16.0, empty arrays in 'in' clauses within OR
conditions are now optimized to return no results. This caused integration
tests to fail when querying memberships with empty memberUserIds arrays.

Changes:
- Dynamically build OR conditions to exclude empty 'in' arrays
- Simplify getAllAdminMemberships to use 'in' instead of OR for roles
- Fixes retrieveScopedAccessibleUsers and getAccessibleUsers queries

Fixes integration test failures:
- apps/api/v1/test/lib/utils/retrieveScopedAccessibleUsers.integration-test.ts
- apps/api/v1/test/lib/bookings/[id]/_patch.integration-test.ts

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-22 13:00:26 -03:00
Joe Au-Yeung 4d3727db12 Default fetchCrm to true for getRoutedUrl (#24635) 2025-10-22 14:44:42 +00:00
emrysal f6641d5173 chore: release v5.8.3 2025-10-22 13:59:25 +00:00
Keith Williams f9b21206a2 fix: Don't run migrations for bundle analysis (#24634) 2025-10-22 13:25:51 +00:00
Keith Williams 96821c0026 fix: CI env var blocking migrations (#24628)
* fix: CI env var blocking migrations

* Remove the DIRECT_DATABASE_URL from production build to not trigger migrations
2025-10-22 12:50:46 +00:00
Keith Williams 3d5e4754af chore: Remove DIRECT_DATABASE_URL from production build in CI (#24632) 2025-10-22 12:22:56 +00:00
613e9f0788 fix: invalidate old password reset tokens when new one is requested (#24607)
* fix: invalidate old password reset tokens when new one is requested

Security fix: Previously, old password reset tokens remained valid
even after requesting a new one, creating a potential account takeover
vulnerability. This change ensures that when a user requests a new
password reset link, all previous valid tokens for that email are
immediately invalidated.

Changes:
- Expire all existing valid tokens before creating new one
- Add E2E test to verify old tokens are invalidated
- Prevent potential account takeover scenario

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

* Clean up code by removing blank line

Removed unnecessary blank line in forgot-password.e2e.ts.

* test: fix strict mode violation in password reset test

Use getByRole to specifically target the heading element instead of
text locator which was matching both the heading and button.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-22 12:35:07 +01:00
Saurabh SinghandKeith Williams 1261247e94 chore: added missing packages/prisma/client/* (#24624)
* addedd missing packages/prisma/client/*

* Keep packages/prisma/client/index.ts

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-10-22 10:48:08 +00:00
2ad843ffbb test: fix few flaky e2e tests (#24564)
* Add consistent ordering to seat references queries and fix ESLint warnings

- Add orderBy clause to seatsReferences in bookings list query
- Add orderBy clause to seatsReferences in getBookingToDelete query
- Ensures seat references are always returned in consistent order (by ID ascending)
- Prevents race conditions when matching seat references by user email
- Fix ESLint no-unused-expressions warnings by converting to if statements

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

* update

* Add ordering to attendee queries in tests for consistency

- Add orderBy clause to all attendee queries in booking-seats.e2e.ts
- Ensures attendees are queried in consistent order before mapping to seats
- Prevents mismatch between test expectations and actual seat order
- Fix unused variable warning

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

* Fix seat ordering to use attendeeId instead of seat ID

- Order seats by attendeeId in both bookings list and getBookingToDelete queries
- This ensures seats are returned in the same order as attendees were created
- Fixes mismatch where seat IDs and attendee IDs use different auto-increment sequences
- Also add attendeeId to seatsReferences select for consistency
- Fix ESLint warnings by converting && expressions to if statements

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

* Remove orderBy from bookingSeats queries

* update

* fix another test

* fix flakes

* Fix test to order seats by attendeeId to match backend ordering

The test was ordering seats by seat ID, but the backend orders by attendeeId. This caused flakiness because seat IDs and attendee IDs use different auto-increment sequences and can be in different orders. Now both test and backend use the same ordering (attendeeId), ensuring references[1] consistently refers to the second attendee's seat.

Also remove unused 'user' variable from test.

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

* Change orderBy from attendeeId to id in query

* Implement seatReferenceUid check in booking flow

Added logic to handle missing seatReferenceUid by reloading the page and retrying the action.

* Reduce wait time and update visibility check

* Update visibility check for 'Seats available' text

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-22 11:14:15 +05:30
6923b97cd2 feat: upgrade Prisma to 6.16.0 with no-rust engine (#23816)
* feat: upgrade Prisma to 6.16.0 with no-rust engine

- Update Prisma packages to 6.16.0
- Add PostgreSQL adapter dependency
- Configure engineType: 'client' and provider: 'prisma-client' in schema
- Update Prisma client instantiation with PostgreSQL adapter
- Remove binaryTargets from generators (not needed with library engine)
- Fix schema view issue by removing @id decorator from BookingTimeStatusDenormalized
- Fix ESLint warning by removing non-null assertion

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

* Web app running but types wrecked

* web app running but build and type issues

* Removed the connection pool

* Fixed zod type issue

* Fixed types in booking reference extension

* Fixed test issues

* Type checks passing it seems

* Using cjs as moduleFormat

* Fixing Prisma undefined

* fix: update prismock initialization for Prisma 6.16 compatibility

- Add @prisma/internals dependency for getDMMF()
- Restructure prismock initialization to use createPrismock() with DMMF
- Create Proxy that's returned from mock factory for proper spy support
- Fixes 89 failing unit tests with 'Cannot read properties of undefined (reading datamodel)' error
- Based on workaround from prismock issue #1482

All unit tests now pass (375 test files, 3323 tests passed)

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

* fix: cast serviceAccountKey to Prisma.InputJsonValue in bookingScenario.ts

- Apply type cast at lines 2493 and 2535
- Fixes type errors from Prisma 6.16 upgrade
- Follows established pattern from delegationCredential.ts
- Add eslint-disable for pre-existing any types
- Rename unused appStoreLookupKey parameter to satisfy lint
- All 3323 tests passing

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

* chore: remove whitespace-only lines from bookingScenario.ts

- Remove blank lines where eslint-disable comments were replaced
- Cleanup from pre-commit hook formatting

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

* Update is-prisma-available-check.ts

* fix: remove datasources config when using Prisma Driver Adapters

- Update customPrisma to create new adapter when datasources URL is provided
- Remove datasources config from API v2 Prisma services (already in adapter)
- Fixes 'Custom datasource configuration is not compatible with Prisma Driver Adapters' error

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

* fix: use Pool instances for PrismaPg adapters in index.ts

- Create Pool instance before passing to PrismaPg adapter
- Update customPrisma to create Pool for custom connection strings
- Matches working pattern from API v2 services
- Fixes 'Invalid `prisma.$queryRawUnsafe()` invocation' error

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

* Not using queryRawUnsafe

* Trying anything at this point

* Make sure the DB is ready first

* Don't auto run migrations in CI mode

* Revert "Make sure the DB is ready first"

This reverts commit 2b20bd45c974f3d7e07d8b904bc7fcdae37cce03.

* Dynamic import of prisma

* Commenting where it seems to break

* Backwards compatability for API v2

* fix: add explicit type annotations for map callbacks in API v2

- Add type annotation for map parameter in memberships.repository.ts
- Add type annotation for map parameter in stripe.service.ts
- Fixes implicit 'any' type errors from stricter Prisma 6.16.0 type inference

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

* fix: add explicit type annotations for API v2 map callbacks

- users.repository.ts:292: add Profile & { user: User } type
- memberships.service.ts:19-20: add Membership type to filter callbacks

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

* fix: add explicit type annotation for attributeToUser in organizations-users.repository.ts

- organizations-users.repository.ts:63: add AttributeToUser with nested relations type

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

* fix: add explicit Membership type annotations in teams.repository.ts

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

* fix: use API v2 dedicated Prisma client to support adapter in PrismaClientOptions

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

* Running API v2 build commands together so they all get the space size var

* Fixing Maximum call depth exceeded error

* fixed type issues

* Trying to make the seed more stable

* Revert "Trying to make the seed more stable"

This reverts commit 1fd4495e6af7acd7981cda7dedec3168979b0e9d.

* Fixed path to prisma client

* Fixed type check

* Fix eslint warnings

* fix: externalize @prisma/adapter-pg and pg in platform-libraries Vite config

- Add @prisma/adapter-pg and pg to external dependencies list
- Add corresponding globals for these packages
- Fix Prisma client aliases to point to packages/prisma/client instead of node_modules
- Add Node.js resolve conditions to prefer Node.js exports
- Keep commonjsOptions.include for proper CommonJS transformation
- Add eslint-disable for __dirname in Vite config file
- Remove problematic prettier/prettier eslint comment

This fixes the 'Extensions.defineExtension is unable to run in this browser environment' error when running yarn generate-swagger in apps/api/v2 after upgrading to Prisma v6.16 with the no-rust engine approach.

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

* fix: update Prisma imports in API v2 services to use package path

- Change imports from '../../../generated/prisma/client' to '@calcom/prisma/client'
- Fixes CI error: Cannot find module '../../../../../packages/prisma/generated/prisma/client.ts'
- Aligns with backwards compatibility re-export structure after Prisma v6.16 upgrade

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

* fix: remove .ts extension from Prisma client path mapping in tsconfig

- Remove file extension from @calcom/prisma/client path mapping
- Fixes runtime error: Cannot find module '../../../../../packages/prisma/generated/prisma/client.ts'
- TypeScript path mappings should not include file extensions per best practices
- Allows Node.js to correctly resolve to .js files at runtime

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

* fix: resolve Prisma 6.16.0 type incompatibilities in bookingScenario tests

- Changed InputPayment.data type from PaymentData to Prisma.InputJsonValue
- Changed createCredentials key parameter from JsonValue to InputJsonValue
- Removed unused PaymentData type definition
- Resolves type errors at lines 709 and 1088 without using 'as any' casts

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

* fix: remove non-existent Watchlist fields from test fixtures

- Remove createdById from isLockedOrBlocked.test.ts (lines 15, 20)
- Remove severity and createdById from _post.test.ts (line 110)
- These fields don't exist in Watchlist model schema after Prisma 6.16.0 upgrade
- Resolves TS2353 errors without using 'as any' casts

Relates to PR #23816

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

* fix: api v2 imports generated prisma and platform libraries

* fix: resolve type errors from Prisma 6.16 upgrade

- Add missing markdownToSafeHTML import in AppCard.tsx
- Fix organizationId null handling in fresh-booking.test.ts
- Remove non-existent createdById field from Watchlist test utils

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

* Put back some external rollups

* Added back the resolve conditions

* Stop using Pool directly

* chore: remove prisma bookingReferenceExtension and update calls

* fix: organizations-admin-not-team-member-event-types.e2e-spec.ts

* chore: bring back POOL in api v2 prisma clients

* chore: remove Pool but await connect

* fixup! chore: remove Pool but await connect

* chore: bring back Pool on all clients

* chore: end pool manually

* chore: test with pool max 1

* chore: e2e test prisma max  pool of 1 connection

* chore: give more control over pool for prisma module with env

* remove pool from base prisma client

* chore: prisma client in libraries use pool

* Fixed types

* chore: log pool events and improve pooling

* Fixing some types and tests

* Changing the parsing of USE_POOL

* fix: ensure Prisma client is connected before seeding to prevent transaction errors

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

* chore: adjust pools

* chore: add process.env.USE_POOL to libraries vite config

* fix: v1 _patch reference check bookingRef on the booking find

* fix: v1 get references deleted null for system admin

* test: add integration tests for bookingReference soft-delete behavior

- Add bookingReference.integration-test.ts to test repository methods
- Add handleDeleteCredential.integration-test.ts to test credential deletion cascade
- Add booking-references.integration-test.ts for API v1 integration tests
- All tests verify soft-delete behavior without using mocks
- Tests use real database operations to ensure soft-deleted records persist
- Cover scenarios: replacing references, credential deletion, querying with filters

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

* refactor: convert booking-references test to actual API endpoint testing

- Modified _get.ts to export handler function for testing
- Refactored integration test to call API handler instead of directly testing Prisma
- Added timestamps to test data to avoid conflicts
- Tests now verify API layer correctly filters soft-deleted references
- All 4 tests passing

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

* fix: add explicit prisma.$connect() call to seed-insights script

With Prisma 6.16 and the PostgreSQL adapter, scripts need to explicitly call $connect() before running database operations to ensure the connection pool is properly initialized. This prevents 'Transaction already closed' errors.

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

* fix: add $connect() to main() execution in seed-insights

Both main() and createPerformanceData() entry points need explicit prisma.$connect() calls with the Prisma 6.16 PostgreSQL adapter.

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

* fix: always use connection pool for Prisma PostgreSQL adapter

Enable connection pooling by default for the Prisma adapter to prevent
transaction state issues during seed operations. Without a pool, each
operation creates a new connection which can lead to 'Transaction already
closed' errors during heavy database operations like seeding.

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

* Revert "fix: always use connection pool for Prisma PostgreSQL adapter"

This reverts commit 6724bb08e42bc0a94846069de83b04db0aeb8e8b.

* fix: enable connection pool for db-seed in cache-db action

Set USE_POOL=true when running yarn db-seed to use connection pooling
with the Prisma PostgreSQL adapter. This prevents 'Transaction already
closed' errors during seeding by maintaining stable database connections.

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

* fix: add safety check for undefined ownerForEvent in seed script

Prevent 'Cannot read properties of undefined' error when orgMembersInDBWithProfileId
is empty. This can happen if organization members fail to create or when there's a
duplicate constraint violation causing early return.

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

* fix: v1 _patch reference check bookingRef

* fix: increase pool size and add timeout settings to prevent transaction errors

- Increase max connections from 5 to 10
- Add connectionTimeoutMillis: 30000 (30 seconds)
- Add statement_timeout: 60000 (60 seconds)

These settings help prevent 'Unknown transaction status' errors during
heavy database operations like seeding by giving transactions more time
to complete and allowing more concurrent connections.

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

* Revert "fix: increase pool size and add timeout settings to prevent transaction errors"

This reverts commit 148264f1f1861dfb09a082937a3e2b49e78fc41a.

* fix: remove standalone execution in seed-app-store to prevent premature disconnect

The seed-app-store.ts file had a standalone main() call at the bottom
that would execute immediately when imported, including a prisma.$disconnect()
in its .finally() block.

This caused issues because:
1. seed.ts imports and calls mainAppStore()
2. The import triggers the standalone main() execution
3. This standalone execution disconnects prisma after completion
4. seed.ts then tries to call mainHugeEventTypesSeed() but prisma is disconnected
5. This leads to 'Unknown transaction status' errors

Fixed by removing the standalone execution since mainAppStore() is already
called programmatically from seed.ts which manages the connection lifecycle.

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

* fix: use require.main check to prevent premature disconnect when imported

Added require.main === module check so seed-app-store.ts:
- Runs standalone with proper connection management when executed directly
  via 'yarn seed-app-store' or 'ts-node seed-app-store.ts'
- Does NOT run standalone when imported as a module by seed.ts,
  preventing premature prisma disconnect

This fixes 'Unknown transaction status' errors while maintaining
backward compatibility for direct execution.

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

* fix: seed apps before creating users to prevent foreign key constraint violation

Reordered seeding operations to call mainAppStore() before main() because:
- main() creates users with credentials that reference apps via appId foreign key
- mainAppStore() seeds the App table with app records
- Apps must exist before credentials can reference them

This fixes the 'Foreign key constraint violated on Credential_appId_fkey' error
that occurred when creating credentials before the apps they reference existed.

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

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

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

* Removing functional changes of deleted: null

* Apply suggestion from @keithwillcode

* refactor: move seedAppData call to bottom of main() in seed.ts

Moved seedAppData() call from seed-app-store.ts to the bottom of main()
in seed.ts to ensure the 'pro' user is created before attempting to
create routing form data for them.

Changes:
- Exported seedAppData function from seed-app-store.ts
- Removed seedAppData() call from the main() export in seed-app-store.ts
- Added seedAppData() call at the bottom of main() in seed.ts
- Updated standalone execution in seed-app-store.ts to still call
  seedAppData() when run directly via 'yarn seed-app-store'

This ensures proper ordering: apps seeded → users created → routing
form data created for existing users.

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

* refactor: move routing form seeding from seed-app-store.ts to seed.ts

Moved the routing form seeding logic (previously in seedAppData function)
from seed-app-store.ts to be inline at the bottom of main() in seed.ts.

This ensures the 'pro' user is created before attempting to create routing
form data for them.

Changes:
- Removed seedAppData function and seededForm export from seed-app-store.ts
- Removed import of seedAppData from seed.ts
- Added routing form seeding logic inline at bottom of main() in seed.ts

Seeding order: apps → users (including 'pro') → routing forms

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

* fix: add deleted: null filter to bookingReference update operations

- Add deleted: null filter to API v1 PATCH endpoint to prevent updating soft-deleted booking references
- Add deleted: null filter to DailyVideo updateMeetingTokenIfExpired and setEnableRecordingUIAndUserIdForOrganizer
- Add comprehensive test coverage for PATCH endpoint soft-delete behavior
- Tests verify that soft-deleted booking references cannot be updated
- Tests verify that only active booking references can be updated successfully

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

* revert: remove deleted: null filters to preserve existing functionality

Per @keithwillcode's feedback, reverting the soft-delete filtering changes to preserve existing functionality in this PR. This PR should focus only on the Prisma upgrade itself.

- Reverted API v1 PATCH endpoint change
- Reverted DailyVideo adapter changes (updateMeetingTokenIfExpired and setEnableRecordingUIAndUserIdForOrganizer)
- Removed test file that was added for soft-delete behavior testing

Addresses comments:
- https://github.com/calcom/cal.com/pull/23816#discussion_r2448854197
- https://github.com/calcom/cal.com/pull/23816#discussion_r2448860594
- https://github.com/calcom/cal.com/pull/23816#discussion_r2448860833

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

* test: restore and update booking reference tests to match existing functionality

Updated tests to verify existing behavior where PATCH endpoint can update
booking references regardless of their deleted status. This matches the
current implementation after reverting the deleted: null filters.

Changes:
- Restored test file that was previously deleted
- Updated PATCH tests to expect successful updates of soft-deleted references
- Renamed test suite to 'Existing functionality' to clarify intent
- Tests now verify that the PATCH endpoint preserves existing behavior

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

* test: rename booking-references test to integration-test

The test requires a database connection and should run in the integration
test job, not the unit test job. Renamed from .test.ts to .integration-test.ts
to match the repository's testing conventions.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-10-21 20:02:03 +01:00
e639237088 feat: SMS workflow action for form triggers #3 (#23673)
* allow routing forms for activeOn

* use repository function to get routing forms

* remove unnecessary code

* adjust logic in update handler

* add triggers to api v2

* remvoe unused file

* rename to getAcitveOnOptions handler

* remove routingFormOptions handler

* clean up getActiveOnOptions

* refactor WorkflowService

* remove logs

* remove unused

* fix: type check

* fix: missed before after events for recurring

* fix: calendarEvent handleMarkNoShow

* fix error message

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* don't query disabled routing forms

* create tasker function

* add tasker code

* move isFormTrigger function

* small adjustments + todo comments

* remove email to host action for form triggers

* throw trpc error if email to host is added as step

* fix dialog on how to use form responses as variables

* remove add variable dropdown for form triggers

* remove form workfows in event workflows tab

* improvements for workflow logic on form submission

* review fixes

* base setup for seperate schedule functions (evt and form)

* add missing BOOKING_PAID workflow trigger

* fix pathname

* fix: test for BOOKING_REQUESTED

* fix activeOn ids

* pass hideBranding and smsReminderNumber

* adjustments to reminderScheduler

* create empty scheduelForForm functions

* pass locale and timezone with form user

* pass formData instead of responses

* pass timeFormat and locale

* reusable function for email sending and reminder creation

* implement scheduleEmailReminderForForm

* remove added editor field from merge conflict

* don't support cal.ai action with form triggers

* throw bad request if form trigger and cal.ai is combined

* add tests for scheduleFormWorkflows

* add form submission tests

* remove form response varibe info

* clean up workflow actions

* fixes for getting template options

* pass triggerType to getAllWorkflows

* move reusable logic to scheduleSMSReminder

* add formdata to param type

* type fixes for text reminder managers

* implement scheduleSMSReminderForForm

* fix import

* fix isAuthorizedToAddActiveOnIds

* disble whatsapp action

* implement triggerFormSubmittedNoEventWorkflow

* code clean up

* Merge branch 'devin/1755107037-add-workflow-triggers' into feat/routing-form-workflow-triggers

* fix type errors

* remove async from getSubmitterEmail

* fix type errors

* revert cal.ai changes

* fix type error

* add sublogger

* code clean up

* fix type errors

* remove label for attendee whatsapp action

* code clean up

* fixes saving teams on org workflows

* fix type error

* code improvements for activeOn ids

* Revert "code improvements for activeOn ids"

This reverts commit 0a3590a4e2ce541b17d63483ad86ed458795a6a3.

* improve variable name

* fix unit tests

* small fixes

* type fixes

* remove unused translation keys

* fix merge conflict issues

* code clean up

* remove SMS action support

* remove more SMS code

* add missing imports

* set custom template for form action

* type fixes

* fix tasker endpoint

* fix duplicate check

* fix workfows.test.ts

* use repository funciton to getHideBranding

* add  back SMS action

* add back changes to smsReminderManager

* code clean up

* fix hasDuplicateSubmission

* code clean up

* select only needed properties

* remove repository functions

* Revert "remove repository functions"

This reverts commit 7aa47b1c59c9abd7f964ebf26f746934c53a44f4.

* add scheduleWorkflows function

* Revert "add scheduleWorkflows function"

This reverts commit fe5db4fe3b65e2743c95475d585300cab98beed7.

* move type to /types

* Revert "move type to /types"

This reverts commit 91e0152154594b3772a801a426260068a8ccea54.

* revert changes causing type errors

* remove import

* remove unused import

* Revert "remove unused import"

This reverts commit 1916768c875ea5d0ac5598ccb8f9c796c5622dc9.

* revert changed from attempt to fix type errors

* pass object to gt all workflows

* fix isAuthorized check

* trigger filtering

* remove form submitted no event booked code

* remove form submitted no event from schema

* remove more code

* remove test

* fixes

* add getSubmitterEmail function

* add trigger

* small fixes

* add missing workflow DTOs

* small fixes

* use activeOnWithChildren

* fix active on when switching trigger type

* remove add variable dropdown

* add getAllWorkflowsFromRoutingForm to WorkflowService

* fix error caused by undefined evt

* fix type error

* fix type error

* fix tests

* code clean up

* final fixes and clean up

* remove console.log

* remove template text form from triggers

* add routing form repoditory function

* fix bug with key

* add comments

* fix test

* add missing await

* use predefined FormSubmissionData type

* add .trim() to sms message

* pass contextData instead

* add missing trigger in update-workflow.input.ts

* ForEvt and ForForm function for aiPhoneCallManager

* chore: add support for form workflows on api v2

* fixup! chore: add support for form workflows on api v2

* use only repository functions in update handler

* move all prisma queries from list.handler

* review suggestions

* chore: handle workflows api v2

* chore: handle workflows api v2, split in 2 endpoints

* fix workflow step creation

* remove connect agent and fixes types

* add type to workflow

* chore: use workflow type in apiv2 WorkflowsOutputService

* update worklfow type on update

* chore: use workflow type in apiv2 WorkflowsOutputService

* fix template body for torm trigger

* some UI fixes for email subject/body

* resetting email body when changing form triggers

* use type field to query workflows

* clean up all old active on values

* remove responseId from all funciton calls

* remove undefined from updateTemplate

* refactor: split routing form and event-type workflows code

* refactor: split routing form and event-type workflows code

* fix template  text when adding action

* chore: don't rename WorkflowActivationDto to avoid ci blocking

* refine update schedule to use only allowed actions

* fix type error

* don't allow whatsapp action with form trigger

* fix type error

* return early if activeOn array is empty

* fix: from step type in BaseFormWorkflowStepDto

* fixup! fix: from step type in BaseFormWorkflowStepDto

* api v2 updates

* move all prisma calls to repository (service/workflows.ts)

* use FORM_TRIGGER_WORKFLOW_EVENTS for form queries

* use userRepository

* use FORM_TRIGGER_WORKFLOW_EVENTS in isFormTrigger

* code clean up

* code clean up

* use repository functions in formSubmissionValidation.ts

* remove action check in update handler

* add back trpc  import

* fix agent repository functions

* add SMS actions to allowed form action constants

* remove unsued import

* fixes for offset api v2

* add missing responseId

* fix failing test

* fix failing test

* remove unused imports

* chore: handle sms step action for form worklfow in dtos

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-10-21 15:28:10 +00:00
Dhairyashil Shinde 5bc1fbb8c2 fix: Allow booking details view for round robin with no hosts (#24250)
* Fix: Allow booking details view for round robin with no hosts

* Fix: Allow booking details view for round robin with no hosts

* fix: include the missing fields with defaults

* fix: include the missing fields with defaults
2025-10-21 14:04:42 +00:00
Volnei MunhozandDevin AI e310931417 fix: remove circular dependency in packages/prisma (#24596)
- Replaced DeploymentRepository import with inline implementation
- Created InlineDeploymentRepository class implementing IDeploymentRepository
- Breaks circular dependency: deployment.ts -> prisma/index.ts -> usage-tracking.ts -> deployment.ts
- Verified with madge: no circular dependencies found

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-21 13:14:15 +00:00
Alex van Andel 17944311c3 chore: Remove inMaintenanceMode from APIv1 for increased robustness (#24576) 2025-10-21 12:58:31 +00:00
Volnei MunhozandDevin AI d82f87aa2a perf: Remove circular dependencies - wip (#24586)
* wip

* refactor(emails): remove circular dependencies from packages/emails

- Extract renderEmail imports to use direct path (../src/renderEmail) instead of index
- Create utility files for shared types and functions:
  - lib/utils/team-invite-utils.ts: TeamInvite type and utility functions
  - lib/utils/booking-redirect-types.ts: IBookingRedirect type
  - lib/utils/email-types.ts: OrganizationCreation and EmailVerifyCode types
  - lib/utils/date-formatting.ts: getFormattedDate utility function
- Update all template files to import renderEmail directly from ../src/renderEmail
- Update React template components to import from utility files instead of class templates
- This eliminates all circular dependencies within packages/emails (reduced from 234 to 0 internal circular deps)

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

* refactor(trpc): remove circular dependency in routing-forms

- Extract ZFormByResponseIdInputSchema to separate schema file
- Create getResponseWithFormFields.schema.ts to break circular dependency
- Update imports in _router.ts and getResponseWithFormFields.handler.ts
- Fix eslint warnings by updating deprecated rule names
- This eliminates the circular dependency within packages/trpc (reduced from 1 to 0 internal circular deps)

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

* fix(emails): correct TeamInvite type definition

- Make isExistingUserMovedToOrg required (not optional)
- Make prevLink and newLink non-optional (string | null)
- Add back JSDoc comment for isAutoJoin field
- This fixes type check errors in CI

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

* wip

* Fixes types folder

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-21 12:24:47 +00:00
Volnei MunhozandDevin AI 6aa21aceb8 refactor: remove unused packages from transpiledPackages (#24595)
Remove @calcom/embed-react and @calcom/embed-snippet from the
transpiledPackages configuration in apps/web/next.config.js as they
are not imported from source in apps/web.

Only @calcom/embed-core is actually used and requires transpilation
since it's imported from source paths like @calcom/embed-core/src/embed-iframe.

The other two packages have pre-built dist files and are not directly
imported in the web app, so Next.js doesn't need to transpile them.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-21 12:07:33 +00:00
Hariom Balhara 870ad57a7e Rename handleNewBooking to BookingCreateService (#23682) 2025-10-21 14:52:16 +03:00
fddee71829 refactor: extract bookings list and calendar views (#24486)
* refactor: extract bookings list and calendar views with nuqs state management

- Extract list-related code into BookingsListView component
- Create empty BookingsCalendarView component for future implementation
- Add nuqs query param state management for view toggle (defaults to list)
- Update bookings-listing-view to conditionally render views
- No visible changes to users (list view remains default)

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

* refactor: move data fetching logic to parent component

- Keep useFilterValue calls, trpc query, columns, flatData, bookingsToday, finalData, and table setup in parent component
- BookingsListView now receives data as props instead of fetching it
- This allows both list and calendar views to share the same data source

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

* fix: add customView column back to render booking items

The customView column was inadvertently removed during refactoring. This column is crucial as it renders the actual BookingListItem components, the "today" header, and the "next" header for the bookings list.

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

* refactor: rename files for better clarity

- Renamed bookings-listing-view.tsx → bookings-view.tsx (parent view)
- Renamed bookings-list-view.tsx → BookingsList.tsx (list component)
- Renamed bookings-calendar-view.tsx → BookingsCalendar.tsx (calendar component)
- Moved list and calendar components from views/ to components/ directory
- Updated all imports to reflect new structure

This creates a clearer hierarchy where -view is the orchestrator and components are the renderers.

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

* clean up implementation

* clean up types

* revert unnecessary changes

* Update packages/features/data-table/GUIDE.md

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: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-10-21 11:09:47 +00:00
David Johnson 6f15a5ab0f Add turbo package to dependencies (#24572)
This is needed due to Docker build errors indicating that turbo was not an available command.
2025-10-21 09:05:55 +00:00
Dhairyashil Shinde 1fd64c5d7b chore: improve text for booker active booking limit (#24526)
* chore: improve text for booker active booking limit

* update keys to new one for lingo .dev future recognition
2025-10-21 08:54:16 +00:00
Parship Chowdhury f8614f11ff fix: Description field duplicating content when editing Event Types (#24233)
* fix: description field duplicating content when editing Event Types

Signed-off-by: Parship Chowdhury <i.am.parship@gmail.com>

* optimal approach

Signed-off-by: Parship Chowdhury <i.am.parship@gmail.com>

* rm dead code

Signed-off-by: Parship Chowdhury <i.am.parship@gmail.com>

---------

Signed-off-by: Parship Chowdhury <i.am.parship@gmail.com>
2025-10-21 04:35:37 -03:00
Hariom Balhara 5e5e77236a chore: Add skeleton for BookingCancelService (#24038) 2025-10-21 12:29:17 +05:30
Daniel Adeboye 4cee53b07c feat: support northflank deployment (#24571) 2025-10-20 19:47:06 +00:00
Eunjae LeeandDevin AI c289fac008 refactor: make timezone a required prop in Calendar component (#24567)
* refactor: make timezone a required prop in Calendar component

- Remove useBookerTime hook from Calendar.tsx
- Add timezone as required prop in CalendarState type
- Update all parent components to pass timezone prop:
  - packages/features/bookings/Booker/components/LargeCalendar.tsx
  - packages/features/calendar-view/LargeCalendar.tsx
  - packages/features/troubleshooter/components/LargeCalendar.tsx
- All parent components now use useTimePreferences hook to get timezone

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

* use right implementation

* remove a line

* use user's timezone instead of booker timezone

* get timezone from calendar store

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-20 13:59:59 +00:00
Keith WilliamsandDevin AI 68f9e86cdd fix: remove circular dependency by moving useLastUsed hook to auth features (#24552)
- Move useLastUsed hook from packages/lib/hooks to packages/features/auth/lib/hooks
- Update imports in SAMLLogin.tsx and login-view.tsx
- Remove cross-package tsconfig include of ../trpc/types/router.d.ts from packages/lib/tsconfig.json

This fixes the issue where the IDE was unnecessarily initializing packages/ui/tsconfig.json
when navigating to definitions in packages/lib. The hook was importing from @calcom/ui/classNames,
creating a circular dependency since packages/ui depends on packages/lib.

By moving the hook to packages/features/auth where it's actually used, we eliminate the
circular dependency without adding tailwind dependencies to packages/lib.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-19 10:14:45 -03:00
Keith Williams 5da754db65 perf: Remove tRPC types from @calcom/lib (#24551) 2025-10-19 13:14:25 +00:00
781 changed files with 18868 additions and 3808 deletions
+1 -1
View File
@@ -111,7 +111,7 @@ The event types page UI components are located in `apps/web/modules/event-types/
Changes to shared UI patterns (like tab layouts and button alignments) need to be checked across multiple views to maintain consistency:
- Event types page layout: `apps/web/modules/event-types/views/event-types-listing-view.tsx`
- Bookings page layout: `apps/web/modules/bookings/views/bookings-listing-view.tsx`
- Bookings page layout: `apps/web/modules/bookings/views/bookings-view.tsx`
- Common elements like tabs, search bars, and filter buttons should maintain consistent alignment across views
## When working on workflow triggers or similar enum-based features in the Cal.com codebase
+36
View File
@@ -0,0 +1,36 @@
# Dependencies
node_modules
**/node_modules
# Build outputs
.next
**/.next
dist
**/dist
build
**/build
# Git
.git
.github
# Testing
coverage
**/coverage
test-results
**/test-results
.turbo
**/.turbo
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# OS
.DS_Store
Thumbs.db
# Examples and docs
docs
+3
View File
@@ -486,3 +486,6 @@ CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY=
SALESFORCE_GRAPHQL_DELAY_MS=500
SALESFORCE_GRAPHQL_MAX_DELAY_MS=2000
SALESFORCE_GRAPHQL_MAX_RETRIES=3
# Force Node.js to use UTC as process timezone
TZ=UTC
+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: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && yarn db-seed
- run: USE_POOL=true yarn db-seed
if: steps.cache-db.outputs.cache-hit != 'true'
shell: bash
- name: Postgres Dump Backup
@@ -0,0 +1,182 @@
name: "Docker Build and Test"
description: "Reusable action to build and test Docker images for Cal.com"
inputs:
platform:
description: "Target platform (linux/amd64 or arm64)"
required: true
platform-suffix:
description: "Suffix to add to image tags (e.g., -arm)"
required: false
default: ""
dockerhub-username:
description: "Docker Hub username"
required: true
dockerhub-token:
description: "Docker Hub token"
required: true
github-token:
description: "GitHub token for GHCR"
required: true
postgres-user:
description: "PostgreSQL user"
required: true
postgres-password:
description: "PostgreSQL password"
required: true
postgres-db:
description: "PostgreSQL database name"
required: true
database-host:
description: "Database host"
required: true
push-image:
description: "Whether to push the built image"
required: false
default: "false"
runs:
using: "composite"
steps:
- name: Log in to the Docker Hub registry
uses: docker/login-action@v3
with:
username: ${{ inputs.dockerhub-username }}
password: ${{ inputs.dockerhub-token }}
logout: true
- name: Log in to the Github Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ inputs.github-token }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
docker.io/calendso/calendso
docker.io/calcom/cal.com
ghcr.io/calcom/cal.com
flavor: |
latest=${{ !github.event.release.prerelease }}
suffix=${{ inputs.platform-suffix }}
- name: Copy env
shell: bash
run: |
grep -o '^[^#]*' .env.example > .env
cat .env >> $GITHUB_ENV
echo "DATABASE_HOST=localhost:5432" >> $GITHUB_ENV
eval $(sed -e '/^#/d' -e 's/^/export /' -e 's/$/;/' .env) ;
- name: Start database
shell: bash
run: |
docker compose up -d database
- name: Show database logs and container status
shell: bash
run: |
echo "--- Container Status ---"
docker compose ps database
echo "--- Container Logs ---"
docker compose logs database
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
network=container:database
buildkitd-flags: |
--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host
- name: Build image
id: docker_build
uses: docker/build-push-action@v6
with:
context: ./
file: ./Dockerfile
load: true
push: false
platforms: ${{ inputs.platform }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
NEXT_PUBLIC_API_V2_URL=http://localhost:5555/api/v2
NEXT_PUBLIC_LICENSE_CONSENT=agree
DATABASE_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
DATABASE_DIRECT_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
- name: Test runtime
shell: bash
run: |
tags="${{ steps.meta.outputs.tags }}"
IFS=',' read -ra ADDR <<< "$tags"
tag=${ADDR[0]}
docker run --rm --network stack \
-p 3000:3000 \
-e DATABASE_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@database/${{ inputs.postgres-db }} \
-e DATABASE_DIRECT_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@database/${{ inputs.postgres-db }} \
-e NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }} \
-e CALENDSO_ENCRYPTION_KEY=${{ env.CALENDSO_ENCRYPTION_KEY }} \
$tag &
server_pid=$!
echo "Waiting for the server to start..."
sleep 120
echo http://localhost:3000/auth/login
for i in {1..60}; do
echo "Checking server health ($i/60)..."
response=$(curl -o /dev/null -s -w "%{http_code}" http://localhost:3000/auth/login)
echo "HTTP Status Code: $response"
if [[ "$response" == "200" ]] || [[ "$response" == "307" ]]; then
echo "Server is healthy"
kill $server_pid
exit 0
fi
sleep 1
done
echo "Server health check failed"
kill $server_pid
exit 1
env:
NEXTAUTH_SECRET: "EI4qqDpcfdvf4A+0aQEEx8JjHxHSy4uWiZw/F32K+pA="
CALENDSO_ENCRYPTION_KEY: "0zfLtY99wjeLnsM7qsa8xsT+Q0oSgnOL"
- name: Push image
id: docker_push
uses: docker/build-push-action@v6
if: ${{ inputs.push-image == 'true' && !github.event.release.prerelease }}
with:
context: ./
file: ./Dockerfile
push: true
platforms: ${{ inputs.platform }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
NEXT_PUBLIC_API_V2_URL=http://localhost:5555/api/v2
NEXT_PUBLIC_LICENSE_CONSENT=agree
DATABASE_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
DATABASE_DIRECT_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
- name: Image digest
shell: bash
run: echo ${{ steps.docker_build.outputs.digest }}
- name: Cleanup
shell: bash
if: always()
run: |
docker compose down
@@ -56,8 +56,8 @@ jobs:
if: steps.cache-api-v2-build.outputs.cache-hit != 'true'
run: |
export NODE_OPTIONS="--max_old_space_size=8192"
yarn workspace @calcom/api-v2 run generate-schemas
rm -rf apps/api/v2/node_modules
yarn install
yarn workspace @calcom/api-v2 run generate-schemas &&
rm -rf apps/api/v2/node_modules &&
yarn install &&
yarn workspace @calcom/api-v2 run build
shell: bash
+16 -9
View File
@@ -6,23 +6,31 @@ on:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions: {} # remove permissions and set explicitly under "permissions:" below
permissions:
id-token: write # for NPM provenance - developers can verify where and how it was built
contents: write # to create release by changesets/action below
pull-requests: write # to create pull request by changesets/action below
jobs:
release:
if: github.repository == 'calcom/cal.com' # prevent this action from running on forks
permissions:
id-token: write # for NPM provenance - developers can verify where and how it was built
contents: write # to create release by changesets/action below
pull-requests: write # to create pull request by changesets/action below
name: Release
runs-on: buildjet-4vcpu-ubuntu-2204
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node and registry for OIDC
uses: actions/setup-node@v4
with:
node-version: "20.17.0"
registry-url: "https://registry.npmjs.org"
always-auth: false
- name: Setup node, yarn and install dependencies
uses: ./.github/actions/yarn-install
- name: Install npm at version supporting OIDC
run: npm install -g npm@11.5.1
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
@@ -33,8 +41,7 @@ jobs:
title: "chore: version packages" # PR title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: "true"
VITE_BOOKER_EMBED_OAUTH_CLIENT_ID: ${{ secrets.VITE_BOOKER_EMBED_OAUTH_CLIENT_ID }}
VITE_BOOKER_EMBED_API_URL: ${{ secrets.VITE_BOOKER_EMBED_API_URL }}
NEXT_PUBLIC_WEBAPP_URL: ${{ secrets.NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBAPP_URL: ${{ secrets.NEXT_PUBLIC_WEBAPP_URL }}
+4 -4
View File
@@ -20,8 +20,8 @@ jobs:
days-before-close: -1
days-before-issue-stale: 60
days-before-issue-close: -1
days-before-pr-stale: 14
days-before-pr-close: -1
stale-pr-message: "This PR is being marked as stale due to inactivity."
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
days-before-pr-stale: 7
days-before-pr-close: 14
stale-pr-message: "This PR has been marked as stale due to inactivity. If you're still working on it or need any help, please let us know or update the PR to keep it active."
close-pr-message: "This PR has been closed due to inactivity. Please feel free to reopen it if you'd like to continue the work."
operations-per-run: 100
@@ -16,7 +16,6 @@ env:
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
@@ -10,7 +10,6 @@ env:
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
@@ -36,7 +35,6 @@ env:
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
jobs:
build:
name: Build Web App
+91 -29
View File
@@ -1,10 +1,15 @@
name: "Release Docker"
on: # yamllint disable-line rule:truthy
release:
types:
- created
- published
env:
POSTGRES_USER: "unicorn_user"
POSTGRES_PASSWORD: "magical_password"
POSTGRES_DB: "calendso"
DATABASE_HOST: "database:5432"
on:
push:
tags:
- "v*"
# in case manual trigger is needed
workflow_dispatch:
inputs:
@@ -12,11 +17,9 @@ on: # yamllint disable-line rule:truthy
description: "v{Major}.{Minor}.{Patch}"
jobs:
release:
name: "Remote Release"
runs-on: "ubuntu-latest"
release-amd64:
name: "Release AMD64"
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: checkout
uses: actions/checkout@v4
@@ -24,25 +27,84 @@ jobs:
- name: "Determine tag"
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
- name: "Run remote release workflow"
uses: "actions/github-script@v6"
- name: Build and test Docker image
uses: ./.github/actions/docker-build-and-test
with:
# Requires a personal access token with Actions Read and write permissions on calcom/docker.
github-token: "${{ secrets.DOCKER_REPO_ACCESS_TOKEN }}"
script: |
try {
const response = await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: 'docker',
workflow_id: 'create-release.yaml',
ref: 'main',
inputs: {
"RELEASE_TAG": process.env.RELEASE_TAG
},
});
platform: "linux/amd64"
platform-suffix: ""
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
postgres-user: ${{ env.POSTGRES_USER }}
postgres-password: ${{ env.POSTGRES_PASSWORD }}
postgres-db: ${{ env.POSTGRES_DB }}
database-host: ${{ env.DATABASE_HOST }}
push-image: "true"
console.log(response);
} catch (error) {
console.error(error);
core.setFailed(error.message);
- name: Notify Slack on Success
if: success()
uses: slackapi/slack-github-action@v1.24.0
with:
payload: |
{
"text": ":large_green_circle: Workflow *${{ github.workflow }}* (AMD64) succeeded in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
- name: Notify Slack on Failure
if: failure()
uses: slackapi/slack-github-action@v1.24.0
with:
payload: |
{
"text": ":red_circle: Workflow *${{ github.workflow }}* (AMD64) failed in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
release-arm:
name: "Release ARM"
runs-on: ubuntu-24.04-arm
steps:
- name: checkout
uses: actions/checkout@v4
- name: "Determine tag"
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
- name: Build and test Docker image
uses: ./.github/actions/docker-build-and-test
with:
platform: "arm64"
platform-suffix: "-arm"
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
postgres-user: ${{ env.POSTGRES_USER }}
postgres-password: ${{ env.POSTGRES_PASSWORD }}
postgres-db: ${{ env.POSTGRES_DB }}
database-host: ${{ env.DATABASE_HOST }}
push-image: "true"
- name: Notify Slack on Success
if: success()
uses: slackapi/slack-github-action@v1.24.0
with:
payload: |
{
"text": ":large_green_circle: Workflow *${{ github.workflow }}* (ARM) succeeded in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
- name: Notify Slack on Failure
if: failure()
uses: slackapi/slack-github-action@v1.24.0
with:
payload: |
{
"text": ":red_circle: Workflow *${{ github.workflow }}* (ARM) failed in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
+3
View File
@@ -67,6 +67,9 @@ tsconfig.tsbuildinfo
# Prisma generated files
packages/prisma/client/*
!packages/prisma/client/index.ts
apps/api/v2/generated/prisma/*
packages/prisma/generated/prisma/*
packages/prisma/zod/**/*.ts
packages/prisma/enums
View File
View File
View File
View File
View File
View File
+2
View File
@@ -0,0 +1,2 @@
undecided:
- calcom-monorepo
+91
View File
@@ -0,0 +1,91 @@
FROM --platform=$BUILDPLATFORM node:20 AS builder
WORKDIR /calcom
## If we want to read any ENV variable from .env file, we need to first accept and pass it as an argument to the Dockerfile
ARG NEXT_PUBLIC_LICENSE_CONSENT
ARG NEXT_PUBLIC_WEBSITE_TERMS_URL
ARG NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL
ARG CALCOM_TELEMETRY_DISABLED
ARG DATABASE_URL
ARG NEXTAUTH_SECRET=secret
ARG CALENDSO_ENCRYPTION_KEY=secret
ARG MAX_OLD_SPACE_SIZE=4096
ARG NEXT_PUBLIC_API_V2_URL
ARG CSP_POLICY
## We need these variables as required by Next.js build to create rewrites
ARG NEXT_PUBLIC_SINGLE_ORG_SLUG
ARG ORGANIZATIONS_ENABLED
ENV NEXT_PUBLIC_WEBAPP_URL=http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER \
NEXT_PUBLIC_API_V2_URL=$NEXT_PUBLIC_API_V2_URL \
NEXT_PUBLIC_LICENSE_CONSENT=$NEXT_PUBLIC_LICENSE_CONSENT \
NEXT_PUBLIC_WEBSITE_TERMS_URL=$NEXT_PUBLIC_WEBSITE_TERMS_URL \
NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL=$NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL \
CALCOM_TELEMETRY_DISABLED=$CALCOM_TELEMETRY_DISABLED \
DATABASE_URL=$DATABASE_URL \
DATABASE_DIRECT_URL=$DATABASE_URL \
NEXTAUTH_SECRET=${NEXTAUTH_SECRET} \
CALENDSO_ENCRYPTION_KEY=${CALENDSO_ENCRYPTION_KEY} \
NEXT_PUBLIC_SINGLE_ORG_SLUG=$NEXT_PUBLIC_SINGLE_ORG_SLUG \
ORGANIZATIONS_ENABLED=$ORGANIZATIONS_ENABLED \
NODE_OPTIONS=--max-old-space-size=${MAX_OLD_SPACE_SIZE} \
BUILD_STANDALONE=true \
CSP_POLICY=$CSP_POLICY
COPY package.json yarn.lock .yarnrc.yml playwright.config.ts turbo.json i18n.json ./
COPY .yarn ./.yarn
COPY apps/web ./apps/web
COPY apps/api/v2 ./apps/api/v2
COPY packages ./packages
COPY tests ./tests
RUN yarn config set httpTimeout 1200000
RUN npx turbo prune --scope=@calcom/web --scope=@calcom/trpc --docker
RUN yarn install
# Build and make embed servable from web/public/embed folder
RUN yarn workspace @calcom/trpc run build
RUN yarn --cwd packages/embeds/embed-core workspace @calcom/embed-core run build
RUN yarn --cwd apps/web workspace @calcom/web run build
RUN rm -rf node_modules/.cache .yarn/cache apps/web/.next/cache
FROM node:20 AS builder-two
WORKDIR /calcom
ARG NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
ENV NODE_ENV=production
COPY package.json .yarnrc.yml turbo.json i18n.json ./
COPY .yarn ./.yarn
COPY --from=builder /calcom/yarn.lock ./yarn.lock
COPY --from=builder /calcom/node_modules ./node_modules
COPY --from=builder /calcom/packages ./packages
COPY --from=builder /calcom/apps/web ./apps/web
COPY --from=builder /calcom/packages/prisma/schema.prisma ./prisma/schema.prisma
COPY scripts scripts
RUN chmod +x scripts/*
# Save value used during this build stage. If NEXT_PUBLIC_WEBAPP_URL and BUILT_NEXT_PUBLIC_WEBAPP_URL differ at
# run-time, then start.sh will find/replace static values again.
ENV NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL \
BUILT_NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL
RUN scripts/replace-placeholder.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_WEBAPP_URL}
FROM node:20 AS runner
WORKDIR /calcom
COPY --from=builder-two /calcom ./
ARG NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
ENV NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL \
BUILT_NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=30s --retries=5 \
CMD wget --spider http://localhost:3000 || exit 1
CMD ["/calcom/scripts/start.sh"]
+272 -10
View File
@@ -4,7 +4,7 @@
<img src="https://user-images.githubusercontent.com/8019099/210054112-5955e812-a76e-4160-9ddd-58f2c72f1cce.png" alt="Logo">
</a>
<h3 align="center">Cal.com (formerly Calendso)</h3>
<h3 align="center">Cal.com</h3>
<p align="center">
The open-source Calendly successor.
@@ -175,6 +175,7 @@ yarn dx
```
#### Development tip
1. Add `export NODE_OPTIONS=“--max-old-space-size=16384”` to your shell script to increase the memory limit for the node process. Alternatively, you can run this in your terminal before running the app. Replace 16384 with the amount of RAM you want to allocate to the node process.
2. Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
@@ -226,7 +227,7 @@ for Logger level to be set at info, for example.
3. Now open your psql shell with the DB you created: `psql -h localhost -U postgres -d <DB name>`
4. Inside the psql shell execute `\conninfo`. And you will get the following info.
4. Inside the psql shell execute `\conninfo`. And you will get the following info.
![image](https://user-images.githubusercontent.com/39329182/236612291-51d87f69-6dc1-4a23-bf4d-1ca1754e0a35.png)
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
@@ -234,9 +235,10 @@ for Logger level to be set at info, for example.
</details>
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
If you don't want to create a local DB. Then you can also consider using services like railway.app, Northflank or render.
- [Setup postgres DB with railway.app](https://docs.railway.app/guides/postgresql)
- [Setup postgres DB with Northflank](https://northflank.com/guides/deploy-postgres-database-on-northflank)
- [Setup postgres DB with render](https://render.com/docs/databases)
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
@@ -374,15 +376,268 @@ Executable doesn't exist at /Users/alice/Library/Caches/ms-playwright/chromium-1
### Docker
The Docker configuration for Cal.com is an effort powered by people within the community.
**Official support**: Our team will begin to officially support the Dockerfile and docker-compose resources in this
repository.
If you want to contribute to the Docker repository, [reply here](https://github.com/calcom/docker/discussions/32).
**Important**: Cal.com will **not** be supporting installations that use these Docker resources. While we provide and maintain the Docker configurations, support for Docker-based installations is the responsibility of the user.
The Docker configuration can be found [in our docker repository](https://github.com/calcom/docker).
This image can be found on DockerHub at [https://hub.docker.com/r/calcom/cal.com](https://hub.docker.com/r/calcom/cal.com).
Issues with Docker? Find your answer or open a new discussion [here](https://github.com/calcom/docker/discussions) to ask the community.
**Note for ARM Users**: Use the {version}-arm suffix for pulling images. Example: `docker pull calcom/cal.com:v5.6.19-arm`.
Cal.com, Inc. does not provide official support for Docker, but we will accept fixes and documentation. Use at your own risk.
#### Requirements
Make sure you have `docker` & `docker compose` installed on the server / system. Both are installed by most docker utilities, including Docker Desktop and Rancher Desktop.
Note: `docker compose` without the hyphen is now the primary method of using docker-compose, per the Docker documentation.
#### (Most users) Running Cal.com with Docker Compose
If you are evaluating Cal.com or running with minimal to no modifications, this option is for you.
1. Clone calcom/cal.com
```bash
git clone --recursive https://github.com/calcom/cal.com.git
```
2. Change into the directory
```bash
cd cal.com
```
3. Prepare your configuration: Rename `.env.example` to `.env` and then update `.env`
```bash
cp .env.example .env
```
Most configurations can be left as-is, but for configuration options see [Important Run-time variables](#important-run-time-variables) below.
**Push Notifications (VAPID Keys)**
If you see an error like:
```
Error: No key set vapidDetails.publicKey
```
This means your environment variables for Web Push are missing.
You must generate and set `NEXT_PUBLIC_VAPID_PUBLIC_KEY` and `VAPID_PRIVATE_KEY`.
Generate them with:
```bash
npx web-push generate-vapid-keys
```
Then update your `.env` file:
```env
NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_public_key_here
VAPID_PRIVATE_KEY=your_private_key_here
```
Do **not** commit real keys to `.env.example` — only placeholders.
Update the appropriate values in your .env file, then proceed.
4. (optional) Pre-Pull the images by running the following command:
```bash
docker compose pull
```
This will use the default image locations as specified by `image:` in the docker-compose.yaml file.
Note: To aid with support, by default Scarf.sh is used as registry proxy for download metrics.
5. Start Cal.com via docker compose
(Most basic users, and for First Run) To run the complete stack, which includes a local Postgres database, Cal.com web app, and Prisma Studio:
```bash
docker compose up -d
```
To run Cal.com web app and Prisma Studio against a remote database, ensure that DATABASE_URL is configured for an available database and run:
```bash
docker compose up -d calcom studio
```
To run only the Cal.com web app, ensure that DATABASE_URL is configured for an available database and run:
```bash
docker compose up -d calcom
```
**Note: to run in attached mode for debugging, remove `-d` from your desired run command.**
6. Open a browser to [http://localhost:3000](http://localhost:3000), or your defined NEXT_PUBLIC_WEBAPP_URL. The first time you run Cal.com, a setup wizard will initialize. Define your first user, and you're ready to go!
#### Updating Cal.com
1. Stop the Cal.com stack
```bash
docker compose down
```
2. Pull the latest changes
```bash
docker compose pull
```
3. Update env vars as necessary.
4. Re-start the Cal.com stack
```bash
docker compose up -d
```
#### (Advanced users) Build and Run Cal.com
1. Clone calcom/docker.
```bash
git clone https://github.com/calcom/cal.com.git calcom-docker
```
2. Change into the directory
```bash
cd calcom-docker
```
3. Update the calcom submodule. This project depends on the Cal.com source code, which is included here as a Git submodule. To make sure you get everything you need, update the submodule with the command below.
```bash
git submodule update --remote --init
```
Note: DO NOT use recursive submodule update, otherwise you will receive a git authentication error.
4. Rename `.env.example` to `.env` and then update `.env`
For configuration options see [Build-time variables](#build-time-variables) below. Update the appropriate values in your .env file, then proceed.
5. Build the Cal.com docker image:
Note: Due to application configuration requirements, an available database is currently required during the build process.
a) If hosting elsewhere, configure the `DATABASE_URL` in the .env file, and skip the next step
b) If a local or temporary database is required, start a local database via docker compose.
```bash
docker compose up -d database
```
6. Build Cal.com via docker compose (DOCKER_BUILDKIT=0 must be provided to allow a network bridge to be used at build time. This requirement will be removed in the future)
```bash
DOCKER_BUILDKIT=0 docker compose build calcom
```
7. Start Cal.com via docker compose
(Most basic users, and for First Run) To run the complete stack, which includes a local Postgres database, Cal.com web app, and Prisma Studio:
```bash
docker compose up -d
```
To run Cal.com web app and Prisma Studio against a remote database, ensure that DATABASE_URL is configured for an available database and run:
```bash
docker compose up -d calcom studio
```
To run only the Cal.com web app, ensure that DATABASE_URL is configured for an available database and run:
```bash
docker compose up -d calcom
```
**Note: to run in attached mode for debugging, remove `-d` from your desired run command.**
8. Open a browser to [http://localhost:3000](http://localhost:3000), or your defined NEXT_PUBLIC_WEBAPP_URL. The first time you run Cal.com, a setup wizard will initialize. Define your first user, and you're ready to go!
#### Configuration
##### Important Run-time variables
These variables must also be provided at runtime
| Variable | Description | Required | Default |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------- |
| DATABASE_URL | database url with credentials - if using a connection pooler, this setting should point there | required | `postgresql://unicorn_user:magical_password@database:5432/calendso` |
| CALCOM_LICENSE_KEY | Enterprise License Key | optional | |
| NEXT_PUBLIC_WEBAPP_URL | Base URL of the site. NOTE: if this value differs from the value used at build-time, there will be a slight delay during container start (to update the statically built files). | optional | `http://localhost:3000` |
| NEXTAUTH_URL | Location of the auth server. By default, this is the Cal.com docker instance itself. | optional | `{NEXT_PUBLIC_WEBAPP_URL}/api/auth` |
| NEXTAUTH_SECRET | must match build variable | required | `secret` |
| CALENDSO_ENCRYPTION_KEY | must match build variable | required | `secret` |
##### Build-time variables
If building the image yourself, these variables must be provided at the time of the docker build, and can be provided by updating the .env file. Currently, if you require changes to these variables, you must follow the instructions to build and publish your own image.
Updating these variables is not required for evaluation, but is required for running in production. Instructions for generating variables can be found in the [Cal.com instructions](https://github.com/calcom/cal.com)
| Variable | Description | Required | Default |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------- |
| DATABASE_URL | database url with credentials - if using a connection pooler, this setting should point there | required | `postgresql://unicorn_user:magical_password@database:5432/calendso` |
| MAX_OLD_SPACE_SIZE | Needed for Nodejs/NPM build options | required | 4096 |
| NEXT_PUBLIC_LICENSE_CONSENT | license consent - true/false | required | |
| NEXTAUTH_SECRET | Cookie encryption key | required | `secret` |
| CALENDSO_ENCRYPTION_KEY | Authentication encryption key | required | `secret` |
| NEXT_PUBLIC_WEBAPP_URL | Base URL injected into static files | optional | `http://localhost:3000` |
| NEXT_PUBLIC_WEBSITE_TERMS_URL | custom URL for terms and conditions website | optional | `https://cal.com/terms` |
| NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL | custom URL for privacy policy website | optional | `https://cal.com/privacy` |
| NEXT_PUBLIC_API_V2_URL | URL for the v2 API, only required for custom integrations or custom booking experiences using [Cal.com Platform](https://cal.com/platform) | optional | |
| CALCOM_TELEMETRY_DISABLED | Allow Cal.com to collect anonymous usage data (set to `1` to disable) | optional | |
| NEXT_PUBLIC_SINGLE_ORG_SLUG | Required if ORGANIZATIONS_ENABLED is true | optional | |
| ORGANIZATIONS_ENABLED | Used for Enterprise or Organizations plan | optional | |
#### Troubleshooting
##### SSL edge termination
If running behind a load balancer which handles SSL certificates, you will need to add the environmental variable `NODE_TLS_REJECT_UNAUTHORIZED=0` to prevent requests from being rejected. Only do this if you know what you are doing and trust the services/load-balancers directing traffic to your service.
##### Failed to commit changes: Invalid 'prisma.user.create()'
Certain versions may have trouble creating a user if the field `metadata` is empty. Using an empty json object `{}` as the field value should resolve this issue. Also, the `id` field will autoincrement, so you may also try leaving the value of `id` as empty.
##### CLIENT_FETCH_ERROR
If you experience this error, it may be the way the default Auth callback in the server is using the WEBAPP_URL as a base url. The container does not necessarily have access to the same DNS as your local machine, and therefor needs to be configured to resolve to itself. You may be able to correct this by configuring `NEXTAUTH_URL=http://localhost:3000/api/auth`, to help the backend loop back to itself.
```
docker-calcom-1 | @calcom/web:start: [next-auth][error][CLIENT_FETCH_ERROR]
docker-calcom-1 | @calcom/web:start: https://next-auth.js.org/errors#client_fetch_error request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost {
docker-calcom-1 | @calcom/web:start: error: {
docker-calcom-1 | @calcom/web:start: message: 'request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost',
docker-calcom-1 | @calcom/web:start: stack: 'FetchError: request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost\n' +
docker-calcom-1 | @calcom/web:start: ' at ClientRequest.<anonymous> (/calcom/node_modules/next/dist/compiled/node-fetch/index.js:1:65756)\n' +
docker-calcom-1 | @calcom/web:start: ' at ClientRequest.emit (node:events:513:28)\n' +
docker-calcom-1 | @calcom/web:start: ' at ClientRequest.emit (node:domain:489:12)\n' +
docker-calcom-1 | @calcom/web:start: ' at Socket.socketErrorListener (node:_http_client:494:9)\n' +
docker-calcom-1 | @calcom/web:start: ' at Socket.emit (node:events:513:28)\n' +
docker-calcom-1 | @calcom/web:start: ' at Socket.emit (node:domain:489:12)\n' +
docker-calcom-1 | @calcom/web:start: ' at emitErrorNT (node:internal/streams/destroy:157:8)\n' +
docker-calcom-1 | @calcom/web:start: ' at emitErrorCloseNT (node:internal/streams/destroy:122:3)\n' +
docker-calcom-1 | @calcom/web:start: ' at processTicksAndRejections (node:internal/process/task_queues:83:21)',
docker-calcom-1 | @calcom/web:start: name: 'FetchError'
docker-calcom-1 | @calcom/web:start: },
docker-calcom-1 | @calcom/web:start: url: 'http://testing.localhost:3000/api/auth/session',
docker-calcom-1 | @calcom/web:start: message: 'request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost'
docker-calcom-1 | @calcom/web:start: }
```
<img referrerpolicy="no-referrer-when-downgrade" src="https://static.scarf.sh/a.png?x-pxid=81cda9f7-a102-453b-ac01-51c35650bd70" />
### Railway
@@ -390,6 +645,12 @@ Cal.com, Inc. does not provide official support for Docker, but we will accept f
You can deploy Cal.com on [Railway](https://railway.app) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal.com on their platform.
### Northflank
[![Deploy on Northflank](https://assets.northflank.com/deploy_to_northflank_smm_36700fb050.svg)](https://northflank.com/stacks/deploy-calcom)
You can deploy Cal.com on [Northflank](https://northflank.com) using the button above. The team at Northflank also have a [detailed blog post](https://northflank.com/guides/deploy-calcom-with-northflank) on deploying Cal.com on their platform.
### Vercel
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
@@ -418,7 +679,7 @@ See the [roadmap project](https://cal.com/roadmap) for a list of proposed featur
Cal.com, Inc. is a commercial open source company, which means some parts of this open source repository require a commercial license. The concept is called "Open Core" where the core technology (99%) is fully open source, licensed under [AGPLv3](https://opensource.org/license/agpl-v3) and the last 1% is covered under a commercial license (["/ee" Enterprise Edition](https://github.com/calcom/cal.com/tree/main/packages/features/ee)) which we believe is entirely relevant for larger organisations that require enterprise features. Enterprise features are built by the core engineering team of Cal.com, Inc. which is hired in full-time. Find their compensation on https://cal.com/open.
> [!NOTE]
> [!NOTE]
> Our philosophy is simple, all "Singleplayer APIs" are open-source under AGPLv3. All commercial "Multiplayer APIs" are under a commercial license.
| | AGPLv3 | EE |
@@ -490,8 +751,8 @@ Don't code but still want to contribute? Join our [Discussions](https://github.c
- Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
## Single Org Mode
Refer to docs [here](./docs/self-hosting/guides/organization/single-organization-setup) for a detailed documentation with screenshots.
Refer to docs [here](./docs/self-hosting/guides/organization/single-organization-setup) for a detailed documentation with screenshots.
## Integrations
@@ -648,6 +909,7 @@ following
We use changesets to generate changelogs and publish public packages (packages with `private: true` are ignored).
An example of good readme is [atoms readme](https://github.com/calcom/cal.com/blob/main/packages/platform/atoms/README.md). Every public package must:
1. Follow semantic versioning when using changesets.
2. Mark breaking changes using `❗️Breaking change`
@@ -1,23 +0,0 @@
import { get } from "@vercel/edge-config";
import type { NextMiddleware } from "next-api-middleware";
const safeGet = async <T = unknown>(key: string): Promise<T | undefined> => {
try {
return get<T>(key);
} catch (error) {
// Don't crash if EDGE_CONFIG env var is missing
}
};
export const config = { matcher: "/:path*" };
export const checkIsInMaintenanceMode: NextMiddleware = async (req, res, next) => {
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
if (isInMaintenanceMode) {
return res
.status(503)
.json({ message: "API is currently under maintenance. Please try again at a later time." });
}
await next();
};
+2 -6
View File
@@ -3,7 +3,6 @@ import { label } from "next-api-middleware";
import { addRequestId } from "./addRequestid";
import { captureErrors } from "./captureErrors";
import { captureUserId } from "./captureUserId";
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
import { extendRequest } from "./extendRequest";
import {
HTTP_DELETE,
@@ -26,7 +25,6 @@ const middleware = {
HTTP_POST,
HTTP_DELETE,
addRequestId,
checkIsInMaintenanceMode,
verifyApiKey,
rateLimitApiKey,
extendRequest,
@@ -38,16 +36,14 @@ const middleware = {
type Middleware = keyof typeof middleware;
const middlewareOrder = [
// The order here, determines the order of execution
"checkIsInMaintenanceMode",
const middlewareOrder: Middleware[] = [
"extendRequest",
"captureErrors",
"verifyApiKey",
"rateLimitApiKey",
"addRequestId",
"captureUserId",
] as Middleware[]; // <-- Provide a list of middleware to call automatically
];
const withMiddleware = label(middleware, middlewareOrder);
@@ -27,7 +27,7 @@ const getAllAdminMemberships = async (userId: number) => {
where: {
userId: userId,
accepted: true,
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] },
},
select: {
team: {
@@ -56,16 +56,19 @@ export const getAccessibleUsers = async ({
memberUserIds,
adminUserId,
}: AccessibleUsersType): Promise<number[]> => {
const orConditions = [];
if (memberUserIds.length > 0) {
orConditions.push({ userId: { in: memberUserIds } });
}
orConditions.push({ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } });
const memberships = await prisma.membership.findMany({
where: {
team: {
isOrganization: true,
},
accepted: true,
OR: [
{ userId: { in: memberUserIds } },
{ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } },
],
OR: orConditions,
},
select: {
userId: true,
@@ -12,7 +12,7 @@ async function authMiddleware(req: NextApiRequest) {
if (isSystemWideAdmin) return;
// Find all references where the user has bookings
const bookingReference = await prisma.bookingReference.findFirst({
where: { id, booking: { userId } },
where: { id, booking: { userId }, deleted: null },
});
if (!bookingReference) throw new HttpError({ statusCode: 403, message: "Forbidden" });
}
@@ -37,7 +37,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
export async function deleteHandler(req: NextApiRequest) {
const { query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
await prisma.bookingReference.delete({ where: { id } });
await prisma.bookingReference.update({ where: { id }, data: { deleted: true } });
return { message: `BookingReference with id: ${id} deleted` };
}
@@ -38,7 +38,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
export async function getHandler(req: NextApiRequest) {
const { query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id } });
const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id, deleted: null } });
return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) };
}
@@ -29,13 +29,13 @@ import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-refe
* 404:
* description: No booking references were found
*/
async function getHandler(req: NextApiRequest) {
export async function handler(req: NextApiRequest) {
const { userId, isSystemWideAdmin } = req;
const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin
? {}
: { where: { booking: { userId } } };
? { where: { deleted: null } }
: { where: { booking: { userId }, deleted: null } };
const data = await prisma.bookingReference.findMany(args);
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
}
export default defaultResponder(getHandler);
export default defaultResponder(handler);
+2 -2
View File
@@ -3,8 +3,8 @@ import utc from "dayjs/plugin/utc";
import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
import { getAvailableSlotsService } from "@calcom/features/di/containers/AvailableSlots";
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { createContext } from "@calcom/trpc/server/createContext";
@@ -48,7 +48,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
: availableSlots.slots;
return { slots: slotsInProvidedTimeZone };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (cause) {
if (cause instanceof TRPCError) {
const statusCode = getHTTPStatusCodeFromError(cause);
@@ -0,0 +1,486 @@
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import prisma from "@calcom/prisma";
import type { Booking, Credential, EventType, User } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import { patchHandler } from "../../pages/api/booking-references/[id]/_patch";
import { handler } from "../../pages/api/booking-references/_get";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
describe("GET /api/booking-references - Soft-delete filtering", () => {
let testUser: User;
let testCredential: Credential;
let testEventType: EventType;
let testBooking: Booking;
const createdBookingReferenceIds: number[] = [];
beforeAll(async () => {
testUser = await prisma.user.create({
data: {
email: "api-bookingreference-test@example.com",
username: "api-bookingreference-test",
},
});
testCredential = await prisma.credential.create({
data: {
type: "google_calendar",
key: {},
userId: testUser.id,
},
});
testEventType = await prisma.eventType.create({
data: {
title: "Test Event Type",
slug: "api-test-event-type",
length: 30,
userId: testUser.id,
},
});
testBooking = await prisma.booking.create({
data: {
uid: "api-test-booking-uid",
title: "API Test Booking",
startTime: new Date(Date.now() + 86400000),
endTime: new Date(Date.now() + 90000000),
userId: testUser.id,
eventTypeId: testEventType.id,
status: BookingStatus.ACCEPTED,
},
});
});
afterAll(async () => {
if (createdBookingReferenceIds.length > 0) {
await prisma.bookingReference.deleteMany({
where: {
id: {
in: createdBookingReferenceIds,
},
},
});
}
await prisma.booking.delete({
where: {
id: testBooking.id,
},
});
await prisma.eventType.delete({
where: {
id: testEventType.id,
},
});
await prisma.credential.delete({
where: {
id: testCredential.id,
},
});
await prisma.user.delete({
where: {
id: testUser.id,
},
});
});
it("should only return active booking references, excluding soft-deleted ones", async () => {
const timestamp = Date.now();
const activeRef = await prisma.bookingReference.create({
data: {
type: "google_calendar",
uid: `api-active-ref-${timestamp}`,
meetingId: `api-active-meeting-${timestamp}`,
credentialId: testCredential.id,
bookingId: testBooking.id,
},
});
createdBookingReferenceIds.push(activeRef.id);
const deletedRef = await prisma.bookingReference.create({
data: {
type: "google_calendar",
uid: `api-deleted-ref-${timestamp}`,
meetingId: `api-deleted-meeting-${timestamp}`,
credentialId: testCredential.id,
bookingId: testBooking.id,
deleted: true,
},
});
createdBookingReferenceIds.push(deletedRef.id);
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
});
req.userId = testUser.id;
const responseData = await handler(req);
expect(responseData.booking_references).toBeDefined();
expect(Array.isArray(responseData.booking_references)).toBe(true);
const returnedActiveRef = responseData.booking_references.find(
(ref: { id: number }) => ref.id === activeRef.id
);
const returnedDeletedRef = responseData.booking_references.find(
(ref: { id: number }) => ref.id === deletedRef.id
);
expect(returnedActiveRef).toBeDefined();
expect(returnedDeletedRef).toBeUndefined();
});
it("should filter booking references by user when not system admin", async () => {
const timestamp = Date.now();
const otherUser = await prisma.user.create({
data: {
email: `other-user-bookingreference-${timestamp}@example.com`,
username: `other-user-bookingreference-${timestamp}`,
},
});
const otherBooking = await prisma.booking.create({
data: {
uid: `other-user-booking-uid-${timestamp}`,
title: "Other User Booking",
startTime: new Date(Date.now() + 86400000),
endTime: new Date(Date.now() + 90000000),
userId: otherUser.id,
eventTypeId: testEventType.id,
status: BookingStatus.ACCEPTED,
},
});
const otherUserRef = await prisma.bookingReference.create({
data: {
type: "zoom_video",
uid: `other-user-ref-${timestamp}`,
meetingId: `other-user-meeting-${timestamp}`,
credentialId: testCredential.id,
bookingId: otherBooking.id,
},
});
createdBookingReferenceIds.push(otherUserRef.id);
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
});
req.userId = testUser.id;
const responseData = await handler(req);
const userRefs = responseData.booking_references.filter(
(ref: { id: number }) => ref.id === otherUserRef.id
);
expect(userRefs).toHaveLength(0);
await prisma.booking.delete({ where: { id: otherBooking.id } });
await prisma.user.delete({ where: { id: otherUser.id } });
});
it("should return only active booking references for user, not soft-deleted ones for system admin", async () => {
const timestamp = Date.now();
const activeRef1 = await prisma.bookingReference.create({
data: {
type: "daily_video",
uid: `admin-active-ref-1-${timestamp}`,
meetingId: `admin-active-meeting-1-${timestamp}`,
credentialId: testCredential.id,
bookingId: testBooking.id,
},
});
createdBookingReferenceIds.push(activeRef1.id);
const activeRef2 = await prisma.bookingReference.create({
data: {
type: "daily_video",
uid: `admin-active-ref-2-${timestamp}`,
meetingId: `admin-active-meeting-2-${timestamp}`,
credentialId: testCredential.id,
bookingId: testBooking.id,
},
});
createdBookingReferenceIds.push(activeRef2.id);
const deletedRef = await prisma.bookingReference.create({
data: {
type: "daily_video",
uid: `admin-deleted-ref-${timestamp}`,
meetingId: `admin-deleted-meeting-${timestamp}`,
credentialId: testCredential.id,
bookingId: testBooking.id,
deleted: true,
},
});
createdBookingReferenceIds.push(deletedRef.id);
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
});
req.userId = testUser.id;
const responseData = await handler(req);
const activeRefs = responseData.booking_references.filter(
(ref: { id: number }) => ref.id === activeRef1.id || ref.id === activeRef2.id
);
const returnedDeletedRef = responseData.booking_references.find(
(ref: { id: number }) => ref.id === deletedRef.id
);
expect(activeRefs.length).toBe(2);
expect(returnedDeletedRef).toBeUndefined();
});
it("should verify soft-deleted references are never returned through the API", async () => {
const timestamp = Date.now();
const activeRef = await prisma.bookingReference.create({
data: {
type: "office365_calendar",
uid: `never-return-active-${timestamp}`,
meetingId: `never-return-active-meeting-${timestamp}`,
credentialId: testCredential.id,
bookingId: testBooking.id,
},
});
createdBookingReferenceIds.push(activeRef.id);
await prisma.bookingReference.update({
where: { id: activeRef.id },
data: { deleted: true },
});
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
});
req.userId = testUser.id;
const responseData = await handler(req);
const softDeletedRef = responseData.booking_references.find(
(ref: { id: number }) => ref.id === activeRef.id
);
expect(softDeletedRef).toBeUndefined();
const refInDb = await prisma.bookingReference.findUnique({
where: { id: activeRef.id },
});
expect(refInDb).toBeDefined();
expect(refInDb?.deleted).toBe(true);
});
});
describe("PATCH /api/booking-references/[id] - Existing functionality", () => {
let testUser: User;
let testCredential: Credential;
let testEventType: EventType;
let testBooking: Booking;
const createdBookingReferenceIds: number[] = [];
beforeAll(async () => {
testUser = await prisma.user.create({
data: {
email: "api-bookingreference-patch-test@example.com",
username: "api-bookingreference-patch-test",
},
});
testCredential = await prisma.credential.create({
data: {
type: "google_calendar",
key: {},
userId: testUser.id,
},
});
testEventType = await prisma.eventType.create({
data: {
title: "Test Event Type",
slug: "api-patch-test-event-type",
length: 30,
userId: testUser.id,
},
});
testBooking = await prisma.booking.create({
data: {
uid: "api-patch-test-booking-uid",
title: "API Patch Test Booking",
startTime: new Date(Date.now() + 86400000),
endTime: new Date(Date.now() + 90000000),
userId: testUser.id,
eventTypeId: testEventType.id,
status: BookingStatus.ACCEPTED,
},
});
});
afterAll(async () => {
if (createdBookingReferenceIds.length > 0) {
await prisma.bookingReference.deleteMany({
where: {
id: {
in: createdBookingReferenceIds,
},
},
});
}
await prisma.booking.delete({
where: {
id: testBooking.id,
},
});
await prisma.eventType.delete({
where: {
id: testEventType.id,
},
});
await prisma.credential.delete({
where: {
id: testCredential.id,
},
});
await prisma.user.delete({
where: {
id: testUser.id,
},
});
});
it("should successfully update an active booking reference", async () => {
const timestamp = Date.now();
const activeRef = await prisma.bookingReference.create({
data: {
type: "google_calendar",
uid: `patch-active-ref-${timestamp}`,
meetingId: `patch-active-meeting-${timestamp}`,
credentialId: testCredential.id,
bookingId: testBooking.id,
},
});
createdBookingReferenceIds.push(activeRef.id);
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "PATCH",
query: { id: activeRef.id.toString() },
body: {
meetingPassword: "new-password-123",
},
});
req.userId = testUser.id;
req.isSystemWideAdmin = false;
const responseData = await patchHandler(req);
expect(responseData.booking_reference).toBeDefined();
expect(responseData.booking_reference.id).toBe(activeRef.id);
expect(responseData.booking_reference.meetingPassword).toBe("new-password-123");
const updatedRef = await prisma.bookingReference.findUnique({
where: { id: activeRef.id },
});
expect(updatedRef?.meetingPassword).toBe("new-password-123");
});
it("should allow updating a soft-deleted booking reference (existing functionality)", async () => {
const timestamp = Date.now();
const deletedRef = await prisma.bookingReference.create({
data: {
type: "google_calendar",
uid: `patch-deleted-ref-${timestamp}`,
meetingId: `patch-deleted-meeting-${timestamp}`,
credentialId: testCredential.id,
bookingId: testBooking.id,
deleted: true,
meetingPassword: "original-password",
},
});
createdBookingReferenceIds.push(deletedRef.id);
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "PATCH",
query: { id: deletedRef.id.toString() },
body: {
meetingPassword: "updated-password",
},
});
req.userId = testUser.id;
req.isSystemWideAdmin = false;
const responseData = await patchHandler(req);
expect(responseData.booking_reference).toBeDefined();
expect(responseData.booking_reference.id).toBe(deletedRef.id);
expect(responseData.booking_reference.meetingPassword).toBe("updated-password");
const updatedRef = await prisma.bookingReference.findUnique({
where: { id: deletedRef.id },
});
expect(updatedRef?.meetingPassword).toBe("updated-password");
expect(updatedRef?.deleted).toBe(true);
});
it("should verify that booking references can be updated regardless of deleted status", async () => {
const timestamp = Date.now();
const activeRef = await prisma.bookingReference.create({
data: {
type: "zoom_video",
uid: `patch-verify-active-${timestamp}`,
meetingId: `patch-verify-active-meeting-${timestamp}`,
credentialId: testCredential.id,
bookingId: testBooking.id,
meetingPassword: "original-password",
},
});
createdBookingReferenceIds.push(activeRef.id);
await prisma.bookingReference.update({
where: { id: activeRef.id },
data: { deleted: true },
});
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "PATCH",
query: { id: activeRef.id.toString() },
body: {
meetingPassword: "updated-password",
},
});
req.userId = testUser.id;
req.isSystemWideAdmin = false;
const responseData = await patchHandler(req);
expect(responseData.booking_reference).toBeDefined();
expect(responseData.booking_reference.meetingPassword).toBe("updated-password");
const refInDb = await prisma.bookingReference.findUnique({
where: { id: activeRef.id },
});
expect(refInDb?.meetingPassword).toBe("updated-password");
expect(refInDb?.deleted).toBe(true);
});
});
@@ -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";
@@ -11,6 +11,30 @@ type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
describe("PATCH /api/bookings", () => {
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,
},
});
}
});
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" } });
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi, afterEach } from "vitest";
import { buildBooking } from "@calcom/lib/test/builder";
import {
getRecordingsOfCalVideoByRoomName,
getDownloadLinkOfCalVideoByRecordingId,
} from "@calcom/features/conferencing/lib/videoClient";
import { buildBooking } from "@calcom/lib/test/builder";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -5,8 +5,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi, afterEach } from "vitest";
import { buildBooking } from "@calcom/lib/test/builder";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/features/conferencing/lib/videoClient";
import { buildBooking } from "@calcom/lib/test/builder";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -1,10 +1,10 @@
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, it } from "vitest";
import { describe, expect, it, beforeAll } from "vitest";
import { ZodError } from "zod";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import { handler } from "../../../pages/api/bookings/_get";
@@ -17,6 +17,30 @@ const DefaultPagination = {
};
describe("GET /api/bookings", async () => {
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 proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
const proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
@@ -193,6 +217,22 @@ describe("GET /api/bookings", async () => {
describe("Expand feature to add relational data in return payload", () => {
it("Returns only team data when expand=team is set", async () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
// Find a team booking and a non-team booking from seed data
const team1 = await prisma.team.findFirst({ where: { slug: "team1" } });
const teamEventType = await prisma.eventType.findFirst({
where: { teamId: team1?.id },
include: { bookings: true },
});
const teamBooking = teamEventType?.bookings[0];
const nonTeamBooking = await prisma.booking.findFirst({
where: {
eventType: { teamId: null },
userId: proUser.id,
},
});
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
@@ -205,14 +245,23 @@ describe("GET /api/bookings", async () => {
req.isOrganizationOwnerOrAdmin = true;
const responseData = await handler(req);
console.log("bookings=>", responseData.bookings);
responseData.bookings.forEach((booking) => {
if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1");
if (booking.id === 19) {
// The team field can be either null or undefined due to nullish() in the schema
expect(booking.eventType?.team === null || booking.eventType?.team === undefined).toBe(true);
// Verify team booking has team data
if (teamBooking) {
const returnedTeamBooking = responseData.bookings.find((b) => b.id === teamBooking.id);
expect(returnedTeamBooking?.eventType?.team?.slug).toBe("team1");
}
// Verify non-team booking has null/undefined team
if (nonTeamBooking) {
const returnedNonTeamBooking = responseData.bookings.find((b) => b.id === nonTeamBooking.id);
if (returnedNonTeamBooking) {
expect(
returnedNonTeamBooking.eventType?.team === null ||
returnedNonTeamBooking.eventType?.team === undefined
).toBe(true);
}
});
}
});
});
@@ -318,11 +367,6 @@ describe("GET /api/bookings", async () => {
const testUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
const testUserBooking = await prisma.booking.findFirstOrThrow({
where: { userId: testUser.id },
include: { attendees: true },
});
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
@@ -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" } });
+2 -1
View File
@@ -42,4 +42,5 @@ lerna-debug.log*
.env.*
!.env.example
!.env.appStore.example
/client/*
/client/*
/generated/prisma/*
-6
View File
@@ -16,14 +16,8 @@ ENV DATABASE_URL=${DATABASE_URL}
COPY . .
RUN yarn install
# Build prisma schema and make sure that it is linked to v2 node_modules
RUN yarn workspace @calcom/api-v2 run generate-schemas
RUN rm -rf apps/api/v2/node_modules
RUN yarn install
RUN yarn workspace @calcom/platform-libraries run build
RUN yarn workspace @calcom/api-v2 run build
EXPOSE 80
+4
View File
@@ -22,6 +22,10 @@ const loadConfig = (): AppConfig => {
db: {
readUrl: getEnv("DATABASE_READ_URL"),
writeUrl: getEnv("DATABASE_WRITE_URL"),
readPoolMax: getEnv("DATABASE_READ_POOL_MAX", 9),
writePoolMax: getEnv("DATABASE_WRITE_POOL_MAX", 7),
workerReadPoolMax: getEnv("DATABASE_READ_WORKER_POOL_MAX", 4),
workerWritePoolMax: getEnv("DATABASE_WRITE_WORKER_POOL_MAX", 6),
redisUrl: getEnv("REDIS_URL"),
},
next: {
+4
View File
@@ -13,6 +13,10 @@ export type AppConfig = {
db: {
readUrl: string;
writeUrl: string;
readPoolMax: number;
writePoolMax: number;
workerReadPoolMax: number;
workerWritePoolMax: number;
redisUrl: string;
};
next: {
@@ -9,6 +9,8 @@ import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/
import { InstantBookingModule } from "@/lib/modules/instant-booking.module";
import { RecurringBookingModule } from "@/lib/modules/recurring-booking.module";
import { RegularBookingModule } from "@/lib/modules/regular-booking.module";
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { BillingModule } from "@/modules/billing/billing.module";
@@ -55,6 +57,8 @@ import { Module } from "@nestjs/common";
AppsRepository,
CalendarsRepository,
SelectedCalendarsRepository,
PrismaEventTypeRepository,
PrismaTeamRepository,
],
controllers: [BookingsController_2024_04_15],
})
@@ -1,6 +1,7 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
import { CreateRecurringBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-recurring-booking.input";
import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output";
import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
@@ -21,6 +22,7 @@ import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { BookingResponse } from "@calcom/platform-libraries";
import { type RegularBookingCreateResult } from "@calcom/platform-libraries/bookings";
import type { ApiSuccessResponse, ApiErrorResponse } from "@calcom/platform-types";
import type { User } from "@calcom/prisma/client";
@@ -40,6 +42,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
let user: User;
let eventTypeId: number;
let recEventTypeId: number;
let createdBooking: RegularBookingCreateResult;
@@ -81,6 +84,18 @@ describe("Bookings Endpoints 2024-04-15", () => {
},
user.id
);
const recEventType = await eventTypesRepositoryFixture.create(
{
title: `rec-bookings-2024-04-15-event-type-${randomString()}-${describe.name}`,
slug: `rec-bookings-2024-04-15-event-type-${randomString()}-${describe.name}`,
length: 60,
recurringEvent: { freq: 2, count: 4, interval: 1 },
},
user.id
);
recEventTypeId = recEventType.id;
eventTypeId = event.id;
app = moduleRef.createNestApplication();
@@ -404,6 +419,210 @@ describe("Bookings Endpoints 2024-04-15", () => {
});
});
it("should create recurring bookings", async () => {
const bookingEventTypeId = recEventTypeId;
const bookingTimeZone = "Europe/London";
const bookingLanguage = "en";
const bookingHashedLink = "";
const bookingMetadata = {
timeFormat: "12",
meetingType: "organizer-phone",
};
const bookingResponses = {
name: "tester",
email: "tester@example.com",
location: {
value: "link",
optionValue: "",
},
notes: "test",
};
const body: CreateRecurringBookingInput_2024_04_15[] = [
{
start: "2040-06-21T09:30:00.000Z",
end: "2040-06-21T10:30:00.000Z",
eventTypeId: bookingEventTypeId,
timeZone: bookingTimeZone,
language: bookingLanguage,
metadata: bookingMetadata,
hashedLink: bookingHashedLink,
responses: bookingResponses,
recurringEventId: "test-recurring-event-id",
},
{
start: "2040-06-27T09:30:00.000Z",
end: "2040-06-27T10:30:00.000Z",
eventTypeId: bookingEventTypeId,
timeZone: bookingTimeZone,
language: bookingLanguage,
metadata: bookingMetadata,
hashedLink: bookingHashedLink,
responses: bookingResponses,
recurringEventId: "test-recurring-event-id-2",
},
{
start: "2040-06-04T09:30:00.000Z",
end: "2040-06-04T10:30:00.000Z",
eventTypeId: bookingEventTypeId,
timeZone: bookingTimeZone,
language: bookingLanguage,
metadata: bookingMetadata,
hashedLink: bookingHashedLink,
responses: bookingResponses,
recurringEventId: "test-recurring-event-id-3",
},
{
start: "2040-06-11T09:30:00.000Z",
end: "2040-06-11T10:30:00.000Z",
eventTypeId: bookingEventTypeId,
timeZone: bookingTimeZone,
language: bookingLanguage,
metadata: bookingMetadata,
hashedLink: bookingHashedLink,
responses: bookingResponses,
recurringEventId: "test-recurring-event-id-4",
},
];
return request(app.getHttpServer())
.post("/v2/bookings/recurring")
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<BookingResponse[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
responseBody.data.forEach((booking) => {
expect(booking.userPrimaryEmail).toBeDefined();
expect(booking.userPrimaryEmail).toEqual(userEmail);
expect(booking.id).toBeDefined();
expect(booking.uid).toBeDefined();
expect(booking.eventTypeId).toEqual(bookingEventTypeId);
expect(booking.user.timeZone).toEqual(bookingTimeZone);
expect(booking.metadata).toEqual(bookingMetadata);
expect(booking.responses).toEqual(bookingResponses);
expect(booking.creationSource).toEqual("API_V2");
});
});
});
describe("event type booking requires authentication", () => {
let eventTypeRequiringAuthenticationId: number;
let unauthorizedUser: User;
let unauthorizedUserApiKeyString: string;
beforeAll(async () => {
const eventTypeRequiringAuthentication = await eventTypesRepositoryFixture.create(
{
title: `event-type-requiring-authentication-${randomString()}`,
slug: `event-type-requiring-authentication-${randomString()}`,
length: 60,
requiresConfirmation: true,
bookingRequiresAuthentication: true,
},
user.id
);
eventTypeRequiringAuthenticationId = eventTypeRequiringAuthentication.id;
const unauthorizedUserEmail = `unauthorized-user-${randomString()}@api.com`;
unauthorizedUser = await userRepositoryFixture.create({
email: unauthorizedUserEmail,
});
const { keyString } = await apiKeysRepositoryFixture.createApiKey(unauthorizedUser.id, null);
unauthorizedUserApiKeyString = keyString;
});
afterAll(async () => {
if (unauthorizedUser) {
await userRepositoryFixture.deleteByEmail(unauthorizedUser.email);
}
});
it("can't be booked without credentials", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-23T09:30:00.000Z",
end: "2040-05-23T10:30:00.000Z",
eventTypeId: eventTypeRequiringAuthenticationId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: "External Attendee",
email: "external@example.com",
location: {
value: "link",
optionValue: "",
},
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(401);
});
it("can't be booked with unauthorized user credentials", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-23T10:30:00.000Z",
end: "2040-05-23T11:30:00.000Z",
eventTypeId: eventTypeRequiringAuthenticationId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: "External Attendee",
email: "external@example.com",
location: {
value: "link",
optionValue: "",
},
},
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set({ Authorization: `Bearer cal_test_${unauthorizedUserApiKeyString}` })
.expect(403);
});
it("can be booked with event type owner credentials", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-23T11:30:00.000Z",
end: "2040-05-23T12:30:00.000Z",
eventTypeId: eventTypeRequiringAuthenticationId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: "External Attendee",
email: "external@example.com",
location: {
value: "link",
optionValue: "",
},
},
};
const response = await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(201);
const responseBody: ApiSuccessResponse<RegularBookingCreateResult> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.id).toBeDefined();
if (responseBody.data.id) {
await bookingsRepositoryFixture.deleteById(responseBody.data.id);
}
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
@@ -7,13 +7,20 @@ import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/ma
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { sha256Hash, isApiKey, stripApiKey } from "@/lib/api-key";
import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions";
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
import { InstantBookingCreateService } from "@/lib/services/instant-booking-create.service";
import { RecurringBookingService } from "@/lib/services/recurring-booking.service";
import { RegularBookingService } from "@/lib/services/regular-booking.service";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import {
AuthOptionalUser,
GetOptionalUser,
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { BillingService } from "@/modules/billing/services/billing.service";
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
@@ -38,6 +45,8 @@ import {
NotFoundException,
UseGuards,
BadRequestException,
UnauthorizedException,
ForbiddenException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
@@ -113,7 +122,9 @@ export class BookingsController_2024_04_15 {
private readonly usersService: UsersService,
private readonly regularBookingService: RegularBookingService,
private readonly recurringBookingService: RecurringBookingService,
private readonly instantBookingCreateService: InstantBookingCreateService
private readonly instantBookingCreateService: InstantBookingCreateService,
private readonly eventTypeRepository: PrismaEventTypeRepository,
private readonly teamRepository: PrismaTeamRepository
) {}
@Get("/")
@@ -166,8 +177,12 @@ export class BookingsController_2024_04_15 {
}
@Get("/:bookingUid/reschedule")
async getBookingForReschedule(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
const booking = await getBookingForReschedule(bookingUid);
@UseGuards(OptionalApiAuthGuard)
async getBookingForReschedule(
@Param("bookingUid") bookingUid: string,
@GetOptionalUser() user: AuthOptionalUser
): Promise<ApiResponse<unknown>> {
const booking = await getBookingForReschedule(bookingUid, user?.id);
if (!booking) {
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
@@ -190,6 +205,7 @@ export class BookingsController_2024_04_15 {
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
const { orgSlug, locationUrl } = body;
try {
await this.checkBookingRequiresAuthentication(req, body.eventTypeId);
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed);
const booking = await this.regularBookingService.createBooking({
bookingData: bookingRequest.body,
@@ -318,11 +334,9 @@ export class BookingsController_2024_04_15 {
recurringEvent.recurringEventId = recurringEventId;
}
}
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
const createdBookings: BookingResponse[] = await this.recurringBookingService.createBooking({
bookingData: bookingRequest.body,
bookingData: body.map((booking) => ({ ...booking, creationSource: CreationSource.API_V2 })),
bookingMeta: {
userId: bookingRequest.userId,
hostname: bookingRequest.headers?.host || "",
@@ -331,6 +345,7 @@ export class BookingsController_2024_04_15 {
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
noEmail: bookingRequest.body.noEmail,
},
});
@@ -444,6 +459,48 @@ export class BookingsController_2024_04_15 {
return oAuthClientParams.platformClientId;
}
private async checkBookingRequiresAuthentication(req: Request, eventTypeId: number): Promise<void> {
const eventType = await this.eventTypeRepository.findByIdIncludeHostsAndTeamMembers({
id: eventTypeId,
});
if (!eventType?.bookingRequiresAuthentication) {
return;
}
const userId = await this.getOwnerId(req);
if (!userId) {
throw new UnauthorizedException(
"This event type requires authentication. Please provide valid credentials."
);
}
const isEventTypeOwner = eventType.userId === userId;
const isHost = eventType.hosts.some((host) => host.userId === userId);
const isTeamAdminOrOwner =
eventType.team?.members.some((member) => member.userId === userId) ?? false;
let isOrgAdminOrOwner = false;
if (eventType.team?.parentId) {
const orgTeam = await this.teamRepository.getTeamByIdIfUserIsAdmin({
userId,
teamId: eventType.team.parentId,
});
isOrgAdminOrOwner = !!orgTeam;
} else if (eventType.team?.isOrganization) {
isOrgAdminOrOwner = isTeamAdminOrOwner;
}
const isAuthorized = isEventTypeOwner || isHost || isTeamAdminOrOwner || isOrgAdminOrOwner;
if (!isAuthorized) {
throw new ForbiddenException(
"You are not authorized to book this event type. You must be the event type owner, a host, a team admin/owner, or an organization admin/owner."
);
}
}
private async getOAuthClientsParams(clientId: string, isEmbed = false): Promise<OAuthRequestParams> {
const res = { ...DEFAULT_PLATFORM_PARAMS };
@@ -503,7 +560,10 @@ export class BookingsController_2024_04_15 {
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
async setPlatformAttendeesEmails(requestBody: any, oAuthClientId: string): Promise<void> {
async setPlatformAttendeesEmails(
requestBody: { responses?: { email?: string; guests?: string[] } },
oAuthClientId: string
): Promise<void> {
if (requestBody?.responses?.email) {
requestBody.responses.email = await this.platformBookingsService.getPlatformAttendeeEmail(
requestBody.responses.email,
@@ -565,6 +625,9 @@ export class BookingsController_2024_04_15 {
if (err instanceof Error) {
const error = err as Error;
if (err instanceof HttpException) {
throw new HttpException(err.getResponse(), err.getStatus());
}
if (Object.values(ErrorCode).includes(error.message as unknown as ErrorCode)) {
throw new HttpException(error.message, 400);
}
@@ -21,7 +21,7 @@ export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2
@IsOptional()
@ApiPropertyOptional({ type: [Object] })
allRecurringDates?: Record<string, string>[];
allRecurringDates?: { start: string; end: string | undefined }[];
@IsOptional()
@IsNumber()
@@ -1,6 +1,8 @@
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { 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 { 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 { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
@@ -15,6 +17,7 @@ import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { InstantBookingModule } from "@/lib/modules/instant-booking.module";
import { RecurringBookingModule } from "@/lib/modules/recurring-booking.module";
@@ -71,9 +74,11 @@ import { Module } from "@nestjs/common";
OAuthClientRepository,
OAuthClientUsersService,
BookingsService_2024_08_13,
BookingGuestsService_2024_08_13,
InputBookingsService_2024_08_13,
OutputBookingsService_2024_08_13,
OutputBookingReferencesService_2024_08_13,
OutputEventTypesService_2024_06_14,
BookingsRepository_2024_08_13,
EventTypesRepository_2024_06_14,
BookingSeatRepository,
@@ -92,7 +97,7 @@ import { Module } from "@nestjs/common";
BookingReferencesRepository_2024_08_13,
CalVideoService,
],
controllers: [BookingsController_2024_08_13],
controllers: [BookingsController_2024_08_13, BookingGuestsController_2024_08_13],
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
})
export class BookingsModule_2024_08_13 {}
@@ -0,0 +1,67 @@
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
import { AddGuestsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-guests.output";
import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service";
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { Controller, Post, Logger, Body, UseGuards, Param, HttpCode, HttpStatus } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags, ApiHeader } from "@nestjs/swagger";
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { AddGuestsInput_2024_08_13 } from "@calcom/platform-types";
@Controller({
path: "/v2/bookings/:bookingUid/guests",
version: VERSION_2024_08_13_VALUE,
})
@UseGuards(PermissionsGuard)
@DocsTags("Bookings / Guests")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to ${VERSION_2024_08_13}. This header is required as this endpoint does not exist in older API versions.`,
example: VERSION_2024_08_13,
required: true,
})
export class BookingGuestsController_2024_08_13 {
private readonly logger = new Logger("BookingGuestsController_2024_08_13");
constructor(private readonly bookingGuestsService: BookingGuestsService_2024_08_13) {}
@Post("/")
@HttpCode(HttpStatus.OK)
@Permissions([BOOKING_WRITE])
@UseGuards(ApiAuthGuard, BookingUidGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Add guests to an existing booking",
description: `Add one or more guests to an existing booking.
**Email Notifications:**
When guests are added, the following notifications are sent (unless disabled by event type settings):
- **Organizer & Team Members:** Receive an "Add Guests" notification email informing them that new guests have been added to the booking.
- **New Guests:** Receive a "Scheduled Event" email with full booking details and calendar invite. If they have a phone number, they also receive an SMS notification.
- **Existing Guests:** Receive an "Add Guests" notification email informing them that additional guests have been added to the booking.
<Note>The cal-api-version header is required for this endpoint. Without it, the request will fail with a 404 error.</Note>
`,
})
async addGuests(
@Param("bookingUid") bookingUid: string,
@Body() body: AddGuestsInput_2024_08_13,
@GetUser() user: ApiAuthGuardUser
): Promise<AddGuestsOutput_2024_08_13> {
const booking = await this.bookingGuestsService.addGuests(bookingUid, body, user);
return {
status: SUCCESS_STATUS,
data: booking,
};
}
}
@@ -134,6 +134,8 @@ export class BookingsController_2024_08_13 {
If you are creating 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.
For event types that have SMS reminders workflow, you need to pass the attendee's phone number in the request body via \`attendee.phoneNumber\` (e.g., "+19876543210" in international format). This is an optional field, but becomes required when SMS reminders are enabled for the event type. For the complete attendee object structure, see the [attendee object](https://cal.com/docs/api-reference/v2/bookings/create-a-booking#body-attendee) documentation.
<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>
`,
})
@@ -288,7 +290,7 @@ export class BookingsController_2024_08_13 {
{ $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) },
],
},
description: `Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2).
description: `Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2). If you're rescheduling a seated booking as org admin of booking host, pass booking input for Reschedule Booking (Option 1) along with your access token in the request header.
If you are rescheduling 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.`,
@@ -320,10 +322,13 @@ export class BookingsController_2024_08_13 {
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"}\`.
\nCancelling seated bookings:
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 - host can cancel booking for all attendees aka for every seat. 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.
\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
@@ -343,7 +348,7 @@ export class BookingsController_2024_08_13 {
],
},
description:
"Accepts different types of cancel booking input: Cancel Booking (Option 1) or Cancel Seated Booking (Option 2)",
"Accepts different types of cancel booking input: Cancel Booking (Option 1 which is for normal or recurring bookings) or Cancel Seated Booking (Option 2 which is for seated bookings)",
})
@ApiExtraModels(CancelBookingInput_2024_08_13, CancelSeatedBookingInput_2024_08_13)
async cancelBooking(
@@ -0,0 +1,523 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { AddGuestsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-guests.output";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { 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 { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import {
CAL_API_VERSION_HEADER,
ERROR_STATUS,
SUCCESS_STATUS,
VERSION_2024_08_13,
} from "@calcom/platform-constants";
import {
AttendeeAddGuestsEmail,
AttendeeScheduledEmail,
OrganizerAddGuestsEmail,
} from "@calcom/platform-libraries/emails";
import type { BookingOutput_2024_08_13, CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
import type { User, Team } from "@calcom/prisma/client";
const attendeeAddGuestsEmailSpy = jest
.spyOn(AttendeeAddGuestsEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
const organizerAddGuestsEmailSpy = jest
.spyOn(OrganizerAddGuestsEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
const attendeeScheduledEmailSpy = jest
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
type TestUser = {
user: User;
accessToken: string;
refreshToken: string;
};
type TestSetup = {
organizer: TestUser;
unrelatedUser: TestUser;
guest: TestUser;
eventTypeId: number;
bookingUid: string;
};
describe("Bookings Endpoints 2024-08-13 add guests", () => {
let app: INestApplication;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let tokensRepositoryFixture: TokensRepositoryFixture;
let testSetup: TestSetup;
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);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({
name: `user-emails-2024-08-13-organization-${randomString()}`,
});
await setupTestData();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function setupTestData() {
const oAuthClient = await createOAuthClient(organization.id, true);
const organizerUser = await userRepositoryFixture.create({
email: `user-emails-2024-08-13-organizer-${randomString()}@api.com`,
platformOAuthClients: {
connect: { id: oAuthClient.id },
},
});
const unrelatedUserData = await userRepositoryFixture.create({
email: `user-emails-2024-08-13-unrelated-${randomString()}@api.com`,
platformOAuthClients: {
connect: { id: oAuthClient.id },
},
});
const guestUser = await userRepositoryFixture.create({
email: `user-emails-2024-08-13-guest-${randomString()}@api.com`,
platformOAuthClients: {
connect: { id: oAuthClient.id },
},
});
const organizerTokens = await tokensRepositoryFixture.createTokens(organizerUser.id, oAuthClient.id);
const unrelatedUserTokens = await tokensRepositoryFixture.createTokens(
unrelatedUserData.id,
oAuthClient.id
);
const guestTokens = await tokensRepositoryFixture.createTokens(guestUser.id, oAuthClient.id);
const schedule: CreateScheduleInput_2024_04_15 = {
name: `user-add-guests-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(organizerUser.id, schedule);
const eventType = await eventTypesRepositoryFixture.create(
{
title: `user-add-guests-2024-08-13-event-type-${randomString()}`,
slug: `user-add-guests-2024-08-13-event-type-${randomString()}`,
length: 60,
},
organizerUser.id
);
testSetup = {
organizer: {
user: organizerUser,
accessToken: organizerTokens.accessToken,
refreshToken: organizerTokens.refreshToken,
},
unrelatedUser: {
user: unrelatedUserData,
accessToken: unrelatedUserTokens.accessToken,
refreshToken: unrelatedUserTokens.refreshToken,
},
guest: {
user: guestUser,
accessToken: guestTokens.accessToken,
refreshToken: guestTokens.refreshToken,
},
eventTypeId: eventType.id,
bookingUid: "",
};
}
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
areEmailsEnabled: emailsEnabled,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
beforeEach(async () => {
attendeeAddGuestsEmailSpy.mockClear();
organizerAddGuestsEmailSpy.mockClear();
attendeeScheduledEmailSpy.mockClear();
});
describe("POST /v2/bookings/:bookingUid/guests", () => {
beforeAll(async () => {
const createBookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: testSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
const createBookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(createBookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const createBookingResponseBody: CreateBookingOutput_2024_08_13 = createBookingResponse.body;
expect(createBookingResponseBody.status).toEqual(SUCCESS_STATUS);
if (!responseDataIsBooking(createBookingResponseBody.data)) {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
testSetup.bookingUid = createBookingResponseBody.data.uid;
});
describe("Authentication", () => {
it("should return 401 when adding guests without authentication", async () => {
const addGuestsBody = {
guests: [{ email: "unauthenticated.guest@example.com" }],
};
const addGuestsResponse = await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
.send(addGuestsBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(addGuestsResponse.status).toBe(401);
});
});
describe("Authorization - Organizer", () => {
it("should allow booking organizer to add multiple guests", async () => {
const addGuestsBody = {
guests: [{ email: "organizer.guest1@example.com" }, { email: "organizer.guest2@example.com" }],
};
const addGuestsResponse = await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
.send(addGuestsBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(200);
const addGuestsResponseBody: AddGuestsOutput_2024_08_13 = addGuestsResponse.body;
verifyAddGuestsResponse(
addGuestsResponseBody,
["organizer.guest1@example.com", "organizer.guest2@example.com"],
true
);
});
it("should return 403 when unrelated user tries to add guests", async () => {
const addGuestsBody = {
guests: [
{ email: "non-organizer.guest1@example.com" },
{ email: "non-organizer.guest2@example.com" },
],
};
await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
.send(addGuestsBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
.expect(403);
});
});
describe("Authorization - guest", () => {
it("should allow organizer to add a user as guest, then guest can add more guests", async () => {
// Step 1: Organizer adds guest user as a guest
const addguestBody = {
guests: [{ email: testSetup.guest.user.email }],
};
const addguestResponse = await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
.send(addguestBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
.expect(200);
const addguestResponseBody: AddGuestsOutput_2024_08_13 = addguestResponse.body;
verifyAddGuestsResponse(addguestResponseBody, [testSetup.guest.user.email], true);
// Step 2: Now the guest can add their own guests
const addGuestsBody = {
guests: [{ email: "guest.guest1@example.com" }],
};
const addGuestsResponse = await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
.send(addGuestsBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.guest.accessToken}`)
.expect(200);
const addGuestsResponseBody: AddGuestsOutput_2024_08_13 = addGuestsResponse.body;
verifyAddGuestsResponse(addGuestsResponseBody, ["guest.guest1@example.com"], true);
});
it("should return 403 when non-guest user tries to add guests", async () => {
const addGuestsBody = {
guests: [{ email: "non-guest.guest1@example.com" }, { email: "non-guest.guest2@example.com" }],
};
await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
.send(addGuestsBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
.expect(403);
});
});
describe("Validation", () => {
it("should return 400 when attempting to add duplicate guest email", async () => {
const addGuestsBody = {
guests: [{ email: "organizer.guest1@example.com" }],
};
const addGuestsResponse = await request(app.getHttpServer())
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
.send(addGuestsBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`);
expect(addGuestsResponse.status).toEqual(400);
expect(addGuestsResponse.body.status).toEqual(ERROR_STATUS);
});
});
});
describe("POST /v2/bookings/:bookingUid/guests - Emails Disabled", () => {
let emailsDisabledSetup: TestSetup;
beforeAll(async () => {
const oAuthClientDisabled = await createOAuthClient(organization.id, false);
const organizerUser = await userRepositoryFixture.create({
email: `user-no-emails-2024-08-13-organizer-${randomString()}@api.com`,
platformOAuthClients: {
connect: { id: oAuthClientDisabled.id },
},
});
const organizerTokens = await tokensRepositoryFixture.createTokens(
organizerUser.id,
oAuthClientDisabled.id
);
const schedule: CreateScheduleInput_2024_04_15 = {
name: `user-no-emails-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(organizerUser.id, schedule);
const eventType = await eventTypesRepositoryFixture.create(
{
title: `user-no-emails-2024-08-13-event-type-${randomString()}`,
slug: `user-no-emails-2024-08-13-event-type-${randomString()}`,
length: 60,
},
organizerUser.id
);
const createBookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
eventTypeId: eventType.id,
attendee: {
name: "No Email User",
email: "no.email.user@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/no-emails",
bookingFieldsResponses: {
customField: "noEmailValue",
},
metadata: {
userId: "200",
},
};
const createBookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(createBookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const createBookingResponseBody: CreateBookingOutput_2024_08_13 = createBookingResponse.body;
if (!responseDataIsBooking(createBookingResponseBody.data)) {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
emailsDisabledSetup = {
organizer: {
user: organizerUser,
accessToken: organizerTokens.accessToken,
refreshToken: organizerTokens.refreshToken,
},
unrelatedUser: testSetup.unrelatedUser,
guest: testSetup.guest,
eventTypeId: eventType.id,
bookingUid: createBookingResponseBody.data.uid,
};
});
it("should NOT send emails when adding guests (emails disabled)", async () => {
attendeeAddGuestsEmailSpy.mockClear();
organizerAddGuestsEmailSpy.mockClear();
attendeeScheduledEmailSpy.mockClear();
const addGuestsBody = {
guests: [{ email: "no-email.guest1@example.com" }, { email: "no-email.guest2@example.com" }],
};
const addGuestsResponse = await request(app.getHttpServer())
.post(`/v2/bookings/${emailsDisabledSetup.bookingUid}/guests`)
.send(addGuestsBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${emailsDisabledSetup.organizer.accessToken}`)
.expect(200);
const addGuestsResponseBody: AddGuestsOutput_2024_08_13 = addGuestsResponse.body;
verifyAddGuestsResponse(
addGuestsResponseBody,
["no-email.guest1@example.com", "no-email.guest2@example.com"],
false
);
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.organizer.user.email);
await bookingsRepositoryFixture.deleteAllBookings(
emailsDisabledSetup.organizer.user.id,
emailsDisabledSetup.organizer.user.email
);
});
});
afterAll(async () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(testSetup.organizer.user.email);
await userRepositoryFixture.deleteByEmail(testSetup.unrelatedUser.user.email);
await userRepositoryFixture.deleteByEmail(testSetup.guest.user.email);
await bookingsRepositoryFixture.deleteAllBookings(
testSetup.organizer.user.id,
testSetup.organizer.user.email
);
await app.close();
});
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data !== null && "id" in data;
}
function verifyAddGuestsResponse(
responseBody: AddGuestsOutput_2024_08_13,
expectedGuestEmails: string[],
shouldEmailsBeSent: boolean
): BookingOutput_2024_08_13 {
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (!responseDataIsBooking(responseBody.data)) {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
const bookingData = responseBody.data;
expect(bookingData.guests).toBeDefined();
const guestEmails = bookingData.guests;
expectedGuestEmails.forEach((email) => {
expect(guestEmails).toContain(email);
});
expect(bookingData.guests).toBeDefined();
expectedGuestEmails.forEach((email) => {
expect(bookingData.guests).toContain(email);
});
if (shouldEmailsBeSent) {
expect(attendeeAddGuestsEmailSpy).toHaveBeenCalled();
expect(organizerAddGuestsEmailSpy).toHaveBeenCalled();
expect(attendeeScheduledEmailSpy).toHaveBeenCalled();
} else {
expect(attendeeAddGuestsEmailSpy).not.toHaveBeenCalled();
expect(organizerAddGuestsEmailSpy).not.toHaveBeenCalled();
expect(attendeeScheduledEmailSpy).not.toHaveBeenCalled();
}
return bookingData;
}
});
@@ -70,6 +70,7 @@ describe("Managed user bookings 2024-08-13", () => {
let firstManagedUserEventTypeId: number;
let eventTypeRequiresConfirmationId: number;
let seatedEventTypeId: number;
const orgAdminManagedUserEmail = `managed-user-bookings-2024-08-13-org-admin-${randomString()}@api.com`;
let orgAdminManagedUser: CreateManagedUserData;
@@ -309,6 +310,20 @@ describe("Managed user bookings 2024-08-13", () => {
eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id;
});
it("should create a seated event type for first managed user", async () => {
const seatedEventType = await eventTypesRepositoryFixture.create(
{
title: `managed-user-bookings-seated-event-type-${randomString()}`,
slug: `managed-user-bookings-seated-event-type-${randomString()}`,
length: 30,
seatsPerTimeSlot: 5,
seatsShowAttendees: true,
},
firstManagedUser.user.id
);
seatedEventTypeId = seatedEventType.id;
});
describe("bookings using original emails", () => {
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
const body: CreateBookingInput_2024_08_13 = {
@@ -773,6 +788,107 @@ describe("Managed user bookings 2024-08-13", () => {
});
});
describe("seated booking management by org admin", () => {
let seatedBookingUid: string;
it("should create a seated booking with multiple attendees", async () => {
const bodyOne: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 10, 10, 0, 0)).toISOString(),
eventTypeId: seatedEventTypeId,
attendee: {
name: secondManagedUser.user.name!,
email: secondManagedUser.user.email,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
};
const responseOne = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bodyOne)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const responseBodyForFirstAttendee: ApiSuccessResponse<BookingOutput_2024_08_13> = responseOne.body;
expect(responseBodyForFirstAttendee.status).toEqual(SUCCESS_STATUS);
expect(responseBodyForFirstAttendee.data).toBeDefined();
const bookingDataForFirstAttendee = responseBodyForFirstAttendee.data as BookingOutput_2024_08_13;
seatedBookingUid = bookingDataForFirstAttendee.uid;
expect(bookingDataForFirstAttendee.attendees).toBeDefined();
expect(bookingDataForFirstAttendee.attendees.length).toBeGreaterThan(0);
const bodyTwo: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 10, 10, 0, 0)).toISOString(),
eventTypeId: seatedEventTypeId,
attendee: {
name: thirdManagedUser.user.name!,
email: thirdManagedUser.user.email,
timeZone: thirdManagedUser.user.timeZone,
language: thirdManagedUser.user.locale,
},
};
const responseTwo = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bodyTwo)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const responseBodyForSecondAttendee: ApiSuccessResponse<BookingOutput_2024_08_13> = responseTwo.body;
expect(responseBodyForSecondAttendee.status).toEqual(SUCCESS_STATUS);
expect(responseBodyForSecondAttendee.data).toBeDefined();
const bookingDataForSecondAttendee = responseBodyForSecondAttendee.data as BookingOutput_2024_08_13;
expect(bookingDataForSecondAttendee.attendees).toBeDefined();
expect(bookingDataForSecondAttendee.attendees.length).toBeGreaterThan(0);
});
it("should allow org admin to reschedule all seats in a seated booking for a managed user", async () => {
const newStartTime = new Date(Date.UTC(2030, 0, 10, 11, 0, 0)).toISOString();
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${seatedBookingUid}/reschedule`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
.send({
start: newStartTime,
})
.expect(201);
const responseBody: ApiSuccessResponse<BookingOutput_2024_08_13> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const bookingData = responseBody.data as BookingOutput_2024_08_13;
expect(bookingData.start).toEqual(newStartTime);
seatedBookingUid = bookingData.uid;
});
it("should allow org admin to cancel all seats in a seated booking for a managed user", async () => {
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${seatedBookingUid}/cancel`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
.send({
cancellationReason: "Org admin cancelled the booking",
})
.expect(200);
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const bookingData = responseBody.data as BookingOutput_2024_08_13;
expect(bookingData.status).toEqual("cancelled");
expect(bookingData.uid).toEqual(seatedBookingUid);
const cancelledBooking = await bookingsRepositoryFixture.getByUid(seatedBookingUid);
expect(cancelledBooking?.status).toEqual("CANCELLED");
});
});
describe("event type booking requires authentication", () => {
let eventTypeRequiringAuthenticationId: number;
@@ -23,10 +23,13 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
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 } from "@calcom/platform-types";
import type { Booking, User, PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Reassign bookings", () => {
let app: INestApplication;
@@ -47,12 +50,23 @@ describe("Bookings Endpoints 2024-08-13", () => {
const teamUserEmail = `reassign-bookings-2024-08-13-user1-${randomString()}@api.com`;
const teamUserEmail2 = `reassign-bookings-2024-08-13-user2-${randomString()}@api.com`;
const teamUserEmail3 = `reassign-bookings-2024-08-13-user3-${randomString()}@api.com`;
let teamUser1: User;
let teamUser2: User;
let teamUser3: User;
let teamRoundRobinEventTypeId: number;
let teamRoundRobinFixedHostEventTypeId: number;
let teamRoundRobinNonFixedEventTypeId: number;
let teamRoundRobinWithRescheduleReasonEventTypeId: number;
let teamRoundRobinNonFixedEventTypeTitle: string;
let teamRoundRobinFixedHostEventTypeTitle: string;
let teamRoundRobinWithRescheduleReasonEventTypeTitle: string;
let roundRobinBooking: Booking;
let rescheduleReasonBookingUid: string;
let rescheduleReasonBookingInitialHostId: number;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -96,23 +110,37 @@ describe("Bookings Endpoints 2024-08-13", () => {
teamUser1 = await userRepositoryFixture.create({
email: teamUserEmail,
locale: "it",
locale: "en",
name: `reassign-bookings-2024-08-13-user1-${randomString()}`,
});
teamUser2 = await userRepositoryFixture.create({
email: teamUserEmail2,
locale: "it",
locale: "en",
name: `reassign-bookings-2024-08-13-user2-${randomString()}`,
});
teamUser3 = await userRepositoryFixture.create({
email: teamUserEmail3,
locale: "en",
name: `reassign-bookings-2024-08-13-user3-${randomString()}`,
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: `reassign-bookings-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
availabilities: [
{
days: [0, 1, 2, 3, 4, 5, 6], // All days of the week
startTime: new Date(Date.UTC(2024, 0, 1, 0, 0, 0)), // 00:00 UTC
endTime: new Date(Date.UTC(2024, 0, 1, 23, 59, 0)), // 23:59 UTC
},
],
};
await schedulesService.createUserSchedule(teamUser1.id, userSchedule);
await schedulesService.createUserSchedule(teamUser2.id, userSchedule);
await schedulesService.createUserSchedule(teamUser3.id, userSchedule);
await profileRepositoryFixture.create({
uid: `usr-${teamUser1.id}`,
@@ -144,6 +172,21 @@ describe("Bookings Endpoints 2024-08-13", () => {
},
});
await profileRepositoryFixture.create({
uid: `usr-${teamUser3.id}`,
username: teamUserEmail3,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: teamUser3.id,
},
},
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
team: { connect: { id: team.id } },
@@ -158,6 +201,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
team: { connect: { id: team.id } },
user: { connect: { id: teamUser3.id } },
accepted: true,
});
const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "ROUND_ROBIN",
team: {
@@ -230,12 +280,187 @@ describe("Bookings Endpoints 2024-08-13", () => {
create: {
email: "bob@gmail.com",
name: "Bob",
locale: "it",
locale: "en",
timeZone: "Europe/Rome",
},
},
});
const teamNonFixedEventType = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "ROUND_ROBIN",
team: {
connect: { id: team.id },
},
users: {
connect: [{ id: teamUser2.id }, { id: teamUser3.id }],
},
title: `reassign-bookings-2024-08-13-non-fixed-event-type-${randomString()}`,
slug: `reassign-bookings-2024-08-13-non-fixed-event-type-${randomString()}`,
length: 60,
assignAllTeamMembers: false,
bookingFields: [],
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
});
teamRoundRobinNonFixedEventTypeId = teamNonFixedEventType.id;
teamRoundRobinNonFixedEventTypeTitle = teamNonFixedEventType.title;
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: teamUser2.id,
},
},
eventType: {
connect: {
id: teamNonFixedEventType.id,
},
},
});
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: teamUser3.id,
},
},
eventType: {
connect: {
id: teamNonFixedEventType.id,
},
},
});
const team2EventType = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "ROUND_ROBIN",
team: {
connect: { id: team.id },
},
users: {
connect: [{ id: teamUser1.id }, { id: teamUser2.id }, { id: teamUser3.id }],
},
title: `reassign-bookings-2024-08-13-fixed-event-type-${randomString()}`,
slug: `reassign-bookings-2024-08-13-fixed-event-type-${randomString()}`,
length: 60,
assignAllTeamMembers: false,
bookingFields: [],
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
});
teamRoundRobinFixedHostEventTypeId = team2EventType.id;
teamRoundRobinFixedHostEventTypeTitle = team2EventType.title;
await hostsRepositoryFixture.create({
isFixed: true,
user: {
connect: {
id: teamUser1.id,
},
},
eventType: {
connect: {
id: team2EventType.id,
},
},
});
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: teamUser2.id,
},
},
eventType: {
connect: {
id: team2EventType.id,
},
},
});
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: teamUser3.id,
},
},
eventType: {
connect: {
id: team2EventType.id,
},
},
});
const teamEventTypeWithRescheduleReason = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "ROUND_ROBIN",
team: {
connect: { id: team.id },
},
users: {
connect: [{ id: teamUser1.id }, { id: teamUser2.id }],
},
title: `reassign-bookings-2024-08-13-reschedule-reason-event-type-${randomString()}`,
slug: `reassign-bookings-2024-08-13-reschedule-reason-event-type-${randomString()}`,
length: 60,
assignAllTeamMembers: false,
bookingFields: [
{
name: "rescheduleReason",
type: "textarea",
defaultLabel: "Reason for rescheduling",
required: true,
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system",
views: [
{
id: "reschedule",
label: "Reschedule View",
},
],
},
],
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
});
teamRoundRobinWithRescheduleReasonEventTypeId = teamEventTypeWithRescheduleReason.id;
teamRoundRobinWithRescheduleReasonEventTypeTitle = teamEventTypeWithRescheduleReason.title;
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: teamUser1.id,
},
},
eventType: {
connect: {
id: teamEventTypeWithRescheduleReason.id,
},
},
});
await hostsRepositoryFixture.create({
isFixed: false,
user: {
connect: {
id: teamUser2.id,
},
},
eventType: {
connect: {
id: teamEventTypeWithRescheduleReason.id,
},
},
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
@@ -307,6 +532,198 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should have correct title when reassigning round robin booking with non-fixed host", async () => {
const nonFixedHostBookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2050, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: teamRoundRobinNonFixedEventTypeId,
attendee: {
name: "Charlie",
email: "charlie@gmail.com",
timeZone: "Europe/Rome",
language: "en",
},
meetingUrl: "https://meet.google.com/abc-def-ghi",
};
const createResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(nonFixedHostBookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingUid = createResponse.body.data.uid;
const booking = await bookingsRepositoryFixture.getByUid(bookingUid);
expect(booking).toBeDefined();
expect(booking?.userId).toEqual(teamUser2.id);
const expectedInitialTitle = `${teamRoundRobinNonFixedEventTypeTitle} between ${teamUser2.name} and Charlie`;
expect(booking?.title).toEqual(expectedInitialTitle);
return request(app.getHttpServer())
.post(`/v2/bookings/${bookingUid}/reassign`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
expect(data.bookingUid).toEqual(bookingUid);
expect(data.reassignedTo.id).toEqual(teamUser3.id);
const reassigned = await bookingsRepositoryFixture.getByUid(bookingUid);
expect(reassigned?.userId).toEqual(teamUser3.id);
const expectedReassignedTitle = `${teamRoundRobinNonFixedEventTypeTitle} between ${teamUser3.name} and Charlie`;
expect(reassigned?.title).toEqual(expectedReassignedTitle);
});
});
it("should have correct title when reassigning round robin booking with fixed host", async () => {
const fixedHostBookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: teamRoundRobinFixedHostEventTypeId,
attendee: {
name: "Alice",
email: "alice@gmail.com",
timeZone: "Europe/Rome",
language: "en",
},
meetingUrl: "https://meet.google.com/abc-def-ghi",
};
const createResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(fixedHostBookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingUid = createResponse.body.data.uid;
const booking = await bookingsRepositoryFixture.getByUid(bookingUid);
expect(booking).toBeDefined();
expect(booking?.userId).toEqual(teamUser1.id);
const expectedInitialTitle = `${teamRoundRobinFixedHostEventTypeTitle} between ${team.name} and Alice`;
expect(booking?.title).toEqual(expectedInitialTitle);
return request(app.getHttpServer())
.post(`/v2/bookings/${bookingUid}/reassign/${teamUser3.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
expect(data.bookingUid).toEqual(bookingUid);
expect(data.reassignedTo.id).toEqual(teamUser1.id);
const reassigned = await bookingsRepositoryFixture.getByUid(bookingUid);
expect(reassigned?.userId).toEqual(teamUser1.id);
const expectedReassignedTitle = `${teamRoundRobinFixedHostEventTypeTitle} between ${team.name} and Alice`;
expect(reassigned?.title).toEqual(expectedReassignedTitle);
});
});
it("should preserve attendee name when reassigning round robin host manually with rescheduleReason required", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2050, 0, 10, 13, 0, 0)).toISOString(),
eventTypeId: teamRoundRobinWithRescheduleReasonEventTypeId,
attendee: {
name: "David",
email: "david@gmail.com",
timeZone: "Europe/Rome",
language: "en",
},
meetingUrl: "https://meet.google.com/abc-def-ghi",
};
const createResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingUid = createResponse.body.data.uid;
rescheduleReasonBookingUid = bookingUid;
const booking = await bookingsRepositoryFixture.getByUid(bookingUid);
expect(booking).toBeDefined();
expect(booking?.userId).toBeDefined();
const initialHostId = booking!.userId!;
rescheduleReasonBookingInitialHostId = initialHostId;
const expectedInitialTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${
booking?.userId === teamUser1.id ? teamUser1.name : teamUser2.name
} and David`;
expect(booking?.title).toEqual(expectedInitialTitle);
expect(booking?.title).not.toContain("Nameless");
const reassignToHostId = initialHostId === teamUser1.id ? teamUser2.id : teamUser1.id;
const reassignToHostName = reassignToHostId === teamUser1.id ? teamUser1.name : teamUser2.name;
return request(app.getHttpServer())
.post(`/v2/bookings/${bookingUid}/reassign/${reassignToHostId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
expect(data.bookingUid).toEqual(bookingUid);
expect(data.reassignedTo.id).toEqual(reassignToHostId);
const reassigned = await bookingsRepositoryFixture.getByUid(bookingUid);
expect(reassigned?.userId).toEqual(reassignToHostId);
const expectedReassignedTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${reassignToHostName} and David`;
expect(reassigned?.title).toEqual(expectedReassignedTitle);
expect(reassigned?.title).not.toContain("Nameless");
});
});
it("should preserve attendee name when reassigning round robin host automatically with rescheduleReason required", async () => {
const bookingUid = rescheduleReasonBookingUid;
const booking = await bookingsRepositoryFixture.getByUid(bookingUid);
expect(booking).toBeDefined();
expect(booking?.userId).toBeDefined();
const currentHostId = booking!.userId!;
const initialHostId = rescheduleReasonBookingInitialHostId;
const initialHostName = initialHostId === teamUser1.id ? teamUser1.name : teamUser2.name;
expect(currentHostId).not.toEqual(initialHostId);
return request(app.getHttpServer())
.post(`/v2/bookings/${bookingUid}/reassign`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
expect(data.bookingUid).toEqual(bookingUid);
expect(data.reassignedTo.id).toEqual(initialHostId);
const autoReassigned = await bookingsRepositoryFixture.getByUid(bookingUid);
expect(autoReassigned?.userId).toEqual(initialHostId);
const expectedAutoReassignedTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${initialHostName} and David`;
expect(autoReassigned?.title).toEqual(expectedAutoReassignedTitle);
expect(autoReassigned?.title).not.toContain("Nameless");
});
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
@@ -325,9 +742,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(teamUser1.email);
await userRepositoryFixture.deleteByEmail(teamUserEmail2);
await userRepositoryFixture.deleteByEmail(teamUserEmail3);
await bookingsRepositoryFixture.deleteAllBookings(teamUser1.id, teamUser1.email);
await bookingsRepositoryFixture.deleteAllBookings(teamUser2.id, teamUser2.email);
await bookingsRepositoryFixture.deleteAllBookings(teamUser3.id, teamUser3.email);
await app.close();
});
});
});
});
@@ -0,0 +1,3 @@
import { GetBookingOutput_2024_08_13 } from "@calcom/platform-types";
export class AddGuestsOutput_2024_08_13 extends GetBookingOutput_2024_08_13 {}
@@ -9,7 +9,7 @@ export class BookingReferencesRepository_2024_08_13 {
constructor(private readonly dbRead: PrismaReadService) {}
async getBookingReferences(bookingId: number, filter?: BookingReferencesFilterInput_2024_08_13) {
const whereClause: Prisma.BookingReferenceWhereInput = { bookingId };
const whereClause: Prisma.BookingReferenceWhereInput = { bookingId, deleted: null };
if (filter?.type) {
whereClause.type = filter.type;
@@ -30,6 +30,7 @@ export class BookingReferencesRepository_2024_08_13 {
return this.dbRead.prisma.bookingReference.findFirst({
where: {
uid: eventUid,
deleted: null,
},
include: {
credential: true,
@@ -50,6 +50,20 @@ export class BookingsRepository_2024_08_13 {
});
}
async getByUidWithUserIdAndSeatsReferencesCount(bookingUid: string) {
return this.dbRead.prisma.booking.findUnique({
where: {
uid: bookingUid,
},
select: {
userId: true,
seatsReferences: {
take: 1,
},
},
});
}
async getByUid(bookingUid: string) {
return this.dbRead.prisma.booking.findUnique({
where: {
@@ -0,0 +1,46 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { Injectable, Logger, HttpException, NotFoundException } from "@nestjs/common";
import { addGuestsHandler } from "@calcom/platform-libraries/bookings";
import type { AddGuestsInput_2024_08_13 } from "@calcom/platform-types";
@Injectable()
export class BookingGuestsService_2024_08_13 {
private readonly logger = new Logger("BookingGuestsService_2024_08_13");
constructor(
private readonly bookingsRepository: BookingsRepository_2024_08_13,
private readonly bookingsService: BookingsService_2024_08_13,
private readonly platformBookingsService: PlatformBookingsService
) {}
async addGuests(bookingUid: string, input: AddGuestsInput_2024_08_13, user: ApiAuthGuardUser) {
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
}
const platformClientParams = booking.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
const res = await addGuestsHandler({
ctx: { user },
input: { bookingId: booking.id, guests: input.guests },
emailsEnabled,
});
if (res.message === "Guests added") {
return await this.bookingsService.getBooking(bookingUid, user);
} else {
throw new HttpException(
"Failed to add guests to the booking",
500
);
}
}
}
@@ -1,5 +1,5 @@
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { 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 { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
@@ -1,4 +1,4 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
@@ -49,6 +49,7 @@ import {
confirmBookingHandler,
getCalendarLinks,
} from "@calcom/platform-libraries";
import { PrismaOrgMembershipRepository } from "@calcom/platform-libraries/bookings";
import {
CreateBookingInput_2024_08_13,
CreateBookingInput,
@@ -64,6 +65,7 @@ import {
RescheduleBookingInput,
CancelBookingInput,
} from "@calcom/platform-types";
import type { RescheduleSeatedBookingInput_2024_08_13 } from "@calcom/platform-types";
import type { PrismaClient } from "@calcom/prisma";
import type { EventType, User, Team } from "@calcom/prisma/client";
@@ -762,16 +764,26 @@ export class BookingsService_2024_08_13 {
authUser: AuthOptionalUser
) {
try {
await this.canRescheduleBooking(bookingUid);
const isIndividualSeatRequest = this.isRescheduleSeatedBody(body);
const isIndividualSeatReschedule = await this.shouldRescheduleIndividualSeat(
bookingUid,
isIndividualSeatRequest,
authUser
);
const bookingRequest = await this.inputService.createRescheduleBookingRequest(
request,
bookingUid,
body
body,
isIndividualSeatReschedule
);
await this.canRescheduleBooking(bookingUid);
const booking = await this.regularBookingService.createBooking({
bookingData: bookingRequest.body,
bookingMeta: {
userId: bookingRequest.userId,
userId: bookingRequest.userId ?? authUser?.id,
hostname: bookingRequest.headers?.host || "",
platformClientId: bookingRequest.platformClientId,
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
@@ -840,6 +852,57 @@ export class BookingsService_2024_08_13 {
return booking;
}
async shouldRescheduleIndividualSeat(
bookingUid: string,
isIndividualSeatReschedule: boolean,
authUser: AuthOptionalUser
) {
const booking = await this.bookingsRepository.getByUidWithUserIdAndSeatsReferencesCount(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const hasSeatsPresent = booking.seatsReferences.length > 0;
if (!hasSeatsPresent) return false;
return await this.isIndividualSeatOrOrgAdminReschedule(
isIndividualSeatReschedule,
booking.userId,
authUser?.id
);
}
async isIndividualSeatOrOrgAdminReschedule(
isIndividualSeatReschedule: boolean,
bookingUserId: number | null,
authUserId?: number | null
) {
if (isIndividualSeatReschedule) {
return true;
}
if (!authUserId) {
throw new Error(`No auth user found`);
}
if (!bookingUserId) {
throw new Error(`No user found for booking`);
}
const isOrgAdmin = await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost(
authUserId,
bookingUserId
);
return isOrgAdmin;
}
isRescheduleSeatedBody(body: RescheduleBookingInput): body is RescheduleSeatedBookingInput_2024_08_13 {
return "seatUid" in body;
}
async cancelBooking(
request: Request,
bookingUid: string,
@@ -1,4 +1,4 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
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 { Injectable, Logger, NotFoundException } from "@nestjs/common";
@@ -1,4 +1,4 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import {
eventTypeBookingFieldsSchema,
EventTypeWithOwnerAndTeam,
@@ -507,11 +507,13 @@ export class InputBookingsService_2024_08_13 {
async createRescheduleBookingRequest(
request: Request,
bookingUid: string,
body: RescheduleBookingInput
body: RescheduleBookingInput,
isIndividualSeatReschedule: boolean
): Promise<BookingRequest> {
const bodyTransformed = this.isRescheduleSeatedBody(body)
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
: await this.transformInputRescheduleBooking(bookingUid, body);
const bodyTransformed =
isIndividualSeatReschedule && "seatUid" in body
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
: await this.transformInputRescheduleBooking(bookingUid, body, isIndividualSeatReschedule);
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(
bodyTransformed.eventTypeId
@@ -554,7 +556,7 @@ export class InputBookingsService_2024_08_13 {
}
isRescheduleSeatedBody(body: RescheduleBookingInput): body is RescheduleSeatedBookingInput_2024_08_13 {
return body.hasOwnProperty("seatUid");
return Object.prototype.hasOwnProperty.call(body, "seatUid");
}
async transformInputRescheduleSeatedBooking(
@@ -606,7 +608,11 @@ export class InputBookingsService_2024_08_13 {
};
}
async transformInputRescheduleBooking(bookingUid: string, inputBooking: RescheduleBookingInput_2024_08_13) {
async transformInputRescheduleBooking(
bookingUid: string,
inputBooking: RescheduleBookingInput_2024_08_13,
isIndividualSeatReschedule: boolean
) {
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
@@ -618,7 +624,7 @@ export class InputBookingsService_2024_08_13 {
if (!eventType) {
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
}
if (eventType.seatsPerTimeSlot) {
if (eventType.seatsPerTimeSlot && !isIndividualSeatReschedule) {
throw new BadRequestException(
`Booking with uid=${bookingUid} is a seated booking which means you have to provide seatUid in the request body to specify which seat specifically you want to reschedule. First, fetch the booking using https://cal.com/docs/api-reference/v2/bookings/get-a-booking and then within the attendees array you will find the seatUid of the booking you want to reschedule. Second, provide the seatUid in the request body to specify which seat specifically you want to reschedule using the reschedule endpoint https://cal.com/docs/api-reference/v2/bookings/reschedule-a-booking#option-2`
);
@@ -652,7 +658,6 @@ export class InputBookingsService_2024_08_13 {
const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone);
const endTime = startTime.plus({ minutes: eventType.length });
return {
start: startTime.toISO(),
end: endTime.toISO(),
@@ -770,7 +775,7 @@ export class InputBookingsService_2024_08_13 {
}
isCancelSeatedBody(body: CancelBookingInput): body is CancelSeatedBookingInput_2024_08_13 {
return body.hasOwnProperty("seatUid");
return Object.prototype.hasOwnProperty.call(body, "seatUid");
}
async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) {
@@ -1,4 +1,4 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import {
defaultBookingMetadata,
defaultBookingResponses,
@@ -1,5 +1,5 @@
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { 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 { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
@@ -22,29 +22,17 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
import {
BookingWindowPeriodInputTypeEnum_2024_06_14,
BookerLayoutsInputEnum_2024_06_14,
ConfirmationPolicyEnum,
NoticeThresholdUnitEnum,
FrequencyInput,
} from "@calcom/platform-enums";
import { BookingWindowPeriodInputTypeEnum_2024_06_14, BookerLayoutsInputEnum_2024_06_14, ConfirmationPolicyEnum, NoticeThresholdUnitEnum, FrequencyInput } from "@calcom/platform-enums";
import { SchedulingType } from "@calcom/platform-libraries";
import {
type ApiSuccessResponse,
type CreateEventTypeInput_2024_06_14,
type EventTypeOutput_2024_06_14,
type GuestsDefaultFieldOutput_2024_06_14,
type NameDefaultFieldInput_2024_06_14,
type NotesDefaultFieldInput_2024_06_14,
type SplitNameDefaultFieldOutput_2024_06_14,
type UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { type ApiSuccessResponse, type CreateEventTypeInput_2024_06_14, type EventTypeOutput_2024_06_14, type GuestsDefaultFieldOutput_2024_06_14, type NameDefaultFieldInput_2024_06_14, type NotesDefaultFieldInput_2024_06_14, type SplitNameDefaultFieldOutput_2024_06_14, type UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import { FAILED_RECURRING_EVENT_TYPE_WITH_BOOKER_LIMITS_ERROR_MESSAGE } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/validators/CantHaveRecurrenceAndBookerActiveBookingsLimit";
import { REQUIRES_AT_LEAST_ONE_PROPERTY_ERROR } from "@calcom/platform-types/utils/RequiresOneOfPropertiesWhenNotDisabled";
import type { PlatformOAuthClient, Team, User, Schedule, EventType } from "@calcom/prisma/client";
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
if (a.slug < b.slug) return -1;
if (a.slug > b.slug) return 1;
@@ -1641,6 +1629,197 @@ describe("Event types Endpoints", () => {
}
await app.close();
});
describe("confirmationPolicy", () => {
describe("negative tests", () => {
it("should not create an event type with type 'time' without noticeThreshold", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Event requiring confirmation with time",
slug: "event-confirmation-time-missing-threshold",
description: "This event requires confirmation based on time.",
lengthInMinutes: 60,
confirmationPolicy: {
type: ConfirmationPolicyEnum.TIME,
blockUnconfirmedBookingsInBooker: true,
},
};
const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(400);
expect(response.body.error).toBeDefined();
expect(response.status).toBe(400);
});
it("should not update an event type to type 'time' without noticeThreshold", async () => {
// First create an event type with type 'always'
const createBody: CreateEventTypeInput_2024_06_14 = {
title: "Event requiring confirmation always",
slug: "event-confirmation-always-to-time",
description: "This event always requires confirmation.",
lengthInMinutes: 60,
confirmationPolicy: {
type: ConfirmationPolicyEnum.ALWAYS,
blockUnconfirmedBookingsInBooker: true,
},
};
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(createBody)
.expect(201);
const createdEventType = createResponse.body.data;
// Try to update to type 'time' without noticeThreshold
const updateBody: UpdateEventTypeInput_2024_06_14 = {
confirmationPolicy: {
type: ConfirmationPolicyEnum.TIME,
blockUnconfirmedBookingsInBooker: true,
},
};
const updateResponse = await request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(updateBody)
.expect(400);
expect(updateResponse.body.error).toBeDefined();
expect(updateResponse.status).toBe(400);
// Cleanup
await eventTypesRepositoryFixture.delete(createdEventType.id);
});
});
describe("positive tests", () => {
let eventTypeWithConfirmationTimeId: number;
it("should create an event type with type 'always'", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Event requiring confirmation always",
slug: "event-confirmation-always",
description: "This event always requires confirmation.",
lengthInMinutes: 60,
confirmationPolicy: {
type: ConfirmationPolicyEnum.ALWAYS,
blockUnconfirmedBookingsInBooker: true,
},
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.confirmationPolicy).toEqual(body.confirmationPolicy);
await eventTypesRepositoryFixture.delete(createdEventType.id);
});
});
it("should create an event type with type 'time' and noticeThreshold", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Event requiring confirmation with time",
slug: "event-confirmation-time-with-threshold",
description: "This event requires confirmation based on time.",
lengthInMinutes: 60,
confirmationPolicy: {
type: ConfirmationPolicyEnum.TIME,
noticeThreshold: {
unit: NoticeThresholdUnitEnum.HOURS,
count: 24,
},
blockUnconfirmedBookingsInBooker: true,
},
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.confirmationPolicy).toEqual(body.confirmationPolicy);
eventTypeWithConfirmationTimeId = createdEventType.id;
});
});
it("should update an event type from type 'time' to 'always'", async () => {
const body: UpdateEventTypeInput_2024_06_14 = {
confirmationPolicy: {
type: ConfirmationPolicyEnum.ALWAYS,
blockUnconfirmedBookingsInBooker: false,
},
};
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventTypeWithConfirmationTimeId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const updatedEventType = responseBody.data;
const policy = updatedEventType.confirmationPolicy as any;
expect(policy?.type).toEqual(ConfirmationPolicyEnum.ALWAYS);
expect(policy?.blockUnconfirmedBookingsInBooker).toEqual(false);
expect(policy?.noticeThreshold).toBeUndefined();
});
});
it("should update an event type from type 'always' to 'time' with noticeThreshold", async () => {
const body: UpdateEventTypeInput_2024_06_14 = {
confirmationPolicy: {
type: ConfirmationPolicyEnum.TIME,
noticeThreshold: {
unit: NoticeThresholdUnitEnum.MINUTES,
count: 120,
},
blockUnconfirmedBookingsInBooker: true,
},
};
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventTypeWithConfirmationTimeId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const updatedEventType = responseBody.data;
const policy = updatedEventType.confirmationPolicy as any;
expect(policy?.type).toEqual(ConfirmationPolicyEnum.TIME);
expect(policy?.noticeThreshold).toEqual({
unit: NoticeThresholdUnitEnum.MINUTES,
count: 120,
});
expect(policy?.blockUnconfirmedBookingsInBooker).toEqual(true);
await eventTypesRepositoryFixture.delete(eventTypeWithConfirmationTimeId);
});
});
});
});
});
describe("Handle event-types booking fields", () => {
@@ -2614,4 +2793,4 @@ describe("Event types Endpoints", () => {
await app.close();
});
});
});
});
@@ -39,9 +39,11 @@ export class EventTypesService_2024_06_14 {
}
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { destinationCalendar: _destinationCalendar, ...rest } = body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { eventType: eventTypeCreated } = await createEventType({
input: rest,
ctx: {
+4
View File
@@ -34,6 +34,10 @@ export type Environment = {
IS_TEAM_BILLING_ENABLED: boolean;
// Used to enable/disable the rewrite of /api/v2 to /v2, active by default.
REWRITE_API_V2_PREFIX: string;
DATABASE_READ_POOL_MAX: number;
DATABASE_WRITE_POOL_MAX: number;
DATABASE_READ_WORKER_POOL_MAX: number;
DATABASE_WRITE_WORKER_POOL_MAX: number;
};
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
@@ -16,12 +16,12 @@ import { FilterHostsService } from "@/lib/services/filter-hosts.service";
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { PrismaWorkerModule } from "@/modules/prisma/prisma-worker.module";
import { RedisService } from "@/modules/redis/redis.service";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
imports: [PrismaWorkerModule],
providers: [
PrismaOOORepository,
PrismaScheduleRepository,
@@ -0,0 +1,10 @@
import { BookingCancelService } from "@/lib/services/booking-cancel.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [BookingCancelService],
exports: [BookingCancelService],
})
export class BookingCancelModule {}
@@ -7,6 +7,7 @@ import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"
import { CacheService } from "@/lib/services/cache.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 { PrismaModule } from "@/modules/prisma/prisma.module";
@@ -24,9 +25,10 @@ import { Module } from "@nestjs/common";
CacheService,
CheckBookingAndDurationLimitsService,
CheckBookingLimitsService,
HashedLinkService,
LuckyUserService,
RegularBookingService,
],
exports: [RegularBookingService],
})
export class RegularBookingModule {}
export class RegularBookingModule { }
@@ -0,0 +1,13 @@
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { BookingCancelService as BaseBookingCancelService } from "@calcom/features/bookings/lib/handleCancelBooking";
@Injectable()
export class BookingCancelService extends BaseBookingCancelService {
constructor(prismaWriteService: PrismaWriteService) {
super({
prismaClient: prismaWriteService.prisma,
});
}
}
@@ -0,0 +1,10 @@
import { Injectable } from "@nestjs/common";
import { HashedLinkService as BaseHashedLinkService } from "@calcom/platform-libraries/private-links";
@Injectable()
export class HashedLinkService extends BaseHashedLinkService {
constructor() {
super();
}
}
@@ -1,12 +1,8 @@
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 { 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 { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
@@ -21,26 +17,18 @@ export class RegularBookingService extends BaseRegularBookingService {
checkBookingAndDurationLimitsService: CheckBookingAndDurationLimitsService,
prismaWriteService: PrismaWriteService,
bookingRepository: PrismaBookingRepository,
featuresRepository: PrismaFeaturesRepository,
checkBookingLimitsService: CheckBookingLimitsService,
hashedLinkService: HashedLinkService,
luckyUserService: LuckyUserService,
hostRepository: PrismaHostRepository,
oooRepository: PrismaOOORepository,
userRepository: PrismaUserRepository,
attributeRepository: PrismaAttributeRepository
userRepository: PrismaUserRepository
) {
super({
cacheService,
checkBookingAndDurationLimitsService,
prismaClient: prismaWriteService.prisma as unknown as PrismaClient,
bookingRepository,
featuresRepository,
checkBookingLimitsService,
hashedLinkService,
luckyUserService,
hostRepository,
oooRepository,
userRepository,
attributeRepository,
});
}
}
@@ -286,10 +286,9 @@ export class EventTypesAtomService {
.filter(({ ...app }) => app.slug === slug)
.map(
async ({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
credentials: _,
credential,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
key: _2 /* don't leak to frontend */,
...app
}: EnabledAppType) => {
@@ -368,7 +367,7 @@ export class EventTypesAtomService {
endTime: endTime.toString(),
};
if (!eventType) throw new NotFoundException(`Event type with uid ${uid} not found`);
if (eventType.users.length === 0 && !!!eventType.team)
if (eventType.users.length === 0 && !eventType.team)
throw new NotFoundException(`No users found or no team present for event type with uid ${uid}`);
const [user] = eventType?.users.length
? eventType.users
@@ -1,4 +1,4 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { BillingProcessor } from "@/modules/billing/billing.processor";
import { BillingRepository } from "@/modules/billing/billing.repository";
import { BillingController } from "@/modules/billing/controllers/billing.controller";
@@ -1,6 +1,7 @@
import { PlatformPlan } from "@/modules/billing/types";
import type { StripeService } from "@/modules/stripe/stripe.service";
import Stripe from "stripe";
import { PlatformBilling, Team } from "@calcom/prisma/client";
export type BillingData = {
@@ -1,5 +1,5 @@
import { AppConfig } from "@/config/type";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor";
import { BillingRepository } from "@/modules/billing/billing.repository";
import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface";
@@ -1,4 +1,4 @@
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
@@ -1,4 +1,4 @@
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
import { GoogleCalendarService as GCalService } from "@/ee/calendars/services/gcal.service";
import { GoogleCalendarEventResponse } from "@/modules/cal-unified-calendars/pipes/get-calendar-event-details-output-pipe";
import { calendar_v3 } from "@googleapis/calendar";
@@ -107,7 +107,7 @@ export class MembershipsRepository {
},
});
return userOrgAdminMemberships.map((m) => m.teamId);
return userOrgAdminMemberships.map((m: { teamId: number }) => m.teamId);
}
async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) {
@@ -2,6 +2,8 @@ import { MembershipsRepository } from "@/modules/memberships/memberships.reposit
import { Injectable } from "@nestjs/common";
import { intersectionBy } from "lodash";
import type { Membership } from "@calcom/prisma/client";
@Injectable()
export class MembershipsService {
constructor(private readonly membershipsRepository: MembershipsRepository) {}
@@ -16,8 +18,8 @@ export class MembershipsService {
const secondUserMemberships = await this.membershipsRepository.findUserMemberships(secondUserId);
return intersectionBy(
firstUserMemberships.filter((m) => m.accepted),
secondUserMemberships.filter((m) => m.accepted),
firstUserMemberships.filter((m: Membership) => m.accepted),
secondUserMemberships.filter((m: Membership) => m.accepted),
"teamId"
);
}
@@ -31,6 +31,7 @@ import {
Delete,
Query,
NotFoundException,
ParseIntPipe,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags, ApiHeader } from "@nestjs/swagger";
@@ -111,7 +112,7 @@ export class OAuthClientUsersController {
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
async getUserById(
@Param("clientId") clientId: string,
@Param("userId") userId: number
@Param("userId", ParseIntPipe) userId: number
): Promise<GetManagedUserOutput> {
const user = await this.validateManagedUserOwnership(clientId, userId);
@@ -127,7 +128,7 @@ export class OAuthClientUsersController {
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
async updateUser(
@Param("clientId") clientId: string,
@Param("userId") userId: number,
@Param("userId", ParseIntPipe) userId: number,
@Body() body: UpdateManagedUserInput,
@GetOrgId() organizationId: number
): Promise<GetManagedUserOutput> {
@@ -153,7 +154,7 @@ export class OAuthClientUsersController {
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
async deleteUser(
@Param("clientId") clientId: string,
@Param("userId") userId: number
@Param("userId", ParseIntPipe) userId: number
): Promise<GetManagedUserOutput> {
const user = await this.validateManagedUserOwnership(clientId, userId);
await this.userRepository.delete(userId);
@@ -174,7 +175,7 @@ export class OAuthClientUsersController {
})
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
async forceRefresh(
@Param("userId") userId: number,
@Param("userId", ParseIntPipe) userId: number,
@Param("clientId") oAuthClientId: string
): Promise<KeysResponseDto> {
this.logger.log(`Forcing new access tokens for managed user with ID ${userId}`);
@@ -434,20 +434,19 @@ describe("Organizations Event Types Endpoints", () => {
const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id);
expect(responseTeamEvent).toBeDefined();
expect(responseTeamEvent?.hosts).toEqual([
{
userId: teammate1.id,
name: teammate1.name,
username: teammate1.username,
avatarUrl: teammate1.avatarUrl,
},
{
userId: teammate2.id,
name: teammate2.name,
username: teammate2.username,
avatarUrl: teammate2.avatarUrl,
},
]);
expect(responseTeamEvent?.hosts?.find((host) => host.userId === teammate1.id)).toEqual({
userId: teammate1.id,
name: teammate1.name,
username: teammate1.username,
avatarUrl: teammate1.avatarUrl,
});
expect(responseTeamEvent?.hosts?.find((host) => host.userId === teammate2.id)).toEqual({
userId: teammate2.id,
name: teammate2.name,
username: teammate2.username,
avatarUrl: teammate2.avatarUrl,
});
if (!responseTeamEvent) {
throw new Error("Team event not found");
@@ -1,6 +1,6 @@
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
@@ -122,7 +122,6 @@ describe("Organizations Teams Invite Endpoints", () => {
.expect(404);
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await organizationsRepositoryFixture.delete(org.id);
@@ -131,5 +130,3 @@ describe("Organizations Teams Invite Endpoints", () => {
});
});
});
@@ -1,5 +1,9 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { OPTIONAL_API_KEY_HEADER, OPTIONAL_X_CAL_CLIENT_ID_HEADER, OPTIONAL_X_CAL_SECRET_KEY_HEADER } from "@/lib/docs/headers";
import {
OPTIONAL_API_KEY_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
@@ -11,10 +15,8 @@ import { Controller, UseGuards, Post, Param, ParseIntPipe, HttpCode, HttpStatus
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { TeamService } from "@calcom/platform-libraries";
import { CreateInviteOutputDto } from "./outputs/invite.output";
@Controller({
@@ -39,5 +41,3 @@ export class OrganizationsTeamsInviteController {
return { status: SUCCESS_STATUS, data: result };
}
}
@@ -11,7 +11,8 @@ export class InviteDataDto {
@ApiProperty({
description:
"Complete invitation URL that can be shared with prospective members. Opens the signup page with the token and redirects to getting started after signup.",
example: "http://app.cal.com/signup?token=f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2&callbackUrl=/getting-started",
example:
"http://app.cal.com/signup?token=f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2&callbackUrl=/getting-started",
})
inviteLink!: string;
}
@@ -23,5 +24,3 @@ export class CreateInviteOutputDto {
@ApiProperty({ type: InviteDataDto })
data!: InviteDataDto;
}
@@ -404,7 +404,7 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
invalidWorkflow.steps = [
{
stepNumber: 1,
action: "sms_number",
action: "cal_ai_phone_call",
recipient: PHONE_NUMBER,
template: REMINDER,
verifiedPhoneId: verifiedPhoneId,
@@ -439,6 +439,18 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
html: "<p>Reminder for your event {EVENT_NAME}.</p>",
},
},
{
stepNumber: 2,
action: "sms_attendee",
recipient: EMAIL,
template: REMINDER,
phoneRequired: false,
sender: "updatedSender",
message: {
subject: "Update Upcoming: {EVENT_NAME}",
text: "Update Reminder for your event {EVENT_NAME}.</p>",
},
},
];
return request(app.getHttpServer())
.post(`${basePath}/routing-form`)
@@ -459,12 +471,15 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
}
expect(responseBody.data.trigger.type).toEqual(sampleCreateWorkflowRoutingFormDto.trigger.type);
expect(responseBody.data.steps).toHaveLength(sampleCreateWorkflowRoutingFormDto.steps.length);
expect(responseBody.data.steps).toHaveLength(validWorkflow.steps.length);
expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.id).toBeDefined();
expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.sender).toEqual(
"CalcomE2EStep1"
);
expect(responseBody.data.steps.find((step) => step.stepNumber === 2)?.id).toBeDefined();
expect(responseBody.data.steps.find((step) => step.action === "sms_attendee")).toBeDefined();
const trigger = sampleCreateWorkflowRoutingFormDto.trigger as OnFormSubmittedTriggerDto;
expect(responseBody.data.trigger?.type).toEqual(trigger.type);
@@ -1,7 +1,8 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import type { Prisma } from "@calcom/prisma/client";
import type { Prisma, AttributeToUser, Membership, User, Profile } from "@calcom/prisma/client";
@Injectable()
export class OrganizationsUsersRepository {
@@ -60,7 +61,11 @@ export class OrganizationsUsersRepository {
skip,
take,
});
return attributeToUsersWithProfile.map((attributeToUser) => attributeToUser.member.user);
return attributeToUsersWithProfile.map(
(
attributeToUser: AttributeToUser & { member: Membership & { user: User & { profiles: Profile[] } } }
) => attributeToUser.member.user
);
}
async getOrganizationUsersByEmails(
@@ -1,36 +1,79 @@
import type { OnModuleDestroy, OnModuleInit } from "@nestjs/common";
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PrismaPg } from "@prisma/adapter-pg";
import { Pool } from "pg";
import { PrismaClient } from "@calcom/prisma/client";
const DB_MAX_POOL_CONNECTION = 10;
export interface PrismaServiceOptions {
readUrl?: string;
maxReadConnections?: number;
e2e?: boolean;
type: "main" | "worker";
}
@Injectable()
export class PrismaReadService implements OnModuleInit, OnModuleDestroy {
private logger = new Logger("PrismaReadService");
public prisma: PrismaClient;
public prisma!: PrismaClient;
private pool!: Pool;
private options!: PrismaServiceOptions;
constructor(readonly configService: ConfigService) {
const dbUrl = configService.get("db.readUrl", { infer: true });
constructor(private configService?: ConfigService) {
if (configService) {
// Use ConfigService defaults
const readUrl = configService.get<string>("db.readUrl", { infer: true });
const poolMax = parseInt(
configService.get<number>("db.readPoolMax", { infer: true }) ?? DB_MAX_POOL_CONNECTION
);
const e2e = configService.get<boolean>("e2e", { infer: true }) ?? false;
this.prisma = new PrismaClient({
datasources: {
db: {
url: dbUrl,
},
},
this.setOptions({
readUrl,
maxReadConnections: poolMax,
e2e,
type: "main",
});
}
}
setOptions(options: PrismaServiceOptions) {
this.options = options;
const dbUrl = options.readUrl;
const isE2E = options.e2e ?? false;
this.pool = new Pool({
connectionString: dbUrl,
max: isE2E ? 1 : options.maxReadConnections ?? DB_MAX_POOL_CONNECTION,
idleTimeoutMillis: 300000,
});
const adapter = new PrismaPg(this.pool);
this.prisma = new PrismaClient({ adapter });
}
async onModuleInit() {
this.prisma.$connect();
if (!this.prisma) return;
try {
await this.prisma.$connect();
this.logger.log("Connected to read database");
} catch (error) {
this.logger.error("Database connection failed", error);
}
}
async onModuleDestroy() {
try {
await this.prisma.$disconnect();
if (this.prisma) await this.prisma.$disconnect();
if (this.pool) await this.pool.end();
} catch (error) {
this.logger.error(error);
this.logger.error("Error disconnecting from read database", error);
}
}
}
@@ -0,0 +1,40 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
@Module({
imports: [ConfigModule],
providers: [
{
provide: PrismaReadService,
useFactory: (config: ConfigService) => {
const service = new PrismaReadService();
service.setOptions({
readUrl: config.get<string>("db.readUrl", { infer: true }),
maxReadConnections: parseInt(config.get<number>("db.workerReadPoolMax", { infer: true })),
e2e: config.get<boolean>("e2e", { infer: true }) ?? false,
type: "worker",
});
return service;
},
inject: [ConfigService],
},
{
provide: PrismaWriteService,
useFactory: (config: ConfigService) => {
const service = new PrismaWriteService();
service.setOptions({
writeUrl: config.get<string>("db.writeUrl", { infer: true }),
maxWriteConnections: parseInt(config.get<number>("db.workerWritePoolMax", { infer: true })),
e2e: config.get<boolean>("e2e", { infer: true }) ?? false,
type: "worker",
});
return service;
},
inject: [ConfigService],
},
],
exports: [PrismaReadService, PrismaWriteService],
})
export class PrismaWorkerModule {}
@@ -1,36 +1,75 @@
import { OnModuleDestroy, OnModuleInit } from "@nestjs/common";
import { Injectable, Logger } from "@nestjs/common";
import { OnModuleDestroy, OnModuleInit, Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PrismaPg } from "@prisma/adapter-pg";
import { Pool } from "pg";
import { PrismaClient } from "@calcom/prisma/client";
const DB_MAX_POOL_CONNECTION = 5;
export interface PrismaServiceOptions {
writeUrl?: string;
maxWriteConnections?: number;
e2e?: boolean;
type: "main" | "worker";
}
@Injectable()
export class PrismaWriteService implements OnModuleInit, OnModuleDestroy {
private logger = new Logger("PrismaWriteService");
public prisma!: PrismaClient;
private pool!: Pool;
private options!: PrismaServiceOptions;
public prisma: PrismaClient;
constructor(private configService?: ConfigService) {
if (configService) {
const writeUrl = configService.get<string>("db.writeUrl", { infer: true });
const poolMax = parseInt(
configService.get<number>("db.writePoolMax", { infer: true }) ?? DB_MAX_POOL_CONNECTION
);
const e2e = configService.get<boolean>("e2e", { infer: true }) ?? false;
constructor(readonly configService: ConfigService) {
const dbUrl = configService.get("db.writeUrl", { infer: true });
this.setOptions({
writeUrl,
maxWriteConnections: poolMax,
e2e,
type: "main",
});
}
}
this.prisma = new PrismaClient({
datasources: {
db: {
url: dbUrl,
},
},
setOptions(options: PrismaServiceOptions) {
this.options = options;
const dbUrl = options.writeUrl;
const isE2E = options.e2e ?? false;
this.pool = new Pool({
connectionString: dbUrl,
max: isE2E ? 1 : options.maxWriteConnections ?? DB_MAX_POOL_CONNECTION,
idleTimeoutMillis: 300000,
});
const adapter = new PrismaPg(this.pool);
this.prisma = new PrismaClient({ adapter });
}
async onModuleInit() {
this.prisma.$connect();
if (!this.prisma) return;
try {
await this.prisma.$connect();
this.logger.log("Connected to write database");
} catch (error) {
this.logger.error("Database connection failed", error);
}
}
async onModuleDestroy() {
try {
await this.prisma.$disconnect();
if (this.prisma) await this.prisma.$disconnect();
if (this.pool) await this.pool.end();
} catch (error) {
this.logger.error(error);
this.logger.error("Error disconnecting from write database", error);
}
}
}
@@ -1,9 +1,40 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
@Module({
providers: [PrismaReadService, PrismaWriteService],
imports: [ConfigModule],
providers: [
{
provide: PrismaReadService,
useFactory: (config: ConfigService) => {
const service = new PrismaReadService();
service.setOptions({
readUrl: config.get<string>("db.readUrl", { infer: true }),
maxReadConnections: parseInt(config.get<number>("db.readPoolMax", { infer: true })),
e2e: config.get<boolean>("e2e", { infer: true }) ?? false,
type: "main",
});
return service;
},
inject: [ConfigService],
},
{
provide: PrismaWriteService,
useFactory: (config: ConfigService) => {
const service = new PrismaWriteService();
service.setOptions({
writeUrl: config.get<string>("db.writeUrl", { infer: true }),
maxWriteConnections: parseInt(config.get<number>("db.writePoolMax", { infer: true })),
e2e: config.get<boolean>("e2e", { infer: true }) ?? false,
type: "main",
});
return service;
},
inject: [ConfigService],
},
],
exports: [PrismaReadService, PrismaWriteService],
})
export class PrismaModule {}
@@ -2,7 +2,7 @@
// Needed if ConfigService is used anywhere
import appConfig from "@/config/app";
import { AvailableSlotsModule } from "@/lib/modules/available-slots.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { PrismaWorkerModule } from "@/modules/prisma/prisma-worker.module";
import { Module, Logger } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { SentryModule } from "@sentry/nestjs/setup";
@@ -15,7 +15,7 @@ import { SentryModule } from "@sentry/nestjs/setup";
isGlobal: true,
load: [appConfig],
}),
PrismaModule,
PrismaWorkerModule,
AvailableSlotsModule,
],
providers: [Logger],
@@ -118,7 +118,7 @@ export class StripeService {
userId
);
const credentialIdsToDelete = existingCredentials.map((item) => item.id);
const credentialIdsToDelete = existingCredentials.map((item: Credential) => item.id);
if (credentialIdsToDelete.length > 0) {
await this.appsRepository.deleteAppCredentials(credentialIdsToDelete, userId);
}
@@ -17,7 +17,7 @@ export const MembershipUserSelect: Prisma.UserSelect = {
name: true,
metadata: true,
bio: true,
};
} satisfies Prisma.UserSelect;
@Injectable()
export class TeamsMembershipsRepository {
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable, NotFoundException } from "@nestjs/common";
import { teamMetadataSchema } from "@calcom/platform-libraries";
import { Prisma } from "@calcom/prisma/client";
import type { Membership, Prisma } from "@calcom/prisma/client";
@Injectable()
export class TeamsRepository {
@@ -41,7 +41,7 @@ export class TeamsRepository {
return [];
}
return teamMembers.map((member) => member.userId);
return teamMembers.map((member: Membership) => member.userId);
}
async getTeamManagedUsersIds(teamId: number) {
@@ -57,7 +57,7 @@ export class TeamsRepository {
return [];
}
return teamMembers.map((member) => member.userId);
return teamMembers.map((member: Membership) => member.userId);
}
async getTeamsUserIsMemberOf(userId: number) {
@@ -289,7 +289,7 @@ export class UsersRepository {
user: true,
},
});
return profiles.map((profile) => profile.user);
return profiles.map((profile: Profile & { user: User }) => profile.user);
}
async setDefaultConferencingApp(userId: number, appSlug?: string, appLink?: string) {
@@ -7,8 +7,12 @@ import {
EMAIL_ADDRESS,
EMAIL_ATTENDEE,
FORM_ALLOWED_STEP_ACTIONS,
SMS_NUMBER,
WorkflowEmailAddressStepDto,
WorkflowEmailAttendeeStepDto,
WorkflowPhoneNumberStepDto,
WorkflowPhoneAttendeeStepDto,
SMS_ATTENDEE,
} from "./workflow-step.input";
import {
RoutingFormWorkflowTriggerDto,
@@ -85,6 +89,8 @@ export class CreateFormWorkflowDto {
oneOf: [
{ $ref: getSchemaPath(WorkflowEmailAddressStepDto) },
{ $ref: getSchemaPath(WorkflowEmailAttendeeStepDto) },
{ $ref: getSchemaPath(WorkflowPhoneAttendeeStepDto) },
{ $ref: getSchemaPath(WorkflowPhoneNumberStepDto) },
],
type: "array",
})
@@ -99,8 +105,15 @@ export class CreateFormWorkflowDto {
subTypes: [
{ value: WorkflowEmailAddressStepDto, name: EMAIL_ADDRESS },
{ value: WorkflowEmailAttendeeStepDto, name: EMAIL_ATTENDEE },
{ value: WorkflowPhoneAttendeeStepDto, name: SMS_ATTENDEE },
{ value: WorkflowPhoneNumberStepDto, name: SMS_NUMBER },
],
},
})
steps!: (WorkflowEmailAddressStepDto | WorkflowEmailAttendeeStepDto)[];
steps!: (
| WorkflowEmailAddressStepDto
| WorkflowEmailAttendeeStepDto
| WorkflowPhoneAttendeeStepDto
| WorkflowPhoneNumberStepDto
)[];
}
@@ -8,6 +8,8 @@ import {
EMAIL_ADDRESS,
EMAIL_ATTENDEE,
FORM_ALLOWED_STEP_ACTIONS,
SMS_ATTENDEE,
SMS_NUMBER,
UpdateEmailAddressWorkflowStepDto,
UpdateEmailAttendeeWorkflowStepDto,
UpdateEmailHostWorkflowStepDto,
@@ -75,6 +77,8 @@ export class UpdateFormWorkflowDto {
oneOf: [
{ $ref: getSchemaPath(UpdateEmailAddressWorkflowStepDto) },
{ $ref: getSchemaPath(UpdateEmailAttendeeWorkflowStepDto) },
{ $ref: getSchemaPath(UpdatePhoneAttendeeWorkflowStepDto) },
{ $ref: getSchemaPath(UpdatePhoneNumberWorkflowStepDto) },
],
type: "array",
})
@@ -90,8 +94,15 @@ export class UpdateFormWorkflowDto {
subTypes: [
{ value: UpdateEmailAddressWorkflowStepDto, name: EMAIL_ADDRESS },
{ value: UpdateEmailAttendeeWorkflowStepDto, name: EMAIL_ATTENDEE },
{ value: UpdatePhoneAttendeeWorkflowStepDto, name: SMS_ATTENDEE },
{ value: UpdatePhoneNumberWorkflowStepDto, name: SMS_NUMBER },
],
},
})
steps?: (UpdateEmailAddressWorkflowStepDto | UpdateEmailAttendeeWorkflowStepDto)[];
steps?: (
| UpdateEmailAddressWorkflowStepDto
| UpdateEmailAttendeeWorkflowStepDto
| UpdatePhoneNumberWorkflowStepDto
| UpdatePhoneAttendeeWorkflowStepDto
)[];
}
@@ -24,7 +24,7 @@ export const STEP_ACTIONS = [
CAL_AI_PHONE_CALL,
] as const;
export const FORM_ALLOWED_STEP_ACTIONS = [EMAIL_ATTENDEE, EMAIL_ADDRESS] as const;
export const FORM_ALLOWED_STEP_ACTIONS = [EMAIL_ATTENDEE, EMAIL_ADDRESS, SMS_ATTENDEE, SMS_NUMBER] as const;
export const STEP_ACTIONS_TO_ENUM = {
[EMAIL_HOST]: WorkflowActions.EMAIL_HOST,
+1 -1
View File
@@ -14,7 +14,7 @@
"jsx": "react-jsx",
"paths": {
"@/*": ["./src/*"],
"@prisma/client/*": ["./client/*"],
"@calcom/prisma/client": ["../../../packages/prisma/generated/prisma/client"],
"@calcom/prisma/enums": ["../../../packages/prisma/enums"],
"@calcom/platform-constants": ["../../../packages/platform/constants/index.ts"],
"@calcom/platform-types": ["../../../packages/platform/types/index.ts"],
+3
View File
@@ -0,0 +1,3 @@
.git
.github
.env.example
@@ -3,6 +3,7 @@ import { unstable_cache } from "next/cache";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
import { getTeamData } from "@calcom/features/ee/teams/lib/getTeamData";
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
import {
getEventTypeHosts,
getProfileFromEvent,
@@ -11,10 +12,9 @@ import {
} from "@calcom/features/eventtypes/lib/getPublicEvent";
import { getTeamEventType } from "@calcom/features/eventtypes/lib/getTeamEventType";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import type { SchedulingType } from "@calcom/prisma/enums";

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