Compare commits

...
Author SHA1 Message Date
Amit Sharma 81224f324a feat: google ads conversion tracking (#25198)
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
* feat: google ads conversion tracking

* gaClientid

* store gclid in stripe metadata

* tracking only in the us

* track google campaign id as well

* rename gclid -> google ads

* fix: build

* fix

* refactor

* fix: type check

* fix: type check

* fix: type check

* fix: type check

* fix: store it in cookie

* refactor

* fix

* cleanup

* linked ads tracking

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

* update

* revert

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

* add tests

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

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

## How should this be tested?

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

Create a new account
disable billing
Test again 

## Checklist

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

- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my changes generate no new warnings
2025-11-25 09:14:59 +00:00
2b2bf36703 fix: Grab booking organizer credentials when team admins request reschedule (#24645)
* Change arg name from `bookingUId` to `bookingUid`

* Lint fix

* Use `BookingRepository` to find booking to reschedule

* Move early return further up if no booking is found

* Use `PermissionCheckService` if request rescheduling a team booking

* Remove redundent check

* Remove redundent eventType query

* Using `BookingRepository` to update the booking to rescheduled

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

* Get booking organizer credentials

* Type fixes

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

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

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

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

* fix: Address code review comments for request reschedule

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

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

* Type fix

* Remove businesss logic references from repository methods.

* Move business logic to handler

* Type fix

---------

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

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

* remove unnecessary wasenabled

---------

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

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

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

## Key Changes

### Overlapping Event Layout Algorithm

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

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

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

### Visual Improvements

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

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

## Visual Demo


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



## Mandatory Tasks (DO NOT REMOVE)

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

## How should this be tested?

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

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

## Human Review Checklist

**⚠️ CRITICAL ITEMS:**

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

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

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

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

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

## Checklist

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

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

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

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

* fix: resolve type check errors in circular dependency fix

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

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

* refactor

* refactor

* wip

* wip

---------

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

* update: used slugify and replace logic

* update

* Fix formatting of rules in CreateANewTeamForm

---------

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

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

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

* refactor: use TeamRepository in listOwnedTeamsHandler

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

* fix: update tests to use renamed buildTeamsAndInvites method

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

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

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

* fix: mock TeamRepository in tests to prevent database calls

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

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

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

* refactor: improve readability of ensureConflictingSlugTeamIsMigrated

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

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

---------

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

* Remove endpoint

* Do not send email verification email if premium username

* Remove logic from verify-view

* Callback send verification email

* Add `create` method to `VerificationToken` repository

* Create `VerificationTokenService`

* Early return if payment failed

* Refactor token generation

* Add tests

* Type fixes

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

* type fix and test fix

* update env example

* improvements

* more fix

* tada

---------

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

* fix: update change role validation

* chore: add tests

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

* Revert "chore: update seed.ts"

This reverts commit 2bd0c5a6dc57142632f8fbd267aa6b5eba6d1cea.

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

* add tests

---------

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

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

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

* Add `findAcceptedMembershipsByUserIdsInTeam` to `MembershipRepository`

* Validate that passed `userIds` belong to a team

* Add tests

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

* Improve add users to org

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

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

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

---------

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

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

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

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

---------

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

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

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

* Rename variable

* Add trial days to `purchaseTeamOrOrgSubscription`

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

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

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

* feat: Add organization trial warning to all wizard steps

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

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

* fix: Correct environment variable name to STRIPE_ORG_TRIAL_DAYS

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

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

* refactor: Create shared OrganizationWizardLayout component

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

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

* Fix lint error

* Add `ORG_TRIAL_DAYS` as constant

* Add comment

* Ensure no negative number is passed

---------

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

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

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

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

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

* test

* Refactor Office 365 Calendar OAuth redirect URI

Removed debug logging for redirect URI and client ID.

* test

* check

* Update Prisma connection logic for integration tests

Refactor database connection handling for integration tests.

* update

* Refactor conditionals and simplify role handling

* Add test comment in index.ts

* Remove integration test flag from index.ts

Remove integration test flag from database connection logic.

* update

---------

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

* Removed some redundant tests, some type fixes

* Further type fixes

* More type fixes re. tests

* Next iteration, couple of fixes remaining

* Remove cache from CredentialActionsDropdown

* Fix tests by mocking credential, instead of db queries

* Remove Cache DI wiring from v2

* Make sure apiv2 build passes

* Remove another cache cron

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

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

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

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

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

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

* feat: transform null values to empty strings in bookingFieldsResponses

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

* remove extra spaces

* test: add rescheduleReason null value test case

---------

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

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

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

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

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

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

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

* feat: add avatarUrl to MeOutput DTO for OpenAPI docs

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

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

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

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

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

---------

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

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

* docs: restructure Stripe Connect docs with Steps component

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

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

* chore: add link to stripe connect atom

* chore: update docs

* chore: implement feedback

---------

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

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

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

* Abstract filtering logic

* test: add comprehensive tests for bulk location update filtering

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

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

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

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

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

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

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

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

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

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

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

* fix: use flexible PrismaLike type for better test compatibility

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

* fix: properly type mock Prisma objects in test files

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

* fix: properly mock Prisma methods in test file

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

* Filter out metadata

* Undo change in embed file

* Address feedback

---------

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

This reverts commit 1f102bf3b4.

* add bookings-v3 feature flag

* put things behind a feature flag

* remove no longer needed test

* revert e2e tests

* put back description

* revert AvatarGroup

* apply feedback

* remove "view" booking action

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

* address feedback

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

* WIP refactor team internal billing service

* Remove duplicate billing repository files

* Remove logic check in repository for billing is enabled

* Rename repository to `TeamBillingData`

* Use repository factory in main service

* Fix new import paths

* Rename  to

* Ensure `IS_TEAM_BILLING_ENABLED` is of type boolean

* Rename classes to TeamBillingService and TeamBillingServiceFactory

* Implement DI in `BookingServiceFactory`

* `TeamBillingService` use repository in `getOrgIfNeeded`

* DI `isTeamBillingEnabled` to `TeamBillingServiceFactory`

* Rename files for consistency

* Return stub BillingRepository if billing is not enabled

* Move Stripe billing service to service folder

* Rename file

* `StripeBillingService.getSubscriptionStatus` return `SubscriptionStatus`

* Type fices in StripeBillingService

* Type fix in `stubTeamBillingService`

* DI the `BillingProviderService` into the `TeamBillingService`

* Implement DI in `skipTeamTrials.handler`

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

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

* Implement DI for billing in `hasActiveTeamPlan.handler`

* Type fixes

* Implement DI in `bulkDeleteUsers.handler`

* Implement `BillingProviderServiceFactory` in `updateProfile.handler`

* Implment `BillingProviderServiceFactory` in `buyCredits.handler`

* Fix import in `stripeCustomer.handler`

* Add a constructor to `teamBillingServiceFactory`

* Add DI to `PrismaTeamBillingRepository`

* Add DI to `StripeBillingService`

* Implement singleton in `BillingProviderServiceFactory`

* Add DI folder and contents to billing folder

* Use `getTeamBillingServiceFactory` in `inviteMember.handler`

* Add `saveTeamBilling` method to `ITeamBillingService`

* Implement DI in new team route

* Implement DI in `teamService`

* Implement DI in `OrganizationPaymentService`

* Implement DI in `credit-service`

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

* Implemnt DI in `_invoice.paid.org`

* Refactor `hasActiveTeamPlan` to use `getTeamBillingFactory`

* Refactor `skipTeamTrials` to use `getTeamBillingFactory`

* Refactor `skipTeamTrials` to use `getTeamBillingServiceFactory`

* `stripeCustomer.handler` to use `getBillingProviderService`

* Remove old factories

* Type fix

* Remove unused factory

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

* Change name to `TeamBillingDataRepositoryFactory`

* Type Prisma return in `prisma.module`

* Type fix

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

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

* Type fix

* Add `getTeamBillingDataRepository`

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

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

* Refactor `calcomHandler` to use DI container

* Refactor `getCustomerAndCheckoutSession` to use DI container

* Refactor `verify-email` to use DI containers

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

* Refactor downgradeUsers to use DI container

* Type fix

* Clean up console.logs

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

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

* Fix type errors

* Address comments

* fix: update tests to work with new DI pattern

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

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

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

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

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

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

* fix: update unit tests to mock DI container properly

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

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

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

* fix: update remaining unit tests to use DI pattern

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

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

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

* Undo changes made to Prisma module

* fix: address test-related PR comments

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

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

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

* Address feedback

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

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

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

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

* Remove unused index file

* `getBySubscriptionId` to return team or null

* Address feedback

* Merge fix

* Refactor file names

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

* fix: resolve infinite loop in sidebar close effect

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

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

---------

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

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

* docs: remove V2 suffix from event names in documentation

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

Added note about using wildcard listener for future-proofing.

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

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

* docs: update embed events to use V2 event names

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

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

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

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

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

---------

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

---------

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

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

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

* feat: complete autofill disable implementation

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

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

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

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

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

* test: simplify autofill disable test to single focused test

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

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

* fix: add missing disableAutofillOnBookingPage to organizationSettings select statements

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

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

* Remove disableAutofillOnBookingPage setting

Removed 'disableAutofillOnBookingPage' setting from organization configuration.

* update

* Remove duplicate settings in common.json

Removed duplicate entries for automatic transcription and autofill settings.

* Fix syntax error in common.json

* update

* add tests

* Remove comments for autofill disabled check

Removed comments explaining scenarios for autofill check.

* addressed review

* fix

* change

* Add handling for disableAutofillOnBookingPage input

---------

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

* Non hierarchical feature check

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

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

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

* Fix: Sanitize logging to avoid exposing PII

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

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

* Apply suggestion from @volnei

---------

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

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

* update

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

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

* fix type error

* addressed review

---------

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

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

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


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

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

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

## Key Implementation Details

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

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

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

## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [x] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox. **N/A** - This is a UI behavior change that doesn't require documentation updates.
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works. **Note:** No automated tests were added for this change. Manual testing is recommended.

## How should this be tested?

**Test Cases:**

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

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

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

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

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

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

## Important Review Points

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

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

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

## Checklist

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

* update

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

This reverts commit 4e5d4f67d5.

* update

* test

* Remove connection pool setup in Prisma index

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

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

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

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

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

---------

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

* apply correct padding in mobile view

---------

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

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

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

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

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

* refactor: export CountryCode from store to avoid duplication

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

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

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

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

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

* docs: add defaultPhoneCountry prop documentation and changeset

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

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

---------

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

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

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

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

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

* test: added test for show all columns in ColumnVisibilityButton

---------

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

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

* update: removed user repository and directly used prisma

* clean up code

---------

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

This reverts commit 7a0b8a9eba.

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

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

* clean up

* revert something

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

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

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

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

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

* Remove accidental change by AI

* Add special character handling test

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-14 15:10:59 +09:00
emrysal 528b27f31e chore: release v5.8.13
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-14 02:32:38 +00:00
Alex van Andel 4282cd221c fix: NextJS does not support non-ascii in NextResponse headers (#25148)
* fix: NextJS does not support non-ascii in NextResponse headers

* Fixed type error
2025-11-14 01:27:17 +00:00
Dhairyashil ShindeandPallav 38738c1a3a fix: learn more link missing in add question (#25104)
Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-11-14 04:57:31 +05:30
Alex van Andel b5dcc7a991 fix: change edge runtime to prevent weird header override in unkey sdk (#25140) 2025-11-13 22:47:07 +00:00
Joe Au-YeungandDevin AI 5ceda11a0b fix: add 'use client' to routing-forms components to resolve React error 185 (#25107)
Adds 'use client' directive to FormInputFields.tsx and widgets.tsx in the
routing-forms package. These components use React hooks (useLocale, setState)
but were missing the client directive, causing React error 185 (invalid hook
call) in production builds due to Next.js App Router bundling differences.

The error only occurred in production, not local dev, because production
builds optimize and bundle packages differently. The widgets.tsx file uses
the useLocale hook in the Button component, and FormInputFields.tsx passes
state setters to child components.

Fixes the reroute dialog error reported in production.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-13 18:36:46 +00:00
787828d0ca feat: Toggle auto adding users to an org if they signup without an invite (#25051)
* Remove auto adding users to an org

* Update tests

* Fix tests

* fix: Update organization invitation E2E tests to not expect auto-accept before signup

- Changed isMemberShipAccepted expectations from true to false before signup
- Users with emails matching orgAutoAcceptEmail are no longer auto-accepted
- They must explicitly accept the invitation after signup
- Fixed lint warnings for unused parameters

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

* fix: Update E2E tests to expect pending membership after signup without auto-accept

Since auto-accept functionality was removed, users with emails matching
orgAutoAcceptEmail are no longer automatically accepted into organizations
after signup. They remain in pending state until explicitly accepted.

Updated assertions in:
- 'nonexisting user is invited to Org' test
- 'nonexisting user is invited to a team inside organization' test

Both tests now correctly expect isMemberShipAccepted: false after signup.

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

* Restore `verify-email` and tests from `main`

* Add `orgAutoJoinOnSignup` to `organizationSettings`

* Update
`OrganizationRepository.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail`
to find orgs where `orgAutoJoinOnSignup` is true

* `organization.update` lint fix

* `organization.update` to handle `orgAutoJoinOnSignup`

* Create toggle for `orgAutoJoinOnSignup`

* test: Add comprehensive tests for orgAutoJoinOnSignup functionality

- Update existing test to expect null instead of error when multiple orgs match
- Add test for when orgAutoJoinOnSignup is false (should return null)
- Add test for when orgAutoJoinOnSignup is true (should return org)
- Add test for default behavior (orgAutoJoinOnSignup defaults to true)

These tests verify that the new orgAutoJoinOnSignup setting correctly controls
whether users are automatically added to organizations during email verification.

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

* Type fix

* e2e: invited users should be accepted after signup (address cubic r2511916791)

Reverted post-signup isMemberShipAccepted assertions from false to true for
explicit invite scenarios. When users are explicitly invited to an org/team
and complete signup via invite link, their membership should be accepted.

This is distinct from auto-join by domain (controlled by orgAutoJoinOnSignup),
which only affects users who sign up without an invite but match the org's
email domain.

Backend sets membership.accepted = true on invite completion in:
packages/features/auth/signup/utils/createOrUpdateMemberships.ts:61,67,77,83

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

* Fix API V2 build

---------

Co-authored-by: Alex van Andel <me@alexvanandel.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>
2025-11-13 15:13:10 -03:00
89230474a5 feat: bookings page redesign v3 with calendar view (#24664)
* bookings page redesign work in progress

fix: duplicate translation key

chore: use newly supported separator type

remove outdated BookingDetailsSheet

remove dropdown and related code

revert unncessary changes

* fix wrong rebase

* fix type error

* refactor: separate bookings columns into filter and display columns (#24959)

* refactor: separate bookings columns into filter and display columns

- Extract filter-only columns into shared filterColumns.ts
- Extract list display columns into listColumns.tsx
- Create BookingsListContainer for list view with both column sets
- Create BookingsCalendarContainer for calendar view with filter columns only
- Refactor bookings-view.tsx to use dynamic imports for containers
- Remove column/table creation logic from bookings-view.tsx

This ensures calendar view doesn't import list-specific UI components (AvatarGroup, Badge, etc.)

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

* fix: return null instead of false for separator rows in filter accessors

The filter accessor functions were returning false for separator rows instead of null,
which would pollute the multi-select filters with bogus 'false' values.

This fix ensures that separator rows return null so they are properly excluded from filters.

Addresses cubic AI reviewer feedback on PR #24959

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

* clean up filter column visibility

* feat: integrate booking calendar view with re-designed list (#24973)

* add toggle button

* remove the dateRange filter when switching from calendar to list view

* move "view" to the action dropdown

* add close button the details sheet

* move close button

* fix more button behavior

---------

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

* fix type error

* update the test case

* fix e2e util

* fix actions dropdown

* revert e2e tests

* fix type error on BookingActionsDropdown.tsx

* fix: include today's bookings in flatData for past status

Previously, flatData excluded groupedBookings.today, which caused
past bookings that happened today to not show up when status === 'past'.
This fix includes today's bookings in flatData for all statuses.

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

* improve attendee cell

* fix e2e tests

* change max

* fix e2e

* add reschedule requested message

* fix e2e

* update e2e

* remove flaky checks

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-13 16:20:48 +00:00
d7fc11df5e feat: use form responses as workflow variables (#24716)
* remove add variable dropdown

* feat: lang support

* fix: type errors

* feat: select voice agent

* refactor: address feedback

* refactor: address feedback

* refactor: missing import

* fix: types

* add getAllWorkflowsFromRoutingForm to WorkflowService

* fix error caused by undefined evt

* fix type error

* fix type error

* fix tests

* feat: add inbound calls

* chore: formatting

* chore

* feat: finish inbound call

* chore: formatting

* fix: update bug

* fix: types

* code clean up

* final fixes and clean up

* remove console.log

* remove template text form from triggers

* add routing form repoditory function

* refactor: Agent Configuration Sheet (#23930)

* refactor: agent configuration sheet

* chore: use default phone numbre

* refactor: improvements

* refactor: improvements

* fix: types

* fix: feedback

* fix bug with key

* chore:

* fix: feedback

* fix: prompt

* add comments

* fix: review

* fix: review

* refactor: class

* refactor: class

* fix test

* allow cal ai action on form triggers

* move any reusable code to scheduleAIPhoneCall

* add missing await

* use predefined FormSubmissionData type

* add .trim() to sms message

* pass contextData instead

* finish base setup

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

* allow cal.ai action for form triggers in handler

* chore: add support for form workflows on api v2

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

* ai phone call on form submissions (WIP)

* use existing type for Option array

* pass chosen event type id

* refactor: rename

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

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

* add missing imports

* chore: update set value

* fix: remove index

* fix: type error

* fix: update tetss

* use only repository functions in update handler

* move all prisma queries from list.handler

* review suggestions

* fix: use logger

* 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: don't use static

* fix: type

* 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

* fix: schema

* refactor:

* remove action check in update handler

* add event type selection

* event type selector improvements

* adjust update.handler

* set outboundEventTypeId

* add back trpc  import

* fix agent repository functions

* clean up

* fix bugs caused by merge

* pass eventTypeId to updateToolsFromAgentId

* add migration for outboundEventTypeId

* add SMS actions to allowed form action constants

* add cal ai to allowed form actions

* pick correct event type for web call

* pass correct routed event type id

* remove unsued import

* fixes for offset api v2

* add missing responseId

* fix failing test

* fix failing test

* improve error message

* remove unused imports

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

* fix typo

* missing missing newStep

* minor fixes

* remove changes

* add routedEventTypeId

* fix type error

* fix type error

* fix typ error in executAPIPhoneCall.tsx

* add back inboundEventTypeId

* remove console.log

* remove outdated code

* small fixes

* don't throw error for missing phone number

* add  back filtered triggerOptions

* fix eventTypeId in testCall handler

* fix type error

* update migration

* fix trigger is not defined

* convert eventTypeId to string

* only use outboundEventTypeId for FORM_SUBMITTED trigger

* show toast when no event type selected

* fix type errors

* add missing translation

* fix type error

* remove callType

* fix tests

* small fixes

* clean up AgentConfigurationSheet

* remove EventTypeSelector file

* code clean up

* clean up

* clean up

* use resusable function for TestPhoneCallDialog and WebCallDialog

* rename result

* fix types for event type id

* use repository runction in workflowReminder.ts

* fix type error

* pass eventTypeIds correctly

* fix typo

* custom variables from form responses

* remove comment

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

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>

* use watch instead of getValues

* change to  z.record(z.unknown()) instead of any()

* fix type of eventTypeId

* check permissinon for outBoundEventTypeId

* add isNaN check

* improve function name

* add tests

* fixes for custom variables

* improve test

* update tools when outbound agent event type id changes

* handle undefined eventDate in customTemplate

* add info how to use form responses as variables

* rename responses to routingFormResponses

* remove cal ai from allowed steps api v2

* remove old migration file

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Udit Takkar <udit222001@gmail.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>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-11-13 15:34:44 +00:00
Udit TakkarandHariom Balhara 1ff5d57ded feat: distributed tracing - 2 (#24861)
* feat: distributed tracing 2

* feat: distributed tracing 2

* refactor: feedback

* refactor: feedback

* fix: type error

* fix: trpc error

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-11-13 11:51:54 +00:00
sean-brydon 19641ffc05 feat: organization v3 redesign onboarding (#24967)
## What does this PR do?

- Redesigns the organization onboarding flow by merging the brand and details pages
- Improves the organization details page with a scrollable interface and visual previews
- Adds a new organization-specific browser preview component

## Visual Demo (For contributors especially)

#### Image Demo:
- The PR replaces the separate brand page with an integrated details page that includes logo and banner uploads
- The new organization browser view shows a preview of the organization profile with the selected branding

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

- Go through the organization onboarding flow
- Test uploading logos and banners
- Verify that the organization browser preview updates in real-time with the form inputs
- Confirm that the form validation works correctly for organization name and slug
- Check that the scrollable interface works properly with fade effects at top and bottom

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

















































<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Split organization brand from the details step and added live previews for organizations and teams. Revamped org/team invites with reusable components, a dedicated email-invite page, and a CSV upload modal.

- **New Features**
  - Separate Brand step with logo, banner, and color; instant org preview via OnboardingOrganizationBrowserView.
  - Teams browser preview added; invites include email substep (/onboarding/organization/invite/email, /onboarding/teams/invite/email), CSV upload (template + parsing), and Google Workspace (behind flag).
  - Shared components (EmailInviteForm, InviteOptions, RoleSelector) used across org and team invites.

- **Refactors**
  - Updated org flow: Details → Brand → Teams → Invites; OnboardingLayout now supports dynamic step counts (org=4, team=3, personal=2).
  - UI polish (OnboardingCard header padding) and org-specific previews now replace generic views across details/brand/invites/teams; ensured org welcome modal takes precedence over personal.

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

<!-- End of auto-generated description by cubic. -->
2025-11-13 10:45:42 +00:00
Eunjae LeeandDevin AI ea80e8c8d4 fix: enable webhook form submit button when all required fields are filled (#25109)
* fix: enable webhook form submit button when all required fields are filled

The submit button was disabled even when all fields were filled because
the form relied on isDirty state. Since eventTriggers are pre-populated
with default values, the form never became dirty until the user manually
changed the triggers.

This fix adds validation logic that:
- For new webhooks: checks if required fields (URL, triggers) are filled
- For editing webhooks: preserves the existing isDirty behavior
- Handles the conditional time/timeUnit requirement for no-show triggers

Fixes the issue where users had to manually change event triggers to
enable the submit button even though all required information was filled.

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

* refactor: extract webhook form validation logic to computed variables

Replaced IIFE in disabled prop with clean computed variables:
- Extracted all watch() calls to the top of the component
- Computed validation logic as clear, named variables
- Reused needsTime for showTimeSection to avoid duplicate watch calls
- Simplified button disabled prop to just !canSubmit

This improves code readability and maintainability while preserving
the same validation behavior.

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

* fix: resolve type error by moving canSubmit computation after changeSecret declaration

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

* fix: auto-initialize time/timeUnit for webhooks with no-show triggers

When creating a webhook with default event triggers that include no-show
triggers (AFTER_HOSTS_CAL_VIDEO_NO_SHOW or AFTER_GUESTS_CAL_VIDEO_NO_SHOW),
the time and timeUnit fields are now automatically initialized to default
values (5 minutes). This ensures the submit button is enabled when all
required fields are filled, fixing E2E test failures in webhook.e2e.ts.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-13 10:32:17 +00:00
Benny JooandDevin AI cba0fa8551 chore: Clean up billing-related redundant files (#25060)
* remove billing service factory

* remove index file

* update internal team billing

* update test

* fix unit test

* fix: update internal-team-billing tests to use direct StripeBillingService mocking

- Changed mock path from @calcom/features/ee/billing/stripe-billing-service to ../stripe-billing-service to match actual import
- Used vi.hoisted() to ensure mock functions are available in hoisted mock factory context
- Changed vi.resetAllMocks() to vi.clearAllMocks() and re-apply mock implementation in beforeEach to preserve constructor mock
- Removed obsolete vi.mock("..") for deleted billing singleton module
- Fixed test pollution by creating new instances instead of mutating shared state
- Removed describe-level instance creation that was executed before mocks were set up
- All 12 tests now pass successfully

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-13 09:17:36 +00:00
Alex van Andel 530cb94f0b fix: make identifier url safe 2 (#25112)
* fix: Make identifier for rate limit URL safe

* fix: Make identifier for rate limit URL safe using hasing
2025-11-13 01:42:13 +00:00
Alex van Andel d4ef6e83cf fix: Make identifier for rate limit URL safe (#25111) 2025-11-13 01:20:05 +00:00
Alex van Andel cddd8d7d9d Revert "chore: Rate limit top level of public booking pages (#25055)" (#25086)
This reverts commit e239b67171.
2025-11-12 22:13:50 +00:00
Alex van AndelandVolnei Munhoz 1d6959c4ac chore: system wide ratelimit per path (#25080)
* chore: Add system wide rate limiting

* Handle and convert HttpError to 429

* Make algo 32-bit to prevent <ES2020 error + fix sms manager unit test

* Change core to common to go from 10 requests per minute to 200

* Remove redundant function

* Fix integration tests

* Make sure we allow all legal POST routes

* Allow tRPC post calls

* Add matcher tests on middleware

* Add matcher tests on middleware

* Fix matcher to not use regex

* Fix missing POST allow rule for /api/auth/callback/credentials

* Missed the api/book/event endpoints

* Add missing pages/api routes

* Remove POST middleware for now, very risky

* Remove tests for POST protection

---------

Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
2025-11-12 22:01:59 +00:00
Anik Dhabal BabuandDevin AI a3fc17bc75 fix: add team installation support for HitPay payment integration (#24738)
- Add teamId support to HitPay add.ts installation handler
- Update callback.ts to fetch credentials by teamId for team bookings
- Update webhook.ts to fetch credentials by teamId for team bookings
- Update setup page to accept and handle teamId from query params
- Add permission checks using throwIfNotHaveAdminAccessToTeam
- Ensure credentials are created with teamId for team installs, userId for user installs

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-12 21:58:17 +05:30
ad4b5957fc refactor: optimize credit-service imports with lazy loading (#25091)
* refactor: optimize credit-service imports with lazy loading

- Remove top-level imports of heavy modules (reminderScheduler, email services, i18n, billing services)
- Implement dynamic imports for modules only when needed:
  - reminderScheduler: loaded only when SMS credit limit reached
  - email services: loaded only when sending credit notifications
  - getTranslation: loaded only when handling low credit balance
  - InternalTeamBilling: loaded only in getMonthlyCredits method
  - billing singleton: loaded only when calculating warning limits
- Break circular dependency: credit-service → reminderScheduler → ... → credit-service
- Update tests to mock StripeBillingService for dynamic imports
- All 30 tests passing, no type errors, lint clean

This reduces baseline import cost by deferring:
- Stripe SDK initialization (loaded twice before)
- 557KB+ i18n English translation file
- Email template classes
- Workflow reminder scheduler

Verified with madge: circular dependency successfully resolved

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

* fix: add null checks for billing.getPrice() return value

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-13 00:40:51 +09:00
a7cb71726a fix: hide duplicate phone field when attendee phone location selected (#23118)
* fix: hide duplicate phone field when attendee phone location selected

* Instead of hiding use autofill the value to all other phone fields

* add e2e test

* add not sync test and use changeHandler instead of useEffect

* address cubics comments

* adding the phone check back

* use zod schema instead of this type casting

* use Enum instead of hardcoded string for phone

* Fix e2e test

* Fix e2e tests

* Delete .retracify.html

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
Co-authored-by: Volnei Munhoz <volnei@cal.com>
2025-11-12 13:54:36 +00:00
chauhan_s 3e7a848769 refactor: extract back button logic into dedicated wrapper component (#25093)
* refactor: extract back button logic into dedicated wrapper component

* Added changeset
2025-11-12 11:19:35 +00:00
sean-brydon e6e67848b3 fix: Polish, fixes, and i18n updates for onboarding (#24949)
## What does this PR do?

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

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

## Visual Demo (For contributors especially)

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

#### Video Demo (if applicable):

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

#### Image Demo (if applicable):

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

## Mandatory Tasks (DO NOT REMOVE)

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

## How should this be tested?

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

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

## Checklist

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

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








































<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Polished the onboarding experience with a live calendar preview, a team bio field, and improved username handling. Updated i18n strings and added responsive breakpoints for large screens.

- **New Features**
  - Added right-column previews: OnboardingBrowserView in org teams and a live weekly OnboardingCalendarBrowserView for personal calendar setup.
  - Introduced team bio in onboarding store and UI.
  - Expanded i18n: team bio, browser view labels, and CSV invite flow.
  - Added Tailwind screens for 3xl and 4xl to improve large-display layouts.

- **Bug Fixes**
  - Normalized usernames with slugify on default and change to prevent invalid slugs.
  - Unified disabled/readonly handling for premium vs. standard username fields; respects external disabled prop and org membership.

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

<!-- End of auto-generated description by cubic. -->
2025-11-12 10:07:06 +00:00
Lauris Skraucis bf42cd4de9 fix: v2 get event type by id return type (#25090) 2025-11-12 09:26:07 +00:00
chauhan_s 6af7f8b0db fix: add recurringEventCount to URL params (#24986)
* fix: validate occurrence count from URL params and refactor validation logic
- Initialize occurrence count from URL query parameter on page load

* fix: change query parameter name from occurenceCount to recurringEventCount

* refactor: simplify occurrence count validation logic

* fix: prevent NaN from being set in recurring event count query parameter

* fix: prevent overlay calendar toggle from overwriting query params

* fix: sync occurrence count state with max occurrences limit

* refactor: rename occurenceCount to recurringEventCountQueryParam

* fix: prevent invalid recurring event count from updating state

- Added validation guard to only update state when recurringEventCountQueryParam is valid (not null or NaN)
- Simplified URL update logic by removing unnecessary empty string fallback

* feat: add recurring event count badge for mobile (#24991)

* feat: display recurring event count badge in mobile

* feat: update recurring event translation key for clarity
2025-11-12 09:12:49 +00:00
Benny JooandMorgan 421d03eb1a import stripe billing service dynamically in credit-service (#25058)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-11-12 10:18:16 +02:00
mintlify[bot] 9d39742802 Add API v1 deprecation banners (#25065)
* Update docs/api-reference/v1/introduction.mdx

* Update docs/api-reference/v1/authentication.mdx

* Update docs/api-reference/v1/errors.mdx

* Update docs/api-reference/v1/rate-limit.mdx

* Update docs/mint.json

---------

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2025-11-12 04:07:28 +00:00
Joe Au-Yeung 5267f8c3c1 Escape special characters in email query (#25079) 2025-11-11 23:56:51 +00:00
Alex van Andel c6ec42e735 chore: Add diagnostics tool for confirming visitor IP (#25083) 2025-11-11 23:56:36 +00:00
Hariom BalharaandVolnei Munhoz 1325e8488f feat: Reduce webhook response payload to status-only (#25072)
* feat: reduce webhook payload

* Update packages/trpc/server/routers/viewer/webhook/testTrigger.handler.ts

Co-authored-by: Volnei Munhoz <volnei@cal.com>

---------

Co-authored-by: Volnei Munhoz <volnei@cal.com>
2025-11-11 17:18:05 +00:00
Dhairyashil ShindeandUdit Takkar d50a7b9d00 feat: custom-labels-for-attendee-location-fields (#25070)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-11 17:17:35 +00:00
Anik Dhabal Babu 452729f4ab fix: skip email verification for reschedules (#25061) 2025-11-11 16:52:11 +00:00
0206b364ee feat: add tooltip to Group Options label in attributes form (#25063)
* feat: add tooltip to Group Options label in attributes form

- Add InfoBadge component to Group Options label with descriptive tooltip
- Add translation keys 'group_options' and 'group_options_description' to common.json
- Update label to use flex layout for proper InfoBadge alignment
- Tooltip explains that group options create logical groupings of regular options

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

* fix: wrap AttributeForm tests with TooltipProvider

- Add Tooltip.Provider wrapper from @radix-ui/react-tooltip to test renders
- Fixes failing tests caused by InfoBadge component requiring tooltip context
- All AttributeForm tests now pass locally

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-11 16:44:14 +00:00
Eunjae Lee 86353191d2 feat: add booking actions into BookingDetailsSheet (#24912)
* move booking actions into a subfolder

* fix logic to render booking actions dropdown

* skip dropdown portal when rendered from detail sheet

* fix type error
2025-11-11 16:14:48 +00:00
spandev 38f96d97d3 fix: added empty screen for routing response results table (#25076) 2025-11-11 15:15:06 +00:00
e6d44ce620 feat: Add delegation credential error webhook trigger (#24871)
* feat: add delegation credential error webhook trigger

- Add DELEGATION_CREDENTIAL_ERROR to WebhookTriggerEvents enum
- Create DelegationCredentialErrorDTO type for webhook payload
- Implement DelegationCredentialErrorWebhookService
- Add translation for delegation_credential_error
- Enable webhook for API v2 organization webhooks

This webhook will send delegation credential error data to configured URLs when errors occur during calendar authentication with delegation credentials.

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

* fix: add delegation credential error payload type and type guards

- Add DelegationCredentialErrorPayloadType to sendPayload.ts
- Update WebhookPayloadType union to include new payload type
- Add isDelegationCredentialErrorPayload type guard function
- Update isEventPayload to exclude delegation credential errors
- Update template application logic to handle new payload type
- Add corresponding payload type to dto/types.ts for consistency

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

* feat: add delegation credential error handling to WebhookNotificationHandler

- Add DELEGATION_CREDENTIAL_ERROR case to createPayload switch
- Return payload with error, credential, and user data
- Ensures exhaustive type checking passes for new trigger

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

* feat: restrict DELEGATION_CREDENTIAL_ERROR to organization webhooks only

- Add validation in UserWebhooksService to reject DELEGATION_CREDENTIAL_ERROR
- Add validation in EventTypeWebhooksService to reject DELEGATION_CREDENTIAL_ERROR
- Ensures trigger is only available for API v2 organization webhooks as requested

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

* feat: wire up delegation credential error webhook emission in calendar services

- Add webhook emission calls in CalendarAuth.ts for Google Calendar delegation errors
- Add webhook emission calls in Office365 CalendarService.ts for Azure AD delegation errors
- Implement actual webhook emission using WebhookRepository pattern
- Fix pre-existing lint warnings in Office365 CalendarService.ts (unused catch variables, unsafe optional chaining)

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

* test: add e2e tests for DELEGATION_CREDENTIAL_ERROR webhook trigger

- Add comprehensive e2e tests for creating, retrieving, updating, and deleting webhooks with DELEGATION_CREDENTIAL_ERROR trigger
- Test combining DELEGATION_CREDENTIAL_ERROR with other triggers
- Fix import in triggerDelegationCredentialErrorWebhook.ts to use default import for sendPayload
- Tests follow existing patterns in organizations-webhooks.e2e-spec.ts

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

* refactor: remove name field from webhook payload and delete unused service

- Remove name field from triggerDelegationCredentialErrorWebhook function signature and payload
- Update all call sites in GoogleCalendar and Office365 calendar services
- Update DelegationCredentialErrorDTO and DelegationCredentialErrorPayloadType to remove name field
- Delete unused DelegationCredentialErrorWebhookService.ts (dead code - not used anywhere)
- The helper function approach is more appropriate for app-store integrations without DI

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

* refactor: make fire-and-forget intent explicit for webhook emission

- Add void cast to all triggerDelegationCredentialErrorWebhook calls
- Remove redundant .catch() handlers (helper already handles errors internally)
- This makes it clear that webhook emission is non-blocking by design
- Avoids delaying error propagation while webhook HTTP requests complete

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

* feat: await webhook emission and add HTTP timeout for guaranteed delivery

- Change all triggerDelegationCredentialErrorWebhook calls from void to await
- Add 10-second timeout to webhook HTTP requests using AbortController
- Remove name field from DelegationCredentialErrorPayloadType to match payload
- Ensures webhooks are sent before error is thrown (per user requirement)
- Prevents indefinite hangs on unresponsive webhook endpoints

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

* fix: make getAuthUrl async and await all call sites

- Make getAuthUrl async to support awaiting webhook emission
- Add await to all 3 getAuthUrl call sites (constructor, getAzureUserId, testDelegationCredentialSetup)
- Remove leftover name field from getAzureUserId webhook call
- Fixes TS1308 error about await in non-async function

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

* docs: add JSDoc clarifying error handling guarantees for webhook trigger

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

* Revert "docs: add JSDoc clarifying error handling guarantees for webhook trigger"

This reverts commit 3e33090197bfe2f2e3fb890402b32e04c44305e3.

* Revert "fix: make getAuthUrl async and await all call sites"

This reverts commit de28b7337149104412c861fd9b05e76fffc1fed7.

* Revert "feat: await webhook emission and add HTTP timeout for guaranteed delivery"

This reverts commit 9da7241f83a8373b4fadc03ccf34e097c28adf3a.

* Revert "refactor: make fire-and-forget intent explicit for webhook emission"

This reverts commit f4f7fa06b7dfa151bfbea29905b8783261d9f353.

* feat: await webhook emission to match standard pattern

- Updated all webhook call sites to await triggerDelegationCredentialErrorWebhook
- Made getAuthUrl async and updated all 3 call sites to await it
- Removed .catch() wrappers at call sites (error handling is in trigger function)
- Matches standard pattern used in WebhookService.sendPayload with Promise.allSettled
- Ensures webhooks are sent before delegation errors are thrown

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

* fix: address PR feedback - add migration, remove 'as any', remove user.name

- Add Prisma migration for DELEGATION_CREDENTIAL_ERROR enum
- Replace 'as any' type casting with safe type-narrowing helper in CalendarAuth.ts
- Remove user.name field from DelegationCredentialErrorPayloadType (email is sufficient)
- Ensure all type definitions are consistent across sendPayload.ts and dto/types.ts

Addresses feedback from alishaz-polymath and morgan@cal.com

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

* chore: cleanup type in CalendarAuth

* fix: missing DELEGATION_CREDENTIAL_ERROR in WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP constant

* fix: review

* fit: import webhook dto

* fit: type error

* feat: add delegation credential error webhook handling to Office365 video adapter

- Emit webhook before throwing delegation credential errors in Office365 video
- Added webhook emission in 4 locations:
  1. Missing clientId/Secret in fetchNewTokenObject
  2. Missing tenantId in getAuthUrl
  3. Missing clientId/Secret in getAzureUserId
  4. User doesn't exist in Azure AD
- Made getAuthUrl async to support webhook emission
- Follows same pattern as GoogleCalendar and Office365Calendar implementations

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

* fixup! Merge branch 'devin/delegation-credential-errors-webhook-1762171203' of https://git-manager.devin.ai/proxy/github.com/calcom/cal.com into devin/delegation-credential-errors-webhook-1762171203

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: cal.com <morgan@cal.com>
2025-11-11 16:02:06 +02:00
9e1e50e90b feat: cal.ai form triggers #4 (#23587)
* add trigger

* small fixes

* add missing workflow DTOs

* small fixes

* use activeOnWithChildren

* fix active on when switching trigger type

* remove add variable dropdown

* feat: lang support

* fix: type errors

* feat: select voice agent

* refactor: address feedback

* refactor: address feedback

* refactor: missing import

* fix: types

* add getAllWorkflowsFromRoutingForm to WorkflowService

* fix error caused by undefined evt

* fix type error

* fix type error

* fix tests

* feat: add inbound calls

* chore: formatting

* chore

* feat: finish inbound call

* chore: formatting

* fix: update bug

* fix: types

* code clean up

* final fixes and clean up

* remove console.log

* remove template text form from triggers

* add routing form repoditory function

* refactor: Agent Configuration Sheet (#23930)

* refactor: agent configuration sheet

* chore: use default phone numbre

* refactor: improvements

* refactor: improvements

* fix: types

* fix: feedback

* fix bug with key

* chore:

* fix: feedback

* fix: prompt

* add comments

* fix: review

* fix: review

* refactor: class

* refactor: class

* fix test

* allow cal ai action on form triggers

* move any reusable code to scheduleAIPhoneCall

* add missing await

* use predefined FormSubmissionData type

* add .trim() to sms message

* pass contextData instead

* finish base setup

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

* allow cal.ai action for form triggers in handler

* chore: add support for form workflows on api v2

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

* ai phone call on form submissions (WIP)

* use existing type for Option array

* pass chosen event type id

* refactor: rename

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

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

* add missing imports

* chore: update set value

* fix: remove index

* fix: type error

* fix: update tetss

* use only repository functions in update handler

* move all prisma queries from list.handler

* review suggestions

* fix: use logger

* 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: don't use static

* fix: type

* 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

* fix: schema

* refactor:

* remove action check in update handler

* add event type selection

* event type selector improvements

* adjust update.handler

* set outboundEventTypeId

* add back trpc  import

* fix agent repository functions

* clean up

* fix bugs caused by merge

* pass eventTypeId to updateToolsFromAgentId

* add migration for outboundEventTypeId

* add SMS actions to allowed form action constants

* add cal ai to allowed form actions

* pick correct event type for web call

* pass correct routed event type id

* remove unsued import

* fixes for offset api v2

* add missing responseId

* fix failing test

* fix failing test

* improve error message

* remove unused imports

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

* fix typo

* missing missing newStep

* minor fixes

* remove changes

* add routedEventTypeId

* fix type error

* fix type error

* fix typ error in executAPIPhoneCall.tsx

* add back inboundEventTypeId

* remove console.log

* remove outdated code

* small fixes

* don't throw error for missing phone number

* add  back filtered triggerOptions

* fix eventTypeId in testCall handler

* fix type error

* update migration

* fix trigger is not defined

* convert eventTypeId to string

* only use outboundEventTypeId for FORM_SUBMITTED trigger

* show toast when no event type selected

* fix type errors

* add missing translation

* fix type error

* remove callType

* fix tests

* small fixes

* clean up AgentConfigurationSheet

* remove EventTypeSelector file

* code clean up

* clean up

* clean up

* use resusable function for TestPhoneCallDialog and WebCallDialog

* rename result

* fix types for event type id

* use repository runction in workflowReminder.ts

* fix type error

* pass eventTypeIds correctly

* fix typo

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

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>

* use watch instead of getValues

* change to  z.record(z.unknown()) instead of any()

* fix type of eventTypeId

* check permissinon for outBoundEventTypeId

* add isNaN check

* improve function name

* update tools when outbound agent event type id changes

* pass missing outboundEventTypeId

* update migration

* fix test

* remove cal-ai step from test

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Udit Takkar <udit222001@gmail.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>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-11-11 15:24:31 +02:00
a73b804d48 refactor: Split EmailManager into focused service files (#24997)
* refactor: Split EmailManager into focused service files

- Created separate service files for different email categories:
  - auth-email-service.ts: Authentication and verification emails
  - organization-email-service.ts: Organization and team emails
  - billing-email-service.ts: Payment and credit-related emails
  - integration-email-service.ts: Integration and app-related emails
  - workflow-email-service.ts: Workflow and custom emails
  - recording-email-service.ts: Recording and transcript emails

- Refactored email-manager.ts to keep only core booking lifecycle functions
- Removed unused imports from email-manager.ts
- Updated index.ts to export from all new service files
- Updated all imports across the codebase to use package root (@calcom/emails)
- Fixed lint warnings in handleChildrenEventTypes.ts

This reduces the import cost of EmailManager by allowing consumers to import only the specific email services they need.

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

* refactor: Update all imports to use direct service file paths

- Update 49 files to import directly from service files instead of barrel file
- Update packages/emails/index.ts to keep only email-manager and renderEmail exports
- Fix dynamic import in passwordResetRequest.ts
- Update renderEmail imports to use direct path
- Update test file to import from specific service module
- Fix ESLint warnings in modified files (unused variables, unused expressions)

This ensures consumers only import the specific email services they need,
reducing import cost by avoiding the barrel file pattern for service files.

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

* fix: Use default import for renderEmail

renderEmail is exported as a default export, not a named export.
Changed from 'import { renderEmail }' to 'import renderEmail'.

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

* fix: Update test mocks to use direct service file imports

- Update handleNoShowFee.test.ts to mock @calcom/emails/billing-email-service
- Update credit-service.test.ts to mock @calcom/emails/billing-email-service
- These tests were failing because they were mocking the barrel file @calcom/emails
  which no longer exports service functions after the refactoring

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

* fix: unit test spy

* fix: unit test mock

* address cubic comments

* fix: type error sendMonthlyDigestEmail

* remove barrel file and sendEmail unused task

* fixup! remove barrel file and sendEmail unused task

* fixup! fixup! remove barrel file and sendEmail unused task

* fix: integration test mock emails

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-11-11 14:21:10 +02:00
4407a1952c feat: add booking details sheet for /bookings (#24795)
* spike: initial booking detail sheet

* rename button

* revert some changes

* remove unnecessary test

* disable booking details sheet

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-11 08:40:55 -03:00
sean-brydon dc3742d322 chore: Add calendar weekly view enhancements and welcome modal feature (#24948)
## What does this PR do?

- Adds a welcome modal for new Cal.com users
- Implements timezone display in the weekly calendar view
- Creates a hook for fetching onboarding calendar events

## Visual Demo (For contributors especially)

#### Image Demo:

![Welcome Modal](https://user-images.githubusercontent.com/1234567/example-welcome-modal.png)

![Timezone Display](https://user-images.githubusercontent.com/1234567/example-timezone-display.png)

## Mandatory Tasks

- [x] I have self-reviewed the code
- [x] I have updated the developer docs in /docs
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.

## How should this be tested?

1. **Welcome Modal:**
   - Create a new user account
   - Verify the welcome modal appears with correct content
   - Test the "Continue" button closes the modal
   - Check that the modal can be triggered via URL parameter `?welcomeToCalcomModal=true`

2. **Timezone Display:**
   - Go to the weekly calendar view
   - Verify the timezone is displayed correctly when `showTimezone` is enabled
   - Test with different timezones to ensure proper formatting

3. **Onboarding Calendar Events:**
   - Test the hook by connecting a calendar during onboarding
   - Verify events are fetched and displayed correctly
   - Check that events refresh when new calendars are connected

## 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-11-11 10:47:22 +00:00
Lauris Skraucis 8b4f675cee feat: v2 api allow switching event type between collective and round robin (#25045)
* refactor: create team event type hosts

* refactor: update team event type hosts

* feat: allow switching between collective and round robin

* fix: make schedulingType optional when updating

* fix: e2e tests

* fix: e2e and add more tests

* test: only hosts update

* fix: remove test that makes no sense
2025-11-11 09:40:21 +01:00
Volnei Munhoz 53a931da4e Improve check user exists (#25057) 2025-11-11 06:05:37 +00:00
KartikandDhairyashil Shinde 228fbaf72e fix: skip email verification for reschedules and added test case (#24867)
* fix: skip email verification for reschedules and added test case

* used getNewBookingHandler helper instead of direct import in test

---------

Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-11-11 04:05:55 +00:00
Lauris Skraucis 0c53717d0f docs: api v2 rate limits (#24971) 2025-11-11 04:01:33 +00:00
Alex van AndelandKeith Williams e239b67171 chore: Rate limit top level of public booking pages (#25056)
* chore: Rate limit top level of public booking pages

* Prefix rateLimit keys

* Adding more rate limits

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-11-11 00:51:00 +00:00
Volnei Munhoz fd390f9d43 fix: add authorization checks to booking reassignment endpoints (#25054) 2025-11-10 19:20:48 -03:00
Rajiv SahalandAnik Dhabal Babu eae779b81c chore: update api v2 /me endpoint (#23984)
* fix: update profile me ednpoint to include name of user

* fix: update user schema

* pass name for user

* implement PR feedback

* chore: implement PR feedback

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-11-10 19:14:44 +00:00
Dhairyashil ShindeandUdit Takkar 6d621e0b60 docs: update Docker documentation to reference main cal.com repository (#24899)
- Replace all references from github.com/calcom/docker to github.com/calcom/cal.com
- Update Contributing section to clarify Docker setup is officially maintained in main repository
- Update Getting Started instructions to clone main cal.com repository
- Update Advanced Users section to reference Dockerfile and docker-compose.yml in root of main repo
- Clarify that Dockerfile and docker-compose.yml are located in root of calcom/cal.com repository

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-10 19:00:02 +00:00
Muhammad UsmanandKartik Saini ed3de528f3 fix: events type apps tab (Available Apps) section overflow small screens (#23972)
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-11-10 18:30:42 +00:00
Pasquale Vitiello 92f1baa320 style: fix dropdown spacing issues (#24917) 2025-11-10 18:29:34 +00:00
984cd64083 test: add routing-forms tests (#25044)
* test: Add comprehensive security tests for routing forms vulnerability

Add comprehensive test coverage for the getIncompleteBookingSettings handler
vulnerability and ensure entityPrismaWhereClause changes won't break functionality.

Tests added:
1. getIncompleteBookingSettings.handler.test.ts (15 tests)
   - Authorization tests for personal and team forms
   - Credential sanitization tests (key field should never be exposed)
   - Organization hierarchy tests (parent org credentials)
   - App filtering tests (only enabled apps)
   - Edge cases (no credentials, form not found, etc.)

2. entityPrismaWhereClause.integration.test.ts (13 tests)
   - Verifies formQuery, deleteForm, and forms handlers properly scope queries
   - Ensures accepted membership is required for team access
   - Validates consistent entityPrismaWhereClause usage across handlers
   - Prevents regressions when adding role-based filtering

Expected Test Failures:
The getIncompleteBookingSettings tests currently have 4 expected failures that
document the existing vulnerability:
- 2 authorization tests fail (handler doesn't check user access)
- 2 sanitization tests fail (handler leaks the 'key' field with OAuth tokens)

These failures prove the vulnerability exists and document the secure behavior
that should be implemented.

Test Results:
- All 13 entityPrismaWhereClause integration tests pass
- All 18 existing routing-forms test files pass (156 tests)
- 4 security tests fail as expected (documenting the vulnerability)

The tests ensure that:
1. Fixing the vulnerability by adding entityPrismaWhereClause won't break other handlers
2. The key field is never returned in credentials
3. Only authorized users can access forms
4. Team membership requires accepted: true
5. Organization hierarchy is properly handled

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

* Add suggested fix

* Add suggested fix

* fix: enforce authorization scoping and credential sanitization in routing-forms handler

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

* Fix types

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-11-10 16:54:15 +00:00
Alex van Andel db8c7942a9 refactor: Removed asStringOrNull functions (#25029) 2025-11-10 16:17:09 +00:00
4e5d4f67d5 fix: resolve flaky integration tests (#25030)
* fix: resolve flaky org-admin integration tests

- Fixed isAdminGuard Prisma query to use explicit 'is' filter for organizationSettings
- Fixed async describe with top-level awaits in _get.integration-test.ts
- Added global setup in setupVitest.ts to prevent race conditions
- Removed duplicate setup logic from individual test files

Root cause: Tests were running in parallel with independent beforeAll setups,
causing race conditions where organizationSettings weren't created before
tests executed. The async describe with top-level awaits made this worse by
executing queries before beforeAll hooks ran.

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

* fix: move org-admin setup to integration-only setup file

The global setup in setupVitest.ts was running for ALL test workspaces
(including unit tests), causing ECONNREFUSED errors because unit tests
don't have database access.

Changes:
- Created setupVitest.integration.ts with org-admin seeding logic
- Removed database seeding from setupVitest.ts
- Updated vitest.workspace.ts to use integration-only setup file
- Added DATABASE_URL guard to prevent errors when DB is unavailable

This fixes the unit test failures while preserving the fix for flaky
integration tests.

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

* fix: use globalSetup instead of setupFiles for org-admin seeding

The previous fix using setupFiles didn't work because setupFiles run
AFTER test modules are evaluated. This meant any top-level Prisma
queries in test files would execute before the org-admin seeding.

Changes:
- Moved org-admin seeding to tests/integration/global-setup.ts
- Updated vitest.workspace.ts to use globalSetup for IntegrationTests
- globalSetup runs BEFORE any test modules are loaded, ensuring org
  settings exist before tests execute
- Added teardown function to properly disconnect Prisma after tests

This ensures org-admin state is seeded once before all integration
tests run, eliminating the race condition and ensuring tests have
the correct database state.

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

* debug: add logging to globalSetup to diagnose why tests are failing

Added console.log statements throughout the globalSetup to verify:
- Whether the globalSetup is running at all
- Whether DATABASE_URL is available
- Whether the org teams are found in the database
- Whether the upserts are executing successfully

This will help diagnose why the integration tests are still failing
with org-admin not being detected.

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

* fix: use absolute path for globalSetup in vitest.workspace.ts

Changed from relative path 'tests/integration/global-setup.ts' to
absolute path using new URL().pathname to ensure Vitest can properly
locate and load the globalSetup file.

This should fix the issue where the globalSetup wasn't being executed
at all (no logs appearing in CI).

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

* fix: add serial execution to IntegrationTests workspace

Added sequence.concurrent: false to IntegrationTests workspace to eliminate
inter-file race conditions while stabilizing org-admin seeding. This ensures
tests run one at a time, preventing parallel execution issues that could
cause flaky test failures.

This is a temporary stabilizer that can be reverted once the globalSetup
seeding is confirmed working.

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

* refactor: use TeamRepository in globalSetup to follow architectural rule

Refactored globalSetup to use TeamRepository instead of direct Prisma
access, following the 'No prisma outside of repositories' architectural
rule.

Changes:
- Created TeamRepository class with methods for finding organizations
  and upserting organization settings
- Updated globalSetup to use TeamRepository.withGlobalPrisma()
- Removed direct Prisma imports from globalSetup

This ensures proper separation of concerns and follows the repository
pattern established in the codebase.

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

* fix: use relative path import for TeamRepository in globalSetup

Changed from package-scoped import '@calcom/lib/server/repository/team'
to relative path import '../../packages/lib/server/repository/team' to
fix module resolution issue.

Added try/catch with logging around the import to surface any remaining
resolution issues in CI logs. This should allow the globalSetup to
execute properly and seed org-admin state before tests run.

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

* debug: add membership logging to globalSetup to diagnose test failures

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

* fix: ensure owner1-acme membership exists in globalSetup

Root cause: CI database snapshot doesn't include the owner1-acme OWNER membership that exists in the current seed file, because cache-db action's cache key doesn't include scripts/seed.ts.

Solution: Add ensureMembership method to TeamRepository and call it in globalSetup to ensure the owner1-acme user has an accepted OWNER membership in the Acme org before tests run.

This fixes the 5 failing org-admin integration tests that depend on this membership.

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

* fix: ensure all 10 member{0-9}-acme users exist in globalSetup

Add ensureUser method to TeamRepository to create users if they don't exist.
Update ensureMembership to accept MEMBER role in addition to OWNER and ADMIN.
Ensure all 10 member{0-9}-acme users are created with MEMBER role and accepted: true in the Acme org.

This should fix the remaining 4 failing tests that expect multiple org members to exist.

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

* fix: use upsert instead of create in ensureUser to avoid unique constraint violations

The ensureUser method was using create which could fail if a user with that email already exists.
Switch to upsert to make the operation idempotent and avoid P2002 unique constraint errors.

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

* refactor: clean up debug code and move test repository to proper location

- Remove all console.log debug statements from global-setup.ts
- Remove serial execution from IntegrationTests workspace (restore parallel execution)
- Move TeamRepository to tests/lib/test-team-repository.ts and rename to TestTeamRepository
- Keep all actual fixes: isAdmin Prisma query fix, ensureUser/ensureMembership methods, globalSetup seeding

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-10 15:47:39 +00:00
Hariom Balhara 83488f0a35 refactor: Enhance event type retrieval logic (#24985)
- Deleted the getEventTypeById.test.ts file as it was no longer needed.
- Updated getEventTypeById.ts to include OrganizationRepository for improved organization admin checks.
- Refactored event type retrieval logic to ensure proper access control for organization admins.
2025-11-10 13:50:21 +05:30
Kartik 9a52938f64 fix: strip internal $RCH$ prefix from cancellation reason in ICS files (#25016)
* fix: strip internal RCH prefix from cancellation reason used in ICS

* fixed

* trimmed
2025-11-09 21:15:17 +00:00
e24e1d8769 fix(apps/web): enhance JWT response with token type and expiration details (#24841)
* fix: enhance JWT response with token type and expiration details

* fix: update email generation in createUsersFixture to use v4 UUID without dashes

* revert: update email generation in createUsersFixture to use short-uuid for unique email identifiers

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Volnei Munhoz <volnei@cal.com>
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-11-10 02:15:56 +05:30
Dhairyashil Shinde ef4be48ea6 fix: configure correct error message for duplicate event type slug (#24928)
* fix/duplicate-event-type-to-existing slug

* use existing pattern to to verify the conflict on slug
2025-11-09 20:22:12 +00:00
spandevandDhairyashil Shinde 5c9ad93923 fix: back button in settings updated to use window history (#24952)
* fix: back button in settings updated to use window history

* fix: removed label from button

* update: added aria-label, Button and useRouter

---------

Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2025-11-10 01:23:31 +05:30
emrysal 85fe40688e chore: release v5.8.12
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-07 20:12:13 +00:00
Lauris SkraucisandUdit Takkar 829edec0ea refactor: v2 api event-types/:eventTypeId access (#24969)
* refactor: EventTypeAccess service

* feat: event-types/:id system admin access and team event access

* fix: implement cubic feedback

* fix: e2e

* fix: e2e

* fix: oasdiff ignore non-breaking change

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-07 21:17:16 +02:00
Rajiv Sahal 81a99ea0b2 chore: add bookingUid filter to get org teams booking endpoint (#24960)
* chore: add bookingUid filter to get org teams booking input

* chore: update tests
2025-11-07 17:52:45 +00:00
Lauris Skraucis 799e354059 refactor: api v2 breaking changes CI check (#24999) 2025-11-07 15:13:48 +00:00
Dhairyashil Shinde 584c991172 feat: new workflow variable booking title (#24308)
* feat(workflow): new variable booking title

* feat(workflow): new variable booking title

* change the description of variable to The booking title

* revert previous logic changes
2025-11-07 15:08:20 +01:00
emrysal 47e976df96 chore: release v5.8.11
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-07 12:38:40 +00:00
Rajiv Sahal b44efddc0f fix: make sure we update the correct item when generating invoice for PER_ACTIVE_USER plan (#24940) 2025-11-07 11:58:31 +00:00
bc665cbeab fix: APIV2 team membership - Member not getting added to event-type automatically (#24780)
* fix: APIV2 team membership addition

* feat: Add trimming for email domain and orgAutoAcceptEmail in auto-accept logic

- Trim whitespace from both user email domain and orgAutoAcceptEmail
- Ensures consistent matching even with accidental whitespace
- Addresses feedback from PR review

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

* simplify

* feat: Use shared OrganizationMembershipService in TRPC for consistent auto-accept logic

- Create OrganizationMembershipService.container.ts for DI in TRPC
- Update getOrgConnectionInfo to apply trimming + case-insensitive comparison
- Precompute auto-accept decisions in createNewUsersConnectToOrgIfExists using the service
- Use service in handleNewUsersInvites for consistent auto-accept determination
- Ensures both API v2 and TRPC paths use identical trimming and normalization logic

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

* Revert "feat: Use shared OrganizationMembershipService in TRPC for consistent auto-accept logic"

This reverts commit 0b2bd28b6e32e8c1d3dea139ca8ff9cbf402ac00.

* refactor: Unify OrganizationRepository and remove duplicate PrismaOrganizationRepository (#24869)

* refactor: Convert OrganizationRepository from static to instance methods

- Add constructor accepting deps object with prismaClient
- Convert all static methods to instance methods
- Add getOrganizationAutoAcceptSettings method
- Create singleton instance export in repository barrel file
- Update API v2 OrganizationsRepository to extend from OrganizationRepository
- Update all call sites to use singleton instance
- Add platform-libraries organizations.ts export
- Fix mock imports to use repository barrel
- Fix unsafe optional chaining in next-auth-options.ts
- Fix any types in test files with proper type inference

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

* fix: Update all imports to use OrganizationRepository barrel export

- Update imports from direct OrganizationRepository file to barrel export
- This ensures mocks work correctly in tests
- Fixes 202 failing tests related to organizationRepository mock

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

* fix: Update test mocks to use partial mock pattern

- Convert organizationMock to partial mock that preserves real class
- Add proper prisma mocks to failing test files
- Remove old OrganizationRepository mocks from test files
- This fixes test failures related to mock interception

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

* fix: Export mocked singleton and update tests to use it directly

- Export mockedSingleton as organizationRepositoryMock from organizationMock
- Update delegationCredential.test.ts to import and use the exported mock
- This fixes 'vi.mocked(...).mockResolvedValue is not a function' errors

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

* fix: Use platform-libraries import for API v2 OrganizationRepository

API v2 should import shared features through @calcom/platform-libraries
instead of directly from @calcom/features to maintain proper architectural
boundaries and packaging/licensing separation.

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

* refactor: Implement DI pattern for OrganizationRepository

- Create OrganizationRepository.module.ts and .container.ts for DI
- Replace singleton pattern with getOrganizationRepository() across 20 files
- Update platform-libraries to export getOrganizationRepository
- Delete duplicate PrismaOrganizationRepository.ts
- Remove singleton export file (repositories/index.ts)
- Update test mocks to use DI container pattern
- All type checks and unit tests passing

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

* fix: Implement read/write client separation in OrganizationRepository

- Updated OrganizationRepository constructor to accept optional prismaWriteClient parameter
- Routed all write operations (create, update) through prismaWrite client
- Routed all read operations (find, get) through prismaRead client
- Updated API v2 OrganizationsRepository to pass both dbRead.prisma and dbWrite.prisma to super()
- Optimized getOrganizationRepository() calls by storing in local variables to avoid repeated function calls
- This fixes the critical issue where API v2 was passing read-only client to base class with write methods

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

* fix: Fix mock setup and optimize getOrganizationRepository() calls

- Fixed verify-email.test.ts mock to return mocked repository instance instead of scenario helper object
- Added mockReset to organizationMock.ts beforeEach to properly reset mock implementations between tests
- Added local variables in page.tsx to store getOrganizationRepository() result for consistency

This fixes the issue where getOrganizationRepository() was returning organizationScenarios.organizationRepository (scenario helper) instead of the actual mocked repository instance, causing findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail to be undefined.

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

* refactor: Simplify OrganizationRepository to use single prismaClient

- Updated base OrganizationRepository constructor to accept only { prismaClient } instead of { prismaClient, prismaWriteClient? }
- Replaced this.prismaRead and this.prismaWrite with single this.prismaClient property
- Updated API v2 OrganizationsRepository to pass only dbWrite.prisma as prismaClient
- Removed unused PrismaReadService import from API v2
- All read and write operations now use the same client instance

This simplifies the architecture as requested - API v2 uses write client for all operations, and apps/web uses the client from DI container.

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

---------

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

* fix: Match OrganizationsRepository.findById signature with base class

The findById method in OrganizationsRepository was using a different signature
than the base OrganizationRepository class, causing type errors in CI.

Changed from: findById(organizationId: number)
Changed to: findById({ id }: { id: number })

This matches the base class signature and resolves the CI unit test failures.

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

* fix: Update all findById call sites to use object parameter

Fixed 6 call sites in API v2 that were calling findById with a number
instead of the required { id: number } object parameter:

- is-org.guard.ts
- is-admin-api-enabled.guard.ts
- is-webhook-in-org.guard.ts
- organizations.service.ts
- managed-organizations.service.ts (2 call sites)

This resolves the API v2 build failure caused by the signature change
in OrganizationsRepository.findById to match the base class.

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

* fix: Remove redundant findById override from OrganizationsRepository

The findById method was duplicating the base class OrganizationRepository
implementation. Both methods had identical logic (filtering by isOrganization: true),
so the override was unnecessary.

Since OrganizationsRepository extends OrganizationRepository and passes
dbWrite.prisma to the base constructor, the base class method already
provides the exact same functionality.

This resolves the API v2 build failure by eliminating the duplicate method
that was causing conflicts.

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

* Remove unncessary changes

* Store in variable

* Revert "Remove unncessary changes"

This reverts commit af9351786a21616c9508c441191c17f2374fb2cc.

* Revert dbRead/dbWrite changes

* Add organizations library to tsconfig.json

---------

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-11-07 10:50:00 +00:00
sean-brydon 02b1393ff4 chore: Update personal and team onboarding flows (remove profile/video steps, add settings, improve team invites) (#24947)
## What does this PR do?

- Redesigns and streamlines the onboarding flow for personal, team, and organization accounts
- Consolidates shared components and improves code organization
- Adds browser preview for better user experience during onboarding
- Simplifies the personal onboarding flow by removing the video integration step
- Enhances team onboarding with CSV upload functionality

## Visual Demo (For contributors especially)

#### Image Demo:
- The PR adds a new browser preview component that shows users how their profile/team will look during onboarding
- Redesigned UI with a more consistent layout across all onboarding steps
- Improved mobile responsiveness with better component organization

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

1. Test the complete onboarding flow for personal accounts:
   - Start at `/onboarding/getting-started`
   - Proceed through personal details and calendar setup
   - Verify the flow completes successfully

2. Test the team onboarding flow:
   - Start at `/onboarding/getting-started` and select team
   - Complete team details
   - Test the invite options including CSV upload
   - Verify team creation works correctly

3. Test the organization onboarding flow:
   - Start at `/onboarding/getting-started` and select organization
   - Complete organization details and branding
   - Test member invitations
   - Verify organization creation works correctly

4. Verify browser preview functionality:
   - Check that the preview updates in real-time as you enter information
   - Confirm it displays correctly on different screen sizes

## 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-11-07 08:54:55 +00:00
sean-brydon 52e27bab53 chore: Add shared onboarding components (OnboardingCard, OnboardingLayout, browser views) (#24946)
## What does this PR do?

Creates new onboarding UI components for the user onboarding flow, including:

- `OnboardingCard` - A reusable card component with title, subtitle, content, and footer sections
- `OnboardingLayout` - A layout component with progress indicators and sign-out functionality
- `OnboardingBrowserView` - A browser preview showing how the user's booking page will look
- `OnboardingCalendarBrowserView` - A calendar preview showing sample events
- `OnboardingInviteBrowserView` - A preview of the team invitation email
- Enhanced `PlanIcon` component with new variants and animations for organization and team plans

## Visual Demo (For contributors especially)

#### Image Demo:
The PR adds several visual components for the onboarding flow, including browser previews, calendar views, and animated plan icons with concentric rings and user avatars.

## 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 to the onboarding flow to see the new components in action
- Test the layout with different screen sizes to ensure responsive behavior
- Verify that the browser previews render correctly with sample data
- Check that the plan icon animations work properly for different variants (single, team, organization)
- Ensure the calendar view displays sample events 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-11-07 07:58:34 +00:00
Lauris Skraucis 45278a2195 feat: api v2 pbac support (#24402)
* feat: Pbac decorator and guard

* feat: v2 roles endpoints

* fix: test

* fix: starting v2

* fix: test error

* fix: test api keys

* fix fixture

* test permission creation

* feat: permissions endpoints

* refactors

* refactor: project structure

* test: role permissions crud

* test: permissions endpoint negative tests

* docs: org, team permissions swagger

* unit tests for validator

* Update roles.guard.ts

* fix type

* test: error messages

* refactor: dont throw error in pbac

* delete redundant test file

* feedback: logging error

* fix: persist role.permissions when updating role.otherProperty

* refactor: use output service to return permissions

* refactor: service functions return current permissions

* refactor: remove OrganizationsRepository from providers

* refactor: try catch possibly duplicate create

* refactor: require min length name if provided

* refactor: org role has orgId and team role teamId

* fix: pbac guard caching

* fix: e2e tests in parallel

* refactor: use IsTeamInOrg guard for orgs teams roles and permissions endpoints

* refactor: use redis service getter and setter

* refactor: invalidate team permissions cache when permissions change

* refactor: delete keys instead of versioning when caching
2025-11-06 17:14:55 +00:00
Hariom BalharaandUdit Takkar 2e8b89c4ee fix: enhance bot ID initialization check in instrumentation client (#24961)
- Add additional check for the presence of window.crypto.randomUUID function
- Ensures compatibility with environments that support both getRandomValues and randomUUID

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-06 12:00:31 +00:00
Joe Au-Yeung 7d17b443cf chore: Add cache log (#24954)
* Add log

* Update getCalendar.ts
2025-11-06 08:24:52 +00:00
Rodrigo EhlersandUdit Takkar c6ab6b3d13 fix: keep dialog state by decoupling it from list (#24914)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-06 13:37:26 +05:30
spandev c741d8133e fix: added icon and description in team event type empty page (#24951) 2025-11-06 13:26:06 +05:30
b01cb27c56 feat: improve overlapping events display in weekly calendar view (#24880)
* feat: improve overlapping events display in weekly calendar view

- Add cascading layout algorithm for overlapping events
- First event at 80% width, subsequent events offset by 8%
- Implement hover behavior to bring events to front (z-index 100)
- Extract overlap logic into reusable utility functions
- Add comprehensive unit tests for overlap detection and layout calculation
- Sort events by start time, then by duration for consistent rendering

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

* feat: add weekly calendar playground page for testing overlapping events

- Create comprehensive playground page with 8 test scenarios
- Include two overlapping, three cascading, non-overlapping, same start time, chain overlaps, dense day, touching events, and mixed statuses
- Add focused view with scenario selector and grid view for side-by-side comparison
- Update playground index to include weekly calendar link
- Each scenario includes description, expected behavior, and collapsible event data

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

* update base props

* remove unneccessary things

* fix: isolate Calendar store instances to prevent shared state

- Refactor useCalendarStore to use Zustand's createStore with React Context
- Each Calendar component now creates its own isolated store instance
- Maintains backward compatibility with global store fallback
- Fixes issue where multiple Calendar instances on same page shared state

This allows the playground page to render multiple Calendar instances
without state conflicts between them.

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

* fix: prevent event overflow in dense overlap scenarios

- Dynamically compress offset step based on overlap group size
- Ensure leftOffset + width never exceeds 100% to prevent bleeding into next day
- Add width clamping as safety guard against rounding errors
- Add 3 new tests for overflow prevention with dense event scenarios

Fixes issue where 10+ overlapping events would cascade beyond day boundary
and bleed into the next day's column. The algorithm now calculates the
maximum safe step size per overlap group while maintaining visual cascade.

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

* remove global store

* fix: comprehensive overflow prevention with safety margin and CSS fix

- Add safetyMarginPercent (0.5%) to prevent rounding and CSS box model overflow
- Implement floor3 rounding for width to guarantee left + width <= 100 - safetyMargin
- Calculate width from rounded left offset to avoid rounding mismatch
- Remove inset-x-1 class conflict that was setting both left and right positioning
- Change from marginLeft to left positioning for proper control
- Add test for 20+ overlapping events to verify no overflow
- Update existing tests to verify safety margin is respected

This fixes the slight overflow issue reported by the user where events were
still bleeding into adjacent day columns despite the initial dynamic step
compression fix. The root cause was a combination of:
1. CSS conflict: inset-x-1 class setting both left and right positioning
2. Rounding mismatch: width calculated from unrounded left offset
3. No safety margin for CSS box model effects (borders, padding)

All 21 tests now pass including new safety margin verification.

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

* feat: update Dense Day scenario to 20+ events

- Increase from 11 to 21 overlapping events in Dense Day scenario
- Update title from '10+ Events' to '20+ Events'
- Add 10 more diverse events with staggered start times
- Better stress test for overflow prevention with safety margin

This provides a more comprehensive test case for the overflow prevention
fix and matches the user's request for 20+ events instead of 10+.

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

* WIP

* feat: enhance event tooltip to show full event details

- Replace simple title-only tooltip with rich content tooltip
- Display event title, time range, description, and status
- Add color indicator matching the event's visual style
- Set min-width (200px) and max-width (300px) for better readability
- Use inverted theme colors for better contrast in tooltip

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

* faster animation

* feat: position tooltip based on day of week

- Show tooltip on right side for Monday-Thursday (days 1-4)
- Show tooltip on left side for Friday-Sunday (days 5-0)
- Calculate day of week using dayjs from event start date

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

* add margin for tooltip

* feat: add event duration layout test scenario

- Add new scenario with events of varying durations (3, 7, 15, 20, 30, 53 minutes)
- Test layout logic where eventDuration > 30 changes flex direction
- Events ≤30min show horizontal layout (title and time inline)
- Events >30min show vertical layout (title and time stacked)

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

* update styles

* feat: add minimum height of 15px for events

- Ensure very short events (e.g., 3 minutes) are still visible
- Use CSS max() to apply minimum height while respecting duration-based height
- Prevents events from becoming too small to interact with

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

* update playground

* do not mutate

* pre-compute some values

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Sean Brydon <sean@cal.com>
2025-11-05 15:56:24 +00:00
c48f2043f2 feat: booking reports table and blocklist table for org (#24736)
* feat: booking reports and blocklist table

* chore: save progress

* chore: save progress

* feat: finish

* fix: types and test

* fix: types and test

* refactor: separate tables

* refactor: ffedbac

* refactor: remove delete report

* refactor: add go back button

* feat: add icon and svg

* chore: nit

* fix: type errror

* fix: type errror

* refactor: code improvements

* chore: add docs url

* chore: feedback

* minor UI fix

* fix: add Is-calcom check

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-11-05 14:48:06 +00:00
emrysal 630eee4fab chore: release v5.8.10
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-05 13:06:46 +00:00
Morgan 1ebf553181 refactor: checkBookingRequiresAuthentication with rescheduleUid (#24943) 2025-11-05 13:02:01 +00:00
Hariom Balhara e8a315ac4e Remove cal.cache query parameter to prevent stale calendar data (#24939)
- Remove cal.cache query parameter parsing from booking and slots flows
- Set shouldServeCache to false for all booking availability checks
- Remove _shouldServeCache from schemas and type definitions
- Clean up all references in platform atoms and API endpoints

This ensures fresh Google Calendar data is always fetched for conflict checking,
preventing bookings when there are actual calendar conflicts.
2025-11-05 13:01:22 +00:00
Udit TakkarandVolnei Munhoz c196ff1095 feat: link email to participant (requireEmailForGuests) (#24661)
* feat: link email to participatn

* fix: bugs

* refactor: improve code

* refactor: prevent repload

* chore: remove unued

* fix: type

* refactor

* fix: type

* feat: restrict host

* feat: type

* feat: tests

* fix: don't allow guest

* fix: merk guest

* fix: bugs

* fix: test

* fix: test

* refactor: feedback

* fix: tests

---------

Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
2025-11-05 11:15:41 +00:00
bf0fbe4888 feat: skip authentication check for reschedule bookings with validation (#24903)
* feat: skip authentication check for reschedule bookings with validation

- Skip checkBookingRequiresAuthentication when rescheduleUid is present
- Add validation to ensure rescheduleUid points to a real booking
- Verify booking status is ACCEPTED (upcoming)
- Verify booking uses the same event-type
- Throw appropriate errors for invalid reschedule attempts

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

* refactor: move reschedule validation logic into checkBookingRequiresAuthentication

- Refactor checkBookingRequiresAuthentication to accept optional rescheduleUid parameter
- Move reschedule booking validation logic inside the method
- Simplify createBooking method by removing duplicate validation code
- Maintain same validation logic: check booking exists, is ACCEPTED, and uses same event-type

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

* fix: allow PENDING bookings to be rescheduled

- Update status validation to allow both ACCEPTED and PENDING bookings
- Change error message to reflect both allowed statuses
- PENDING bookings can now be rescheduled without authentication check

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

* refactor: separate reschedule validation from auth check

- Extract validateRescheduleBooking method to handle reschedule-specific validation
- Keep checkBookingRequiresAuthentication strictly for auth checks
- Use conditional logic in createBooking: validate reschedule OR check auth
- Improves code clarity by separating concerns per lauris's feedback

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-05 11:07:43 +00:00
Syed Ali Shahbaz eb6418c0d5 Add enableOverflow prop to DialogContent (#24935) 2025-11-05 10:46:44 +00:00
Udit TakkarandCarinaWolli 2df2868b20 fix: cal ai webhook (#24368)
* fix: cal ai email

* fix: remove

* fix: org

* replace Cal AI with Cal.ai

* fix: use

* fix: feedback

* fix: types

* fix: types

* fix: types

* fix: tests

* Merge branch 'main' into fix/cal-ai-credits

* refactor: feedback

* refactor: imporvement

* fix: type

* refactor: feedback

* fix: tests

* fix: use pbac

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-11-05 13:31:55 +04:00
Anik Dhabal Babu 7f48c7fdad fix: Ignore userIds form filter segment if no permission (#24194)
* Update get.handler.ts

* fix: Ignore userIds form filter segment if no permission

* add tests

* Update booking-filters.e2e.ts

* Update booking-filters.e2e.ts
2025-11-05 13:09:13 +05:30
Udit Takkar 2101051623 style: fix height of report button (#24918)
* style: fix height

* style: fix height
2025-11-05 07:35:58 +00:00
Alex van Andel a241cb0bfc hotfix: Bring back English translation for cal.ai banner (#24922) 2025-11-04 19:30:29 +00:00
Anik Dhabal Babu 5417012cec Revert "fix: sanitize username input to prevent hyperlinks (#24210)" (#24916)
This reverts commit 7d16da5c98.
2025-11-04 17:01:05 +00:00
Dhairyashil Shinde 8e818e0567 refactor: remove-zapier-setup-screen (#24652)
* refactor: remove-zapier-setup-screen

* remove setup page files, related config and update readme

* make zapier a redirecting app

* update readme

* add check for missing website

* add noopener,noreferrer for _blank links
2025-11-04 15:55:09 +00:00
Morgan 1fdd04215d fix: missing await in updateEventType old endpoint (#24911) 2025-11-04 15:15:19 +00:00
Dhairyashil Shinde 0bf0749b29 only custom template needs upgrade and free users cannot edit email title and body of existing templates (#24907) 2025-11-04 13:43:40 +00:00
Dhairyashil Shinde 4006143e3a refactor: remove cal ai tip (#24904) 2025-11-04 11:56:21 +00:00
c6ad767565 refactor: decouple @calcom/prisma from @calcom/features, @calcom/ee, and @calcom/lib (#24802)
* refactor: decouple @calcom/prisma from @calcom/features, @calcom/ee, and @calcom/lib

- Inline helper functions in zod-utils.ts (emailSchema, slugify, getValidRhfFieldName, isPasswordValid, intervalLimitsType, zodAttributesQueryValue)
- Update schema.prisma @zod.import comments to reference zod-utils instead of @calcom/lib
- Inline idempotency key generation in booking-idempotency-key extension using uuid v5
- Move usage-tracking extension to @calcom/ee/prisma-extensions/
- Remove usage-tracking extension from packages/prisma/index.ts
- Move Prisma DI module from @calcom/prisma to @calcom/features/di/modules/Prisma.ts
- Update 20 import paths in @calcom/features to use new Prisma DI module
- Remove @calcom/lib dependency from @calcom/prisma package.json
- Add uuid dependency to @calcom/prisma package.json

This reduces the dependency footprint of @calcom/prisma and breaks circular dependencies between packages.

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

* feat: add EE-specific Prisma DI module with usage tracking

Create packages/ee/di/modules/PrismaEE.ts to apply the usage-tracking extension in EE contexts. This module:
- Imports the base prisma client from @calcom/prisma
- Applies the usageTrackingExtention from @calcom/ee/prisma-extensions/usage-tracking
- Exports the same DI tokens as the base Prisma module for override in EE containers

This ensures usage tracking functionality is preserved in EE builds while keeping the base @calcom/prisma package free of EE dependencies.

Note: EE containers should load this module after the base Prisma module to override the bindings.
Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-04 06:51:13 -03:00
896f8dbd3d fix: align booking limit timezone between availability and validation (#24846)
* fix: align booking limit timezone between availability and validation

- Use eventType.schedule?.timeZone for booking limits in availability calculation
- Previously used user's timezone causing day boundary mismatch
- Add unit tests to verify timezone alignment
- Fixes issue where slots remain available after reaching booking limit

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

* Rename variable limitsTz to eventTimeZone

* update

* fix: correct timezone fallback to include user timezone without forcing UTC

- Changed fallback chain to: schedule → event → user → undefined
- Treats empty strings as missing timezones
- Does not force UTC when all timezones are missing
- Aligns with validation behavior when timezone is undefined
- Fixes getSchedule.test.ts failures

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

* Revert "fix: correct timezone fallback to include user timezone without forcing UTC"

This reverts commit 721e0bdfd7b08004e68df6bdf5b97b9bd6cf3d5c.

* test: remove complex unit tests with 'as any' casts

- Removed Booking Limits Timezone Alignment tests that required heavy mocking
- These tests relied on spying private methods and had many 'as any' casts
- Timezone alignment is already covered by integration tests in getSchedule.test.ts
- Kept BookingDateInPastError test which has no type issues

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

* address review

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-04 12:13:42 +05:30
emrysal f1db27c0ad chore: release v5.8.9
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-04 00:30:52 +00:00
Eunjae LeeandDevin AI 3a1e04bb42 refactor: extract booking actions dropdown to separate component (#24872)
* refactor: extract booking actions dropdown to separate component

- Created BookingActionsDropdown component with all dropdown actions and dialogs
- Component receives booking as prop and handles all action logic internally
- Removed duplicate dialog states and mutations from BookingListItem
- Fixed img tag to use Next.js Image component
- Maintains same functionality and UI while improving code reusability

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

* fix: restore cardCharged variable in BookingListItem

The cardCharged variable was accidentally removed during the refactoring
but is still needed for the actionContext in BookingListItem.tsx

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

* refactor: move BookingItemProps type to separate types.ts file

- Created types.ts to hold shared booking types
- Updated BookingListItem.tsx to import from types.ts
- Updated BookingActionsDropdown.tsx to import from types.ts
- Updated bookingActions.ts to import from types.ts
- Removed unused RouterInputs and RouterOutputs imports from BookingListItem.tsx
- Removes unwanted dependency chain where BookingActionsDropdown imported from BookingListItem

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

* remove unnecessary description

* extract states as a zustand store

* fix dialog issue

* fix type errors

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-04 00:25:48 +00:00
Alex van Andel ab37847ea1 chore: User->UUID as a required field, ready for use in foreign keys (#24881) 2025-11-04 00:20:52 +00:00
Dhairyashil Shinde 0a2ddf05ca refactor: remove cal ai banners (#24885) 2025-11-04 01:57:43 +05:30
Lauris Skraucis 04fc0f1d52 docs: event calVideoSettings not available for platform (#24879) 2025-11-03 18:53:40 +00:00
Amit Sharma b76c4c9dcc fix: alby booking page breaking (#24859)
* fix: alby booking page breaking

* remove logs
2025-11-03 14:35:20 -03:00
Vibhor PhalkeandKartik Saini 68b71bfb0a address linting errors (#24171)
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-11-03 17:29:24 +00:00
Eunjae Lee 763c568797 fix: existence check of searchParams in NavigationItem (#24877) 2025-11-03 16:42:31 +00:00
Rajiv Sahal 4659d08c07 chore: return back timezone of event owner (#24876) 2025-11-03 16:29:29 +00:00
Sachin Yadav b9c350cc45 fix: Recurring events set to "yearly" don't recur (#24361)
* #fix: Recurring events set to “yearly” don’t recur

* fix: handle yearly recurring events with explicit null check

* fix: use explicit null check for recurring event frequency
2025-11-03 16:20:15 +00:00
an_ifrah24 f63d705523 fix: url truncate properly in the input field on diff screen sizes (#24700) 2025-11-03 16:14:49 +00:00
Dhairyashil Shinde 2907c5e696 fix(ui): interface language toggle (#24371) 2025-11-03 21:20:34 +05:30
sean-brydon dcb47e31ed fix: address onboarding v3 teams redirectfrom onboarding (#24875)
* Address redirect in subdomain for teams v3

* Revert constants
2025-11-03 15:43:55 +00:00
Dhairyashil ShindeandAnik Dhabal Babu cd5b75fe2c fix: hide organizer email on cancellation page (#24616)
* fix: hide organizer email on cancellation page

* if cancelled by any host - hide the cancelled by

* Refactor organizer email logic and clean up code

Removed the isOrganizerEmail function and updated conditions for displaying the cancelledBy information.

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-11-03 14:18:38 +00:00
Steven Adger 97bdf837a9 Include setup_intent events in webhook (#24805)
The setup_intent webhook is needed for held payments / no-show fees to work. 

If it is not included, a booking gets stuck saying "Pending payment" and "Unconfirmed" despite successfully submitting the card info. After including it, the booking status will successfully change to "Card held".

See:
https://github.com/calcom/cal.com/blob/817e20f11e3a75ec0bc62535a77c618bd27eaaf4/packages/features/ee/payments/api/webhook.ts#L323

https://cal.com/blog/cal-com-s-held-payments-a-useful-tool-for-organizations-offering-free-consultatio
2025-11-03 14:16:06 +00:00
Dhairyashil Shinde 5663b2ae59 feat: disable form workflows for free plan, add upgrade badge, make reminder template free (#24839) 2025-11-03 14:08:38 +00:00
Chad RobinsonandPallav 02b57eb82e fix: Add refresh token to refresh-token request response (#24831)
* Add refresh token to refresh-token request response

This change addresses an issue reported in https://github.com/calcom/cal.com/issues/23939#issuecomment-3443480099 in which a new refresh token is not included in the OAuth2 refresh-token endpoint response.

* Address failing type checks

---------

Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2025-11-03 19:10:36 +05:30
sean-brydon b23dfa194e fix: video install step v3 (#24865)
* Add teams i18n

* Fix UI nits in invite flow

* chore: update redirect

* Update personal view settings

* Fix video redirect
2025-11-03 13:25:54 +00:00
Eunjae LeeandDevin AI 2484b6fad2 fix: filter deleted fields from routing form insights headers (#24830)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-11-03 12:57:37 +00:00
Hariom Balhara c154bb31b7 fix: Arrows visible when embed has hidden entire page (#24868) 2025-11-03 12:28:54 +00:00
emrysal 0cf4ebd544 chore: release v5.8.8
Post release / release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2025-11-03 12:03:36 +00:00
Dhairyashil Shinde 805c4d4c09 fix: dont allow email exclusion bypass with capital letters (#24741)
* fix: dont allow email exclusion bypass with capital letters

* trim before lowercasing to filter out empty tokens

* update console message

* use only regex change to implement this

* use only regex change to implement this
2025-11-03 11:59:14 +00:00
sean-brydon 99330b3117 chore: fixes UI nits in onboarding v3 flow (#24863)
* Add teams i18n

* Fix UI nits in invite flow

* chore: update redirect

* Update personal view settings
2025-11-03 11:45:27 +00:00
sean-brydon cc90a62090 feat: add banner for upgrade when using company email (#24829)
* Add banner plus mail icon

* Update MailIcon.tsx

* Update profile-view.tsx

* Fix weird line
2025-11-03 11:16:34 +00:00
sean-brydon 79bcd6dc30 feat: welcome to organizations modal after creation (#24823)
## What does this PR do?

Adds a welcome modal for new organizations that appears after organization creation. The modal showcases key features of the Organizations plan and provides a better onboarding experience.

## Visual Demo (For contributors especially)

#### Image Demo:

![CleanShot 2025-10-31 at 12.19.17.gif](https://app.graphite.dev/user-attachments/assets/4f8c3286-9400-40e6-aeb4-8a012f604c64.gif)

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

1. Create a new organization through either:
   - The onboarding flow
   - The settings/organizations/new page
   - The organization creation form

2. After successful creation and redirect, verify the welcome modal appears showing organization features.

3. Verify the modal can be closed by:
   - Clicking the "Continue" button
   - Clicking outside the modal
   - Pressing ESC key

4. Verify the modal doesn't reappear after being closed (query param and session storage should be cleared).

## 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-11-03 11:11:16 +00:00
chauhan_s c6daa61e5e fix: Maximum update depth exceeded error when entering the 6th digit of the email verification code. (#24857) 2025-11-03 11:07:20 +00:00
Udit TakkarandVolnei Munhoz b693b6898a feat: booking report table backend (#24794)
* feat: report table backend

* fix: types

* fix: types

* fix: bugs

* fix: type error

---------

Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
2025-11-03 10:44:09 +00:00
Abir Roy fb55bc2dcd fix(docker): correct build context for calcom-api (#24849) 2025-11-03 10:38:33 +00:00
Udit TakkarandVolnei Munhoz 71bd140fe4 fix: add event type validation (#24429)
* fix: add event type validation

* fix: use isinteger

* fix: use isinteger

* fix: use isNumber

* fix: use Error

---------

Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
2025-11-03 10:26:40 +00:00
Dhairyashil Shinde 4c5608147c feat: Add Preview buttons for every private link (#24623) 2025-11-03 08:17:31 +00:00
7d16da5c98 fix: sanitize username input to prevent hyperlinks (#24210)
* fix(username): sanitize input to prevent hyperlinks

* fix(username): sanitize input to prevent hyperlinks

* Implemented CodeRabbits comments

* clean username before saving and keep input consistent

* sanitize initial username on load to avoid mismatch with input

* remove comments

* remove comments

* Used existing slugify lib instead of creating new one

---------

Co-authored-by: Dhairyashil <dhairyashilshinde@xoxo-2.local>
Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-11-03 13:16:51 +05:30
spandev 0ae6e92558 fix: round robin settings update only on change (#24853) 2025-11-03 11:54:00 +05:30
Volnei Munhoz 052a38e39b cleanup unused env vars (#24819) 2025-10-31 12:14:54 -03:00
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
1597 changed files with 56821 additions and 22923 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
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": minor
---
Fix Booker atom verify email dialog infinite re-render when entering verification code
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": minor
---
Refactor SettingsHeader to accept onBackButtonClick callback instead of using useRouter directly.
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": minor
---
Add defaultPhoneCountry prop to BookerPlatformWrapper with ISO 3166-1 alpha-2 type safety
+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
+11 -1
View File
@@ -214,6 +214,8 @@ STRIPE_TEAM_PRODUCT_ID=
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
STRIPE_ORG_MONTHLY_PRICE_ID=
STRIPE_ORG_PRODUCT_ID=
# Used to pass trial days for orgs during the Stripe checkout session
STRIPE_ORG_TRIAL_DAYS=
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET_APPS=
@@ -423,7 +425,8 @@ VAPID_PRIVATE_KEY=
# Mintlify chat api
# Power AI chat in for docs
NEXT_PUBLIC_MINTLIFY_CHAT_API_KEY=
MINTLIFY_CHAT_API_KEY=
NEXT_PUBLIC_ENABLE_MINTLIFY_CHAT=
NEXT_PUBLIC_CHAT_API_URL=
NEXT_PUBLIC_DOCS_URL=
@@ -486,3 +489,10 @@ 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
# test oauth client for `packages/platform/examples/base` (optional)
SEED_PLATFORM_OAUTH_CLIENT_ID=
SEED_PLATFORM_OAUTH_CLIENT_SECRET=
-1
View File
@@ -1,6 +1,5 @@
/**/package.json @calcom/Foundation
/apps/api/**/* @calcom/API
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
/apps/web/lib/csp.ts @calcom/Foundation
/apps/web/lib/buildNonce.ts @calcom/Foundation
+1 -1
View File
@@ -26,7 +26,7 @@ runs:
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
- run: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && yarn db-seed
- run: 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
+2
View File
@@ -0,0 +1,2 @@
GET /v2/event-types/{eventTypeId} added to the 'data' response property 'oneOf' list for the response status '200'
GET /v2/event-types/{eventTypeId} added '#/components/schemas/EventTypeOutput_2024_06_14, #/components/schemas/TeamEventTypeOutput_2024_06_14' to the 'data' response property 'oneOf' list for the response status '200'
@@ -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 }}
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
with:
repository: calcom/test-results
repository: calcom/test-results-2
ref: gh-pages
token: ${{ secrets.GH_ACCESS_TOKEN }}
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v2
with:
repository: calcom/test-results
repository: calcom/test-results-2
ref: gh-pages
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Set Git User
+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
+12 -8
View File
@@ -72,18 +72,22 @@ jobs:
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/cache-db
- name: Generate Swagger
working-directory: apps/api/v2
run: yarn generate-swagger
- name: Check breaking changes
uses: oasdiff/oasdiff-action/breaking@main
with:
base: https://raw.githubusercontent.com/calcom/cal.com/refs/heads/main/docs/api-reference/v2/openapi.json
revision: docs/api-reference/v2/openapi.json
fail-on: ERR
run: |
docker run --rm \
-v "$PWD:/work" -w /work \
tufin/oasdiff:latest \
breaking \
https://raw.githubusercontent.com/calcom/cal.com/refs/heads/main/docs/api-reference/v2/openapi.json \
docs/api-reference/v2/openapi.json \
--fail-on ERR \
--err-ignore .github/oasdiff-err-ignore.txt
- name: Run Tests
working-directory: apps/api/v2
run: |
@@ -91,7 +95,7 @@ jobs:
EXIT_CODE=$?
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
exit $EXIT_CODE
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v4
+1
View File
@@ -5,6 +5,7 @@ permissions:
contents: read
env:
NODE_OPTIONS: --max-old-space-size=4096
INTEGRATION_TESTS: "true"
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
@@ -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
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
with:
repository: calcom/test-results
repository: calcom/test-results-2
ref: gh-pages
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Set Git User
@@ -67,7 +67,7 @@ jobs:
- name: Output Report URL as Workflow Annotation
id: url
run: |
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results/$HTML_REPORT_URL_PATH
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results-2/$HTML_REPORT_URL_PATH
echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
- name: Find Comment
+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
View File
View File
View File
View File
View File
View File
+2
View File
@@ -0,0 +1,2 @@
undecided:
- calcom-monorepo
View File
View File
View File
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"]
-15
View File
@@ -1,15 +0,0 @@
Possible issue:
- We won't be able to use already built calendar-cache when Delegation Credential is enabled because existing CalendarCache entries don't have userId set.
## Approach of always using SelectedCalendar.credentialId for CalendarCache even for DelegationCredentials
- Delete Credentials when DelegationCredential is disabled
TO Test
- New members beyond the batch size are processed
+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);
+1 -1
View File
@@ -57,7 +57,7 @@ export const eventTypeSelect = {
},
},
owner: {
select: { username: true, id: true },
select: { username: true, id: true, timeZone: true },
},
children: {
select: {
+3 -1
View File
@@ -16,13 +16,15 @@ export const isAdminGuard = async (req: NextApiRequest) => {
where: {
userId: userId,
accepted: true,
role: {
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
},
team: {
isOrganization: true,
organizationSettings: {
isAdminAPIEnabled: true,
},
},
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
},
select: {
team: {
@@ -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,8 @@ export class BookingsController_2024_04_15 {
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
const { orgSlug, locationUrl } = body;
try {
await this.checkBookingRequiresAuthentication(req, body.eventTypeId, body.rescheduleUid);
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed);
const booking = await this.regularBookingService.createBooking({
bookingData: bookingRequest.body,
@@ -318,11 +335,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 +346,7 @@ export class BookingsController_2024_04_15 {
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
noEmail: bookingRequest.body.noEmail,
},
});
@@ -444,6 +460,76 @@ export class BookingsController_2024_04_15 {
return oAuthClientParams.platformClientId;
}
private async isValidRescheduleBooking(rescheduleUid: string, eventTypeId: number): Promise<boolean> {
const { bookingInfo } = await getBookingInfo(rescheduleUid);
if (!bookingInfo) {
return false;
}
if (bookingInfo.status !== "ACCEPTED" && bookingInfo.status !== "PENDING") {
return false;
}
if (bookingInfo.eventTypeId !== eventTypeId) {
return false;
}
return true;
}
private async checkBookingRequiresAuthentication(
req: Request,
eventTypeId: number,
rescheduleUid?: string
): Promise<void> {
const eventType = await this.eventTypeRepository.findByIdIncludeHostsAndTeamMembers({
id: eventTypeId,
});
if (!eventType?.bookingRequiresAuthentication) {
return;
}
if (rescheduleUid) {
const isValidRescheduleBooking = await this.isValidRescheduleBooking(rescheduleUid, eventTypeId);
if (isValidRescheduleBooking) {
return;
} else {
throw new BadRequestException(
"Trying to reschedule an event-type which requires authentication but provided invalid rescheduleUid."
);
}
}
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 +589,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 +654,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);
}
@@ -194,11 +194,6 @@ export class CreateBookingInput_2024_04_15 {
@ApiHideProperty()
skipContactOwner?: boolean;
@IsBoolean()
@IsOptional()
@ApiHideProperty()
_shouldServeCache?: boolean;
@IsBoolean()
@IsOptional()
@ApiHideProperty()
@@ -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>
`,
})
@@ -187,7 +189,7 @@ export class BookingsController_2024_08_13 {
2. uid of one of the recurring booking recurrences
3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).
If you are fetching a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
@@ -211,7 +213,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Get all the recordings for the booking",
description: `Fetches all the recordings for the booking \`:bookingUid\`
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -227,9 +229,9 @@ export class BookingsController_2024_08_13 {
@Get("/:bookingUid/transcripts")
@UseGuards(BookingUidGuard)
@ApiOperation({
summary: "Get all the transcripts download links for the booking",
summary: "Get Cal Video real time transcript download links for the booking",
description: `Fetches all the transcripts download links for the booking \`:bookingUid\`
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -277,7 +279,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Reschedule a booking",
description: `Reschedule a booking or seated booking
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -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.`,
@@ -319,16 +321,19 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Cancel a booking",
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
\nCancelling normal bookings:
If the booking is not seated and not recurring, simply pass :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and optionally cancellationReason in the request body \`{"cancellationReason": "Will travel"}\`.
\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
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.
If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
@@ -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(
@@ -369,7 +374,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Mark a booking absence",
description: `The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -394,15 +399,15 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Reassign a booking to auto-selected host",
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
async reassignBooking(
@Param("bookingUid") bookingUid: string,
@GetUser() user: ApiAuthGuardUser
@GetUser() reassignedByUser: ApiAuthGuardUser
): Promise<ReassignBookingOutput_2024_08_13> {
const booking = await this.bookingsService.reassignBooking(bookingUid, user);
const booking = await this.bookingsService.reassignBooking(bookingUid, reassignedByUser);
return {
status: SUCCESS_STATUS,
@@ -418,20 +423,20 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Reassign a booking to a specific host",
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
async reassignBookingToUser(
@Param("bookingUid") bookingUid: string,
@Param("userId") userId: number,
@GetUser("id") reassignedById: number,
@GetUser() reassignedByUser: ApiAuthGuardUser,
@Body() body: ReassignToUserBookingInput_2024_08_13
): Promise<ReassignBookingOutput_2024_08_13> {
const booking = await this.bookingsService.reassignBookingToUser(
bookingUid,
userId,
reassignedById,
reassignedByUser,
body
);
@@ -449,7 +454,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Confirm a booking",
description: `The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@@ -473,8 +478,8 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Decline a booking",
description: `The provided authorization header refers to the owner of the booking.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
async declineBooking(
@@ -497,8 +502,8 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Get 'Add to Calendar' links for a booking",
description: `Retrieve calendar links for a booking that can be used to add the event to various calendar services. Returns links for Google Calendar, Microsoft Office, Microsoft Outlook, and a downloadable ICS file.
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
`,
})
@HttpCode(HttpStatus.OK)
@@ -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;
}
});
@@ -20,11 +20,7 @@ import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
CreateBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { CreateBookingInput_2024_08_13, GetBookingOutput_2024_08_13, GetSeatedBookingOutput_2024_08_13 } from "@calcom/platform-types";
import { BookingOutput_2024_08_13 } from "@calcom/platform-types";
import type { Booking, PlatformOAuthClient, Team, User, EventType } from "@calcom/prisma/client";
@@ -854,7 +850,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
describe("Booking Field Type Validation", () => {
const basePayload = {
start: "2025-06-19T11:00:00.000Z",
start: new Date(Date.UTC(2030, 5, 19, 11, 0, 0)).toISOString(),
attendee: {
name: "Charlie TypeTest",
email: "charlie.typetest@example.com",
@@ -1162,6 +1158,31 @@ describe("Bookings Endpoints 2024-08-13", () => {
`One or more invalid options for booking field '${fieldName}'. Allowed options are: blue, red.`
);
});
it("should transform null values to empty strings in bookingFieldsResponses", async () => {
const payload = {
...basePayload,
eventTypeId: eventTypeWithBookingFields.id,
bookingFieldsResponses: {
"favorite-movie": "The Matrix",
rescheduleReason: null,
notes: null,
},
};
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(payload)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(response.status).toBe(201);
expect(response.body.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(response.body.data)) {
const data: BookingOutput_2024_08_13 = response.body.data;
expect(data.bookingFieldsResponses.notes).toBe("");
expect(data.bookingFieldsResponses.rescheduleReason).toBe("");
expect(data.bookingFieldsResponses["favorite-movie"]).toBe("The Matrix");
}
});
});
afterAll(async () => {
@@ -1176,4 +1197,4 @@ describe("Bookings Endpoints 2024-08-13", () => {
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
}
});
});
@@ -12,6 +12,7 @@ import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
@@ -20,7 +21,6 @@ import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repo
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
@@ -72,6 +72,7 @@ type EmailSetup = {
team: Team;
member1: User;
member2: User;
member1ApiKey: string;
collectiveEventType: { id: number };
roundRobinEventType: { id: number };
};
@@ -89,25 +90,21 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
let profileRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let hostsRepositoryFixture: HostsRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
// Setup data for tests
let emailsEnabledSetup: EmailSetup;
let emailsDisabledSetup: EmailSetup;
const authEmail = "team-emails-2024-08-13-user-admin@example.com";
// Utility function to check response data type
const responseDataIsBooking = (data: any): data is BookingOutput_2024_08_13 => {
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
const responseDataIsBooking = (data: unknown): data is BookingOutput_2024_08_13 => {
return !Array.isArray(data) && data !== null && typeof data === "object" && data && "id" in data;
};
beforeAll(async () => {
const moduleRef = await withApiAuth(
authEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
)
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
@@ -122,6 +119,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
// Create a base organization for all tests
@@ -131,11 +129,6 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
emailsEnabledSetup = await setupTestEnvironment(true);
emailsDisabledSetup = await setupTestEnvironment(false);
await userRepositoryFixture.create({
email: authEmail,
organization: { connect: { id: organization.id } },
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
@@ -173,10 +166,15 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
const collectiveEvent = await createEventType("COLLECTIVE", team.id, [member1.id, member2.id]);
const roundRobinEvent = await createEventType("ROUND_ROBIN", team.id, [member1.id, member2.id]);
// Create API key for member1 to use in authorized tests
const { keyString } = await apiKeysRepositoryFixture.createApiKey(member1.id, null);
const member1ApiKey = `cal_test_${keyString}`;
return {
team,
member1,
member2,
member1ApiKey,
collectiveEventType: { id: collectiveEvent.id },
roundRobinEventType: { id: roundRobinEvent.id },
};
@@ -351,6 +349,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
: emailsDisabledSetup.member1.id;
const manualReassignResponse = await request(app.getHttpServer())
.post(`/v2/bookings/${rescheduledBookingUid}/reassign/${reassignToId}`)
.set("Authorization", `Bearer ${emailsDisabledSetup.member1ApiKey}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(manualReassignResponse.status).toBe(200);
@@ -359,6 +358,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
// --- 4. Automatic Reassign ---
const autoReassignResponse = await request(app.getHttpServer())
.post(`/v2/bookings/${rescheduledBookingUid}/reassign`)
.set("Authorization", `Bearer ${emailsDisabledSetup.member1ApiKey}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(autoReassignResponse.status).toBe(200);
@@ -484,6 +484,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
const manualReassignResponse = await request(app.getHttpServer())
.post(`/v2/bookings/${rescheduledBookingUid}/reassign/${reassignToId}`)
.set("Authorization", `Bearer ${emailsEnabledSetup.member1ApiKey}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(manualReassignResponse.status).toBe(200);
@@ -499,6 +500,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
jest.clearAllMocks();
const autoReassignResponse = await request(app.getHttpServer())
.post(`/v2/bookings/${rescheduledBookingUid}/reassign`)
.set("Authorization", `Bearer ${emailsEnabledSetup.member1ApiKey}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
expect(autoReassignResponse.status).toBe(200);
@@ -523,7 +525,6 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
afterAll(async () => {
// Clean up database records
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(authEmail);
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.member1.email);
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.member2.email);
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.member1.email);
@@ -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;
@@ -1,6 +1,7 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
import { BOOKING_REASSIGN_PERMISSION_ERROR } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
@@ -11,6 +12,7 @@ import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
@@ -21,7 +23,6 @@ import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repo
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
@@ -44,23 +45,34 @@ describe("Bookings Endpoints 2024-08-13", () => {
let hostsRepositoryFixture: HostsRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
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 teamUser1ApiKey: string;
let teamUser2ApiKey: string;
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(
teamUserEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
)
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
@@ -76,6 +88,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await organizationsRepositoryFixture.create({
@@ -96,23 +109,43 @@ 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 { keyString } = await apiKeysRepositoryFixture.createApiKey(teamUser1.id, null);
teamUser1ApiKey = `cal_test_${keyString}`;
const { keyString: keyString2 } = await apiKeysRepositoryFixture.createApiKey(teamUser2.id, null);
teamUser2ApiKey = `cal_test_${keyString2}`;
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 +177,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 +206,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 +285,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);
@@ -248,6 +478,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign`)
.set("Authorization", `Bearer ${teamUser1ApiKey}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
@@ -287,6 +518,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign/${teamUser1.id}`)
.send(body)
.set("Authorization", `Bearer ${teamUser1ApiKey}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
@@ -307,6 +539,247 @@ 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("Authorization", `Bearer ${teamUser2ApiKey}`)
.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("Authorization", `Bearer ${teamUser1ApiKey}`)
.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("Authorization", `Bearer ${teamUser1ApiKey}`)
.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("Authorization", `Bearer ${teamUser1ApiKey}`)
.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");
});
});
it("should return 403 when unauthorized user tries to reassign booking", async () => {
const unauthorizedUserEmail = `fake-user-${randomString()}@api.com`;
const unauthorizedUser = await userRepositoryFixture.create({
email: unauthorizedUserEmail,
locale: "en",
name: `fake-user-${randomString()}`,
});
const { keyString } = await apiKeysRepositoryFixture.createApiKey(unauthorizedUser.id, null);
const unauthorizedApiKeyString = `cal_test_${keyString}`;
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign`)
.set("Authorization", `Bearer ${unauthorizedApiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(403);
expect(response.body.error.message).toBe(BOOKING_REASSIGN_PERMISSION_ERROR);
await userRepositoryFixture.deleteByEmail(unauthorizedUserEmail);
});
it("should return 403 when unauthorized user tries to reassign booking to specific user", async () => {
const unauthorizedUserEmail = `fake-user-${randomString()}@api.com`;
const unauthorizedUser = await userRepositoryFixture.create({
email: unauthorizedUserEmail,
locale: "en",
name: `fake-user-${randomString()}`,
});
const { keyString } = await apiKeysRepositoryFixture.createApiKey(unauthorizedUser.id, null);
const unauthorizedApiKeyString = `cal_test_${keyString}`;
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign/${teamUser2.id}`)
.send({ reason: "Testing unauthorized access" })
.set("Authorization", `Bearer ${unauthorizedApiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(403);
expect(response.body.error.message).toBe(BOOKING_REASSIGN_PERMISSION_ERROR);
await userRepositoryFixture.deleteByEmail(unauthorizedUserEmail);
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
@@ -325,8 +798,10 @@ 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: {
@@ -58,6 +72,17 @@ export class BookingsRepository_2024_08_13 {
});
}
async getByUidWithEventType(bookingUid: string) {
return this.dbRead.prisma.booking.findUnique({
where: {
uid: bookingUid,
},
include: {
eventType: true,
},
});
}
async getByUidWithUser(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,5 +1,5 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
@@ -13,9 +13,8 @@ import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/ge
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { BillingService } from "@/modules/billing/services/billing.service";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { MembershipsService } from "@/modules/memberships/services/memberships.service";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
@@ -24,7 +23,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import {
ConflictException,
ForbiddenException,
@@ -39,6 +38,8 @@ import { Request } from "express";
import { DateTime } from "luxon";
import { z } from "zod";
export const BOOKING_REASSIGN_PERMISSION_ERROR = "You do not have permission to reassign this booking";
import {
getTranslation,
getAllUserBookings,
@@ -49,6 +50,7 @@ import {
confirmBookingHandler,
getCalendarLinks,
} from "@calcom/platform-libraries";
import { PrismaOrgMembershipRepository } from "@calcom/platform-libraries/bookings";
import {
CreateBookingInput_2024_08_13,
CreateBookingInput,
@@ -64,6 +66,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";
@@ -107,12 +110,11 @@ export class BookingsService_2024_08_13 {
private readonly organizationsRepository: OrganizationsRepository,
private readonly teamsRepository: TeamsRepository,
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
private readonly membershipsRepository: MembershipsRepository,
private readonly membershipsService: MembershipsService,
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13,
private readonly regularBookingService: RegularBookingService,
private readonly recurringBookingService: RecurringBookingService,
private readonly instantBookingCreateService: InstantBookingCreateService
private readonly instantBookingCreateService: InstantBookingCreateService,
private readonly eventTypeAccessService: EventTypeAccessService
) {}
async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) {
@@ -126,7 +128,7 @@ export class BookingsService_2024_08_13 {
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
}
const userIsEventTypeAdminOrOwner = authUser
? await this.userIsEventTypeAdminOrOwner(authUser, eventType)
? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(authUser, eventType)
: false;
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser, userIsEventTypeAdminOrOwner);
@@ -195,44 +197,6 @@ export class BookingsService_2024_08_13 {
}
}
async userIsEventTypeAdminOrOwner(authUser: ApiAuthGuardUser, eventType: EventType) {
const authUserId = authUser.id;
const authUserRole = authUser.role;
const eventTypeId = eventType.id;
const teamId = eventType.teamId;
const eventTypeOwnerId = eventType.userId || null;
if (authUserRole === "ADMIN") return true;
if (eventTypeOwnerId === authUserId) return true;
if (eventTypeId) {
const [isUserHost, isUserAssigned] = await Promise.all([
this.eventTypesRepository.isUserHostOfEventType(authUserId, eventTypeId),
this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId),
]);
if (isUserHost || isUserAssigned) return true;
}
if (teamId) {
const membership = await this.membershipsRepository.getUserAdminOrOwnerTeamMembership(
authUserId,
teamId
);
if (membership) return true;
}
if (
eventTypeOwnerId &&
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, eventTypeOwnerId))
) {
return true;
}
return false;
}
async getBookedEventType(body: CreateBookingInput) {
if (body.eventTypeId) {
return await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(body.eventTypeId);
@@ -614,7 +578,7 @@ export class BookingsService_2024_08_13 {
const booking = await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid);
const userIsEventTypeAdminOrOwner =
authUser && booking?.eventType
? await this.userIsEventTypeAdminOrOwner(authUser, booking.eventType)
? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(authUser, booking.eventType)
: false;
if (booking) {
@@ -762,16 +726,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,
@@ -793,7 +767,7 @@ export class BookingsService_2024_08_13 {
const userIsEventTypeAdminOrOwner =
authUser && databaseBooking.eventType
? await this.userIsEventTypeAdminOrOwner(authUser, databaseBooking.eventType)
? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(authUser, databaseBooking.eventType)
: false;
const isRecurring = !!databaseBooking.recurringEventId;
const isSeated = !!databaseBooking.eventType?.seatsPerTimeSlot;
@@ -840,6 +814,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,
@@ -969,19 +994,34 @@ export class BookingsService_2024_08_13 {
});
}
async reassignBooking(bookingUid: string, requestUser: UserWithProfile) {
const booking = await this.bookingsRepository.getByUid(bookingUid);
async reassignBooking(bookingUid: string, reassignedByUser: ApiAuthGuardUser) {
const booking = await this.bookingsRepository.getByUidWithEventType(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
if (!booking.eventType) {
throw new BadRequestException(
`Event type with id=${booking.eventTypeId} was not found in the database`
);
}
const isAllowed = await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(
reassignedByUser,
booking.eventType
);
if (!isAllowed) {
throw new ForbiddenException(BOOKING_REASSIGN_PERMISSION_ERROR);
}
const platformClientParams = booking.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
const profile = this.usersService.getUserMainProfile(requestUser);
const profile = this.usersService.getUserMainProfile(reassignedByUser);
try {
await roundRobinReassignment({
@@ -989,7 +1029,7 @@ export class BookingsService_2024_08_13 {
orgId: profile?.organizationId || null,
emailsEnabled,
platformClientParams,
reassignedById: requestUser.id,
reassignedById: reassignedByUser.id,
});
} catch (error) {
if (error instanceof Error) {
@@ -1013,13 +1053,27 @@ export class BookingsService_2024_08_13 {
async reassignBookingToUser(
bookingUid: string,
newUserId: number,
reassignedById: number,
reassignedByUser: ApiAuthGuardUser,
body: ReassignToUserBookingInput_2024_08_13
) {
const booking = await this.bookingsRepository.getByUid(bookingUid);
const booking = await this.bookingsRepository.getByUidWithEventType(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
if (!booking.eventType) {
throw new BadRequestException(
`Event type with id=${booking.eventTypeId} was not found in the database`
);
}
const isAllowed = await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(
reassignedByUser,
booking.eventType
);
if (!isAllowed) {
throw new ForbiddenException(BOOKING_REASSIGN_PERMISSION_ERROR);
}
const user = await this.usersRepository.findByIdWithProfile(newUserId);
if (!user) {
@@ -1040,7 +1094,7 @@ export class BookingsService_2024_08_13 {
newUserId,
orgId: profile?.organizationId || null,
reassignReason: body.reason,
reassignedById,
reassignedById: reassignedByUser.id,
emailsEnabled,
platformClientParams,
});
@@ -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";
@@ -129,7 +129,7 @@ export class EventTypesService_2024_04_15 {
}
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_04_15, user: UserWithProfile) {
this.checkCanUpdateEventType(user.id, eventTypeId);
await this.checkCanUpdateEventType(user.id, eventTypeId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
const bookingFields = [...(body.bookingFields || [])];
@@ -32,6 +32,8 @@ import {
} from "@calcom/platform-enums";
import { SchedulingType } from "@calcom/platform-libraries";
import {
BaseConfirmationPolicy_2024_06_14,
TeamEventTypeOutput_2024_06_14,
type ApiSuccessResponse,
type CreateEventTypeInput_2024_06_14,
type EventTypeOutput_2024_06_14,
@@ -95,6 +97,8 @@ describe("Event types Endpoints", () => {
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let apiKeyString: string;
let apiKeyOrgUser: string;
let systemAdminUser: User;
let systemAdminApiKeyString: string;
const userEmail = `event-types-2024-06-14-user-${randomString()}@api.com`;
const falseTestEmail = `event-types-2024-06-14-false-user-${randomString()}@api.com`;
@@ -208,6 +212,17 @@ describe("Event types Endpoints", () => {
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = `cal_test_${keyString}`;
systemAdminUser = await userRepositoryFixture.create({
email: `event-types-2024-06-14-system-admin-${randomString()}@api.com`,
username: `event-types-2024-06-14-system-admin-${randomString()}`,
role: "ADMIN",
});
const { keyString: adminKeyString } = await apiKeysRepositoryFixture.createApiKey(
systemAdminUser.id,
null
);
systemAdminApiKeyString = `cal_test_${adminKeyString}`;
orgUser = await userRepositoryFixture.create({
email: `event-types-2024-06-14-org-user-${randomString()}@example.com`,
name: `event-types-2024-06-14-org-user-${randomString()}`,
@@ -1366,6 +1381,9 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType.locations).toEqual(eventType.locations);
expect(fetchedEventType.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType.ownerId).toEqual(user.id);
expect(Array.isArray(fetchedEventType.users)).toEqual(true);
expect(fetchedEventType.users.length).toEqual(1);
expect(fetchedEventType.users[0].id).toEqual(fetchedEventType.ownerId);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
@@ -1387,6 +1405,99 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType.color).toEqual(eventType.color);
});
it("system admin can access another user's event type by id", async () => {
return request(app.getHttpServer())
.get(`/api/v2/event-types/${orgUserEventType1.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${systemAdminApiKeyString}`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.id).toEqual(orgUserEventType1.id);
expect(responseBody.data.ownerId).toEqual(orgUser.id);
});
});
it("user can access a team event type as team member (using user's API key)", async () => {
const team = await teamRepositoryFixture.create({
name: `event-types-2024-06-14-team-${randomString()}`,
isOrganization: false,
});
await membershipsRepositoryFixture.create({
role: "ADMIN",
user: { connect: { id: user.id } },
team: { connect: { id: team.id } },
accepted: true,
});
const teamEventType = await eventTypesRepositoryFixture.createTeamEventType({
title: `event-types-2024-06-14-team-event-${randomString()}`,
slug: `event-types-2024-06-14-team-event-${randomString()}`,
length: 60,
locations: [],
schedulingType: "COLLECTIVE",
team: { connect: { id: team.id } },
});
return request(app.getHttpServer())
.get(`/api/v2/event-types/${teamEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.id).toEqual(teamEventType.id);
expect(responseBody.data.teamId).toEqual(team.id);
await teamRepositoryFixture.delete(team.id);
});
});
it("user can access a team event type as HOST even if membership role is MEMBER (using user's API key)", async () => {
const team = await teamRepositoryFixture.create({
name: `event-types-2024-06-14-host-team-${randomString()}`,
isOrganization: false,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: user.id } },
team: { connect: { id: team.id } },
accepted: true,
});
const teamEventType = await eventTypesRepositoryFixture.createTeamEventType({
title: `event-types-2024-06-14-host-event-${randomString()}`,
slug: `event-types-2024-06-14-host-event-${randomString()}`,
length: 60,
locations: [],
schedulingType: "COLLECTIVE",
team: { connect: { id: team.id } },
hosts: {
create: [
{
user: { connect: { id: user.id } },
},
],
},
});
return request(app.getHttpServer())
.get(`/api/v2/event-types/${teamEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.id).toEqual(teamEventType.id);
expect(responseBody.data.teamId).toEqual(team.id);
await teamRepositoryFixture.delete(team.id);
});
});
it(`/GET/event-types by username and eventSlug`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${username}&eventSlug=${eventType.slug}`)
@@ -1641,6 +1752,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 BaseConfirmationPolicy_2024_06_14;
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 BaseConfirmationPolicy_2024_06_14;
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", () => {
@@ -7,6 +7,7 @@ import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_202
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import type { DatabaseEventType } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
import {
API_KEY_OR_ACCESS_TOKEN_HEADER,
@@ -23,6 +24,9 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
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 { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
import type { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
@@ -73,7 +77,8 @@ export class EventTypesController_2024_06_14 {
private readonly eventTypesService: EventTypesService_2024_06_14,
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe,
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe
) {}
@Post("/")
@@ -107,24 +112,44 @@ export class EventTypesController_2024_06_14 {
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Get an event type",
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
description: `<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>
Access control: This endpoint fetches an event type by ID and returns it only if the authenticated user is authorized. Authorization is granted to:
- System admins
- The event type owner
- Hosts of the event type or users assigned to the event type
- Team admins/owners of the team that owns the team event type
- Organization admins/owners of the event type owner's organization
- Organization admins/owners of the team's parent organization
Note: Update and delete endpoints remain restricted to the event type owner only.`,
})
async getEventTypeById(
@Param("eventTypeId") eventTypeId: string,
@GetUser() user: UserWithProfile
@GetUser() user: ApiAuthGuardUser
): Promise<GetEventTypeOutput_2024_06_14> {
const eventType = await this.eventTypesService.getUserEventType(user.id, Number(eventTypeId));
const eventType = await this.eventTypesService.getEventTypeByIdIfAuthorized(user, Number(eventTypeId));
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
const responseEventType = this.isTeamEventType(eventType)
? await this.outputTeamEventTypesResponsePipe.transform(eventType)
: this.eventTypeResponseTransformPipe.transform(eventType);
return {
status: SUCCESS_STATUS,
data: this.eventTypeResponseTransformPipe.transform(eventType),
data: responseEventType,
};
}
private isTeamEventType(
eventType: DatabaseTeamEventType | ({ ownerId: number } & DatabaseEventType)
): eventType is DatabaseTeamEventType {
return !!eventType.teamId;
}
@Get("/")
@ApiOperation({
summary: "Get all event types",
@@ -10,10 +10,15 @@ import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/output.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
@@ -26,6 +31,8 @@ import { Module } from "@nestjs/common";
EventTypesService_2024_06_14,
InputEventTypesService_2024_06_14,
OutputEventTypesService_2024_06_14,
EventTypeAccessService,
TeamsRepository,
UsersRepository,
UsersService,
SchedulesRepository_2024_06_11,
@@ -35,6 +42,9 @@ import { Module } from "@nestjs/common";
CredentialsRepository,
AppsRepository,
CalendarsRepository,
OutputTeamEventTypesResponsePipe,
OutputOrganizationsEventTypesService,
TeamsEventTypesRepository,
],
controllers: [EventTypesController_2024_06_14],
exports: [
@@ -42,6 +52,7 @@ import { Module } from "@nestjs/common";
EventTypesRepository_2024_06_14,
InputEventTypesService_2024_06_14,
OutputEventTypesService_2024_06_14,
EventTypeAccessService,
],
})
export class EventTypesModule_2024_06_14 {}
@@ -93,6 +93,19 @@ export class EventTypesRepository_2024_06_14 {
});
}
async getEventTypeByIdWithHosts(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: {
users: true,
schedule: true,
destinationCalendar: true,
calVideoSettings: true,
hosts: true,
},
});
}
async getEventTypeByIdIncludeUsersAndTeam(eventTypeId: number) {
const eventType = await this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
@@ -1,19 +1,22 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsIn, ValidateNested } from "class-validator";
import { IsIn } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
import { EventTypeOutput_2024_06_14, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
@ApiExtraModels(EventTypeOutput_2024_06_14, TeamEventTypeOutput_2024_06_14)
export class GetEventTypeOutput_2024_06_14 {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsIn([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: EventTypeOutput_2024_06_14,
oneOf: [
{ $ref: getSchemaPath(EventTypeOutput_2024_06_14) },
{ $ref: getSchemaPath(TeamEventTypeOutput_2024_06_14) },
],
})
@ValidateNested()
@Type(() => EventTypeOutput_2024_06_14)
data!: EventTypeOutput_2024_06_14 | null;
@Type(() => Object)
data!: EventTypeOutput_2024_06_14 | TeamEventTypeOutput_2024_06_14 | null;
}
@@ -1,10 +1,14 @@
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_06_14/constants/constants";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { DatabaseEventType } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed";
import { SystemField, CustomField } from "@/ee/event-types/event-types_2024_06_14/transformers";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { UsersService } from "@/modules/users/services/users.service";
@@ -30,7 +34,8 @@ export class EventTypesService_2024_06_14 {
private readonly usersService: UsersService,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
private readonly dbWrite: PrismaWriteService,
private readonly schedulesRepository: SchedulesRepository_2024_06_11
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
private readonly eventTypeAccessService: EventTypeAccessService
) {}
async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) {
@@ -39,9 +44,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: {
@@ -77,6 +84,31 @@ export class EventTypesService_2024_06_14 {
};
}
async getEventTypeByIdIfAuthorized(
authUser: ApiAuthGuardUser,
eventTypeId: number
): Promise<DatabaseTeamEventType | ({ ownerId: number } & DatabaseEventType) | null> {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithHosts(eventTypeId);
if (!eventType) {
return null;
}
const hasAccess = await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(
authUser,
eventType as unknown as EventType
);
if (!hasAccess) {
return null;
}
return {
ownerId: eventType.userId ?? 0,
...eventType,
};
}
async checkCanCreateEventType(userId: number, body: InputEventTransformed_2024_06_14) {
const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug);
if (existsWithSlug) {
@@ -231,6 +263,10 @@ export class EventTypesService_2024_06_14 {
return [dynamicEventType];
}
if (authUser?.id) {
return await this.getUserEventTypes(authUser.id);
}
return [];
}
@@ -30,6 +30,7 @@ describe("Me Endpoints", () => {
let profilesRepositoryFixture: ProfileRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
const userEmail = `me-controller-user-${randomString()}@api.com`;
const name = "Me Controller User";
let user: User;
let org: Team;
@@ -55,6 +56,7 @@ describe("Me Endpoints", () => {
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
name,
});
org = await organizationsRepositoryFixture.create({
@@ -92,6 +94,9 @@ describe("Me Endpoints", () => {
expect(responseBody.data.id).toEqual(user.id);
expect(responseBody.data.email).toEqual(user.email);
expect(responseBody.data.name).toEqual(user.name);
expect(responseBody.data.avatarUrl).toEqual(user.avatarUrl);
expect(responseBody.data.bio).toEqual(user.bio);
expect(responseBody.data.timeFormat).toEqual(user.timeFormat);
expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId);
expect(responseBody.data.weekStart).toEqual(user.weekStart);
@@ -114,6 +119,8 @@ describe("Me Endpoints", () => {
expect(responseBody.data.id).toEqual(user.id);
expect(responseBody.data.email).toEqual(user.email);
expect(responseBody.data.avatarUrl).toEqual(user.avatarUrl);
expect(responseBody.data.bio).toEqual(user.bio);
expect(responseBody.data.timeFormat).toEqual(user.timeFormat);
expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId);
expect(responseBody.data.weekStart).toEqual(user.weekStart);
@@ -148,13 +155,13 @@ describe("Me Endpoints", () => {
});
it("should not update user associated with access token given invalid time format", async () => {
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 as any };
const bodyWithIncorrectTimeFormat = { timeFormat: 100 };
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
});
it("should not update user associated with access token given invalid week start", async () => {
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" as any };
const bodyWithIncorrectWeekStart = { weekStart: "waba luba dub dub" };
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
});
@@ -23,6 +23,18 @@ export class MeOutput {
@DocsProperty()
email!: string;
@IsString()
@DocsProperty({ type: String, nullable: true })
name!: string | null;
@IsString()
@DocsProperty({ type: String, nullable: true })
avatarUrl!: string | null;
@IsString()
@DocsProperty({ type: String, nullable: true })
bio!: string | null;
@IsInt()
@DocsProperty()
timeFormat!: number;
+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] => {

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