Compare commits

...
151 Commits
Author SHA1 Message Date
Alex van Andel 02d61da9b0 chore: release v6.0.12
Post release / release (push) Canceled after 0s
Release Docker / Prepare Release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2026-01-14 11:15:04 +00:00
Dhairyashil ShindeandMorgan c0017114ef fix(api): return empty bookingUrl for managed users in API v2 (#26826)
Managed users (isPlatformManaged: true) don't have public booking pages -
their scheduling is handled through the platform's app.

- Fetch isPlatformManaged in event-types repository
- Return empty string for managed users in buildBookingUrl
- Added unit test for managed users case

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2026-01-14 15:28:25 +05:30
Eunjae LeeandDevin AI 4b99072bcc feat: add scope configuration for feature opt-in (#26801)
* feat: add scope configuration for feature opt-in

Add scope field to OptInFeatureConfig that allows features to be scoped
to specific levels (org, team, user). This enables features to be shown
only at certain settings pages rather than all three.

Changes:
- Add OptInFeatureScope type with values 'org', 'team', 'user'
- Add optional scope field to OptInFeatureConfig interface
- Add getOptInFeaturesForScope helper function to filter features by scope
- Update FeatureOptInService to filter features based on scope
- Update tRPC router to pass scope parameter for org/team endpoints

Features without a scope field default to all scopes for backward compatibility.

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

* feat: add scope validation to setUserFeatureState and setTeamFeatureState

- Add isFeatureAllowedForScope helper function to check if a feature is allowed for a scope
- Update setUserFeatureState to reject if feature is not scoped to 'user'
- Update setTeamFeatureState to accept scope parameter and reject if feature is not allowed
- Update tRPC router to pass scope parameter for team and org endpoints
- Fix unit test mock to include new config exports

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

* feat: use ErrorWithCode for scope validation and add tests

- Replace raw Error with ErrorWithCode using ErrorCode.BadRequest
- Add comprehensive tests for setUserFeatureState scope validation
- Add comprehensive tests for setTeamFeatureState scope validation
- Test both enabled/disabled and inherit state scenarios
- Test error messages include feature ID and scope name

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

* feat: improve Features menu visibility to use scope configuration

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

* refactor: remove unused hasOptInFeaturesForScope function

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

* fix: allow features not in config at all scopes (permissive default)

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

* fix: revert permissive default and mock scope validation in integration tests

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-14 09:21:56 +00:00
Dhairyashil ShindeandMorgan 821dce6a04 fix(api): skip platform org subdomain in bookingUrl for API v2 (#26812)
Platform organizations don't have public-facing subdomains, so non-managed
users in platform orgs should get cal.com URLs instead of the platform
org subdomain.

- Updated EventTypeUser type to include isPlatform field
- Modified buildBookingUrl to check isPlatform before using org slug
- Added unit test for platform org users

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2026-01-14 09:04:45 +00:00
3860717ae3 fix: workflow locale (#26509)
* fix: workflow locale

* fix: workflow locale

* fix: check if tmeplate is changed

* fix: regression

* fix: also check subject when determining if template is customized

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

* refactor: extract template matching logic into testable function

- Create detectMatchedTemplate function in separate file
- Add comprehensive unit tests (20 test cases)
- Refactor EmailWorkflowService to use the new function

Addresses review comment from hariombalhara requesting
abstraction and tests for the template matching logic.

Co-Authored-By: udit@cal.com <udit222001@gmail.com>

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: morgan@cal.com <morgan@cal.com>
2026-01-14 13:01:40 +05:30
Syed Ali Shahbaz f4303d8d60 icalsequence fix (#26815) 2026-01-14 10:08:19 +05:30
Rajiv Sahal 851aa30829 fix: respect hideCalendarNotes setting during booking reschedule (#25945)
Add hideCalendarNotes support to reschedule flow

The hideCalendarNotes flag was only handled in CalendarManager.createEvent(), not in updateEvent(). This caused notes to become visible again in ICS files after rescheduling.

Changes:
- Add hideCalendarNotes handling to CalendarManager.updateEvent()
- Add hideCalendarNotes checks to reschedule email functions
- Move description reset before reschedule call (required for cloned event)
2026-01-13 20:31:38 -03:00
2664eb459b feat: Init Cal.com Salesforce package (#26330)
* Add db schema

* Add `CredentialRepository.findByTeamIdAndSlugs`

* Add enabled app slugs for attribute syncing

* Create repository for `IntegrationAttributeSync`

* Create zod schemas

* Create `AttributeSyncUserRuleOutputMapper`

* Create `IntegrationAttributeSyncService`

* Create DI contianer

* Create trpc endpoints

* Create page

* Include team name in `CredentialRepository.findByTeamIdAndSlugs`

* Update schema and relations

* Update types and schemas

* Add more methods to IntegrationAttributeSyncRepository

* Add more services to `IntegrationAttributeSyncService`
- getById
- Init updateIncludeRulesAndMappings

* Refactor `getTeams.handler` to use repository

* Create `createAttributeSync` trpc endpoint

* Create `updateAttributeSync` trpc endpoint

* Add router to trpc

* Create attribute sync child components

* Pass custom actions to `FormCard`

* Create `IntegrationAttributeSyncCard`

* Pass inital props via server side

* Fix prop

* Only refetch on mutation

* Fixes

* Add form error when duplicate field and attribute combo

* Add `updateTransactionWithRUleAndMappings` logic

* Adjust zod schemas

* Service add `updateIncludeRulesAndMappings`

* Pass orgId from server to component

* Rename types

* Add deleteById method to repository

* Add name to integrationAttributeSync

* Add deleteById method to service

* Rename method

* Add deleteAttributeSync trpc endpoint

* Make the IntegrationAttributeSyncCard a dummy component

* test: add tests for IntegrationAttributeSync feature

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

* Move creating a attribute sync record to the service

* Add i18n strings

* Safe select credential in find by id and team

* Fix default credentialId value in form

* Update repository return types

* Add i18n string

* Make credentialId optional for form schema

* Fix label

* Add cascade deletes

* Add verification that syncs belong to org

* Create mapper for repository output

* Type fixes

* Remove old test file

* Pass `attributeOptions` from parent to children

* Infer types from zod schema

* Type fixes

* Type fix

* Clean up

* Add i18n strings

* Remove unused file

* Address feedback

* Add migration file

* Address feedback

* Add validation for new integration values

* Remove unused router

* Move away from z.infer to z.ZodType

* Clean up comments

* Type fix

* Type fixes

* Type fix

* fix: add passthrough to syncFormDataSchema to preserve extra fields

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

* fix: remove incorrect test that expected extra fields to pass through syncFormDataSchema

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

* Add endpoint for SF to call

* Create scratch org config

* Create sf cli scripts

* Create package logic

* Update README

* Remove unused file

* Add indexes

* Add aria label

* Address feedback - consistent validation

* Fix import paths for attribute types

* refactor: change attributeSyncRules array to singular attributeSyncRule

The database schema enforces a one-to-one relationship between
IntegrationAttributeSync and AttributeSyncRule (via @unique constraint),
and the UI only supports a single rule. This change makes the TypeScript
type match the database schema and UI behavior.

Changes:
- Update IntegrationAttributeSync interface to use attributeSyncRule: AttributeSyncRule | null
- Update mapper to return singular rule instead of wrapping in array
- Update UI component to access sync.attributeSyncRule directly
- Update IIntegrationAttributeSyncUpdateParams Omit type
- Update tests to use attributeSyncRule: null instead of attributeSyncRules: []

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

* feat: implement FeatureOptInService (#25805)

* feat: implement FeatureOptInService WIP

* clean up

* feat: consolidate feature repositories and add updateFeatureForUser

- Implement updateFeatureForUser in FeaturesRepository (similar to updateFeatureForTeam)
- Move getUserFeatureState and getTeamFeatureState from PrismaFeatureOptInRepository to FeaturesRepository
- Update FeatureOptInService to use only FeaturesRepository
- Add setUserFeatureState and setTeamFeatureState methods to FeatureOptInService
- Update _router.ts to remove PrismaFeatureOptInRepository usage
- Remove PrismaFeatureOptInRepository.ts and FeatureOptInRepositoryInterface.ts
- Update features.repository.interface.ts and features.repository.mock.ts
- Add integration tests for updateFeatureForUser, getUserFeatureState, getTeamFeatureState
- Update service.integration-test.ts to use FeaturesRepository

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

* refactor: rename updateFeatureForUser to setUserFeatureState

Rename to match the convention used for setTeamFeatureState

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

* refactor: return FeatureState type from getUserFeatureState and getTeamFeatureState

* fix integration tests

* clean up logics

* update services and router

* refactor: change getUserFeatureState and getTeamFeatureState to accept featureIds array

- Renamed getUserFeatureState to getUserFeatureStates
- Renamed getTeamFeatureState to getTeamFeatureStates
- Changed parameter from featureId: string to featureIds: string[]
- Changed return type from FeatureState to Record<string, FeatureState>
- Updated FeatureOptInService to use the new batch methods
- Added tests for querying multiple features in a single call
- Optimized listFeaturesForTeam to fetch all feature states in one query

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

* feat: add getFeatureStateForTeams for batch querying multiple teams

- Added getFeatureStateForTeams method to query a single feature across multiple teams in one call
- Updated FeatureOptInService.resolveFeatureStateAcrossTeams to use the new batch method
- Replaces N+1 queries with a single database query for team states
- Added comprehensive integration tests for the new method

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

* refactor: combine org and team state queries into single call

- Include orgId in the teamIds array passed to getFeatureStateForTeams
- Extract org state and team states from the combined result
- Reduces database queries from 3 to 2 in resolveFeatureStateAcrossTeams

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

* refactor: use team.isOrganization and clarify computeEffectiveState comment

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

* refactor: use MembershipRepository.findAllByUserId with isOrganization

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

* feat: add featureId validation using isOptInFeature type guard

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

* less queries

* add fallback value

* fix type error

* move files

* add autoOptInFeatures column

* use autoOptInFeatures flag within FeatureOptInService

* add setUserAutoOptIn and setTeamAutoOptIn

* fix computeEffectiveState logic

* rewrite computeEffectiveState

* clean up integration tests

* clean up in afterEach

* fix type error

* refactor: use FeaturesRepository methods instead of direct Prisma calls

Replace all manual userFeatures and teamFeatures Prisma operations with
the new setUserFeatureState and setTeamFeatureState repository methods.

Changes include:
- Admin handlers (assignFeatureToTeam, unassignFeatureFromTeam)
- Test fixtures and integration tests
- Playwright fixtures
- Development scripts

This ensures consistent feature flag management through the repository
pattern and supports the new tri-state semantics (enabled/disabled/inherit).

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

* clean up

* fix the logic

* extract some logic into applyAutoOptIn()

* remove wrong code

* refactor: convert setUserFeatureState and setTeamFeatureState to object params with discriminated union

- Convert multiple positional parameters to single object parameter
- Use discriminated union types: assignedBy required for enabled/disabled, omitted for inherit
- Update all callers across repository, service, handlers, fixtures, and tests

* fix type error

* use Promise.all

* fix

---------

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

* Prevent duplicate field and attribute mappings

* Add validation that attribute belongs to the org

* fix: address Cubic AI review feedback

- Add @@index([credentialId]) to IntegrationAttributeSync model for efficient cascade deletes and credential-based queries (confidence: 9/10)
- Fix translation key from 'credential_required' to 'attribute_sync_credential_required' to match existing locale definition (confidence: 10/10)

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

* fix: address remaining cubic review feedback

- RuleBuilder.tsx: Use unique IDs for React keys instead of array index to prevent reconciliation bugs when removing conditions
- updateAttributeSync.handler.ts: Add early organization check before service calls for consistency
- createAttributeSync.handler.ts: Add CredentialNotFoundError class for type-safe error handling instead of string matching
- IntegrationAttributeSyncService.test.ts: Replace expect.fail() with proper Vitest rejects.toSatisfy() pattern

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

* Pull test file from main

* fix: address Cubic AI review feedback (confidence >= 9/10)

- Remove sensitive fields (email, changedFields) from logging in user-sync.ts
- Rename scratch-org:open to scratch-org:start in package.json to match README

Co-Authored-By: unknown <>

* Update README

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2026-01-13 20:01:21 -03:00
ce997447a4 fix: fix email exclusion validation for full emails and and @-prefixed domains (#26727)
* fix(bookings): fix email exclusion validation for full emails and @-prefixed domains

* NIT

* make case insensitive

* fix: make domain matching case-insensitive in doesEmailMatchEntry

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-13 16:56:28 -03:00
Abhay MishraandAnik Dhabal Babu 5fa76f943b fix(phone-input): add Austria mask and prevent double plus prefix (#26626)
* fix(phone-input): add Austria mask and prevent double plus prefix (#24193)

* updated code

* Update Austria phone mask format

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2026-01-13 19:22:15 +00:00
Pedro Castro 4226231b8a perf: batch user queries in schedules output service (#25901)
Replace N sequential queries with single batch query for fetching
user default schedule IDs

Changes:
- Add getUsersScheduleDefaultIds batch method to UsersRepository
- Add getResponseSchedules batch method to OutputSchedulesService
- Extract transformScheduleToOutput for reuse
- Simplify OrganizationsSchedulesService and TeamsSchedulesService
- Remove dead code (formatInput no-op method)

Reduces database round-trips from O(n) to O(1) for schedule lookups
2026-01-13 19:21:17 +00:00
Dhairyashil Shinde 69695d3022 feat(companion): manifest id for safari extension & add delete account link (#26802)
* feat: manifest id for safari

* add delete account button
2026-01-13 16:12:43 -03:00
c13fce4483 fix: changes to error on schedule name empty save (#24044)
* fix #21709: change to error on schedule name empty save

* fix comments

* minor enahncemnts

---------

Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2026-01-13 18:29:58 +00:00
f32ea8b4c5 fix: Booker reschedule behaviour for org admin (#26530)
* fix: reschedule behaviour for org admin

* fix: use pbac for permissions check

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2026-01-13 18:12:32 +00:00
Anshumancanrock 36cf6e3703 fix: Yarn installation fails in windows due to mixed path separators … (#26771)
* fix: Yarn installation fails in windows due to mixed path separators in app-store-cli

* fix: normalize paths for cross-platform Biome compatibility in app-store-cli
2026-01-13 18:07:51 +00:00
455973b394 fix(ui): improve Variables Dropdown styling consistency (#26675)
* fix(ui): improve Variables Dropdown styling consistency

- Change focus ring from brand-800 to subtle for consistent design
- Add rounded-md to dropdown container for visual consistency
- Add rounded-md to DropdownMenuItem for rounded corners
- Remove redundant hover:bg-muted and conditional bg-muted styling

Fixes #25848

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: restore keyboard navigation active state indicator

Keep the conditional bg-muted styling for the selected item during
keyboard navigation, while still removing the redundant hover:bg-muted
since DropdownMenuItem already handles hover styling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: simiondolha <simiondolha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2026-01-13 14:53:11 -03:00
0b73491367 refactor: split large org creation transaction into smaller batches (#25688)
* refactor: split large org creation transaction into smaller batches

The prisma.team.create call at L418 was creating the organization along with
all orgProfiles and memberships in a single large transaction, which could
timeout when there are many members.

This change splits the operation into smaller, separate transactions:
1. Create the organization (team) with just metadata and organizationSettings
2. Create org profiles in batches of 50 using Promise.all
3. Create memberships in batches of 100 using createMany
4. Fetch created profiles to rebuild the member-profile mapping

This approach avoids transaction timeouts while maintaining the same
functionality and data integrity.

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

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

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>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2026-01-13 14:56:46 +00:00
573756eaa2 docs: fix CALENDSO_ENCRYPTION_KEY generation command (#26658)
The AES256 encryption used for 2FA requires exactly 32 bytes.
The previous command `openssl rand -base64 32` generates a 44-character
base64 string (32 bytes encoded in base64), which is too long.

Changed to `openssl rand -base64 24` which generates exactly 32
characters (24 bytes encoded in base64 = 32 characters).

This was causing 2FA setup failures with "Something went wrong" errors
and RangeError: Invalid key length in self-hosted Docker installations.

Fixes #22365

Co-authored-by: simiondolha <simiondolha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2026-01-13 11:46:39 -03:00
5c686d90f9 feat: add tri-state UI settings pages and auto opt-in preference (#25892)
* feat: add tri-state UI settings pages and auto opt-in preference

TASK 5: Tri-state UI with ToggleGroup
- Create shared TeamOrgFeaturesSettings component
- Create user features settings page (features-view.tsx)
- Create team features settings page
- Create org features settings page

TASK 6: Add auto opt-in preference
- Add autoOptInExperimentalFeatures field to User and Team models
- Add migration for new fields
- Add TRPC endpoints for auto opt-in preference (using repository pattern)
- Add auto opt-in checkboxes to settings pages

Also adds i18n keys for feature opt-in UI.

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

refactor: rename autoOptInExperimentalFeatures to autoOptInFeatures

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

refactor: apply PBAC procedure pattern to featureOptIn router

- Create createTeamPbacProcedure for team-scoped endpoints
- Create createOrgPbacProcedure for organization-scoped endpoints
- Refactor all team/org endpoints to use the new PBAC procedures
- Significantly reduces boilerplate code for permission checks

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

add features pages

implement hooks

update style

update style

rename slug to featureId

show Features page on the side bar only if OPT_IN_FEATURES.length > 0

revert unncessary renaming

revert some changes

remove some changes

* fix repository usage

* test: add unit tests for PBAC utility procedures

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

* feat: add 'disabled by organization' badge for Team Level Features page

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

* a little clean up

* feat: add effectiveReason to computeEffectiveStateAcrossTeams for better UI feedback

- Update computeEffectiveStateAcrossTeams to return both enabled state and reason
- Add EffectiveStateReason type with 6 possible values
- Update FeatureOptInService to include effectiveReason in resolved state
- Simplify useUserFeatureOptIn.getBlockedWarning to use effectiveReason directly
- Add feature_no_explicit_enablement_warning translation
- Invalidate feature list when auto-opt-in setting changes in all hooks

* test: add tests for FeatureOptInService.listFeaturesForTeam and refactor router to use TeamRepository

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

* make it clearer about auto opt in

* move hooks to apps/web/module

* fix

* fix: move types to packages/features/feature-opt-in, fix imports and test mocks

- Move types.ts from apps/web/modules/feature-opt-in/hooks to packages/features/feature-opt-in
- Update FeaturesSettings.tsx to import types from new location
- Update hooks to import types from @calcom/features/feature-opt-in/types
- Fix organization features-view import path (~/settings/organizations -> ~/ee/organizations)
- Fix PermissionCheckService mock in util.test.ts to use class syntax

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

* fix: add validation for team ID and remove isPublic from featureOptIn handler

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

* fix: resolve biome lint errors in feature-opt-in files

- Add explicit return types to functions and methods
- Replace ternary operators with if-else statements
- Replace forEach with for...of loops to avoid useIterableCallbackReturn
- Extract helper functions to reduce function complexity
- Add explicit type annotations to variables
- Move exports to end of file to satisfy useExportsLast rule

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

* fix: resolve type errors in featureOptIn router

- Replace ctx.organizationId with ctx.user.organization.id
- Add null checks for organizationId in org procedures
- Remove incorrect type annotation from featureOptInRouter

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

* refactor: address PR feedback on feature opt-in implementation

- Refactor createMutationCallbacks to useMutationCallbacks hook with useLocale internally
- Restore comment on featureOptIn prop in FeaturesSettings.tsx
- Revert getAllTeamIds helper, use ternary operator instead
- Use ctx.user.organizationId with proper guard clauses

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

* fix type error

* feat: disable toggle when feature is blocked by higher level

- Add isBlockedByHigherLevel function to UseFeatureOptInResult interface
- Implement blocking detection for user level (org/team disabled)
- Implement blocking detection for team level (org disabled)
- Organization level never blocked (top level)
- Update FeaturesSettings to disable ToggleGroup when blocked

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

* change order of toggle buttons

* feat: add policy field (permissive/strict) to feature opt-in config

- Add OptInFeaturePolicy type with 'permissive' and 'strict' modes
- Update computeEffectiveStateAcrossTeams to handle both policies:
  - Permissive: user opt-in can activate; disables only win if ALL teams disable
  - Strict: user opt-in alone not enough; ANY explicit disable blocks
- Add new EffectiveStateReason values for strict policy
- Update FeatureOptInService to read policy from config
- Add unit tests for all 9 policy scenarios from spec

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

* refactor: rewrite test file with scenario tables and make policy required

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

* fix type

* fix types

* make policy required

* disable toggle group if blocked by higher level

* refactor: move PBAC procedures to packages/trpc/server/procedures

- Move util.ts to pbacProcedures.ts in procedures folder for better reusability
- Move util.test.ts to pbacProcedures.test.ts alongside the main file
- Update imports in _router.ts to use new location

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

* fix: remove redundant null checks in featureOptIn router

The createOrgPbacProcedure middleware already validates organizationId
and throws if null. Use non-null assertion (!) instead of redundant
runtime checks since the middleware guarantees the value exists.

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

* refactor: improve middleware typing to avoid non-null assertions

Remove explicit return type from createOrgPbacProcedure to let TypeScript
infer the extended context type. This allows handlers to use ctx.organizationId
directly (typed as number) instead of ctx.user.organizationId! assertions.

The middleware validates organizationId and adds it to the context, so
downstream handlers can safely access ctx.organizationId as a number.

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

* feat(pbac): use featureOptIn permissions instead of generic team/org permissions

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

* feat(pbac): add PBAC to team features page and pass canEdit to views

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

* fix: add effectiveReason to IFeatureOptInService and update listFeaturesForTeam interface

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

* fix: add feature_any_team_disabled to isUserBlockedByHigherLevel check

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

* fix: add strict-policy reasons to warning helper function

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2026-01-13 15:37:53 +01:00
9ad8aa981a chore: deploy api v2 on vercel (#26735)
* chore: deploy api v2 on vercel

* fix: replace console.log with logger.log in Vercel handler

Address Cubic AI review feedback to use the logging framework
consistently instead of console.log in the serverless handler.

Co-Authored-By: unknown <>

* chore: enable esModuleInterop

* chore: deploy api v2 on vercel

* chore: deploy api v2 on vercel

* Update apps/api/v2/src/bootstrap.ts

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

* fixup! Merge branch 'main' into deploy-api-v2-vercel

* Revert "chore: deploy api v2 on vercel"

This reverts commit 45c704a48e8396c46118069e1a25d8d7a5ee84be.

* chore: deploy api v2 on vercel

* fix: address Cubic AI review feedback in main.ts

- Replace console.log with logger.log for consistent logging
- Replace console.error with logger.error for consistent error logging
- Restore comma: true option in qs.parse to support comma-separated arrays

Co-Authored-By: unknown <>

* fix: remove comma: true from qs.parse to maintain backward compatibility

The main branch does not have comma: true in the query parser, so adding
it would be a breaking change for existing API consumers. Removing it to
maintain consistency with the current production behavior.

Co-Authored-By: unknown <>

* chore: deploy api v2 on vercel

* small fixes

* chore: add try catch around bootstrap.ts

* fix: use NestJS Logger and throw error instead of process.exit in bootstrap

- Replace console.error with logger.error for consistent logging
- Replace process.exit(1) with throw error to avoid breaking Vercel serverless instance reuse

Addresses Cubic AI review feedback (confidence 10/10 for both issues)

Co-Authored-By: unknown <>

* chore: try log redis url

* fix: sanitize REDIS_URL logging to avoid exposing credentials

Replace full REDIS_URL logging with a boolean check that only indicates
whether Redis is configured, without exposing the connection string.

Addresses Cubic AI review feedback (confidence 9/10)

Co-Authored-By: unknown <>

* chore: remove unnecessary logs

* fix: prisma adapter

* chore: handle USE_POOL platform libraries

* fix: use JSON.stringify for Vite define value

Wrap usePool with JSON.stringify() to properly serialize the string value.
Without this, Vite injects the raw value as an identifier instead of a
string literal, breaking runtime behavior.

Addresses Cubic AI review feedback (confidence 9/10)

Co-Authored-By: unknown <>

* fix: docker file builds

* fix: correct Dockerfile build order for platform packages

Reorder builds to match the dependency graph from dev:build script:
constants → enums → utils → types → libraries → trpc → api-v2

platform-libraries depends on the other platform packages, so they
must be built first.

Addresses Cubic AI review feedback (confidence 9/10)

Co-Authored-By: unknown <>

* fix: docker file builds

* chore: add docker build

* chore: upgrade nest/bull

---------

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>
2026-01-13 11:31:21 -03:00
Carina WollendorferandCarinaWolli bb9581d0d9 fix: oauth flow for safari extension (companion) (#26795)
* oauth flow for safair extension

* code clean up

* fix type and lint errors

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2026-01-13 19:44:26 +05:30
Hariom BalharaandDevin AI 75d611c2e8 chore: Integrate creation/rescheduling booking audit for Recurring/regular booking/seated bookings (#26046)
* Integrate creation/rescheduling booking audit

* fix: add missing hostUserUuid to booking audit test data

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

* fix-ci

* feat: enhance booking audit with seat reference

- Added support for seat reference in booking audit actions.
- Updated localization for booking creation to include seat information.
- Modified relevant services to pass attendee seat ID during booking creation.

* fix: update test data to match schema requirements

- Add seatReferenceUid: null to default mock audit log data
- Add seatReferenceUid: null to multiple audit logs test case
- Convert SEAT_RESCHEDULED test data to use numeric timestamps instead of ISO strings

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

* Allow nullish seatReferenceUid

* feat: enhance booking audit to support rescheduledBy information

- Updated booking audit actions to include rescheduledBy details, allowing tracking of who rescheduled a booking.
- Refactored related services to accommodate the new rescheduledBy parameter in booking events.
- Adjusted type definitions and function signatures to reflect the changes in the booking audit context.

* Avoid possible run time issue

* Fix imoport path

* fix failing test due to merge from main\

* Pass useruuid

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-13 13:16:16 +00:00
Alex van Andel 0408ce0741 chore: release v6.0.11
Post release / release (push) Canceled after 0s
Release Docker / Prepare Release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2026-01-13 13:03:49 +00:00
Udit TakkarandVolnei Munhoz 5818da414b feat: update recording, transcript endpoint and add tests (#25771)
* feat: add tests and update endpoint

* chore: undo

---------

Co-authored-by: Volnei Munhoz <volnei@cal.com>
2026-01-13 09:54:23 -03:00
Syed Ali Shahbaz d381c2c2cb init (#26782) 2026-01-13 12:51:24 +00:00
3aa6499cd4 fix: remove header bug (#26438)
* remove header and add standard pagination

* Remove unused props from OutOfOfficeEntriesList

Removed unnecessary props from the data table component.

* removed standard pagination

* fix

* Update OutOfOfficeEntriesList.tsx

* remove unnecessary padding

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2026-01-13 12:14:19 +00:00
Pedro Castro 1e67f98970 fix: add Icelandic, Lithuanian, and Norwegian locale support for Booker (#25519)
- Create unified date/time formatter with Intl-first approach and dayjs fallback
- Add support for is/lt/nb locales in Booker components
- Unify date formatting across Booker (Header, dates, weekdays)
- Update translation pipeline to include is, lt, nb
- Add tests for new locales and formatter
- Add timezone support in dayjs fallback
- Add formatter caching with circuit breaker
- Use unified formatDateTime in Header.tsx
2026-01-13 08:47:39 -03:00
sean-brydon aaf5958b7d feat: adds admin billing section (#26747)
* feat: adds admin billing section

* use loading prop

* add log on resend error

* add error log in billing portal
2026-01-13 10:34:52 +00:00
Dhairyashil Shinde 80941c17a0 feat: companion unique id firefox (#26791) 2026-01-13 10:29:10 +00:00
Eunjae LeeandDevin AI 829d6c4714 feat(pbac): add FeatureOptIn resource to permission registry (#26761)
* feat(pbac): add Feature resource to permission registry

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

* feat(pbac): add migration for Feature resource permissions

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

* feat(pbac): add English translations for Feature resource

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

* feat(pbac): update Feature resource for opt-in/opt-out control (read and update only)

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

* feat(pbac): rename Feature to FeatureOptIn resource

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

* feat(pbac): simplify update description text

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-13 10:17:41 +00:00
Eunjae LeeandDevin AI fd6e2a9ca5 fix: enable DI for FeatureOptInService (#26061)
* fix integration test

* fix: enable DI for FeatureOptInService

* create containers/FeaturesRepository.ts

* refactor: convert FeatureOptInService and FeaturesRepository to moduleLoader pattern

- Create feature-specific tokens in feature-opt-in/di/tokens.ts and flags/di/tokens.ts
- Update modules to use bindModuleToClassOnToken for type-safe dependency injection
- Simplify containers to use moduleLoader.loadModule() for automatic dependency loading
- Import feature-specific tokens in central tokens.ts using spread operator

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

* refactor: rename FeatureOptInServiceInterface to IFeatureOptInService

Follow the codebase convention of using 'I' prefix for interface files and names.

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

* fix: export featuresRepositoryModule for backward compatibility

The FeaturesRepository module was refactored to use moduleLoader pattern but
AvailableSlots.ts still imports featuresRepositoryModule. This adds the export
to maintain backward compatibility.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-13 11:02:00 +01:00
383314d3e2 feat: add skip trial feature for teams/orgs billing page (#26584)
* feat: add skip trial feature for teams/orgs billing page

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

* feat: add translation strings for skip trial feature and fix lint-staged config

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

* fix: use IS_TEAM_BILLING_ENABLED instead of IS_SELF_HOSTED

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

* fix: use correct DialogHeader props in billing-view.tsx

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

* restore lint staged config file

* invalidate trpc cache

* fix: update role filter and error message in skipTrialForTeam handler

- Changed role filter to include both OWNER and ADMIN roles to match authorization logic
- Updated error message to reflect that both owners and admins can skip the trial

Addresses Cubic AI review feedback on PR #26584

Co-Authored-By: unknown <>

* fix: optimize skipTrialForTeam to query only specific team by ID

Address Cubic AI review feedback: Replace findAllAcceptedTeamMemberships
(which fetches all teams) with TeamRepository.findById (which fetches
only the specific team needed). Authorization is already validated
earlier in the handler, so this redundant query was inefficient.

Co-Authored-By: unknown <>

* fix static methods

* feat: use pbac

* fix: remove redundant team query in getSubscriptionStatus handler

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

* add pbac to skip team handler

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-13 09:17:42 +00:00
Hariom BalharaandDevin AI cee3195708 fix: optimize routing form mutation to avoid costly name update triggers (#26781)
* fix: optimize routing form mutation to avoid costly name update triggers

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

* Add load test script for form responses

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-13 08:53:10 +00:00
7c66f33de2 chore: UX Fixes (#26643)
* Copy changes

* Move search bar inline with new button

* Get rid of no more results message

* Change hidden badge to (hidden)

* Remove Cal.ai badge from sidebar

* Add dropdown to create button when there is multiple options

* Fix delete dialog

* Saved filters updates

* More string fixes

* Switch members table to use names

* Fix member spacing

* Fix routing form identifier field

* Fix routing forms stuff

* Only show SMS hint on SMS options

* Make workflow delete button minimal

* Fix padding on workflow steps

* Remove min width on workflow title

* Fix delete workflow PR

* Fix org profile buttons

* Fix org profile screen partially scrolled down

* Improve logos & banner uploads

* Personal profile fixes

* Fix settings general view stuff

* Sentence case consistency

* Fix stuff I broke

* Fix fab

* Fix hidden translation string

* Fix text fields

* Make button small for solo users too

* fix: update E2E tests to match sentence case labels in routing forms

* fix: update tests to match sentence case label changes

- insights.e2e.ts: chart titles (14 strings)
- event-types.e2e.ts: Organizer phone number location
- EditLocationDialog.test.tsx: phone number labels

* fix: address Cubic AI review feedback (confidence 9+)

- Replace hardcoded text-gray-500 with text-muted in TextField.tsx hint section
- Replace text locator with data-testid in E2E test for location select

Co-Authored-By: unknown <>

* fix: update E2E tests for sentence case label changes

- Use data-testid selectors for location options (more reliable than text)
- Update field identifiers in routing-forms tests to match new labels
- Fix Long text selector in manage-booking-questions test

* fix: replace text locator with data-testid in manage-booking-questions E2E test

Replace fragile text="Long text" locator with resilient
page.getByTestId("select-option-textarea") selector per E2E best practices.

Addresses Cubic AI review feedback (confidence 9/10).

Co-Authored-By: unknown <>

* fix: use .last() for multiple location select items

---------

Co-authored-by: Pedro Castro <pedro@cal.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-13 00:56:08 -05:00
Syed Ali Shahbaz 32d97fbea4 Add parentId condition to findOrganizationIdBySlug (#26729) 2026-01-13 03:53:06 +00:00
Volnei MunhozandDevin AI a20ad44209 feat: filter generic calendars from subscription batch (#26774)
Add filtering to SelectedCalendarRepository.findNextSubscriptionBatch to
exclude generic calendars (holidays, contacts, shared, imported, resources)
based on their externalId suffixes.

Changes:
- Add GENERIC_CALENDAR_SUFFIXES constant mapping providers to their
  generic calendar suffixes in AdaptersFactory
- Add getGenericCalendarSuffixes method to AdapterFactory interface
- Update findNextSubscriptionBatch to accept and use genericCalendarSuffixes
  parameter to filter out calendars with matching externalId suffixes
- Update CalendarSubscriptionService.checkForNewSubscriptions to pass
  generic calendar suffixes from the adapter factory
- Update tests to cover the new filtering logic

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 22:39:49 -03:00
Volnei Munhoz a6d7e4f107 Add claude.md (#26778) 2026-01-12 22:03:21 -03:00
5f21f54bae feat: Add compliance page to settings (#26777)
* Add compliance page to settings

* fix: sanitize error logging in B2 download route

Avoid logging raw error objects which may expose sensitive B2 credentials
or internal information. Use sanitized error message instead.

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 21:50:09 -03:00
Pedro Castro 3a6ca9ef15 chore: update dependencies (#26710)
* chore: update dependencies

   - @modelcontextprotocol/sdk: 1.24.0 → 1.25.2
   via resolution
   - jose: 4.13.1 → 4.15.9
   - serialize-javascript: 6.0.1 → 6.0.2 via
   resolution

* fix: update jose and webpack versions

* chore: bump transitive dependencies

- webpack 5.94.0
- express 5.2.1
- @adobe/css-tools 4.3.2
- jsondiffpatch 0.7.2

* chore: add min-document resolution

* fix: remove jose from resolutions

Incompatible with openid-client@6.x which requires jose 5.x/6.x exports.
jose is updated directly in apps/web and packages/features/auth

* fix: replace express with body-parser, remove webpack

- body-parser: 2.2.1 (CVE-2025-13466)
- Removed webpack 5.94.0 (causes TS2729)
- Removed express 5.2.1 (causes path-to-regexp errors)

* fix: remove body-parser resolution for Express 4.x compatibility

body-parser 2.x is designed for Express 5.x but NestJS uses Express 4.x,
causing API v2 E2E tests to fail
2026-01-12 21:48:55 -03:00
95688b38fa fix(companion): event type links for org user (#26775)
* checkpoint 1

* checkpoint 2

* update extension header

* fix some minor ios ui bugs

* fix(companion): restore username fetching for URL fallback display

Addresses Cubic AI review feedback (confidence 9/10): The username was
hardcoded to 'username' but still used by BasicsTab as a fallback for
URL display when bookingUrl is unavailable. This restores the useEffect
that fetches the actual username from CalComAPIService.getUsername().

Co-Authored-By: unknown <>

* complete devins code change

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-13 05:37:56 +05:30
8dbe3848e7 feat: Create Integration Attribute Sync records (#26007)
* Add db schema

* Add `CredentialRepository.findByTeamIdAndSlugs`

* Add enabled app slugs for attribute syncing

* Create repository for `IntegrationAttributeSync`

* Create zod schemas

* Create `AttributeSyncUserRuleOutputMapper`

* Create `IntegrationAttributeSyncService`

* Create DI contianer

* Create trpc endpoints

* Create page

* Include team name in `CredentialRepository.findByTeamIdAndSlugs`

* Update schema and relations

* Update types and schemas

* Add more methods to IntegrationAttributeSyncRepository

* Add more services to `IntegrationAttributeSyncService`
- getById
- Init updateIncludeRulesAndMappings

* Refactor `getTeams.handler` to use repository

* Create `createAttributeSync` trpc endpoint

* Create `updateAttributeSync` trpc endpoint

* Add router to trpc

* Create attribute sync child components

* Pass custom actions to `FormCard`

* Create `IntegrationAttributeSyncCard`

* Pass inital props via server side

* Fix prop

* Only refetch on mutation

* Fixes

* Add form error when duplicate field and attribute combo

* Add `updateTransactionWithRUleAndMappings` logic

* Adjust zod schemas

* Service add `updateIncludeRulesAndMappings`

* Pass orgId from server to component

* Rename types

* Add deleteById method to repository

* Add name to integrationAttributeSync

* Add deleteById method to service

* Rename method

* Add deleteAttributeSync trpc endpoint

* Make the IntegrationAttributeSyncCard a dummy component

* test: add tests for IntegrationAttributeSync feature

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

* Move creating a attribute sync record to the service

* Add i18n strings

* Safe select credential in find by id and team

* Fix default credentialId value in form

* Update repository return types

* Add i18n string

* Make credentialId optional for form schema

* Fix label

* Add cascade deletes

* Add verification that syncs belong to org

* Create mapper for repository output

* Type fixes

* Remove old test file

* Pass `attributeOptions` from parent to children

* Infer types from zod schema

* Type fixes

* Type fix

* Clean up

* Add i18n strings

* Remove unused file

* Address feedback

* Add migration file

* Address feedback

* Add validation for new integration values

* Remove unused router

* Move away from z.infer to z.ZodType

* Clean up comments

* Type fix

* Type fixes

* Type fix

* fix: add passthrough to syncFormDataSchema to preserve extra fields

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

* fix: remove incorrect test that expected extra fields to pass through syncFormDataSchema

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

* Add indexes

* Add aria label

* Address feedback - consistent validation

* Fix import paths for attribute types

* refactor: change attributeSyncRules array to singular attributeSyncRule

The database schema enforces a one-to-one relationship between
IntegrationAttributeSync and AttributeSyncRule (via @unique constraint),
and the UI only supports a single rule. This change makes the TypeScript
type match the database schema and UI behavior.

Changes:
- Update IntegrationAttributeSync interface to use attributeSyncRule: AttributeSyncRule | null
- Update mapper to return singular rule instead of wrapping in array
- Update UI component to access sync.attributeSyncRule directly
- Update IIntegrationAttributeSyncUpdateParams Omit type
- Update tests to use attributeSyncRule: null instead of attributeSyncRules: []

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

* feat: implement FeatureOptInService (#25805)

* feat: implement FeatureOptInService WIP

* clean up

* feat: consolidate feature repositories and add updateFeatureForUser

- Implement updateFeatureForUser in FeaturesRepository (similar to updateFeatureForTeam)
- Move getUserFeatureState and getTeamFeatureState from PrismaFeatureOptInRepository to FeaturesRepository
- Update FeatureOptInService to use only FeaturesRepository
- Add setUserFeatureState and setTeamFeatureState methods to FeatureOptInService
- Update _router.ts to remove PrismaFeatureOptInRepository usage
- Remove PrismaFeatureOptInRepository.ts and FeatureOptInRepositoryInterface.ts
- Update features.repository.interface.ts and features.repository.mock.ts
- Add integration tests for updateFeatureForUser, getUserFeatureState, getTeamFeatureState
- Update service.integration-test.ts to use FeaturesRepository

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

* refactor: rename updateFeatureForUser to setUserFeatureState

Rename to match the convention used for setTeamFeatureState

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

* refactor: return FeatureState type from getUserFeatureState and getTeamFeatureState

* fix integration tests

* clean up logics

* update services and router

* refactor: change getUserFeatureState and getTeamFeatureState to accept featureIds array

- Renamed getUserFeatureState to getUserFeatureStates
- Renamed getTeamFeatureState to getTeamFeatureStates
- Changed parameter from featureId: string to featureIds: string[]
- Changed return type from FeatureState to Record<string, FeatureState>
- Updated FeatureOptInService to use the new batch methods
- Added tests for querying multiple features in a single call
- Optimized listFeaturesForTeam to fetch all feature states in one query

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

* feat: add getFeatureStateForTeams for batch querying multiple teams

- Added getFeatureStateForTeams method to query a single feature across multiple teams in one call
- Updated FeatureOptInService.resolveFeatureStateAcrossTeams to use the new batch method
- Replaces N+1 queries with a single database query for team states
- Added comprehensive integration tests for the new method

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

* refactor: combine org and team state queries into single call

- Include orgId in the teamIds array passed to getFeatureStateForTeams
- Extract org state and team states from the combined result
- Reduces database queries from 3 to 2 in resolveFeatureStateAcrossTeams

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

* refactor: use team.isOrganization and clarify computeEffectiveState comment

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

* refactor: use MembershipRepository.findAllByUserId with isOrganization

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

* feat: add featureId validation using isOptInFeature type guard

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

* less queries

* add fallback value

* fix type error

* move files

* add autoOptInFeatures column

* use autoOptInFeatures flag within FeatureOptInService

* add setUserAutoOptIn and setTeamAutoOptIn

* fix computeEffectiveState logic

* rewrite computeEffectiveState

* clean up integration tests

* clean up in afterEach

* fix type error

* refactor: use FeaturesRepository methods instead of direct Prisma calls

Replace all manual userFeatures and teamFeatures Prisma operations with
the new setUserFeatureState and setTeamFeatureState repository methods.

Changes include:
- Admin handlers (assignFeatureToTeam, unassignFeatureFromTeam)
- Test fixtures and integration tests
- Playwright fixtures
- Development scripts

This ensures consistent feature flag management through the repository
pattern and supports the new tri-state semantics (enabled/disabled/inherit).

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

* clean up

* fix the logic

* extract some logic into applyAutoOptIn()

* remove wrong code

* refactor: convert setUserFeatureState and setTeamFeatureState to object params with discriminated union

- Convert multiple positional parameters to single object parameter
- Use discriminated union types: assignedBy required for enabled/disabled, omitted for inherit
- Update all callers across repository, service, handlers, fixtures, and tests

* fix type error

* use Promise.all

* fix

---------

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

* Prevent duplicate field and attribute mappings

* Add validation that attribute belongs to the org

* fix: address Cubic AI review feedback

- Add @@index([credentialId]) to IntegrationAttributeSync model for efficient cascade deletes and credential-based queries (confidence: 9/10)
- Fix translation key from 'credential_required' to 'attribute_sync_credential_required' to match existing locale definition (confidence: 10/10)

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

* fix: address remaining cubic review feedback

- RuleBuilder.tsx: Use unique IDs for React keys instead of array index to prevent reconciliation bugs when removing conditions
- updateAttributeSync.handler.ts: Add early organization check before service calls for consistency
- createAttributeSync.handler.ts: Add CredentialNotFoundError class for type-safe error handling instead of string matching
- IntegrationAttributeSyncService.test.ts: Replace expect.fail() with proper Vitest rejects.toSatisfy() pattern

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

* Pull test file from main

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2026-01-12 22:57:28 +00:00
9202028ab6 fix: resolve API v2 test flakes with cleanup and simplified slug (#26766)
* fix: resolve API v2 test flakes by using UUID-only slug

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

* fix: add beforeAll cleanup to prevent test flakes from leftover data

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 18:54:29 -03:00
cal-com-ci[bot]andLingo.dev 2f90138757 feat: update translations via @LingoDotDev (#26773)
Co-authored-by: Lingo.dev <support@lingo.dev>
2026-01-12 21:52:37 +00:00
devin-ai-integration[bot]andsusan@cal.com 41dcfe0307 chore: update company email banner copy (#26767)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: susan@cal.com <susan@cal.com>
2026-01-12 20:52:35 +00:00
913eb47ff1 fix: weekly view scheduler breaking on view switch (#25891)
* fix: weekly view scheduler columns offset

* fix

* fix

---------

Co-authored-by: Anik Dhabal Babu <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2026-01-12 18:58:11 +00:00
Dhairyashil Shinde e879c923ea feat(api): add bookingUrl field to event types API v2 response (#26195)
* feat(api): add bookingUrl field to event types API v2 response

Add a new bookingUrl field to EventTypeOutput_2024_06_14 that contains
the full, correct booking URL for each event type. This fixes the issue
where companion app and Chrome extension were hardcoding cal.com URLs
instead of using organization subdomains.

Changes:
- Add bookingUrl field to EventTypeOutput_2024_06_14 type definition
- Update repository to include organization data when fetching users
- Add buildBookingUrl method to compute URL using getOrgFullOrigin
- Include bookingUrl in getResponseEventType return object

The booking URL is computed using the first user's organization slug
(if any) to generate the correct subdomain URL (e.g., i.cal.com/keith/30min
for organization users instead of cal.com/keith/30min).

* fix: include organization data in getEventTypeByIdWithHosts

Update getEventTypeByIdWithHosts to use usersInclude to fetch
organization slug data, ensuring bookingUrl is computed correctly
for the GET /v2/event-types/{eventTypeId} endpoint.

* test: add unit tests and E2E assertions for bookingUrl field

* fix(api-v2): correct bookingUrl format for org users and fix double slashes

Fix issues with bookingUrl field in event types API v2 response:
- Remove trailing slashes from base URLs to prevent double slashes
- Use profile.username instead of user.username for organization users
  (profile contains clean username without org suffix)
- Include user profiles in repository queries to access profile data
- Create local org-domains utility to replace @calcom/features dependency
  which isn't available in API v2 runtime

Changes:
- Add apps/api/v2/src/lib/org-domains.ts with getOrgFullOrigin function
  adapted from @calcom/features/ee/organizations/lib/orgDomains
- Update event-types.repository.ts to include profiles in usersInclude
- Update buildBookingUrl() to prioritize profile data when available
- Update unit tests to cover organization user profile scenarios

Fixes:
- Double slashes in URLs (e.g., http://localhost:3000//user/slug)
- Incorrect username format for org users (e.g., owner1-acme instead
  of owner1)
- Missing organization subdomain in booking URLs (e.g., should be
  http://acme.localhost:3000/owner1/30min not
  http://localhost:3000/owner1-acme/30min)

* deslop ai code

* handle empty username in both code and write test for it, use select instead of include

* feat(api-v2): reuse core org domain logic for event type bookingUrl

Export getOrgFullOrigin and subdomainSuffix from @calcom/platform-libraries/organizations
to reuse existing core logic instead of duplicating it in API v2. This ensures consistency
across the codebase and reduces maintenance burden.

- Export getOrgFullOrigin and subdomainSuffix from platform-libraries/organizations
- Update output-event-types.service to import from @calcom/platform-libraries/organizations
- Remove duplicate org-domains.ts file from API v2
- Update test mocks to use platform-libraries import path
- buildBookingUrl method now uses core getOrgFullOrigin function

This addresses feedback to reuse core code rather than introducing duplicate logic.

* test(api-v2): exclude bookingUrl from output comparison in e2e test

* refactor(api-v2): reuse core logic for event type bookingUrl

Replace duplicated username/org extraction logic in buildBookingUrl with
core functions. Use getBookerBaseUrlSync from @calcom/platform-libraries/organizations
for base URL generation, and add enrichUserWithProfile method that follows
the same pattern as core's UserRepository.enrichUsersWithTheirProfiles.

The enrichment logic is applied synchronously since profiles are already
fetched by the repository, avoiding the need to make the service async.
This ensures consistency with core patterns while maintaining the existing
synchronous API contract.

- Replace getOrgFullOrigin with getBookerBaseUrlSync
- Add enrichUserWithProfile method following core enrichment pattern
- Simplify buildBookingUrl to use enriched user data
- Update tests to mock getBookerBaseUrlSync and verify enrichment flow

* trailing slash cubic comment

* address review comments

* address review comments 2

* better code

* Updated the test to give the user an organization

* address review comments again

* Revert "address review comments again"

This reverts commit 622ea3fc8dcd7f5c3113614afcfc86beadecca0e.

* allow both the full UserWithProfile and the partial EventTypeUser (from select queries) to use the same getUserMainProfile() method, eliminating code duplication without changing the performance-optimized repository queries.

* reveiw points
2026-01-12 14:57:17 -03:00
Keith WilliamsandDevin AI ae2ea190d8 feat: add DEVIN_ACTIONS_PAT instructions for pushing to forked PRs (#26693)
* feat: add DEVIN_ACTIONS_PAT instructions for pushing to forked PRs

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

* refactor: simplify PAT instructions to be high-level

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

* feat: allow workflow_dispatch to run on draft PRs

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

* fix: only process targeted PR when pr_number is provided

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

* feat: add critical fork PR error handling instruction to stale PR workflow

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>
2026-01-12 14:42:37 -03:00
Anshumancanrock 5c9a5c5729 fix: use cross-platform rimraf for build scripts (#25814)
* use cross-platform rimraf for build scripts

* fix: use cross-platform rimraf for build scripts

* fix: replace cp with npx shx cp for cross-platform compatibility

* Update schema.prisma
2026-01-12 22:56:18 +05:30
Keith WilliamsandDevin AI 77184226e5 ci: use pull_request_target for conflict resolver label trigger (#26762)
* fix: use pull_request_target for conflict resolver label trigger

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

* fix: update if condition to check for pull_request_target event name

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>
2026-01-12 14:24:43 -03:00
Keith WilliamsandPeer Richelsen a4d4626319 revert: "feat: rename --font-cal to --font-heading + use Cal Sans UI (#26064)" (#26759)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2026-01-12 16:57:27 +00:00
Pedro Castro 3e063d6744 fix: use HEAD^2 and relative threshold in conflict (#26758)
resolver validation

- Change git diff from HEAD^1 to HEAD^2 to show PRs contribution
(matches GitHubs Files changed view) instead of what main brought in
- Replace fixed 50-file threshold with relative threshold (original PR files + 10)
- A 20-file PR with 100 files in merge = bad
- A 100-file PR with 108 files in merge = fine
2026-01-12 16:47:08 +00:00
Eunjae LeeandDevin AI 0413783ffb docs: extract DI pattern guide to separate file with moduleLoader pattern (#26756)
* docs: add moduleLoader pattern documentation to knowledge-base

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

* docs: rewrite DI section to use moduleLoader pattern as primary approach

- Replace manual module loading examples with moduleLoader pattern
- Add Mistake 4 about manual module loading being not type-safe
- Update Core Concepts to explain moduleLoader components
- Show both depsMap (multiple deps) and dep (single dep) usage
- Emphasize build-time type safety throughout

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

* docs: extract DI pattern to separate file

- Move DI documentation from knowledge-base.md to di-pattern.md
- Add brief reference in knowledge-base.md pointing to the new file
- Keeps knowledge-base.md focused on quick notes while DI gets its own detailed guide

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 15:26:47 +00:00
Keith WilliamsandDevin AI 92f13cdb56 fix: strengthen validation in conflict resolver to prevent bad merge commits (#26754)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 15:10:18 +00:00
Keith WilliamsandDevin AI 1e295827c7 refactor: change conflict resolver from cron to label-based trigger (#26757)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 15:01:51 +00:00
Morgan c7cd25b7d5 chore: api v2 esModuleInterop more imports (#26755) 2026-01-12 11:39:01 -03:00
Keith WilliamsandDevin AI d24807e0e1 fix: make OOO team member select close after selection (#26736)
* fix: make OOO team member select close after selection

Replace the custom always-visible scrollable list with a proper Select
component that closes after selection like a normal dropdown. The Select
component uses onMenuScrollToBottom for infinite scroll and onInputChange
for search functionality.

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

* fix: also convert Team OOO member select to use Select component

Both 'Select team member' sections now use the proper Select component
that closes after selection. Removed unused useInViewObserver import.

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

* fix: add menuPlacement='bottom' to Team OOO member select

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>
2026-01-12 14:35:19 +00:00
Morgan 6840642cb7 chore: esModuleInterop: true api v2 tsconfig (#26752)
* chore: api v2 esModuleInterop

* chore: api v2 esModuleInterop
2026-01-12 11:24:08 -03:00
d5c2212c41 chore: Add logging eventId from Stripe webhook (#26623)
* Add loggin

* fix: use established logger instead of console.log for Stripe webhook eventId

Co-Authored-By: unknown <>

* fix: remove sensitive stripe-signature header from error logs

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 14:02:35 +00:00
Keith WilliamsandDevin AI c5cb2edd65 feat: add Biome rule to restrict @calcom imports in API v2 (#26749)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 13:39:29 +00:00
Keith WilliamsandDevin AI 023dfc2a5e feat: request maintainer access for fork PRs without edit permissions (#26750)
- Update devin-conflict-resolver.yml to check maintainerCanModify for fork PRs
- Instead of skipping fork PRs entirely, now checks if maintainer access is enabled
- Posts friendly comment asking contributors to enable 'Allow edits from maintainers'
- Adds 'maintainer-access-requested' label to track which PRs have been notified
- Update stale-pr-devin-completion.yml to post similar comment when triggered
- Both workflows avoid duplicate comments by checking for existing requests

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 13:39:15 +00:00
Dhairyashil Shinde dd4e1ba11b fix: api v2 dev server error (#26748) 2026-01-12 13:33:48 +00:00
Eunjae LeeandDevin AI 70d5a78d69 docs: add DI pattern guide to knowledge-base.md (#26751)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 14:33:40 +01:00
AnshumancanrockandPallav ea2036672b fix: normalize backslashes in glob paths for Windows compatibility (#25613)
Co-authored-by: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
2026-01-12 13:33:28 +00:00
Kartik LabhshetwarandAnik Dhabal Babu 77a0d69c1a fix(ui): fixing the hover bug in the dropdown component (#26257)
* fix(ui): fixing the hover bug in the dropdown component

* revert

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2026-01-12 13:26:19 +00:00
Keith WilliamsandDevin AI cf6581eaea ci: only auto-fix Cubic suggestions with confidence >= 9/10 (#26745)
* fix: only auto-fix Cubic suggestions with confidence >= 9/10

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

* Apply suggestion from @keithwillcode

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 18:16:35 +05:30
Volnei Munhoz c36f4d4a65 Revert "fix: add withSentryConfig to enable Sentry tracing in production (#26…" (#26744)
This reverts commit dd4e8344c9.
2026-01-12 12:16:08 +00:00
Volnei MunhozandDevin AI dd4e8344c9 fix: add withSentryConfig to enable Sentry tracing in production (#26743)
This adds the withSentryConfig wrapper to the Next.js configuration,
which is required for Sentry tracing to work properly. The wrapper is
only applied in production when both NEXT_PUBLIC_SENTRY_DSN and
SENTRY_TRACES_SAMPLE_RATE environment variables are set.

Without this wrapper, Sentry cannot properly instrument the build for
performance monitoring, causing spans (including calendar telemetry)
to not be recorded despite the SDK being initialized.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 12:07:31 +00:00
6fb42b63e0 feat: add comprehensive validation tests for event-types/[id] GET endpoint (#23809)
* feat: add comprehensive validation tests for event-types/[id] GET endpoint

- Add integration tests for complex validation scenarios
- Test locations, booking fields, metadata, and seats validation
- Test edge cases that could cause validation mismatches between validators and DB results
- Ensure API response structure matches schemaEventTypeReadPublic schema
- Successfully catch validation discrepancies like missing displayLocationPublicly and assignAllTeamMembers fields

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

* fix: add explicit type annotations for booking fields find callbacks

- Fix TypeScript errors on lines 388 and 395
- Provide proper type annotations for find method callback parameters
- Replace any[] with specific { label: string; value: string }[] type
- Ensure CI type checking passes

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

* feat: convert event-types integration test to use real database

- Replace prismaMock with real Prisma client operations
- Create actual database records for comprehensive test scenarios
- Add proper setup/teardown with unique timestamps for test isolation
- Maintain all existing validation test coverage
- Follow Cal.com integration test patterns for database operations

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

* feat: restore original mock-based test alongside integration test

- Restored original _get.test.ts with prismaMock for unit testing
- Kept _get.integration.test.ts with real database for integration testing
- Both tests provide complementary coverage: mocks for fast unit tests, real DB for validation testing
- Fixed type annotations in find callbacks to maintain type safety

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

* fix: rename integration test to use proper naming convention

- Rename _get.integration.test.ts to _get.integration-test.ts
- Follows Cal.com vitest workspace pattern for integration tests
- Ensures integration test runs in proper CI workflow with database setup

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

* fix: update tests to use handler return value instead of res._getData()

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

* fix: add proper type assertions for locations and bookingFields in tests

Co-Authored-By: unknown <>

* fix: create schedule for test user to fix foreign key constraint in integration tests

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keithwillcode@gmail.com>
2026-01-12 12:01:43 +00:00
Carina WollendorferandCarinaWolli b6df61d287 fixes for safari extension (#26741)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2026-01-12 16:50:44 +05:30
Hariom BalharaandDevin AI 77f59b1e4d chore: Integrate confirmation booking audit (#26523)
* chore: Integrate booking confirmation booking audit

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

* fix: Use BookingStatus type instead of string for booking.status

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

* Make booking-audit integration test utils reusable

* refactor: enhance handlePaymentSuccess to accept appSlug and actor identification

- Updated handlePaymentSuccess function to accept an object with paymentId, bookingId, appSlug, and traceContext.
- Introduced getActor function to determine the actor based on appSlug and credentialId.
- Modified webhook handlers for Alby, BTCPayServer, HitPay, PayPal, and Stripe to pass the new parameters.
- Improved logging for missing credentialId in payment processing.
- Adjusted related schemas to ensure proper type handling for booking status and actor identification.

* fix: Correct import paths and getActor function call

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

* fix: Use ActorIdentification type instead of AuditActor in getActor

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

* fix: Add guard clause for undefined actor in fireBookingAcceptedEvent

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

* fix: Add actionSource to all confirm calls

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

* refactor: Integrate actor identification and action source updates across booking services

- Added `makeUserActor` to various booking service files to enhance actor identification.
- Updated action source handling in booking confirmation and related functions to include "MAGIC_LINK".
- Refactored schemas to accommodate new actor and action source requirements, ensuring consistent type handling.
- Improved error handling and logging for booking actions to enhance traceability.

* Remvoe formatting changes

* Add test

* refactor: Enhance booking confirmation tests and event handling

- Updated `confirm.handler.test.ts` to improve test coverage for booking acceptance and rejection scenarios, including bulk bookings.
- Refactored the mock implementation of `BookingEventHandlerService` to streamline event handling for accepted and rejected bookings.
- Adjusted the `handleConfirmation` function to utilize `acceptedBookings` for better clarity and maintainability.
- Added tests to ensure correct invocation of event handler methods for both single and bulk booking confirmations and rejections.

* fix tests

* refactor: Use confirmHandler directly in link and verify-booking-token routes

Refactors the link and verify-booking-token API routes to call confirmHandler directly instead of using the tRPC caller pattern. This simplifies the code by removing the need for session getter, legacy request building, and context creation.

Changes:
- apps/web/app/api/link/route.ts: Use confirmHandler directly with ctx and input
- apps/web/app/api/verify-booking-token/route.ts: Use confirmHandler directly with ctx and input
- Updated tests to mock confirmHandler instead of tRPC caller

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

* fix ts errors due to merge frommain

* feat: introduce getAppActor utility for actor creation

This commit adds a new utility function, getAppActor, to streamline the process of creating actor objects for apps based on their slug and credential ID. The function is integrated into handlePaymentSuccess and handleStripePaymentSuccess, replacing the previous inline actor creation logic. This refactor enhances code maintainability and readability by centralizing actor creation logic.

Changes:
- New file: packages/app-store/_utils/getAppActor.ts
- Updated handlePaymentSuccess and handleStripePaymentSuccess to use getAppActor
- Removed redundant actor creation code from these functions

* refactor: Update appSlug in payment success handlers to use appConfig.slug

This commit modifies the appSlug parameter in the handlePaymentSuccess function across multiple payment webhook handlers to utilize the appConfig.slug value instead of hardcoded strings. This change enhances consistency and maintainability of the code.

Changes:
- Updated appSlug in handlePaymentSuccess for btcpayserver, hitpay, and paypal webhooks.
- Adjusted a test case to reflect the new appSlug value for consistency.

* test: Enhance booking token verification and audit action tests

This commit adds additional assertions to the booking token verification tests to ensure that the confirmHandler is not called when an error occurs. It also introduces a mechanism to clean up additional booking UIDs after each test in the accepted action integration tests, improving test reliability. Furthermore, it updates the action source schema comment for clarity.

Changes:
- Updated tests in `verify-booking-token` to check that `mockConfirmHandler` is not called on error.
- Implemented cleanup logic for additional booking UIDs in `accepted-action.integration-test.ts`.
- Clarified comment in `actionSource.ts` regarding the schema for action sources.
- Refactored `handleConfirmation.ts` and `confirm.handler.ts` to include tracing logger for error handling in booking events.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 08:17:26 -03:00
Hariom BalharaandDevin AI c9bb03b602 chore: Integrate add guests booking audit (#26568)
* chore: Integrate add guests booking audit

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

* chore: Add actionSource parameter to support API_V2 audit logging

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

* refactor: update attendee audit action service to use new email schema

- Modified the `AttendeeAddedAuditActionService` to replace the old attendees schema with a new `emailSchema` for better validation.
- Adjusted the `addGuestsHandler` to pass the action source explicitly and updated the audit logging to reflect the new structure.
- Cleaned up the code for better readability and consistency.

* test: add happy path integration test for attendee added action

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 11:11:33 +00:00
Abhay Mishra b517adfb66 fix: auto-scroll active installed-apps category tab into view on mobile (#26414)
* fix: auto-scroll active installed-apps category tab into view on mobile

* updated code3
2026-01-12 11:11:15 +00:00
ac90caa81b feat: add DAL for monthly proration tracking (#26588)
* feat(billing): add database schema for monthly proration tracking

add new database models and migration for tracking seat changes and monthly proration on annual subscriptions

- new models: SeatChangeLog, MonthlyProration
- new enums: SeatChangeType, ProrationStatus
- extend TeamBilling and OrganizationBilling with billingPeriod, pricePerSeat, paidSeats fields
- add feature flag 'monthly-proration' (disabled by default)
- squashed migration: 20260106093811_add_monthly_proration_tracking

the schema supports:
- tracking seat additions/removals per month with metadata
- storing proration calculations and stripe invoice details
- high-water mark billing via paidSeats field
- proper foreign key relationships and indexes for query performance

* feat(billing): add repository layer for monthly proration

add repository pattern for proration data access

- MonthlyProrationRepository: crud operations for proration records
  - create, update, and query proration records
  - status management (pending, invoice_created, charged, failed)
  - retry count tracking

- MonthlyProrationTeamRepository: team-specific queries with billing context
  - fetch teams with billing information
  - query annual teams with unprocessed seat changes
  - update billing info and paidSeats
  - metadata fallback for legacy teams without billing records

- update IBillingRepository interface to include proration fields

these repositories provide clean data access layer without business logic

* feat(billing): add seat change tracking service

add service for logging and querying seat changes per month

- SeatChangeTrackingService: track seat additions and removals
  - log seat additions with user and metadata context
  - log seat removals with optional triggered-by info
  - calculate monthly changes (additions, removals, net change)
  - retrieve unprocessed changes for a given month
  - mark changes as processed after proration calculation
  - auto-detect billing entity (team vs organization)

- stripe-subscription-utils: extract billing data from stripe subscriptions
  - determine billing period (monthly vs annually)
  - extract price per seat and quantity

includes comprehensive unit tests

* feat(billing): add billing period service

add service to determine billing period and proration eligibility

- BillingPeriodService: query and manage billing period information
  - check if team is on annual vs monthly plan
  - check if team is in trial period
  - determine if monthly proration should apply (annual + not in trial + feature flag)
  - lazy load billing data from stripe if missing in database
  - backfill missing billing data automatically

supports both TeamBilling and OrganizationBilling models
includes comprehensive unit tests with stripe mocking

* feat(billing): add monthly proration service

add core service for calculating and processing monthly seat prorations

- MonthlyProrationService: orchestrate monthly proration workflow
  - process monthly prorations for teams with seat changes
  - calculate prorated amounts based on remaining subscription days
  - create stripe invoice items for seat additions
  - handle payment success and failure webhooks
  - track high-water mark (paidSeats) for billing
  - update stripe subscription quantities after payment
  - lazy load and backfill missing billing data from stripe
  - support retry logic for failed payments

calculation logic:
- only charge for net seat increases (additions - removals, capped at 0)
- prorate based on remaining days in annual subscription
- track separately from stripe's built-in proration to aggregate monthly

includes unit tests and integration tests with stripe mocking

* refactor(billing): extract prisma logic from services to repositories

move database access logic from services to repository layer for better separation of concerns

- add SeatChangeLogRepository for seat change log data access
- add BillingPeriodRepository for billing period queries and updates
- refactor SeatChangeTrackingService to use SeatChangeLogRepository
- refactor BillingPeriodService to use BillingPeriodRepository
- add create methods to MonthlyProrationTeamRepository for billing records
- refactor MonthlyProrationService to use repository for billing creation
- remove direct prisma calls from services (except FeaturesRepository dependency)

services now focus on business logic while repositories handle data access

* refactor(billing): inject prisma via constructor in repositories

make repositories more testable by accepting prisma client via constructor

- SeatChangeLogRepository: accept optional prisma in constructor
- BillingPeriodRepository: accept optional prisma in constructor
- MonthlyProrationRepository: accept optional prisma in constructor
- MonthlyProrationTeamRepository: accept optional prisma in constructor

all repositories default to global prisma instance when not provided
enables easier mocking in tests by injecting test prisma instances

* refactor: remove verbose comments and unnecessary try-catch

* refactor: change pricePerSeat and proratedAmount to cents (Int)

store monetary values as integers in cents instead of floats for precision
remove unnecessary schema comments

* fix tests and add back try catch error handlign

* Update packages/features/ee/billing/service/proration/__tests__/MonthlyProrationService.integration-test.ts

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

* add faeture flag to config.ts

* Update packages/features/ee/billing/service/proration/__tests__/MonthlyProrationService.integration-test.ts

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

* restore schema

* add hooks types for flag

* fix: correct pricePerSeat calculations for cents-based storage

MonthlyProrationService now correctly handles pricePerSeat stored as
cents (INTEGER) instead of dollars (DOUBLE PRECISION). Removed duplicate
conversions to cents that were causing 100x price inflation.

* Use cents for money values

* fix type error

* Move integration to test to use cents value

* fix integration import

* mock subscription

* include paid seats tracking in proration service

* add mocks for invoice creation

* feat: use stripebilling service instead of using stripe directly

* fix validation erorr handler in proration service

* fix: fix sub

* use correct seat value

* fix(billing): mark seat changes as processed when netChange is zero

When a team has additions and removals that net to zero, the function
was returning null without calling markAsProcessed(). This left seat
change logs in an unprocessed state indefinitely, causing subsequent
proration runs to re-query the same changes repeatedly.

Changed the early return condition from checking netChange === 0 to
checking if there are no changes at all (additions === 0 && removals === 0).
This ensures that when there are actual seat changes that net to zero,
a proration record is still created and seat changes are marked as processed.

Co-Authored-By: unknown <>

* update test to reflect new return type of proration

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.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: Keith Williams <keithwillcode@gmail.com>
2026-01-12 10:58:21 +00:00
Volnei Munhoz 4c0b0115a2 remove explicity json check as it is already in included files (#26739) 2026-01-12 10:43:49 +00:00
Dhairyashil Shinde ec87eee900 added the API v2 Imports section to your AGENTS.md (#26737) 2026-01-12 07:21:22 -03:00
Alex van Andel aed80cdbe1 chore: release v6.0.10
Post release / release (push) Canceled after 0s
Release Docker / Prepare Release (push) Canceled after 0s
Release Docker / Release AMD64 (push) Canceled after 0s
Release Docker / Release ARM (push) Canceled after 0s
2026-01-12 10:18:43 +00:00
Benny JooandDevin AI e3eaa69339 fix: return 400 instead of 500 for invalid eventTypeId in booking flow (#26732)
* fix: return 400 instead of 500 for invalid eventTypeId in booking flow

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

* refactor: use ErrorWithCode instead of HttpError in BotDetectionService

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

* refactor: use vi.stubEnv for safer environment variable handling in tests

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 07:07:35 -03:00
Benny JooandDevin AI fc0a2b5a37 refactor: Remove trpc/server dependency from @calcom/atoms (#26717)
* mv useTeamMembersWithSegment

* remove

* fix ts error

* remove

* remove

* remove

* biome rule

* wip

* refactor: remove remaining @calcom/trpc/server imports from atoms

- Create local types for schedule handlers (CreateScheduleHandlerReturn, DuplicateScheduleHandlerReturn, GetAvailabilityListHandlerReturn, CreateScheduleInput)
- Create local GetAvailableSlotsResponse type for slots hook
- Update imports in useAtomCreateSchedule, useAtomDuplicateSchedule, useAtomGetAllSchedules, and useAvailableSlots

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

* fix

* fix: add userId to Schedule type and create ScheduleForList type

- Schedule type now includes userId for create/duplicate handlers
- Created ScheduleForList type for list handler (uses select without userId)

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

* fix

* fix

* fix

* fix

* fix: correct fromUser/toUser types in GetAvailableSlotsResponse

- fromUser: { id, displayName } (matches IFromUser)
- toUser: { id, username, displayName } (matches IToUser)

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 07:06:22 -03:00
Rajiv Sahal f7dee536d7 fix: dont import directly from @calcom/trpc or @calom/features (#26734) 2026-01-12 10:05:39 +00:00
Anik Dhabal Babu a8e4097c4c test: fix flaky (#26728) 2026-01-12 08:08:01 +00:00
0994050b0f fix(api): return original email without OAuth suffix in bookings (#25593)
* fix(api): remove OAuth client ID suffix from email in booking API responses

Fixes #25494 | Linear: CAL-6843

When managed users create or receive bookings, their emails were being returned with an internal OAuth client ID suffix (e.g., bob+cuid123@example.com). This suffix is used internally for user identification but should not be exposed in API responses.

Changes:
- Add cleanOAuthEmailSuffix() helper using CUID regex pattern
- Clean email suffix in hosts[], attendees[], bookingFieldsResponses.email, bookingFieldsResponses.guests[], and reassignedTo.email
- Pattern consistent with google-calendar.service.ts implementation

Affected output methods:
- getOutputBooking
- getOutputRecurringBooking
- getOutputSeatedBooking
- getOutputRecurringSeatedBooking
- getOutputReassignedBooking
- getHost

* refactor(api): preserve original email, add displayEmail field

Per team discussion, keep original email unchanged to avoid breaking changes for platform customers.
Add displayEmail field with CUID suffix removed for display purposes

* feat(api): add displayEmail to booking output DTOs

Add displayEmail property to BookingAttendee, BookingHost and ReassignedToDto for API documentation and type safety

* test(api): add e2e tests for displayEmail fields in managed user bookings

Add tests to verify that displayEmail fields correctly strip CUID suffix from OAuth managed user emails in booking API responses:

- Test host displayEmail returns email without CUID suffix
- Test attendee displayEmail returns email without CUID suffix
- Test bookingFieldsResponses.displayEmail returns clean email
- Test displayGuests array returns emails without CUID suffix

* false positive breaking change

* false positive breaking change

* test(api): update existing e2e tests to expect displayEmail field

* fix(api): add missing displayEmail to seated booking test assertions

The seated booking tests were missing displayEmail in the attendee
assertions for the second booking test and cancel-as-host test,
causing CI test failures

---------

Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2026-01-12 07:48:37 +00:00
Syed Ali ShahbazandVolnei Munhoz 3e7e376100 chore: Some minor fixes and follow up to #26446 (#26587)
* init

* DI and other improvements

* type fix

---------

Co-authored-by: Volnei Munhoz <volnei@cal.com>
2026-01-12 04:32:55 +00:00
Anik Dhabal Babu 93cac5a1c9 fix: don't include ics for unconfirmed booking (#26718) 2026-01-12 03:48:27 +00:00
a03e64da1b feat: implemented skeleton loader (#23654)
* implemented skeleton loader

* fix: improve skeleton ui

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 08:42:46 +05:30
mintlify[bot] 2e571eaedf docs: Enhanced Booker Atom Documentation (#24773)
* Update docs/platform/atoms/booker.mdx

* Update docs/platform/atoms/booker.mdx

* Update docs/platform/atoms/booker.mdx

* Update docs/platform/atoms/booker.mdx

---------

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2026-01-12 00:56:07 +00:00
Anshumancanrock 258b1a7bb8 fix: make travel schedule delete icon red (#26725) 2026-01-11 23:28:32 +00:00
Keith WilliamsandDevin AI d675f82557 chore: migrate e2e-atoms and nextjs-bundle-analysis to cache-checkout (#26713)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-11 20:10:22 -03:00
Alex van Andel 312733ce68 refactor: Extract schedule determination to seperate fn (#26600)
* refactor: Extract schedule determination to seperate fn

* refactor: Extract schedule detection to seperate file and add tests

* RouterOutputs should be used in hooks, not direct type imports
2026-01-11 19:44:09 -03:00
58798f27d2 feat: add source: 'onboarding' metadata to subscriptions from onboarding v3 (#26619)
When a user signs up to a team/org from onboarding v3, the subscription
now includes source: 'onboarding' in its metadata. This helps track
which subscriptions originated from the onboarding flow.

Changes:
- Add source: 'onboarding' to subscription_data.metadata in team checkout
  when isOnboarding is true
- Add source: 'onboarding' to subscription metadata for organization
  onboarding flow

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2026-01-11 21:49:10 +00:00
Dhairyashil ShindeandDevin AI 478ca05246 feat(companion): update Android booking detail page to match iOS design (#26715)
* feat(companion): update Android booking detail page to match iOS design

* feat(companion): add video icon button to Android header for join meeting

* fix(companion): use eventType.description instead of booking.description

Address Cubic AI review feedback - the booking description may contain
Additional Notes which can cause duplicate display. The event type
description is the proper source for this information.

Co-Authored-By: unknown <>

* Revert "fix(companion): use eventType.description instead of booking.description"

This reverts commit 3e324313f2d72704ab51f723a52b2784b33e7867.

* fix(companion): extract IOSPickerTrigger to platform-specific files for web compatibility

* fix(companion): extract iOS pickers to platform-specific files for web compatibility

Extract iOS-only native picker components from AvailabilityTab, BasicsTab,
LimitsTab, RecurringTab, and LocationsList to platform-specific files to
fix 'requireNativeViewManager is not available on web' error in browser extension.

Each tab now has:
- TabIOSPicker.ios.tsx - iOS implementation with native @expo/ui/swift-ui
- TabIOSPicker.tsx - Web/Android fallback that returns null

* fix(companion): fix Switch toggle thumb color for web/extension

Change thumbColor condition from Platform.OS === 'android' to
Platform.OS !== 'ios' so that web/extension also gets white thumb color
instead of the default green.

* fix(companion): fix Participants section padding for web/extension

Move padding classes from AppPressable to a child View to ensure
padding is properly applied on web. AnimatedPressable doesn't
reliably apply className padding on web platforms.

* fix(companion): fix unexpected text node error on web

Change ternary operator in Hosts section from 'booking.user && (...)' to
'booking.user ? (...) : null' to prevent React Native Web from trying
to render 'false' as a text node when booking.user is falsy.

* fix(companion): wrap Stack.Header in iOS platform check for web compatibility

The Stack.Header components are iOS-specific and cause 'Unexpected text node'
errors on web. Wrap them in Platform.OS === 'ios' check to prevent rendering
on web/Android where they're not supported.

* fix(companion): convert all && conditionals to ternary operators for web compatibility

React Native Web renders boolean 'false' from && operators as text nodes.
Changed all conditional rendering patterns from 'condition && (<Component>)'
to 'condition ? (<Component>) : null' to prevent 'Unexpected text node' errors.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-11 16:02:13 -03:00
Keith Williams f0ffc24d39 chore: Improve Devin prompt for resolving conflicts (#26714) 2026-01-11 14:22:24 -03:00
Beto 5eacd6b807 refactor(companion)(ios): Improve header right button in booking details screen (#26721)
* refactor: improve code formatting and readability in BookingDetailScreen and Bookings components

* refactor: enhance code structure and readability in EventTypesIOS component
2026-01-11 17:11:32 +00:00
Anik Dhabal Babu 98ef45be18 fix: prevent calendar event creation for unconfirmed round robin reschedules (#26719)
* fix rr booking

* fix test

* use actual prisma

* fix

* Refactor
2026-01-11 16:38:46 +00:00
1e98652161 feat: reuse existing Devin sessions when resolving conflicts (#26711)
* feat: reuse existing Devin sessions when resolving conflicts

- Check if PR was created through Devin and use that session
- Check PR comments for existing active Devin sessions
- Send message to existing session instead of creating new one
- Only create new session if no active session exists
- Update comment message to reflect whether session is new or reused

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

* feat: also check for Cubic AI review sessions when reusing Devin sessions

- Added 'Devin AI is addressing Cubic AI' to session patterns
- Now checks for both conflict resolution and Cubic AI review sessions

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-11 14:08:32 +00:00
Keith WilliamsandDevin AI 7a6f4b2594 fix: add global CRM services mock to prevent test flakes (#26716)
* fix: mock CRM services to prevent test flakes in handlePaymentSuccess.test.ts

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

* fix: add global CRM services mock to prevent test flakes

The test flakes were caused by the Salesforce CRM service importing @urql/core,
which triggers fetch calls during module loading. These fetch calls remain
pending when the Vitest worker shuts down, causing the error:
'[vitest-worker]: Closing rpc while fetch was pending'

This fix adds a global mock for CrmServiceMap in setupVitest.ts, similar to
how payment services are already mocked. This prevents the Salesforce module
from being loaded during test execution.

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>
2026-01-11 18:18:05 +05:30
Benny Joo c826e9cea8 refactor: Remove trpc/react dependency from @calcom/atoms (#26694)
* mv useTeamMembersWithSegment

* remove

* fix ts error

* remove

* remove

* remove

* biome rule
2026-01-11 08:02:30 -03:00
784db75b38 ci: optimize sparse-checkout and cache to reduce cache size (#26692)
* ci: optimize sparse-checkout to reduce cache size by ~230MB

Exclude additional large files from CI checkout that are not needed for builds/tests:
- docs/images/ (~90MB) - Documentation images
- packages/app-store/*/static/*.png,jpg,jpeg,gif (~140MB) - App store screenshots

These files are only needed for documentation rendering and app store UI display,
not for CI builds, linting, type checking, or tests.

SVG icons in app-store are preserved as they may be needed for the build.

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

* ci: exclude generated app-store static files from cache

The apps/web/public/app-store directory contains ~151MB of static files
copied from packages/app-store/*/static/ during build. These files are
regenerated during yarn install and don't need to be cached.

Combined with the sparse-checkout exclusions, this should significantly
reduce the git checkout cache size.

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

* ci: fix sparse-checkout by limiting first checkout to .github only

The first checkout in the prepare job was doing a full checkout (~535 MB),
and then dangerous-git-checkout applied sparse-checkout. But the files from
the first checkout remained on disk because sparse-checkout doesn't remove
files that were already checked out.

By limiting the first checkout to only .github (which is needed to access
the local actions), we avoid downloading the full repo twice. The actual
sparse-checkout with exclusions is then applied by dangerous-git-checkout.

This should reduce the cache from ~504 MB to ~148 MB.

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

* fix: update cache-checkout to include sparse-checkout exclusions and fix pr.yml compatibility

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-11 19:17:09 +09:00
BetoandDhairyashil Shinde 75962f7719 fix: update iOS app configuration to disable tablet support (#26705)
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2026-01-11 14:44:35 +05:30
f24f5b2b35 feat: update translations via @LingoDotDev (#26700)
Co-authored-by: Lingo.dev <support@lingo.dev>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2026-01-11 09:01:28 +00:00
08edd4f361 chore: Add logging to msteams (#23684)
* Add logging

* fix: Add response status checking to MS Teams adapter error handling

- Add response.ok checks before parsing JSON in createMeeting and updateMeeting
- Remove console.log statements that could leak credential information
- Ensure error handling matches original handleErrorsRaw behavior for tests

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

* Fix logs

* Revert "fix: Add response status checking to MS Teams adapter error handling"

This reverts commit 35820d21e1a54d9e04d83fd5f53584d64a5459b6.

* fix: Add response status checking to MS Teams adapter error handling

- Check response.ok before parsing JSON in createMeeting and updateMeeting
- Re-throw HttpError instances instead of wrapping them in catch blocks
- This ensures HTTP errors (like 500) are properly propagated

Co-Authored-By: unknown <>

* fix: Correct log message in updateMeeting to say 'updating' instead of 'creating'

Co-Authored-By: unknown <>

---------

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>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2026-01-11 10:34:02 +05:30
Dhairyashil Shinde 8b9c3dcbb9 feat(companion): add skeleton loading for Event Types, Bookings, and Availability pages (#26687)
* feat(companion): add skeleton loading for Event Types list page

- Install skeleton component from react-native-reusables
- Create EventTypeListItemSkeleton and EventTypeListSkeleton components
- Replace LoadingSpinner with skeleton loading for initial load
- Show skeleton during pull-to-refresh instead of spinner
- Update both index.tsx (Android/Web) and index.ios.tsx (iOS)

* fix(companion): use inline styles for skeleton spacing to ensure distinct elements

* update skeleton

* feat(companion): add skeleton loading for Bookings page

* fix(companion): wrap iOS booking skeleton in ScrollView with contentInsetAdjustmentBehavior

* feat(companion): add skeleton loading for Availability page

* fix(companion): fix iOS availability skeleton layout issue

* fix(companion): remove native spinner from pull-to-refresh on skeleton pages

* fix(companion): remove unused refreshing variable from AvailabilityListScreen

* fix(companion): restore native spinner for empty states during pull-to-refresh

* fix(companion): add try/finally to onRefresh to prevent stuck loading state

* fix - react compiler issue, lint, typecheck
2026-01-11 10:02:07 +05:30
Keith WilliamsandDevin AI 539214b164 fix: improve BookingReferenceRepository integration test isolation (#26707)
The integration tests were flaky because they relied on a shared booking
created in beforeAll, which could cause race conditions when tests run
in parallel or when cleanup from previous runs interfered.

Changes:
- Use seed user (member0-acme@example.com) when available, fallback to
  creating a test user for local development
- Create a fresh booking in beforeEach instead of sharing one across tests
- Track created bookings and clean them up properly in afterEach
- Fix import organization to satisfy linter

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-11 10:01:25 +05:30
9d9a85ddd9 docs: calendso encryption key of length 24 (#23045)
* docs: calendso encryption key of length 24

Signed-off-by: Bandhan Majumder <bandhanmajumder16@gmail.com>

* Update .env.example

---------

Signed-off-by: Bandhan Majumder <bandhanmajumder16@gmail.com>
Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2026-01-10 21:40:23 +00:00
Keith WilliamsandDevin AI 560598db56 fix: Check for existing Devin sessions from conflict resolver in cubic workflow (#26706)
When addressing Cubic AI comments, the workflow now also looks for existing
Devin sessions from the conflict resolver workflow and stale PR completion
workflow, not just previous Cubic review sessions.

This ensures that if a Devin session is already working on a PR (e.g., fixing
merge conflicts), new Cubic feedback will be sent to that existing session
instead of creating a new one.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 21:22:46 +00:00
Keith WilliamsandDevin AI 3dd0b2d46b fix: add fallback for ref parameter in cache-checkout action (#26703)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 19:37:06 +00:00
Keith WilliamsandDevin AI bf4cdf6245 fix: refactor cache-checkout to use actions/cache@v4 with fallback (#26702)
This fixes cache propagation delay issues with Blacksmith's distributed cache.

The previous implementation used separate save/restore actions with
fail-on-cache-miss: true, which failed when the cache wasn't immediately
available after being saved by the Prepare job.

The new implementation uses actions/cache@v4 (combined save/restore)
with a fallback to do the checkout if cache miss, matching the pattern
used by cache-build and cache-db which don't have this issue.

Changes:
- Refactored cache-checkout action to use actions/cache@v4
- Added fallback checkout step when cache miss occurs
- Removed mode input (no longer needed)
- Updated all workflows to use the simplified cache-checkout action

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 19:26:28 +00:00
Anik Dhabal BabuandDevin AI eecf9e1ab9 fix: make BookingReferenceRepository integration tests use unique identifiers (#26701)
The integration tests were flaky because they used fixed identifiers
(email, username, uid) that could conflict across parallel test runs
or with leftover data from previous runs.

Changes:
- Add unique testRunId using timestamp and random suffix
- Use testRunId in email, username, and booking uid
- Change afterAll cleanup to use deleteMany instead of delete to
  prevent failures when records are already deleted

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 16:20:11 -03:00
Keith WilliamsandDevin AI 2b4be9c34e fix: remove proactive cache deletion to prevent race condition (#26698)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 15:46:50 -03:00
Keith WilliamsandDevin AI 9c4ed1a303 fix(ci): improve conflict resolver prompt to prevent bad merge commits (#26699)
- Remove explicit git commands from the prompt to avoid Devin reproducing changes
- Add validation step before pushing to detect if merge commit incorrectly includes changes from target branch
- Add guidance to use git's merge functionality properly instead of manually copying content
- Add rule to never reproduce or recreate changes from the target branch

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 15:33:17 -03:00
Keith WilliamsandDevin AI 6aa1dbad90 chore: delete cached build after bundle analysis completes (#26689)
* chore: delete cached build after bundle analysis completes

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

* chore: use useblacksmith/cache-delete action

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>
2026-01-10 15:01:21 -03:00
Keith WilliamsandDevin AI 7a9462b756 fix: parsing error in stale PR workflow for workflow_dispatch (#26691)
* fix: fix parsing error and add token support for fork PRs in stale PR workflow

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

* fix: keep only parsing error fix, remove token support for fork PRs

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>
2026-01-10 15:00:52 -03:00
Keith WilliamsandDevin AI 46b1bc3365 fix: switch conflict resolver from push to schedule trigger to save runner costs (#26690)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 15:00:33 -03:00
cal-com-ci[bot]andLingo.dev f86a8dc93e feat: update translations via @LingoDotDev (#26599)
Co-authored-by: Lingo.dev <support@lingo.dev>
2026-01-10 17:54:22 +00:00
Keith WilliamsandDevin AI d9b49b2c41 fix: mock salesforce graphql queries to prevent flaky test failures (#26688)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 23:22:44 +05:30
Keith WilliamsandDevin AI 728f55e4bc fix: use PR head SHA for cache-checkout key in pull_request_target events (#26686)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 09:57:42 -03:00
d3e7554835 refactor: move WebWrapper files from packages/platform to apps/web/modules (#26629)
* refactor: move WebWrapper files from packages/platform to apps/web/modules

Move the following WebWrapper files to their appropriate locations in apps/web/modules:
- EventTypeWebWrapper and related tab wrappers to apps/web/modules/event-types/components/wrappers/
- BookerWebWrapper to apps/web/modules/bookings/components/
- ConferencingAppsViewWebWrapper to apps/web/modules/apps/components/
- SelectedCalendarsSettingsWebWrapper to apps/web/modules/calendars/components/
- AddMembersWithSwitchWebWrapper to apps/web/modules/event-types/components/

This reduces the number of files in packages/platform that import from @calcom/web,
improving the separation between platform and web-specific code.

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

* fix: move web-specific hooks to apps/web and update imports

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

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* add back comments

* fix

* fix

* fix

* fix

* fix: correct relative import paths in moved WebWrapper files

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

* final

* fix: use package alias for event-type hooks instead of deeply nested relative paths

- Move sortHosts function to @calcom/lib/bookings/hostGroupUtils.ts for shared access
- Add exports for useEventTypeForm, useHandleRouteChange, useTabsNavigations to @calcom/atoms package.json
- Update EventTypeWebWrapper.tsx to use @calcom/atoms package alias instead of ../../../../../packages/... paths
- Update useEventTypeForm.ts to import sortHosts from @calcom/lib instead of @calcom/web
- Re-export sortHosts from HostEditDialogs.tsx for backward compatibility

This addresses the Cubic AI review feedback about fragile deeply nested relative paths that bypass proper package resolution.

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 12:55:39 +00:00
cbb56f6a61 feat: implement git checkout caching to speed up CI workflows (#26636)
* feat: implement git checkout caching to speed up CI workflows

- Create cache-checkout action to save/restore git working directory
- Update prepare job in pr.yml to cache checkout after dangerous-git-checkout
- Update all downstream workflows to restore from cache instead of checkout
- Pass commit-sha to all workflow calls for cache key consistency

This reduces the number of full git checkouts from 20+ per workflow run to just 1,
significantly improving CI performance especially for large repos.

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

* refactor: use actions/cache/restore directly instead of custom action

Remove sparse-checkout and use actions/cache/restore@v4 directly in all
downstream workflows. This eliminates the need for any git fetch operation
before restoring from cache, making the optimization more effective.

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

* refactor: use github.event.pull_request.head.sha for cache key

- Remove commit-sha input from all downstream workflows
- Update cache-checkout action to use github.event.pull_request.head.sha directly
- Remove commit-sha output from prepare job
- Remove commit-sha from all workflow calls in pr.yml
- Keep sparse-checkout of .github to access the cache-checkout action

This eliminates the need to pass commit-sha around while still using
a reusable action for the cache restore logic.

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

* fix: update cache key to include branch name and add cache cleanup

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

* fix: use github.head_ref and github.sha for cache key

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

* fix: add prefix delete before saving cache to clear previous caches

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

* perf: add sparse-checkout to exclude example-apps and mp4 files

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

* fix: restore get_sha step and commit-sha output that was accidentally removed

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

* fix: add trailing dash to cache key prefix to prevent accidental deletion of other branches' caches

The prefix-based cache deletion was using 'git-checkout-{branch}' which would
accidentally match and delete caches for branches with similar prefixes.
For example, branch 'feature' would delete caches for 'feature-2', 'feature-new', etc.

Adding a trailing '-' ensures exact branch matching:
- 'git-checkout-feature-' matches 'git-checkout-feature-abc123' (intended)
- 'git-checkout-feature-' does NOT match 'git-checkout-feature-2-def456' (correct)

Fixes both the cache-checkout action and the PR close cleanup workflow.

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 21:46:21 +09:00
+2 2d514a2a3e feat: companion alert system for browser extension (#26613)
* feat: companion alert system for browser extension

* feat: implement cross-platform alert system for browser extension

- Add ToastContext.tsx with global toast provider for managing toast state
- Add GlobalToast.tsx centered toast component for web/browser extension
- Modify alerts.ts to be platform-aware (native Alert on iOS/Android, toast on web)
- Add showInfoAlert function for informational alerts
- Update _layout.tsx to wrap app with ToastProvider and GlobalToast
- Export new toast context and alert functions from index files

* style: update toast to white/black design system

- Use white background with gray border
- Use black icons and text colors
- Match companion app design system

* open the toast at the center

* refactor: migrate event-types/index.tsx to unified alert system

- Remove inline toast state (showToast, toastMessage) and showToastMessage function
- Remove inline toast UI component at the end of the file
- Migrate all platform-specific Alert.alert calls to showSuccessAlert/showErrorAlert
- Simplifies code by using cross-platform alert utilities

* refactor: migrate useBookingActions.ts to unified alert system

- Add showSuccessAlert import alongside showErrorAlert
- Migrate all Alert.alert('Success', ...) calls to showSuccessAlert
- Affected handlers: handleSubmitReschedule, handleRescheduleWithValues,
  handleSubmitCancel, handleCancelBooking, handleConfirmBooking,
  handleRejectBooking, handleSubmitReject, handleInlineConfirm

* refactor: migrate useBookingActionModals.ts to unified alert system

- Add showSuccessAlert import alongside showErrorAlert
- Migrate all Alert.alert('Success', ...) calls to showSuccessAlert
- Affected handlers: handleAddGuests, handleUpdateLocation, handleMarkNoShow

* refactor: migrate medium-priority screens to unified alert system

- AvailabilityDetailScreen.ios.tsx: 4 Alert.alert calls migrated
- RescheduleScreen.tsx/ios/android: 3 Alert.alert calls each migrated
- EditLocationScreen.tsx/ios: 4 Alert.alert calls each migrated
- Removed unused Alert imports

* refactor: migrate remaining medium-priority screens to unified alert system

- BookingDetailScreen.tsx: 2 Alert.alert calls migrated
- AvailabilityListScreen.tsx: 1 Alert.alert call migrated
- AddGuestsScreen.tsx: 6 Alert.alert calls migrated
- MarkNoShowScreen.tsx: 2 Alert.alert calls migrated
- EditAvailabilityOverrideScreen.tsx: 4 Alert.alert calls migrated
- EditAvailabilityOverrideScreen.ios.tsx: 2 Alert.alert calls migrated
- Removed unused Alert imports

* refactor: migrate medium-priority app routes to unified alert system

Migrated 24 app route files from direct Alert.alert() calls to unified
alert utilities (showErrorAlert, showSuccessAlert, showInfoAlert):

- reschedule.tsx, reschedule.ios.tsx
- edit-location.tsx, edit-location.ios.tsx
- add-guests.tsx, add-guests.ios.tsx
- mark-no-show.tsx, mark-no-show.ios.tsx
- view-recordings.tsx, view-recordings.ios.tsx
- meeting-session-details.tsx, meeting-session-details.ios.tsx
- profile-sheet.tsx, profile-sheet.ios.tsx
- edit-availability-hours.tsx, edit-availability-hours.ios.tsx
- edit-availability-day.tsx, edit-availability-day.ios.tsx
- edit-availability-name.tsx, edit-availability-name.ios.tsx
- edit-availability-override.tsx, edit-availability-override.ios.tsx
- booking-detail.tsx, booking-detail.ios.tsx

This enables cross-platform alert support where native Alert.alert()
is used on iOS/Android and toast notifications on web (browser extension).

* refactor: migrate lower-priority components to unified alert system

Migrated Alert.alert calls to showSuccessAlert/showErrorAlert/showInfoAlert in:
- event-types/index.ios.tsx (copy link, delete, duplicate success alerts)
- event-type-detail.tsx (copy link, create/update success, validation errors, info alerts)
- deep-links.ts (error alerts for link failures)
- LogoutButton.tsx (error alert)
- BookingModals.tsx and .ios.tsx (report booking info alerts)
- AdvancedTab.tsx (info alert for unsaved event type)
- BookingListScreen.tsx (success/error/info alerts for bulk actions)

Confirmation dialogs with buttons remain as Alert.alert (out of scope).

* fix: restore Alert import for confirmation dialogs, migrate remaining info alert

- AdvancedTab.tsx: Added back Alert import for timezone selector confirmation dialog (out of scope), migrated private links info alert to showInfoAlert
- deep-links.ts: Added back Alert import for request reschedule confirmation dialog (out of scope)

* some ui dialoes were missing

* fixed multi hours ui issues on Availability detail page for android and web

* fix: add __DEV__ check for web platform in showErrorAlert

The showErrorAlert function was showing error toasts to production users
on web platform, bypassing the __DEV__ check that exists for native platforms.
This fix ensures consistent behavior across all platforms - error alerts
are only shown in development mode, while production errors are logged
to console.

Co-Authored-By: unknown <>

* fix: address dual dialogs on web and onSuccess timing issues

- BookingDetailScreen.tsx: Add Platform.OS === 'android' guard to AlertDialog
  to prevent dual dialogs rendering on web (FullScreenModal + AlertDialog)
- RescheduleScreen.android.tsx: Restore original Alert.alert with callback
  to ensure onSuccess() is called after user dismisses the alert

Co-Authored-By: unknown <>

* fix: restore Alert.alert with callback for native platforms

- AddGuestsScreen.tsx: Use platform-specific handling - Alert.alert with
  callback on iOS/Android, showSuccessAlert on web
- EditAvailabilityOverrideScreen.tsx: Same pattern - wait for user
  acknowledgment on native platforms before calling onSuccess()

Co-Authored-By: unknown <>

* fix: RescheduleScreen timing and GlobalToast useState pattern

- RescheduleScreen.tsx: Use platform-specific handling - Alert.alert with
  callback on iOS/Android, showSuccessAlert on web
- GlobalToast.tsx: Replace useMemo with useState for Animated.Value to
  guarantee instance preservation across renders

Co-Authored-By: unknown <>

* fix: restore Alert.alert with callback for iOS delete success

AvailabilityDetailScreen.ios.tsx: Use Alert.alert with callback to ensure
router.back() is called after user dismisses the success alert

Co-Authored-By: unknown <>

* feat(companion): new event type detail page  (#26678)

* fix: skip stale run-ci label check on workflow re-runs (#26590)

* fix: trust community PRs when maintainer pushes to the branch

When a maintainer merges main into a community PR branch, the new SHA
invalidates the run-ci label timing check because the label was added
before the new push. This fix adds a check to trust the PR if the
person who pushed the latest commit has write access.

This handles the case where a maintainer:
- Merges main into a community PR to resolve conflicts
- Pushes any changes to help the contributor

The security model is maintained because:
- Only users with write access can trigger this trust
- The maintainer has reviewed the code by pushing to it

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

* fix: skip stale label check on workflow re-runs

Instead of implicitly trusting maintainer pushes (which could be just
a sync action without code review), use github.run_attempt to detect
re-runs. If run_attempt > 1, it means the workflow was explicitly
re-triggered (via run-ci.yml or manual re-run), so we skip the stale
label check.

This avoids the need to remove and re-add the 'run-ci' label after
syncing a community PR with main, while keeping the explicit approval
flow intact.

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

* Simplify comment about re-runs in PR workflow

---------

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

* fix: Text cursor barely visible in calendar event name field (#26563)

* fix: cursor visibility in input fields

* fix: cursor visibility in input fields with suffix addons

* fix: text cursor visibility in input fields

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* fix: PayPal setup page inconsistent spacing and button styling (#26612)

* chore: api v2 generate swagger only in dev (#26617)

* feat: add BUILD_FROM_BRANCH option to release-docker workflow (#26615)

* feat: add BUILD_FROM_BRANCH option to release-docker workflow

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

* fix: sanitize branch names with slashes for valid Docker tags

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

* refactor: extract common logic into prepare job to avoid duplication

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

---------

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

* feat: add Cubic AI to Devin review integration workflow (#26618)

* feat: add Cubic AI to Devin review integration workflow

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

* fix: update permissions to allow posting PR comments

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

* fix: remove sensitive API response logging from CI workflow

Address Cubic AI review feedback: Remove the debug log that outputs the
full Devin API response, which could expose sensitive session tokens or
authentication data in CI logs. The session URL is still logged when
successfully extracted.

Co-Authored-By: unknown <>

---------

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

* chore: enable/disable slots workers via env (#26621)

* chore: enable/disable slots workers via env

* fix: address Cubic AI review feedback

- Fix incorrect JSDoc comment for getSerializableContext method
- Remove debug console.log statement from slots controller
- Fix port suffix condition to preserve original behavior

Co-Authored-By: unknown <>

---------

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

* chore: USE_POOL env var for api v2 prisma pooling

* fix list numbering in Manual setup section of README (#26620)

* Revert "chore: USE_POOL env var for api v2 prisma pooling"

This reverts commit a97092619f.

* feat: limit badges to 2 with hover/click popover in UserListTable (#26556)

* feat: limit badges to 2 with hover tooltip in UserListTable

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

* refactor: reuse LimitedBadges component with clickable popover for mobile

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

* refactor: move LimitedBadges to components/ui with hover+click support

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

* refactor: fix Biome lint issues in LimitedBadges and UserListTable

- Refactor LimitedBadges to concrete component with BadgeItem type
- Move exports to end of files to comply with useExportsLast rule
- Add explicit types to callback parameters for useExplicitType rule
- Convert nested ternary to if-else for filterType calculation
- Remove unused imports (Row, Table types)
- Update ResponseValueCell and UserListTable to use new LimitedBadges API

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

* fix: add proper types for filterType and getFacetedUniqueValues

- Add FilterType import from @calcom/types/data-table
- Add FacetedValue import from @calcom/features/data-table
- Type filterType as FilterType to allow reassignment to different ColumnFilterType values
- Type getFacetedUniqueValues return as Map<FacetedValue, number>

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

* add vertical gap

* fix: address code review feedback for LimitedBadges

- Add index to id for unique keys in ResponseValueCell.tsx
- Restore orange variant for group options in UserListTable.tsx
- Fix useMemo dependency array (add t, remove dispatch)
- Fix import ordering with biome
- Convert ternary operators to if-else statements for biome compliance

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

* refactor: remove id from BadgeItem type, use label as key

- Remove id field from BadgeItem type in LimitedBadges
- Use label as React key instead of id
- Remove unused rowId parameter from ResponseValueCell
- Update all consumers to not pass id field

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

* refactor: use index for key instead of label in LimitedBadges

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

* Revert "refactor: use index for key instead of label in LimitedBadges"

This reverts commit 1daaac47e596fd6b5f5583847faa10b131783349.

---------

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

* chore: add database-backed feature flag for sidebar tips section (#26516)

* chore: remove tips section from sidebar

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

* chore: add feature flag for sidebar tips section

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

* chore: use database-backed feature flag for sidebar tips section

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

---------

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

* feat: add automated stale PR completion workflow with Devin API (#26627)

* feat: add stale PR completion workflow with Devin API

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

* feat: only trigger for community PRs (non-calcom org members)

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

* refactor: use author_association pattern from pr.yml for community check

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

* refactor: simplify workflow - use community label check and let Devin gather PR details

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

---------

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

* feat: add Devin PR conflict resolver workflow (#26624)

* feat: add Devin PR conflict resolver workflow

Adds a GitHub Actions workflow that automatically detects PRs with merge
conflicts and spins up Devin sessions to resolve them.

How it works:
1. Triggers on push to main branch (when main updates could cause conflicts)
2. Also supports manual trigger via workflow_dispatch
3. Lists all open PRs and checks their mergeable status
4. For PRs with conflicts (mergeable=false, mergeable_state=dirty):
   - Checks if a Devin session was already created (avoids duplicates)
   - Creates a new Devin session with instructions to resolve conflicts
   - Posts a comment on the PR with the Devin session link

The workflow follows the same pattern as cubic-devin-review.yml for
consistency.

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

* fix: skip draft PRs in conflict detection workflow

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

* refactor: use GitHub Search API to filter draft PRs at API level

Instead of filtering draft PRs in the loop, use the GitHub Search API
with 'draft:false' filter which is more efficient as it filters at the
API level rather than fetching all PRs and filtering locally.

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

* style: remove explanatory comments from workflow

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

* style: remove all unnecessary explanatory comments

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

* refactor: use GraphQL for batched PR fetching and labels for tracking

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

* feat: add support for resolving conflicts on fork PRs

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

* fix: check response.ok before parsing Devin API response

Address Cubic AI review feedback: Add response.ok check before parsing
the response body to explicitly handle HTTP error status codes from the
Devin API. This distinguishes API failures (authentication errors,
server errors) from successful responses that might be missing expected
fields.

Co-Authored-By: unknown <>

* fix: improve Devin API error logging with PR number

Co-Authored-By: unknown <>

* feat: add pr_number input for manual fork PR conflict resolution

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

* feat: add maximum 15 PRs safety net limit

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>

* feat: make stale PR workflow fork-aware with workflow_dispatch support (#26633)

* feat: make stale PR workflow fork-aware with workflow_dispatch support

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

* fix: add null check for pr.head.repo in stale PR workflow

When a fork is deleted after a PR is created, pr.head.repo can be null
(documented GitHub API behavior). This would cause a TypeError when
accessing properties like fork, full_name, or clone_url.

Added a null check that fails gracefully with a clear error message
when the source repository has been deleted.

Co-Authored-By: unknown <>

---------

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

* feat: reuse existing Devin session for Cubic AI review feedback (#26632)

* feat: reuse existing Devin session for Cubic AI review feedback

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

* refactor: parse session ID from PR comments instead of API search

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

* fix: add error handling for message send API call in Cubic-Devin workflow

Addresses Cubic AI review feedback: The POST request to send a message
to an existing Devin session now checks the HTTP status code and fails
the step if the message wasn't delivered successfully. This prevents
posting a misleading comment claiming feedback was sent when the API
call actually failed.

Co-Authored-By: unknown <>

---------

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

* chore: Remove stale label when work is completed (#26638)

* chore: Remove stale label when work is completed

* Mark as ready for review too

* feat: add retry mechanism for UNKNOWN mergeable status PRs (#26635)

* feat: add retry mechanism for UNKNOWN mergeable status PRs

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

* fix: remove redundant filtering logic for unknown mergeable status PRs

Address Cubic AI review feedback by:
- Having processPR return isTargetPR in the unknown case (like the conflict case)
- Simplifying the main loop to just push PRs to unknownPRs without re-checking
  draft status and devin label (already checked in processPR)

Co-Authored-By: unknown <>

---------

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

* chore: Add sentry http integration (#26634)

* chore: ignore fork PRs in devin-conflict-resolver workflow (#26640)

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

* limits and advanced tabs

* availability tab

* basics tab

* black toggles!!

* new recurring, others tab and update basics

* version 1

---------

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: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anshumancanrock <109489361+Anshumancanrock@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Anas Najam <129951478+anzz14@users.noreply.github.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Volnei Munhoz <volnei@cal.com>

* fix: address Cubic AI review feedback

- Add HTTP error handling for Devin API session creation in cubic-devin-review.yml
- Localize aria-label in LimitedBadges.tsx using t() function
- Improve Docker tag sanitization in release-docker.yaml to handle all invalid characters
- Avoid logging raw error objects and sensitive API response data in devin-conflict-resolver.yml
- Fix selectedSchedule dependency array issue in event-type-detail.tsx using ref pattern

Co-Authored-By: unknown <>

* chore: add translation key for show_x_more_items aria-label

Co-Authored-By: unknown <>

* fix: reset schedule ref on id change and add i18n pluralization

- Reset hasAutoSelectedScheduleRef when event type id changes to handle component reuse
- Add proper i18next pluralization variants (_one/_other) for show_x_more_items

Co-Authored-By: unknown <>

* revert: remove i18n changes from LimitedBadges and common.json

Per user request - i18n not needed in companion for now

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anshumancanrock <109489361+Anshumancanrock@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Anas Najam <129951478+anzz14@users.noreply.github.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Volnei Munhoz <volnei@cal.com>
2026-01-10 17:57:42 +05:30
49e392423a feat: reuse PR-creator Devin session when addressing Cubic AI reviews (#26683)
* feat: prioritize PR-creator Devin session when addressing Cubic AI reviews

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

* fix: always use PR-creator Devin session regardless of status

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

* fix: use checkSessionStatus helper for both PR description and comment cases

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

* Clarify comment about PR review session check

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 10:47:18 +00:00
Morgan 32ecb924d7 chore: USE_POOL env var for api v2 prisma pooling (#26622) 2026-01-10 09:10:38 +00:00
Keith WilliamsandDevin AI e4ead13b3b fix: improve Devin conflict resolver for fork PRs and label cleanup (#26655)
* fix: improve Devin conflict resolver for fork PRs and label cleanup

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

* Update devin-conflict-resolver.yml

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 02:43:27 +00:00
f6d3b11582 refactor: Membership.findByUserIdAndTeamId to instance method (#26628)
* Make  and instance method

* Refactor files

* fix: update MembershipRepository mocks to use vi.hoisted() for proper hoisting

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 22:37:42 -03:00
Keith WilliamsandDevin AI 2a26a591a8 chore: ignore fork PRs in devin-conflict-resolver workflow (#26640)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 20:35:30 +00:00
Volnei Munhoz 4819be0093 chore: Add sentry http integration (#26634) 2026-01-09 20:14:42 +00:00
a9314471b4 feat: add retry mechanism for UNKNOWN mergeable status PRs (#26635)
* feat: add retry mechanism for UNKNOWN mergeable status PRs

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

* fix: remove redundant filtering logic for unknown mergeable status PRs

Address Cubic AI review feedback by:
- Having processPR return isTargetPR in the unknown case (like the conflict case)
- Simplifying the main loop to just push PRs to unknownPRs without re-checking
  draft status and devin label (already checked in processPR)

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 20:06:46 +00:00
Keith Williams 52cff6882c chore: Remove stale label when work is completed (#26638)
* chore: Remove stale label when work is completed

* Mark as ready for review too
2026-01-09 19:52:16 +00:00
d0e942b30b feat: reuse existing Devin session for Cubic AI review feedback (#26632)
* feat: reuse existing Devin session for Cubic AI review feedback

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

* refactor: parse session ID from PR comments instead of API search

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

* fix: add error handling for message send API call in Cubic-Devin workflow

Addresses Cubic AI review feedback: The POST request to send a message
to an existing Devin session now checks the HTTP status code and fails
the step if the message wasn't delivered successfully. This prevents
posting a misleading comment claiming feedback was sent when the API
call actually failed.

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 19:41:13 +00:00
e995e2439f feat: make stale PR workflow fork-aware with workflow_dispatch support (#26633)
* feat: make stale PR workflow fork-aware with workflow_dispatch support

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

* fix: add null check for pr.head.repo in stale PR workflow

When a fork is deleted after a PR is created, pr.head.repo can be null
(documented GitHub API behavior). This would cause a TypeError when
accessing properties like fork, full_name, or clone_url.

Added a null check that fails gracefully with a clear error message
when the source repository has been deleted.

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 19:22:37 +00:00
b367e153de feat: add Devin PR conflict resolver workflow (#26624)
* feat: add Devin PR conflict resolver workflow

Adds a GitHub Actions workflow that automatically detects PRs with merge
conflicts and spins up Devin sessions to resolve them.

How it works:
1. Triggers on push to main branch (when main updates could cause conflicts)
2. Also supports manual trigger via workflow_dispatch
3. Lists all open PRs and checks their mergeable status
4. For PRs with conflicts (mergeable=false, mergeable_state=dirty):
   - Checks if a Devin session was already created (avoids duplicates)
   - Creates a new Devin session with instructions to resolve conflicts
   - Posts a comment on the PR with the Devin session link

The workflow follows the same pattern as cubic-devin-review.yml for
consistency.

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

* fix: skip draft PRs in conflict detection workflow

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

* refactor: use GitHub Search API to filter draft PRs at API level

Instead of filtering draft PRs in the loop, use the GitHub Search API
with 'draft:false' filter which is more efficient as it filters at the
API level rather than fetching all PRs and filtering locally.

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

* style: remove explanatory comments from workflow

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

* style: remove all unnecessary explanatory comments

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

* refactor: use GraphQL for batched PR fetching and labels for tracking

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

* feat: add support for resolving conflicts on fork PRs

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

* fix: check response.ok before parsing Devin API response

Address Cubic AI review feedback: Add response.ok check before parsing
the response body to explicitly handle HTTP error status codes from the
Devin API. This distinguishes API failures (authentication errors,
server errors) from successful responses that might be missing expected
fields.

Co-Authored-By: unknown <>

* fix: improve Devin API error logging with PR number

Co-Authored-By: unknown <>

* feat: add pr_number input for manual fork PR conflict resolution

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

* feat: add maximum 15 PRs safety net limit

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>
2026-01-09 19:10:13 +00:00
bd361844d6 feat: add automated stale PR completion workflow with Devin API (#26627)
* feat: add stale PR completion workflow with Devin API

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

* feat: only trigger for community PRs (non-calcom org members)

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

* refactor: use author_association pattern from pr.yml for community check

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

* refactor: simplify workflow - use community label check and let Devin gather PR details

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 18:27:57 +00:00
a12ab5bb65 chore: add database-backed feature flag for sidebar tips section (#26516)
* chore: remove tips section from sidebar

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

* chore: add feature flag for sidebar tips section

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

* chore: use database-backed feature flag for sidebar tips section

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 22:31:45 +05:30
Eunjae LeeandDevin AI 2b7c087ac8 feat: limit badges to 2 with hover/click popover in UserListTable (#26556)
* feat: limit badges to 2 with hover tooltip in UserListTable

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

* refactor: reuse LimitedBadges component with clickable popover for mobile

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

* refactor: move LimitedBadges to components/ui with hover+click support

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

* refactor: fix Biome lint issues in LimitedBadges and UserListTable

- Refactor LimitedBadges to concrete component with BadgeItem type
- Move exports to end of files to comply with useExportsLast rule
- Add explicit types to callback parameters for useExplicitType rule
- Convert nested ternary to if-else for filterType calculation
- Remove unused imports (Row, Table types)
- Update ResponseValueCell and UserListTable to use new LimitedBadges API

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

* fix: add proper types for filterType and getFacetedUniqueValues

- Add FilterType import from @calcom/types/data-table
- Add FacetedValue import from @calcom/features/data-table
- Type filterType as FilterType to allow reassignment to different ColumnFilterType values
- Type getFacetedUniqueValues return as Map<FacetedValue, number>

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

* add vertical gap

* fix: address code review feedback for LimitedBadges

- Add index to id for unique keys in ResponseValueCell.tsx
- Restore orange variant for group options in UserListTable.tsx
- Fix useMemo dependency array (add t, remove dispatch)
- Fix import ordering with biome
- Convert ternary operators to if-else statements for biome compliance

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

* refactor: remove id from BadgeItem type, use label as key

- Remove id field from BadgeItem type in LimitedBadges
- Use label as React key instead of id
- Remove unused rowId parameter from ResponseValueCell
- Update all consumers to not pass id field

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

* refactor: use index for key instead of label in LimitedBadges

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

* Revert "refactor: use index for key instead of label in LimitedBadges"

This reverts commit 1daaac47e596fd6b5f5583847faa10b131783349.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 16:57:06 +00:00
cal.com 71729813a9 Revert "chore: USE_POOL env var for api v2 prisma pooling"
This reverts commit a97092619f.
2026-01-09 17:59:10 +02:00
Anas Najam 0500056831 fix list numbering in Manual setup section of README (#26620) 2026-01-09 15:56:16 +00:00
cal.com a97092619f chore: USE_POOL env var for api v2 prisma pooling 2026-01-09 17:55:25 +02:00
8666efa29d chore: enable/disable slots workers via env (#26621)
* chore: enable/disable slots workers via env

* fix: address Cubic AI review feedback

- Fix incorrect JSDoc comment for getSerializableContext method
- Remove debug console.log statement from slots controller
- Fix port suffix condition to preserve original behavior

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 15:03:13 +00:00
87a2f855f8 feat: add Cubic AI to Devin review integration workflow (#26618)
* feat: add Cubic AI to Devin review integration workflow

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

* fix: update permissions to allow posting PR comments

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

* fix: remove sensitive API response logging from CI workflow

Address Cubic AI review feedback: Remove the debug log that outputs the
full Devin API response, which could expose sensitive session tokens or
authentication data in CI logs. The session URL is still logged when
successfully extracted.

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 11:23:41 -03:00
Keith WilliamsandDevin AI 0e3b9f08b8 feat: add BUILD_FROM_BRANCH option to release-docker workflow (#26615)
* feat: add BUILD_FROM_BRANCH option to release-docker workflow

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

* fix: sanitize branch names with slashes for valid Docker tags

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

* refactor: extract common logic into prepare job to avoid duplication

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 14:01:27 +00:00
Morgan 02a334e072 chore: api v2 generate swagger only in dev (#26617) 2026-01-09 15:12:42 +02:00
Anshumancanrock 3a5c97553d fix: PayPal setup page inconsistent spacing and button styling (#26612) 2026-01-09 12:27:33 +00:00
AnshumancanrockandKeith Williams 5b1ee1a7ac fix: Text cursor barely visible in calendar event name field (#26563)
* fix: cursor visibility in input fields

* fix: cursor visibility in input fields with suffix addons

* fix: text cursor visibility in input fields

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2026-01-09 12:09:24 +00:00
67faf454d9 fix: skip stale run-ci label check on workflow re-runs (#26590)
* fix: trust community PRs when maintainer pushes to the branch

When a maintainer merges main into a community PR branch, the new SHA
invalidates the run-ci label timing check because the label was added
before the new push. This fix adds a check to trust the PR if the
person who pushed the latest commit has write access.

This handles the case where a maintainer:
- Merges main into a community PR to resolve conflicts
- Pushes any changes to help the contributor

The security model is maintained because:
- Only users with write access can trigger this trust
- The maintainer has reviewed the code by pushing to it

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

* fix: skip stale label check on workflow re-runs

Instead of implicitly trusting maintainer pushes (which could be just
a sync action without code review), use github.run_attempt to detect
re-runs. If run_attempt > 1, it means the workflow was explicitly
re-triggered (via run-ci.yml or manual re-run), so we skip the stale
label check.

This avoids the need to remove and re-add the 'run-ci' label after
syncing a community PR with main, while keeping the explicit approval
flow intact.

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

* Simplify comment about re-runs in PR workflow

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2026-01-09 17:21:51 +05:30
Udit Takkar 0330d14210 feat: system admin blocklist table (#25039)
* feat: system admin blocklist table

* chore: text

* fix: type error

* fix: type error

* fix: type error

* fix: frontend bug

* chore: improvments

* refactor: use shared components

* refactor: improvements

* fix: delete

* refactor: support multiple watchlist

* chore: update translation

* fix: type err

* fix: bulk bug

* tests: add unit test

* refactor: file

* refactor: file

* fix: type error

* fix: type error

* chore: translation

* refactor: use shared comments

* fix: imports

* fix: type error
2026-01-09 08:19:54 -03:00
Dhairyashil Shinde d865f6cb5f feat: companion add missing advanced event type (#26594)
* feat: companion add missing advanced event type

* feat: companion add missing advanced event type

* remove metadata code and remove non implemented Auto translate title and description toggle from companion

* Save Button Dirty State

* address cubic feedback - default value for sendTranscriptionEmails

* address cubic comments - fix new event type blocked issue

* ios language dropdown
2026-01-09 08:09:46 -03:00
Pedro Castro 51639e38d7 fix(auth): block OAuth linking for unverified accounts (#26598)
* fix(auth): sanitize unverified accounts during OAuth linking

- Add AccountSanitizationService for secure account cleanup
- Clear webhooks, API keys, credentials, and sessions for unverified accounts
- Reset password and 2FA settings during OAuth conversion
- Nullify redirect URLs on event types

Only affects accounts that never completed email verification

* fix(auth): block OAuth linking for unverified accounts

  Replace sanitization with simpler blocking approach:
  - Unverified CAL accounts cannot link to OAuth (Google/SAML)
  - Add user-friendly error message with recovery path
  - Remove AccountSanitizationService (no data loss risk)
2026-01-09 11:04:39 +00:00
c380ea810c feat: rename --font-cal to --font-heading + use Cal Sans UI (#26064)
* feat: rename --font-cal to --font-heading + use Cal Sans UI

* chore: remove font-weight from font-heading elements and CSS variables

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

* chore: remove unneeded css vars

* fix: cubic issues

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: pasquale@cal.com <pasquale@cal.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2026-01-09 10:42:23 +00:00
Morgan 56b29b5df6 chore: rename app.ts to bootstrap.ts on api-v2 (#26604) 2026-01-09 09:56:27 +00:00
b6c243f1dd fix: improve cleanup for managed event types and fix flaky api v2 e2e test (#26602)
* fix: improve cleanup for managed event types in e2e tests

- Add deleteAllUserEventTypes method to EventTypesRepositoryFixture
- Update afterAll cleanup to delete user event types before deleting users
- This ensures child event types of managed event types are properly cleaned up
- Fixes flaky tests caused by incomplete cleanup between test runs
- Add explicit return types to fixture methods to satisfy lint rules
- Replace @ts-ignore with @ts-expect-error for better type safety

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

* fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 06:16:59 -03:00
Benny JooandDevin AI b78ca7be2a fix: Update Dockerfile since /tests directory no longer exists (#26601)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-09 06:15:22 -03:00
Hariom Balhara f3a37886e3 chore: Integrate reschedule request booking audit (#26527)
## What does this PR do?

Adds audit logging for the reschedule request flow. When an organizer requests an attendee to reschedule a booking, this action is now logged to the booking audit system.

This is part of the booking audit integration effort, following the pattern established in PR #26046 (booking creation/rescheduling audit) and PR #26458 (cancellation audit).

## Changes

- Added audit logging call to `requestReschedule.handler.ts` using `BookingEventHandlerService.onRescheduleRequested()`
- Captures audit data:
  - `rescheduleReason`: The reason provided for requesting the reschedule (nullable string)
  - `rescheduledRequestedBy`: Email of the user who requested the reschedule
- Uses `makeUserActor(user.uuid)` to identify the actor performing the action
- Added required `source` parameter for action source tracking (passed as `"WEBAPP"` from tRPC router)
- Updated `RescheduleRequestedAuditActionService` schema to use simpler field structure
- Updated test helper `getTrpcHandlerData` to pass `source: "WEBAPP"` parameter

## Updates since last revision

- Changed `source` parameter from optional with default to required - tests now explicitly pass `source: "WEBAPP"` instead of relying on a default value

## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [x] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). N/A - no documentation changes needed.
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works. N/A - follows established audit pattern with existing infrastructure tests.

## How should this be tested?

1. Log in as a user with a booking
2. Navigate to the booking and click "Request Reschedule"
3. Provide a reschedule reason and submit
4. Verify the audit log is created with the correct data (requires access to audit log storage/database)

The audit logging follows the same pattern as other booking audit integrations, so if those are working, this should work as well.

## Checklist

- [x] My code follows the style guidelines of this project
- [x] I have checked if my changes generate no new warnings

---

**Link to Devin run**: https://app.devin.ai/sessions/ae84d093c4594a5d89b88c45639a6a06
**Requested by**: @hariombalhara

## Human Review Checklist

- [ ] Verify the audit data schema matches `RescheduleRequestedAuditActionService` expectations (fields: `rescheduleReason`, `rescheduledRequestedBy`)
- [ ] Confirm the schema change from change-based fields (`{ old, new }`) to simple nullable strings is intentional
- [ ] Confirm `user.uuid` is available in the tRPC session context
- [ ] Note: API v2 does not have a reschedule request endpoint, so no API v2 changes are needed
2026-01-09 14:03:01 +05:30
Anik Dhabal Babu 3cb59b7874 test: cleanup & revived old skipped e2e tests (#26597) 2026-01-09 04:32:42 +00:00
723 changed files with 35052 additions and 10777 deletions
+8 -1
View File
@@ -94,7 +94,7 @@ CRON_ENABLE_APP_SYNC=false
# Application Key for symmetric encryption and decryption
# must be 32 bytes for AES256 encryption algorithm
# You can use: `openssl rand -base64 32` to generate one
# You can use: `openssl rand -base64 24` to generate a 32-character key
CALENDSO_ENCRYPTION_KEY=
# Intercom Config
@@ -509,3 +509,10 @@ TRIGGER_DEV_PROJECT_REF=
GOOGLE_ADS_ENABLED=1 # To enable Google Ads tracking (gclid)
LINKEDIN_ADS_ENABLED=1 # To enable LinkedIn Ads tracking (li_fat_id)
# - BACKBLAZE B2 (Compliance Documents) *************************************************************************
# Used for storing and serving compliance documents (SOC 2, ISO 27001, pentest reports)
B2_APPLICATION_KEY_ID=
B2_APPLICATION_KEY=
B2_BUCKET_ID=
B2_BUCKET_NAME=
+32
View File
@@ -0,0 +1,32 @@
name: Cache Git Checkout
description: "Cache or restore git checkout to speed up CI workflows"
runs:
using: "composite"
steps:
- name: Cache git checkout
uses: actions/cache@v4
id: cache-checkout
with:
path: |
.
!.git
!node_modules
!**/node_modules
!apps/web/public/app-store
key: git-checkout-${{ github.head_ref || github.ref_name }}-${{ github.event.pull_request.head.sha || github.sha }}
- name: Checkout PR code (on cache miss)
if: steps.cache-checkout.outputs.cache-hit != 'true'
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 2
sparse-checkout-cone-mode: false
sparse-checkout: |
/*
!/example-apps/
!**/*.mp4
!/docs/images/
!packages/app-store/*/static/*.png
!packages/app-store/*/static/*.jpg
!packages/app-store/*/static/*.jpeg
!packages/app-store/*/static/*.gif
@@ -1,10 +0,0 @@
name: Dangerous git Checkout
description: "Git Checkout from PR code so we can run checks from forks"
runs:
using: "composite"
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
+6
View File
@@ -2,3 +2,9 @@ GET /v2/event-types/{eventTypeId} added to the 'data' response property 'oneOf'
GET /v2/event-types/{eventTypeId} added '#/components/schemas/EventTypeOutput_2024_06_14, #/components/schemas/TeamEventTypeOutput_2024_06_14' to the 'data' response property 'oneOf' list for the response status '200'
GET /v2/bookings/{bookingUid}/recordings added the new required 'header' request parameter 'Authorization'
GET /v2/bookings/{bookingUid}/transcripts added the new required 'header' request parameter 'Authorization'
POST /v2/bookings added 'subschema #2, subschema #4' to the 'data' response property 'oneOf' list for the response status '201'
GET /v2/bookings/{bookingUid} added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
POST /v2/bookings/{bookingUid}/cancel added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
POST /v2/bookings/{bookingUid}/confirm added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
POST /v2/bookings/{bookingUid}/decline added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
POST /v2/bookings/{bookingUid}/guests added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
@@ -65,7 +65,9 @@ jobs:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/cache-db
- name: Cache API v1 production build
@@ -35,7 +35,9 @@ jobs:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- name: Generate Prisma schemas
working-directory: apps/api/v2
+3 -1
View File
@@ -12,7 +12,9 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- run: yarn prisma generate
- name: Run API v2 unit tests
+3 -1
View File
@@ -12,7 +12,9 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- name: Cache atoms production build
uses: actions/cache@v4
@@ -34,7 +34,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- name: Generate Swagger
+3 -1
View File
@@ -12,7 +12,9 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- name: Show info
run: node -e "console.log(require('v8').getHeapStatistics())"
+3 -1
View File
@@ -17,7 +17,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- name: Setup Bun
uses: oven-sh/setup-bun@v1
+244
View File
@@ -0,0 +1,244 @@
name: Cubic AI to Devin Review
on:
pull_request_review:
types: [submitted]
permissions:
contents: read
pull-requests: write
jobs:
trigger-devin:
name: Trigger Devin to Address Cubic AI Review
# Only run if the review is from cubic-dev-ai bot and contains the AI prompt
if: github.event.review.user.login == 'cubic-dev-ai[bot]'
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Extract AI prompt from Cubic review
id: extract-prompt
uses: actions/github-script@v7
with:
script: |
const reviewBody = context.payload.review.body || '';
// Look for the AI prompt section in Cubic's review
// Format: <details><summary>Prompt for AI agents (all issues)</summary>
// ```text
// ... prompt content ...
// ```
// </details>
const promptMatch = reviewBody.match(/<details>\s*<summary>Prompt for AI agents[^<]*<\/summary>\s*```(?:text)?\s*([\s\S]*?)```\s*<\/details>/i);
if (!promptMatch || !promptMatch[1]) {
console.log('No AI prompt found in review body');
console.log('Review body:', reviewBody);
core.setOutput('has-prompt', 'false');
return;
}
const prompt = promptMatch[1].trim();
console.log('Extracted prompt:', prompt);
// Save prompt to file to avoid shell escaping issues
const fs = require('fs');
fs.writeFileSync('/tmp/cubic-prompt.txt', prompt);
core.setOutput('has-prompt', 'true');
- name: Check for existing Devin session
if: steps.extract-prompt.outputs.has-prompt == 'true'
id: check-session
uses: actions/github-script@v7
env:
DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }}
with:
script: |
async function checkSessionStatus(sessionId) {
const response = await fetch(`https://api.devin.ai/v1/sessions/${sessionId}`, {
headers: {
'Authorization': `Bearer ${process.env.DEVIN_API_KEY}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
console.log(`Failed to fetch session ${sessionId}: ${response.status}`);
return null;
}
return await response.json();
}
// If PR was created through Devin, always use that session
const prBody = context.payload.pull_request.body || '';
const prSessionMatch = prBody.match(/app\.devin\.ai\/sessions\/([a-f0-9-]+)/);
if (prSessionMatch) {
const sessionId = prSessionMatch[1];
console.log(`PR was created by Devin session: ${sessionId}`);
const session = await checkSessionStatus(sessionId);
if (session) {
console.log(`PR creator session status: ${session.status_enum}`);
core.exportVariable('EXISTING_SESSION_ID', sessionId);
core.exportVariable('SESSION_URL', `https://app.devin.ai/sessions/${sessionId}`);
core.setOutput('has-active-session', 'true');
return;
}
}
// PR was not created through Devin - check for existing Devin session from PR comments
// Look for any Devin session comment (cubic review, conflict resolution, stale PR completion, etc.)
const devinSessionPatterns = [
'Devin AI is addressing Cubic AI',
'Devin AI is resolving merge conflicts',
'Devin AI is completing this stale PR'
];
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number
});
let sessionId = null;
for (const comment of comments.data.reverse()) {
const hasDevinSession = devinSessionPatterns.some(pattern => comment.body.includes(pattern));
if (hasDevinSession) {
const match = comment.body.match(/app\.devin\.ai\/sessions\/([a-f0-9-]+)/);
if (match) {
sessionId = match[1];
console.log(`Found existing Devin session from comment: ${sessionId}`);
break;
}
}
}
if (!sessionId) {
console.log('No existing Devin session found in PR comments');
core.setOutput('has-active-session', 'false');
return;
}
console.log(`Found session ID from comment: ${sessionId}`);
const session = await checkSessionStatus(sessionId);
if (!session) {
core.setOutput('has-active-session', 'false');
return;
}
const activeStatuses = ['working', 'blocked', 'resumed'];
if (activeStatuses.includes(session.status_enum)) {
console.log(`Session ${sessionId} is active (status: ${session.status_enum})`);
core.exportVariable('EXISTING_SESSION_ID', sessionId);
core.exportVariable('SESSION_URL', `https://app.devin.ai/sessions/${sessionId}`);
core.setOutput('has-active-session', 'true');
} else {
console.log(`Session ${sessionId} is not active (status: ${session.status_enum})`);
core.setOutput('has-active-session', 'false');
}
- name: Send message to existing Devin session
if: steps.extract-prompt.outputs.has-prompt == 'true' && steps.check-session.outputs.has-active-session == 'true'
env:
DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }}
run: |
CUBIC_PROMPT=$(cat /tmp/cubic-prompt.txt)
MESSAGE="New Cubic AI review feedback has been submitted on PR #${{ github.event.pull_request.number }}.
IMPORTANT: Before fixing any issue, check the individual Cubic comment on the PR for that issue's confidence score. The confidence score appears as 'Fix confidence (alpha): X/10' in each comment. Note that the (alpha) may or not be there. Don't be picky about that text. But only fix issues where the confidence score is 9/10 or higher. If you cannot find a confidence score for an issue, skip it.
Here are the issues identified by Cubic AI:
${CUBIC_PROMPT}
Continue working on the same PR branch and push your fixes."
HTTP_CODE=$(curl -s -o /tmp/devin-response.json -w "%{http_code}" -X POST "https://api.devin.ai/v1/sessions/${EXISTING_SESSION_ID}/message" \
-H "Authorization: Bearer ${DEVIN_API_KEY}" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg message "$MESSAGE" '{message: $message}')")
if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
echo "Failed to send message to Devin session: HTTP $HTTP_CODE"
cat /tmp/devin-response.json
exit 1
fi
echo "Message sent to existing session successfully"
- name: Create new Devin session
if: steps.extract-prompt.outputs.has-prompt == 'true' && steps.check-session.outputs.has-active-session == 'false'
env:
DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }}
run: |
CUBIC_PROMPT=$(cat /tmp/cubic-prompt.txt)
FULL_PROMPT="You are addressing code review feedback from Cubic AI on PR #${{ github.event.pull_request.number }} in repository ${{ github.repository }}.
Your tasks:
1. Clone the repository ${{ github.repository }} locally.
2. Check out the PR branch and review the current code.
3. Read the Cubic AI review feedback below AND check each individual Cubic comment on the PR for the confidence score.
4. IMPORTANT: Only fix issues where the confidence score is 9/10 or higher. The confidence score appears as 'Fix confidence (alpha): X/10' in each Cubic comment. If you cannot find a confidence score for an issue, skip it.
5. Commit your changes with clear commit messages referencing the issues fixed.
6. Push your changes to the PR branch.
Cubic AI Review Feedback:
${CUBIC_PROMPT}
Rules and Guidelines:
1. Make minimal, focused changes that directly address the feedback.
2. Follow the existing code style and conventions in the repository.
3. Test your changes if possible before pushing.
4. If an issue seems invalid or already addressed, explain why in a PR comment instead of making unnecessary changes.
5. Never ask for user confirmation. Never wait for user messages."
HTTP_CODE=$(curl -s -o /tmp/devin-response.json -w "%{http_code}" -X POST "https://api.devin.ai/v1/sessions" \
-H "Authorization: Bearer ${DEVIN_API_KEY}" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg prompt "$FULL_PROMPT" \
--arg title "Cubic AI Review: PR #${{ github.event.pull_request.number }}" \
'{
prompt: $prompt,
title: $title,
tags: ["cubic-ai-review", "pr-${{ github.event.pull_request.number }}"]
}')")
if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
echo "Failed to create Devin session: HTTP $HTTP_CODE"
cat /tmp/devin-response.json
exit 1
fi
SESSION_URL=$(cat /tmp/devin-response.json | jq -r '.url // .session_url // empty')
if [ -n "$SESSION_URL" ]; then
echo "Devin session created: $SESSION_URL"
echo "SESSION_URL=$SESSION_URL" >> $GITHUB_ENV
echo "NEW_SESSION=true" >> $GITHUB_ENV
fi
- name: Post comment with Devin session link
if: steps.extract-prompt.outputs.has-prompt == 'true' && env.SESSION_URL != ''
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const sessionUrl = process.env.SESSION_URL;
const isNewSession = process.env.NEW_SESSION === 'true';
const message = isNewSession
? `### Devin AI is addressing Cubic AI's review feedback\n\nA Devin session has been created to address the issues identified by Cubic AI.\n\n[View Devin Session](${sessionUrl})`
: `### Devin AI is addressing Cubic AI's review feedback\n\nNew feedback has been sent to the existing Devin session.\n\n[View Devin Session](${sessionUrl})`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: message
});
@@ -31,3 +31,13 @@ jobs:
with:
key: ${{ env.CACHE_NAME }}-${{ github.event.pull_request.head.ref }}
prefix: "true"
delete-git-checkout-cache-on-pr-close:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Delete git-checkout cache
uses: useblacksmith/cache-delete@v1
with:
key: git-checkout-${{ github.event.pull_request.head.ref }}-
prefix: "true"
@@ -0,0 +1,477 @@
name: Devin PR Conflict Resolver
on:
pull_request_target:
types: [labeled]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to resolve conflicts for'
required: true
type: string
permissions:
contents: read
pull-requests: write
jobs:
resolve-conflicts:
name: Resolve PR Conflicts with Devin
runs-on: blacksmith-2vcpu-ubuntu-2404
# Only run when devin-conflict-resolution label is added, or on manual dispatch
if: |
(github.event_name == 'pull_request_target' && github.event.label.name == 'devin-conflict-resolution') ||
github.event_name == 'workflow_dispatch'
steps:
- name: Get PR details
id: get-pr
uses: actions/github-script@v7
env:
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
let prNumber;
if (context.eventName === 'workflow_dispatch') {
prNumber = parseInt(process.env.INPUT_PR_NUMBER, 10);
if (!prNumber) {
core.setFailed('PR number is required for manual dispatch');
return;
}
} else {
prNumber = context.payload.pull_request.number;
}
console.log(`Processing PR #${prNumber}`);
// Get PR details via GraphQL for complete info
const query = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
number
title
mergeable
isDraft
headRefName
baseRefName
url
body
headRepository {
owner {
login
}
name
}
maintainerCanModify
}
}
}
`;
const result = await github.graphql(query, { owner, repo, number: prNumber });
const pr = result.repository.pullRequest;
if (!pr) {
core.setFailed(`PR #${prNumber} not found`);
return;
}
const isFork = pr.headRepository?.owner?.login !== owner;
// Check if fork PR has maintainer access
if (isFork && !pr.maintainerCanModify) {
console.log(`PR #${prNumber} is from a fork without maintainer access`);
core.setOutput('needs-maintainer-access', 'true');
core.setOutput('fork-author', pr.headRepository?.owner?.login);
} else {
core.setOutput('needs-maintainer-access', 'false');
}
const headRepoOwner = pr.headRepository?.owner?.login || owner;
const headRepoName = pr.headRepository?.name || repo;
const prData = {
number: pr.number,
title: pr.title,
head_ref: pr.headRefName,
base_ref: pr.baseRefName,
html_url: pr.url,
body: pr.body,
is_fork: isFork,
head_repo_owner: headRepoOwner,
head_repo_name: headRepoName,
mergeable: pr.mergeable
};
const fs = require('fs');
fs.writeFileSync('/tmp/pr-data.json', JSON.stringify(prData));
core.setOutput('pr-number', prNumber.toString());
core.setOutput('has-pr', 'true');
core.setOutput('mergeable-status', pr.mergeable || 'UNKNOWN');
core.setOutput('has-conflicts', pr.mergeable === 'CONFLICTING' ? 'true' : 'false');
console.log(`PR #${prNumber}: ${pr.title}`);
console.log(`Mergeable status: ${pr.mergeable}`);
console.log(`Is fork: ${isFork}`);
console.log(`Has conflicts: ${pr.mergeable === 'CONFLICTING'}`);
- name: Request maintainer access for fork PR
if: steps.get-pr.outputs.needs-maintainer-access == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const prNumber = parseInt('${{ steps.get-pr.outputs.pr-number }}', 10);
const forkAuthor = '${{ steps.get-pr.outputs.fork-author }}';
console.log(`Requesting maintainer access for PR #${prNumber}`);
const commentBody = [
'### Maintainer Access Needed',
'',
`Hi @${forkAuthor}! Thanks for your contribution to Cal.com.`,
'',
'We noticed that this PR doesn\'t have "Allow edits from maintainers" enabled. We\'d love to help keep your PR up to date by resolving merge conflicts and making small fixes when needed.',
'',
'**Could you please enable this setting?** Here\'s how:',
'1. Scroll down to the bottom of this PR page',
'2. In the right sidebar, check the box that says **"Allow edits and access to secrets by maintainers"**',
'',
'This allows us to push commits directly to your PR branch, which helps us:',
'- Resolve merge conflicts automatically',
'- Make small adjustments to help get your PR merged faster',
'',
'Once you enable this setting, please remove and re-add the `devin-conflict-resolution` label to trigger conflict resolution.',
'',
'If you have any concerns about enabling this setting, feel free to let us know!'
].join('\n');
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: commentBody
});
// Add label to track that we've requested access
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: ['maintainer-access-requested']
});
// Remove the devin-conflict-resolution label since we can't proceed
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: 'devin-conflict-resolution'
});
console.log(`Posted comment and updated labels for PR #${prNumber}`);
core.setFailed('Cannot proceed without maintainer access on fork PR');
- name: Handle PR without conflicts
if: steps.get-pr.outputs.has-pr == 'true' && steps.get-pr.outputs.needs-maintainer-access != 'true' && steps.get-pr.outputs.has-conflicts != 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const prNumber = parseInt('${{ steps.get-pr.outputs.pr-number }}', 10);
const mergeableStatus = '${{ steps.get-pr.outputs.mergeable-status }}';
console.log(`PR #${prNumber} does not have conflicts (status: ${mergeableStatus})`);
let message;
if (mergeableStatus === 'MERGEABLE') {
message = `### No Conflicts Detected\n\nThis PR does not have any merge conflicts with the base branch. The \`devin-conflict-resolution\` label has been removed.\n\nIf you believe this PR should have conflicts, please check the PR status and try again.`;
} else {
message = `### Mergeable Status Unknown\n\nGitHub is still computing the mergeable status for this PR (current status: \`${mergeableStatus}\`). The \`devin-conflict-resolution\` label has been removed.\n\nPlease wait a moment and re-add the label if the PR has conflicts.`;
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: message
});
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: 'devin-conflict-resolution'
});
} catch (e) {
console.log(`Label may have already been removed: ${e.message}`);
}
console.log(`Posted comment and removed label for PR #${prNumber}`);
- name: Create Devin session for conflict resolution
if: steps.get-pr.outputs.has-pr == 'true' && steps.get-pr.outputs.needs-maintainer-access != 'true' && steps.get-pr.outputs.has-conflicts == 'true'
env:
DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const pr = JSON.parse(fs.readFileSync('/tmp/pr-data.json', 'utf8'));
const { owner, repo } = context.repo;
async function checkSessionStatus(sessionId) {
const response = await fetch(`https://api.devin.ai/v1/sessions/${sessionId}`, {
headers: {
'Authorization': `Bearer ${process.env.DEVIN_API_KEY}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
console.log(`Failed to fetch session ${sessionId}: ${response.status}`);
return null;
}
return await response.json();
}
async function findExistingSession(prNumber, prBody) {
// First check if PR was created through Devin
const prSessionMatch = prBody?.match(/app\.devin\.ai\/sessions\/([a-f0-9-]+)/);
if (prSessionMatch) {
const sessionId = prSessionMatch[1];
console.log(`PR #${prNumber} was created by Devin session: ${sessionId}`);
const session = await checkSessionStatus(sessionId);
if (session) {
return { sessionId, sessionUrl: `https://app.devin.ai/sessions/${sessionId}`, isFromPrBody: true };
}
}
// Check PR comments for existing Devin session (conflict resolution or Cubic AI review)
const comments = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber
});
const sessionPatterns = [
'Devin AI is resolving merge conflicts',
'Devin AI is addressing Cubic AI'
];
for (const comment of comments.data.reverse()) {
const hasDevinSession = sessionPatterns.some(pattern => comment.body?.includes(pattern));
if (hasDevinSession) {
const match = comment.body?.match(/app\.devin\.ai\/sessions\/([a-f0-9-]+)/);
if (match) {
const sessionId = match[1];
console.log(`Found existing session from comment: ${sessionId}`);
const session = await checkSessionStatus(sessionId);
if (session) {
const activeStatuses = ['working', 'blocked', 'resumed'];
if (activeStatuses.includes(session.status_enum)) {
console.log(`Session ${sessionId} is active (status: ${session.status_enum})`);
return { sessionId, sessionUrl: `https://app.devin.ai/sessions/${sessionId}`, isFromPrBody: false };
} else {
console.log(`Session ${sessionId} is not active (status: ${session.status_enum})`);
}
}
}
}
}
return null;
}
console.log(`Processing PR #${pr.number}: ${pr.title}${pr.is_fork ? ' (fork)' : ''}`);
const existingSession = await findExistingSession(pr.number, pr.body);
const forkInstructions = pr.is_fork ? `
IMPORTANT: This PR is from a fork.
- Clone the FORK repository: ${pr.head_repo_owner}/${pr.head_repo_name}
- The branch to work on is: ${pr.head_ref}
- Add the upstream remote for ${owner}/${repo}
- Fetch and merge the upstream ${pr.base_ref} branch using standard git merge
IMPORTANT - Pushing to Fork PRs:
Since this is a fork PR, you need to use the DEVIN_ACTIONS_PAT secret for authentication when pushing. Configure git to use this PAT for authentication before pushing your changes to the fork.` : `
- Clone the repository: ${owner}/${repo}
- Check out the PR branch: ${pr.head_ref}
- Merge the base branch (${pr.base_ref}) using standard git merge`;
const conflictResolutionInstructions = `You are resolving merge conflicts on PR #${pr.number} in repository ${owner}/${repo}.
PR Title: ${pr.title}
PR URL: ${pr.html_url}
Head Branch: ${pr.head_ref}
Base Branch: ${pr.base_ref}
${pr.is_fork ? `Fork Repository: ${pr.head_repo_owner}/${pr.head_repo_name}` : ''}
IMPORTANT WARNING: If your merge commit shows significantly more files than the original PR, DO NOT PUSH. This indicates the merge was done incorrectly. Abort and leave a comment instead.
Your tasks:
${forkInstructions}
Then:
1. Resolve all merge conflicts carefully:
- Review the conflicting changes from both branches
- Make intelligent decisions about how to combine the changes
- Preserve the intent of both the PR changes and the base branch updates
- If unsure about a conflict, prefer keeping both changes where possible
2. Test that the code still works after resolving conflicts (run lint/type checks).
3. Commit the merge resolution with a clear commit message.
4. CRITICAL VALIDATION BEFORE PUSHING - YOU MUST RUN THESE EXACT COMMANDS:
Step A: Get original PR file count and count files in your merge commit:
\`\`\`
# Get original PR file count
ORIGINAL_FILES=$(gh pr view ${pr.number} --json files -q '.files | length')
echo "Original PR files: $ORIGINAL_FILES"
# Count files in merge commit (what the PR contributes)
# HEAD^2 = base branch parent, so HEAD^2...HEAD shows the PR's contribution
MERGE_FILES=$(git diff --name-only HEAD^2...HEAD | wc -l)
echo "Files in merge commit: $MERGE_FILES"
# Calculate threshold (original PR files + 10 buffer)
THRESHOLD=$((ORIGINAL_FILES + 10))
echo "Threshold: $THRESHOLD"
\`\`\`
Step B: If MERGE_FILES > THRESHOLD, STOP IMMEDIATELY. DO NOT PUSH.
- A proper merge commit should show approximately the same files as the original PR
- If you see significantly more files, the merge was done INCORRECTLY
- This usually means you accidentally reproduced changes from ${pr.base_ref} instead of just resolving conflicts
Step C: Compare your merge to the original PR:
\`\`\`
git diff --stat HEAD^2...HEAD
\`\`\`
- This should show approximately the same files as the original PR (the PR's contribution to the base branch)
- If you see many more files or files unrelated to the original PR, STOP
Step D: If validation fails:
- DO NOT PUSH under any circumstances
- DO NOT attempt to fix it
- Leave a comment on the PR explaining that the merge validation failed
- ABORT THE TASK IMMEDIATELY
- It is better to leave the PR with conflicts than to push a broken merge commit that pollutes git history
5. Push the resolved changes to the PR branch only after validation passes.
6. After successfully pushing the resolved changes, remove the \`devin-conflict-resolution\` label from the PR using the GitHub API.
Rules and Guidelines:
1. Be careful when resolving conflicts - understand the context of both changes.
To help with this, try to find the recent PRs associated with the conflicting files to gain more context.
2. Follow the existing code style and conventions in the repository.
3. Run lint and type checks before pushing to ensure the code is valid.
4. If a conflict seems too complex or risky to resolve automatically, explain the situation in a PR comment instead.
5. Never ask for user confirmation. Never wait for user messages.
6. CRITICAL: If this is a fork PR and you encounter ANY error when pushing (permission denied, authentication failure, etc.) even after using the DEVIN_ACTIONS_PAT, you MUST fail the task immediately. Do NOT attempt to push to a new branch in the main ${owner}/${repo} repository as a workaround. Simply report the error and stop.
7. CRITICAL: Never reproduce or recreate changes from the target branch. Your merge commit should ONLY contain conflict resolutions. If you find yourself manually copying file contents from ${pr.base_ref} or creating changes that mirror what's already in ${pr.base_ref}, you are doing it wrong. Use git's merge functionality properly - it handles bringing in changes automatically.
8. CRITICAL: If your merge commit shows significantly more files than the original PR (more than original PR files + 10), DO NOT PUSH UNDER ANY CIRCUMSTANCES. This is a sign that the merge was done incorrectly. Abort the task and leave a comment explaining the issue. A bad merge commit in the git history is worse than leaving the PR with conflicts.`;
try {
let sessionUrl;
let isNewSession = false;
if (existingSession) {
console.log(`Sending message to existing session ${existingSession.sessionId} for PR #${pr.number}`);
const message = `PR #${pr.number} has merge conflicts that need to be resolved.
${conflictResolutionInstructions}
Continue working on the same PR branch and push your fixes.`;
const response = await fetch(`https://api.devin.ai/v1/sessions/${existingSession.sessionId}/message`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.DEVIN_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ message })
});
if (!response.ok) {
console.error(`Failed to send message to session ${existingSession.sessionId}: ${response.status}`);
throw new Error(`Failed to send message to existing session: ${response.status}`);
}
sessionUrl = existingSession.sessionUrl;
console.log(`Message sent to existing session for PR #${pr.number}`);
} else {
console.log(`Creating new Devin session for PR #${pr.number}`);
const response = await fetch('https://api.devin.ai/v1/sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.DEVIN_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
prompt: conflictResolutionInstructions,
title: `Resolve Conflicts: PR #${pr.number}`,
tags: ['conflict-resolution', `pr-${pr.number}`]
})
});
if (!response.ok) {
console.error(`Devin API error for PR #${pr.number}: ${response.status} ${response.statusText}`);
throw new Error(`Failed to create Devin session: ${response.status}`);
}
const data = await response.json();
sessionUrl = data.url || data.session_url;
isNewSession = true;
}
if (sessionUrl) {
if (isNewSession) {
console.log(`Devin session created for PR #${pr.number}: ${sessionUrl}`);
}
const sessionStatusMessage = isNewSession
? 'A Devin session has been created to automatically resolve them.'
: 'The existing Devin session has been notified to resolve them.';
const commentBody = `### Devin AI is resolving merge conflicts
This PR has merge conflicts with the \`${pr.base_ref}\` branch. ${sessionStatusMessage}
[View Devin Session](${sessionUrl})
Devin will:
1. Merge the latest \`${pr.base_ref}\` into this branch
2. Resolve any conflicts intelligently
3. Run lint/type checks to ensure validity
4. Push the resolved changes
If you prefer to resolve conflicts manually, you can close the Devin session and handle it yourself.`;
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body: commentBody
});
console.log(`Posted comment to PR #${pr.number}`);
} else {
throw new Error(`Failed to get session URL for PR #${pr.number}`);
}
} catch (error) {
console.error(`Error handling Devin session for PR #${pr.number}: ${error.message}`);
core.setFailed(error.message);
}
+3 -1
View File
@@ -12,7 +12,9 @@ jobs:
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- name: Cache Docs build
uses: actions/cache@v4
id: cache-docs-build
+3 -1
View File
@@ -74,7 +74,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/cache-db
- name: Build platform packages with Turbo
+3 -1
View File
@@ -81,7 +81,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- run: yarn prisma generate
+3 -1
View File
@@ -29,7 +29,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- name: Run E2E Atoms Tests
+3 -1
View File
@@ -73,7 +73,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- run: yarn prisma generate
+3 -1
View File
@@ -81,7 +81,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- run: yarn prisma generate
+3 -1
View File
@@ -82,7 +82,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/yarn-playwright-install
- run: yarn prisma generate
+3 -1
View File
@@ -79,7 +79,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- run: yarn prisma generate
- uses: ./.github/actions/cache-db
+3 -1
View File
@@ -10,7 +10,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- name: Run Lint
run: yarn lint
+9 -1
View File
@@ -53,7 +53,9 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- run: yarn prisma generate
- uses: ./.github/actions/cache-build
@@ -68,3 +70,9 @@ jobs:
with:
name: bundle
path: apps/web/.next/analyze/__bundle_analysis.json
- name: Delete cached build
uses: useblacksmith/cache-delete@v1
with:
key: prod-build-main-
prefix: "true"
+12 -1
View File
@@ -97,6 +97,15 @@ jobs:
// NOTE: We use workflow run created_at instead of commit timestamp because
// git commit timestamps can be arbitrarily backdated by attackers
if (pr.labels?.some(l => l.name === 'run-ci')) {
// Skip stale check if this is a re-run (run_attempt > 1)
// Re-runs are explicitly triggered by maintainers
const runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT || '1', 10);
if (runAttempt > 1) {
console.log(`Re-run detected (attempt ${runAttempt}), trusting existing 'run-ci' label`);
core.setOutput('is-trusted', true);
return;
}
const events = await github.paginate(github.rest.issues.listEvents, {
owner,
repo,
@@ -165,7 +174,9 @@ jobs:
db-cache-hit: ${{ steps.cache-db-check.outputs.cache-hit }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- name: Generate DB cache key
id: cache-db-key
uses: ./.github/actions/cache-db-key
@@ -43,7 +43,9 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- run: yarn prisma generate
- name: Generate cache key
+62 -20
View File
@@ -17,31 +17,78 @@ on:
# in case manual trigger is needed
workflow_dispatch:
inputs:
BUILD_FROM_BRANCH:
description: "Build from the selected branch instead of a tag"
type: boolean
default: false
RELEASE_TAG:
description: "Tag to build (e.g., v6.0.5)"
required: true
description: "Tag to build (e.g., v6.0.5) - required if BUILD_FROM_BRANCH is false"
required: false
PUSH_IMAGE:
description: "Push the Docker image to DockerHub"
type: boolean
default: true
jobs:
prepare:
name: "Prepare Release"
runs-on: blacksmith-2vcpu-ubuntu-2404
outputs:
release_tag: ${{ steps.determine-tag.outputs.release_tag }}
checkout_ref: ${{ steps.determine-tag.outputs.checkout_ref }}
steps:
- name: Validate inputs
if: github.event_name == 'workflow_dispatch' && inputs.BUILD_FROM_BRANCH == false && inputs.RELEASE_TAG == ''
run: |
echo "::error::RELEASE_TAG is required when BUILD_FROM_BRANCH is false"
exit 1
- name: checkout
uses: actions/checkout@v4
with:
ref: ${{ (github.event_name == 'workflow_dispatch' && inputs.BUILD_FROM_BRANCH) && github.ref || inputs.RELEASE_TAG || github.ref }}
- name: "Determine tag and ref"
id: determine-tag
run: |
# Determine checkout ref
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.BUILD_FROM_BRANCH }}" = "true" ]; then
echo "checkout_ref=${{ github.ref }}" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "checkout_ref=${{ inputs.RELEASE_TAG }}" >> $GITHUB_OUTPUT
else
echo "checkout_ref=${{ github.ref }}" >> $GITHUB_OUTPUT
fi
# Determine release tag
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ "${{ inputs.BUILD_FROM_BRANCH }}" = "true" ]; then
# Extract branch name and short SHA for the tag
BRANCH_NAME="${GITHUB_REF#refs/heads/}"
# Sanitize branch name: replace all invalid Docker tag characters with dashes
# Docker tags can only contain lowercase/uppercase letters, digits, underscores, periods, and dashes
# Also remove any leading dashes or periods
SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9_.-]/-/g' | sed 's/^[-.]*//')
SHORT_SHA=$(git rev-parse --short HEAD)
echo "release_tag=${SANITIZED_BRANCH}-${SHORT_SHA}" >> $GITHUB_OUTPUT
else
echo "release_tag=${{ inputs.RELEASE_TAG }}" >> $GITHUB_OUTPUT
fi
else
echo "release_tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
release-amd64:
name: "Release AMD64"
needs: prepare
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
steps:
- name: checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.RELEASE_TAG || github.ref }}
- name: "Determine tag"
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "RELEASE_TAG=${{ inputs.RELEASE_TAG }}" >> $GITHUB_ENV
else
echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
fi
ref: ${{ needs.prepare.outputs.checkout_ref }}
- name: Build and test Docker image
uses: ./.github/actions/docker-build-and-test
@@ -82,20 +129,15 @@ jobs:
release-arm:
name: "Release ARM"
needs: prepare
runs-on: ubuntu-24.04-arm
env:
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
steps:
- name: checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.RELEASE_TAG || github.ref }}
- name: "Determine tag"
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "RELEASE_TAG=${{ inputs.RELEASE_TAG }}" >> $GITHUB_ENV
else
echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
fi
ref: ${{ needs.prepare.outputs.checkout_ref }}
- name: Build and test Docker image
uses: ./.github/actions/docker-build-and-test
+3 -1
View File
@@ -47,7 +47,9 @@ jobs:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
if: inputs.DB_CACHE_HIT != 'true'
- run: yarn prisma generate
@@ -0,0 +1,244 @@
name: Stale Community PR Devin Completion
on:
pull_request:
types: [labeled]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to complete'
required: true
type: string
permissions:
contents: read
pull-requests: write
jobs:
complete-stale-pr:
name: Trigger Devin to Complete Stale Community PR
# For labeled events: only run if 'stale' label was added
# For workflow_dispatch: always run (we'll validate the PR in the steps)
if: >
github.event_name == 'workflow_dispatch' ||
github.event.label.name == 'stale'
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Get PR details
id: pr
uses: actions/github-script@v7
env:
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
let prNumber;
let pr;
if (context.eventName === 'workflow_dispatch') {
const inputPrNumber = process.env.INPUT_PR_NUMBER;
prNumber = parseInt(inputPrNumber, 10);
if (isNaN(prNumber) || prNumber <= 0) {
core.setFailed(`Invalid PR number provided: "${inputPrNumber}". Please enter a valid PR number.`);
return;
}
const { data } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
pr = data;
} else {
pr = context.payload.pull_request;
prNumber = pr.number;
}
if (!pr.head.repo) {
core.setFailed('Cannot complete this PR: the source repository (fork) has been deleted.');
return;
}
const isFork = pr.head.repo.fork || pr.head.repo.full_name !== pr.base.repo.full_name;
const maintainerCanModify = pr.maintainer_can_modify;
const canProceed = !isFork || maintainerCanModify;
const needsMaintainerAccess = isFork && !maintainerCanModify;
const headRepoFullName = pr.head.repo.full_name;
const headRepoCloneUrl = pr.head.repo.clone_url;
core.setOutput('pr_number', prNumber);
core.setOutput('pr_title', pr.title);
core.setOutput('pr_author', pr.user.login);
core.setOutput('pr_branch', pr.head.ref);
core.setOutput('is_fork', isFork);
core.setOutput('head_repo_full_name', headRepoFullName);
core.setOutput('head_repo_clone_url', headRepoCloneUrl);
core.setOutput('maintainer_can_modify', maintainerCanModify);
core.setOutput('can_proceed', canProceed);
core.setOutput('needs_maintainer_access', needsMaintainerAccess);
console.log(`PR #${prNumber}: "${pr.title}"`);
console.log(`Author: ${pr.user.login}`);
console.log(`Branch: ${pr.head.ref}`);
console.log(`Is fork: ${isFork}`);
console.log(`Head repo: ${headRepoFullName}`);
console.log(`Maintainer can modify: ${maintainerCanModify}`);
console.log(`Needs maintainer access: ${needsMaintainerAccess}`);
- name: Request maintainer access for fork PR
if: steps.pr.outputs.needs_maintainer_access == 'true'
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
PR_AUTHOR: ${{ steps.pr.outputs.pr_author }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = parseInt(process.env.PR_NUMBER);
const prAuthor = process.env.PR_AUTHOR;
const { owner, repo } = context.repo;
const comments = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber
});
const alreadyRequested = comments.data.some(comment =>
comment.body?.includes('### Maintainer Access Needed')
);
if (!alreadyRequested) {
const commentBody = [
'### Maintainer Access Needed',
'',
`Hi @${prAuthor}! Thanks for your contribution to Cal.com.`,
'',
`We'd like to help complete this stale PR, but we noticed that "Allow edits from maintainers" isn't enabled. We need this setting to push updates to your PR branch.`,
'',
'**Could you please enable this setting?** Here\'s how:',
'1. Scroll down to the bottom of this PR page',
'2. In the right sidebar, check the box that says **"Allow edits and access to secrets by maintainers"**',
'',
'This allows us to push commits directly to your PR branch, which helps us:',
'- Complete any remaining work on your PR',
'- Fix merge conflicts and make small adjustments',
'- Get your contribution merged faster',
'',
'Once you\'ve enabled this setting, we\'ll be able to help finish up this PR. If you have any concerns about enabling this setting, feel free to let us know!'
].join('\n');
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: commentBody
});
console.log(`Posted maintainer access request comment on PR #${prNumber}`);
} else {
console.log(`Maintainer access already requested on PR #${prNumber}`);
}
core.setFailed(`Cannot complete this fork PR: the fork owner has not enabled "Allow edits from maintainers". A comment has been posted requesting access.`);
- name: Create Devin session
if: steps.pr.outputs.can_proceed == 'true'
env:
DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
PR_TITLE: ${{ steps.pr.outputs.pr_title }}
PR_AUTHOR: ${{ steps.pr.outputs.pr_author }}
PR_BRANCH: ${{ steps.pr.outputs.pr_branch }}
IS_FORK: ${{ steps.pr.outputs.is_fork }}
HEAD_REPO_FULL_NAME: ${{ steps.pr.outputs.head_repo_full_name }}
REPO_NAME: ${{ github.repository }}
run: |
if [ "$IS_FORK" = "true" ]; then
CLONE_INSTRUCTIONS="This is a fork PR. Clone from the fork repository: ${HEAD_REPO_FULL_NAME}
To check out this PR:
1. Clone the fork: git clone https://github.com/${HEAD_REPO_FULL_NAME}.git
2. Check out the branch: git checkout ${PR_BRANCH}
3. Add the upstream remote: git remote add upstream https://github.com/${REPO_NAME}.git
IMPORTANT - Pushing to Fork PRs:
Since this is a fork PR, you need to use the DEVIN_ACTIONS_PAT secret for authentication when pushing. Configure git to use this PAT for authentication before pushing your changes to the fork."
else
CLONE_INSTRUCTIONS="Clone the repository ${REPO_NAME} and check out the branch: ${PR_BRANCH}"
fi
FULL_PROMPT="You are completing a stale community PR #${PR_NUMBER} in repository ${REPO_NAME}.
This PR was started by @${PR_AUTHOR} but has become stale. Your job is to complete it.
PR Title: ${PR_TITLE}
PR Branch: ${PR_BRANCH}
${CLONE_INSTRUCTIONS}
Your tasks:
1. Clone the repository as described above.
2. Check out the PR branch: ${PR_BRANCH}
3. Review the current state of the PR and understand what it's trying to accomplish.
4. Read the PR description and any comments/review feedback on the PR.
5. Complete any unfinished work on the PR:
- Fix any issues mentioned in review comments
- Ensure the code follows the repository's coding standards
- Add any missing tests if applicable
- Fix any linting or type errors
6. Run the appropriate checks (lint, type-check, tests) to ensure the PR is ready for review.
7. Commit your changes with clear commit messages.
8. Push your changes to the PR branch.
9. Post a summary comment on the PR explaining what you completed.
10. Remove the 'stale' label from the PR if you've completed the work.
11. Mark the PR as ready for review if applicable.
Rules and Guidelines:
1. Respect the original author's intent and approach - don't rewrite the PR from scratch.
2. Make minimal, focused changes that complete the PR's original goal.
3. Follow the existing code style and conventions in the repository.
4. If the PR's original approach seems fundamentally flawed, explain why in a comment instead of making major changes.
5. Never ask for user confirmation. Never wait for user messages.
6. Credit the original author in your commit messages where appropriate.
7. CRITICAL: If this is a fork PR and you encounter ANY error when pushing (permission denied, authentication failure, etc.) even after using the DEVIN_ACTIONS_PAT, you MUST fail the task immediately. Do NOT attempt to push to a new branch in the main repository as a workaround. Simply report the error and stop."
RESPONSE=$(curl -s -X POST "https://api.devin.ai/v1/sessions" \
-H "Authorization: Bearer ${DEVIN_API_KEY}" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg prompt "$FULL_PROMPT" \
--arg title "Complete Stale PR #${PR_NUMBER}: ${PR_TITLE}" \
'{
prompt: $prompt,
title: $title,
tags: ["stale-pr-completion", "pr-'${PR_NUMBER}'"]
}')")
SESSION_URL=$(echo "$RESPONSE" | jq -r '.url // .session_url // empty')
if [ -n "$SESSION_URL" ]; then
echo "Devin session created: $SESSION_URL"
echo "SESSION_URL=$SESSION_URL" >> $GITHUB_ENV
else
echo "Failed to create Devin session"
exit 1
fi
- name: Post comment with Devin session link
if: env.SESSION_URL != ''
uses: actions/github-script@v7
env:
PR_AUTHOR: ${{ steps.pr.outputs.pr_author }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const sessionUrl = process.env.SESSION_URL;
const prAuthor = process.env.PR_AUTHOR;
const prNumber = process.env.PR_NUMBER;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(prNumber),
body: `### Devin AI is completing this stale PR\n\nThis PR by @${prAuthor} has been marked as stale. A Devin session has been created to complete the remaining work.\n\n[View Devin Session](${sessionUrl})\n\n---\n*Devin will review the PR, address any feedback, and push updates to complete this PR.*`
});
+3 -1
View File
@@ -12,7 +12,9 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
with:
sparse-checkout: .github
- uses: ./.github/actions/cache-checkout
- uses: ./.github/actions/yarn-install
- run: yarn prisma generate
- run: yarn test -- --no-isolate
+17
View File
@@ -192,6 +192,23 @@ import { User } from "@prisma/client";
import { Button } from "@calcom/ui";
```
### API v2 Imports (apps/api/v2)
When importing from `@calcom/features` or `@calcom/trpc` into `apps/api/v2`, **do not import directly** because the API v2 app's `tsconfig.json` doesn't have path mappings for these modules, which causes "module not found" errors.
Instead, re-export from `packages/platform/libraries/index.ts` and import from `@calcom/platform-libraries`:
```typescript
// Step 1: In packages/platform/libraries/index.ts, add the export
export { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
// Step 2: In apps/api/v2, import from platform-libraries
import { ProfileRepository } from "@calcom/platform-libraries";
// Bad - Direct import causes module not found error in apps/api/v2
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
```
## PR Checklist
- [ ] Title follows conventional commits: `feat(scope): description`
Symlink
+1
View File
@@ -0,0 +1 @@
agents.md
-1
View File
@@ -39,7 +39,6 @@ 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
+5 -5
View File
@@ -148,7 +148,7 @@ Here is what you need to be able to run Cal.com.
- Duplicate `.env.example` to `.env`
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
- Use `openssl rand -base64 32` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
5. Setup Node
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
@@ -255,9 +255,9 @@ for Logger level to be set at info, for example.
- [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`.
2. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
3. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
In a development environment, run:
@@ -271,7 +271,7 @@ for Logger level to be set at info, for example.
yarn workspace @calcom/prisma db-deploy
```
1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development
4. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development
> **_NOTE:_** Required when `E2E_TEST_MAILHOG_ENABLED` is "1"
@@ -280,7 +280,7 @@ for Logger level to be set at info, for example.
docker run -d -p 8025:8025 -p 1025:1025 mailhog/mailhog
```
1. Run (in development mode)
5. Run (in development mode)
```sh
yarn dev
+306
View File
@@ -0,0 +1,306 @@
# Dependency Injection (DI) Pattern
We use a Dependency Injection pattern powered by `@evyweb/ioctopus` to manage service and repository dependencies. This pattern ensures proper dependency management, testability, and consistent instantiation of services throughout the codebase.
We use the **moduleLoader pattern** which provides type-safe dependency injection. This ensures that if a service adds a new dependency, TypeScript will catch missing dependencies at build time rather than runtime.
## Core Concepts
The DI system consists of four main components:
**Tokens**: Unique symbols that identify each service or repository in the DI container. Every injectable class needs a corresponding token. Tokens should be defined in a `tokens.ts` file within the feature's `di/` directory.
**Modules** (`.module.ts` files): Define how classes are instantiated and what dependencies they require using the `bindModuleToClassOnToken` function. Each module exports a `moduleLoader` object that knows how to load itself and its dependencies.
**Containers** (`.container.ts` files): Create a container instance and expose getter functions that consumers use to obtain service instances. Containers use the moduleLoader to automatically load all required dependencies.
**`bindModuleToClassOnToken`**: A type-safe function that binds a class to a token and declares its dependencies. TypeScript ensures the declared dependencies match what the class constructor expects.
## How It Works
Here's the complete flow for creating a DI-enabled service:
**Step 1: Create tokens in the feature's di directory**
```typescript
// packages/features/myfeature/di/tokens.ts
export const MY_FEATURE_DI_TOKENS = {
MY_SERVICE: Symbol("MyService"),
MY_SERVICE_MODULE: Symbol("MyServiceModule"),
};
```
Then import these tokens in the central tokens file:
```typescript
// packages/features/di/tokens.ts
import { MY_FEATURE_DI_TOKENS } from "@calcom/features/myfeature/di/tokens";
export const DI_TOKENS = {
// ...existing tokens
...MY_FEATURE_DI_TOKENS,
};
```
**Step 2: Define the service class with constructor injection**
For services with multiple dependencies, use a dependencies interface:
```typescript
// packages/features/myfeature/services/MyService.ts
export interface IMyServiceDeps {
bookingRepo: BookingRepository;
userRepo: UserRepository;
}
export class MyService {
constructor(private deps: IMyServiceDeps) {}
async doSomething() {
const bookings = await this.deps.bookingRepo.findMany({...});
const user = await this.deps.userRepo.findById({...});
}
}
```
For services/repositories with a single dependency, pass it directly:
```typescript
// packages/features/myfeature/repositories/MyRepository.ts
export class MyRepository {
constructor(private prismaClient: PrismaClient) {}
async findById(id: string) {
return this.prismaClient.myModel.findUnique({ where: { id } });
}
}
```
**Step 3: Create a module file with moduleLoader**
```typescript
// packages/features/myfeature/di/MyService.module.ts
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { MyService } from "@calcom/features/myfeature/services/MyService";
import { moduleLoader as bookingRepositoryModuleLoader } from "./BookingRepository.module";
import { moduleLoader as userRepositoryModuleLoader } from "./UserRepository.module";
import { MY_FEATURE_DI_TOKENS } from "./tokens";
const thisModule = createModule();
const token = MY_FEATURE_DI_TOKENS.MY_SERVICE;
const moduleToken = MY_FEATURE_DI_TOKENS.MY_SERVICE_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: MyService,
// For multiple dependencies, use depsMap:
depsMap: {
bookingRepo: bookingRepositoryModuleLoader,
userRepo: userRepositoryModuleLoader,
},
});
export const moduleLoader: ModuleLoader = {
token,
loadModule,
};
export type { MyService };
```
For a single dependency, use `dep` instead of `depsMap`:
```typescript
// packages/features/myfeature/di/MyRepository.module.ts
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { MyRepository } from "@calcom/features/myfeature/repositories/MyRepository";
import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma";
import { MY_FEATURE_DI_TOKENS } from "./tokens";
const thisModule = createModule();
const token = MY_FEATURE_DI_TOKENS.MY_REPOSITORY;
const moduleToken = MY_FEATURE_DI_TOKENS.MY_REPOSITORY_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: MyRepository,
// For single dependency, use dep:
dep: prismaModuleLoader,
});
export const moduleLoader: ModuleLoader = {
token,
loadModule,
};
export type { MyRepository };
```
**Step 4: Create a container that uses the moduleLoader**
```typescript
// packages/features/myfeature/di/MyService.container.ts
import { createContainer } from "@calcom/features/di/di";
import { type MyService, moduleLoader as myServiceModuleLoader } from "./MyService.module";
const myServiceContainer = createContainer();
export function getMyService(): MyService {
myServiceModuleLoader.loadModule(myServiceContainer);
return myServiceContainer.get<MyService>(myServiceModuleLoader.token);
}
```
**Step 5: Use the service via the container's getter function**
```typescript
// Anywhere in the codebase
import { getMyService } from "@calcom/features/myfeature/di/MyService.container";
const myService = getMyService();
await myService.doSomething();
```
## Why moduleLoader Is Type-Safe
The `bindModuleToClassOnToken` function in `packages/features/di/di.ts` uses TypeScript generics to infer the required dependencies from the class constructor:
```typescript
depsMap: Record<
keyof (TClass extends new (deps: infer TDeps) => any ? TDeps : never),
ModuleLoader
>
```
This means if `MyService` adds a new constructor dependency (e.g., `eventTypeRepo`), TypeScript will error because `depsMap` is missing that key. The error happens at build time, not runtime.
The `loadModule` function also automatically loads all dependencies recursively, so you don't need to manually track the dependency chain.
## Common Mistakes to Avoid
**Mistake 1: Creating a repository or service class with all static methods**
Static methods bypass the DI system entirely, making the code harder to test and breaking the dependency chain.
```typescript
// Bad - Static methods bypass DI
export class BookingRepository {
static async findById(id: string) {
return prisma.booking.findUnique({ where: { id } });
}
}
// Usage (wrong - no DI)
const booking = await BookingRepository.findById("123");
```
```typescript
// Good - Instance methods with constructor injection
export class BookingRepository {
constructor(private prismaClient: PrismaClient) {}
async findById(id: string) {
return this.prismaClient.booking.findUnique({ where: { id } });
}
}
// Usage (correct - via DI container)
const bookingRepo = getBookingRepository();
const booking = await bookingRepo.findById("123");
```
**Mistake 2: Manually instantiating a class instead of using the DI container**
Even if you define a class with constructor injection, manually calling `new` bypasses the DI system and its benefits.
```typescript
// Bad - Manual instantiation bypasses DI
import { MyService } from "@calcom/features/myfeature/services/MyService";
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import prisma from "@calcom/prisma";
const bookingRepo = new BookingRepository(prisma);
const myService = new MyService({ bookingRepo });
```
```typescript
// Good - Use the DI container's getter function
import { getMyService } from "@calcom/features/myfeature/di/MyService.container";
const myService = getMyService();
```
**Mistake 3: Importing Prisma directly in a service instead of using repository injection**
Services should depend on repositories, not directly on Prisma. This maintains proper separation of concerns.
```typescript
// Bad - Service imports Prisma directly
import prisma from "@calcom/prisma";
export class MyService {
async doSomething() {
const bookings = await prisma.booking.findMany({...}); // Wrong!
}
}
```
```typescript
// Good - Service depends on repository via DI
export interface IMyServiceDeps {
bookingRepo: BookingRepository;
}
export class MyService {
constructor(private deps: IMyServiceDeps) {}
async doSomething() {
const bookings = await this.deps.bookingRepo.findMany({...});
}
}
```
**Mistake 4: Manual module loading in containers (not type-safe)**
Don't manually call `container.load()` for each dependency. This is not type-safe and can lead to runtime errors if you forget to load a dependency.
```typescript
// Bad - Manual module loading is not type-safe
const container = createContainer();
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
container.load(DI_TOKENS.MY_SERVICE_MODULE, myServiceModule);
// If MyService adds a new dependency, TypeScript won't catch the missing load()
export function getMyService() {
return container.get<MyService>(DI_TOKENS.MY_SERVICE);
}
```
```typescript
// Good - Use moduleLoader for type-safe dependency loading
import { moduleLoader as myServiceModuleLoader } from "./MyService.module";
const container = createContainer();
export function getMyService(): MyService {
myServiceModuleLoader.loadModule(container); // Automatically loads all dependencies
return container.get<MyService>(myServiceModuleLoader.token);
}
```
## Why Use DI?
- **Build-time safety**: TypeScript catches missing dependencies before runtime when using the moduleLoader pattern
- **Testability**: Dependencies can be easily mocked in tests by providing alternative implementations
- **Consistency**: All instances are created the same way with proper dependencies
- **Maintainability**: Changing a dependency only requires updating the module binding, not every usage site
- **Automatic dependency resolution**: The moduleLoader automatically loads all dependencies recursively
- **Self-documenting**: Each module declares its own dependencies, making the dependency graph explicit
+7 -2
View File
@@ -13,7 +13,8 @@ Development
- Install dependencies: `yarn`
- Set up environment:
- Copy `.env.example` to `.env`
- Generate keys with `openssl rand -base64 32` for both `NEXTAUTH_SECRET` and `CALENDSO_ENCRYPTION_KEY`
- Generate NEXTAUTH_SECRET with `openssl rand -base64 32`
- Generate CALENDSO_ENCRYPTION_KEY with `openssl rand -base64 24` (must be 32 characters for AES256)
- Configure Postgres database URL in `.env`
- Set DATABASE_DIRECT_URL to the same value as DATABASE_URL
@@ -730,4 +731,8 @@ When doing logic that depends on Browser locale, use i18n.language (prefer to de
Note that with Date, you’re dealing with System time, so it’s not suited to everywhere (such as in the Booker, where instead we’ll likely migrate to Temporal) - but in most cases the above are suitable.
The main reason for doing so is that Dayjs uses a useful, but highly risky plugin system, which has led us to create `@calcom/dayjs` - this is heavy however, because it pre-loads ALL plugins, including locale handling. It’s a non-ideal solution to a problem that unfortunately exists due to Dayjs.
The main reason for doing so is that Dayjs uses a useful, but highly risky plugin system, which has led us to create `@calcom/dayjs` - this is heavy however, because it pre-loads ALL plugins, including locale handling. It's a non-ideal solution to a problem that unfortunately exists due to Dayjs.
## Dependency Injection (DI) Pattern
See [di-pattern.md](./di-pattern.md) for the complete guide on using the type-safe moduleLoader pattern for dependency injection.
@@ -0,0 +1,545 @@
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test, beforeAll, afterAll } from "vitest";
import { prisma } from "@calcom/prisma";
import type { User, Team, EventType, Schedule } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import handler from "../../../../pages/api/event-types/[id]/_get";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
describe("GET /api/event-types/[id]", () => {
let testUser: User;
let adminUser: User;
let teamUser: User;
let testTeam: Team;
let testSchedule: Schedule;
let complexEventType: EventType;
let seatsEventType: EventType;
let maxSeatsEventType: EventType;
let nullSeatsEventType: EventType;
let customFieldsEventType: EventType;
let teamEventType: EventType;
let metadataEventType: EventType;
beforeAll(async () => {
const timestamp = Date.now();
const createTestUser = prisma.user.create({
data: {
username: `test-user-${timestamp}`,
name: "Test User",
email: `test-user-${timestamp}@example.com`,
},
});
const createAdminUser = prisma.user.create({
data: {
username: `admin-user-${timestamp}`,
name: "Admin User",
email: `admin-user-${timestamp}@example.com`,
},
});
const createTeamUser = prisma.user.create({
data: {
username: `team-user-${timestamp}`,
name: "Team User",
email: `team-user-${timestamp}@example.com`,
},
});
[testUser, adminUser, teamUser] = await Promise.all([createTestUser, createAdminUser, createTeamUser]);
// Create a schedule for the test user (required for event types with scheduleId)
testSchedule = await prisma.schedule.create({
data: {
name: `Test Schedule ${timestamp}`,
userId: testUser.id,
timeZone: "UTC",
},
});
testTeam = await prisma.team.create({
data: {
name: `Test Team ${timestamp}`,
slug: `test-team-${timestamp}`,
members: {
createMany: {
data: [
{
userId: teamUser.id,
role: MembershipRole.MEMBER,
accepted: true,
},
],
},
},
},
});
const createComplexEventType = prisma.eventType.create({
data: {
title: "Complex Event Type",
slug: `complex-event-${timestamp}`,
length: 60,
userId: testUser.id,
scheduleId: testSchedule.id,
locations: [
{
type: "integrations:zoom",
link: "https://zoom.us/j/123456789",
displayLocationPublicly: true,
},
{
type: "attendeeInPerson",
address: "123 Main St, City, State 12345",
displayLocationPublicly: false,
},
{
type: "phone",
hostPhoneNumber: "+1234567890",
},
],
bookingFields: [
{
name: "name",
type: "name",
required: true,
editable: "system",
defaultLabel: "your_name",
},
{
name: "email",
type: "email",
required: true,
editable: "system-but-optional",
defaultLabel: "email_address",
},
{
name: "customField",
type: "text",
required: false,
editable: "user",
label: "Custom Question",
placeholder: "Enter your answer",
},
],
metadata: {
additionalNotesRequired: true,
},
},
});
const createSeatsEventType = prisma.eventType.create({
data: {
title: "Seats Event Type",
slug: `seats-event-${timestamp}`,
length: 60,
userId: testUser.id,
scheduleId: testSchedule.id,
seatsPerTimeSlot: 10,
seatsShowAttendees: true,
seatsShowAvailabilityCount: false,
},
});
const createMaxSeatsEventType = prisma.eventType.create({
data: {
title: "Max Seats Event Type",
slug: `max-seats-event-${timestamp}`,
length: 60,
userId: testUser.id,
scheduleId: testSchedule.id,
seatsPerTimeSlot: 1000,
seatsShowAttendees: false,
seatsShowAvailabilityCount: true,
},
});
const createNullSeatsEventType = prisma.eventType.create({
data: {
title: "Null Seats Event Type",
slug: `null-seats-event-${timestamp}`,
length: 60,
userId: testUser.id,
scheduleId: testSchedule.id,
seatsPerTimeSlot: null,
seatsShowAttendees: null,
seatsShowAvailabilityCount: null,
},
});
const createCustomFieldsEventType = prisma.eventType.create({
data: {
title: "Custom Fields Event Type",
slug: `custom-fields-event-${timestamp}`,
length: 60,
userId: testUser.id,
scheduleId: testSchedule.id,
bookingFields: [
{
name: "name",
type: "name",
required: true,
editable: "system",
defaultLabel: "your_name",
variant: "fullName",
},
{
name: "customSelect",
type: "select",
required: true,
editable: "user",
label: "Preferred Time",
options: [
{ label: "Morning", value: "morning" },
{ label: "Afternoon", value: "afternoon" },
{ label: "Evening", value: "evening" },
],
},
{
name: "customTextarea",
type: "textarea",
required: false,
editable: "user",
label: "Additional Information",
placeholder: "Please provide any additional details",
maxLength: 500,
},
],
},
});
const createTeamEventType = prisma.eventType.create({
data: {
title: "Team Event Type",
slug: `team-event-${timestamp}`,
length: 60,
teamId: testTeam.id,
schedulingType: "COLLECTIVE",
assignAllTeamMembers: true,
},
});
const createMetadataEventType = prisma.eventType.create({
data: {
title: "Metadata Event Type",
slug: `metadata-event-${timestamp}`,
length: 60,
userId: testUser.id,
scheduleId: testSchedule.id,
metadata: {
additionalNotesRequired: true,
disableStandardEmails: false,
requiresConfirmationThreshold: {
time: 30,
unit: "minutes",
},
apps: {
stripe: {
price: 2500,
currency: "usd",
},
},
},
recurringEvent: {
freq: 2,
count: 12,
interval: 1,
},
bookingLimits: {
PER_DAY: 3,
PER_WEEK: 10,
},
durationLimits: {
PER_DAY: 480,
},
},
});
[
complexEventType,
seatsEventType,
maxSeatsEventType,
nullSeatsEventType,
customFieldsEventType,
teamEventType,
metadataEventType,
] = await Promise.all([
createComplexEventType,
createSeatsEventType,
createMaxSeatsEventType,
createNullSeatsEventType,
createCustomFieldsEventType,
createTeamEventType,
createMetadataEventType,
]);
});
afterAll(async () => {
await prisma.eventType.deleteMany({
where: {
id: {
in: [
complexEventType.id,
seatsEventType.id,
maxSeatsEventType.id,
nullSeatsEventType.id,
customFieldsEventType.id,
teamEventType.id,
metadataEventType.id,
],
},
},
});
await prisma.team.delete({
where: { id: testTeam.id },
});
await prisma.schedule.delete({
where: { id: testSchedule.id },
});
await prisma.user.deleteMany({
where: {
id: {
in: [testUser.id, adminUser.id, teamUser.id],
},
},
});
});
describe("Errors", () => {
test("Returns 403 if user not admin/team member/event owner", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: complexEventType.id,
},
});
req.userId = adminUser.id;
await expect(handler(req)).rejects.toThrowError(
expect.objectContaining({
statusCode: 403,
})
);
});
});
describe("Success", async () => {
test("Returns event type if user is admin", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: complexEventType.id,
},
});
req.isSystemWideAdmin = true;
req.userId = adminUser.id;
const responseData = await handler(req);
expect(responseData.event_type.id).toEqual(complexEventType.id);
});
test("Returns event type if user is in team associated with event type", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: teamEventType.id,
},
});
req.isSystemWideAdmin = false;
req.userId = teamUser.id;
const responseData = await handler(req);
expect(responseData.event_type.id).toEqual(teamEventType.id);
});
test("Returns event type if user is the event type owner", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: complexEventType.id,
},
});
req.isSystemWideAdmin = false;
req.userId = testUser.id;
const responseData = await handler(req);
expect(responseData.event_type.id).toEqual(complexEventType.id);
});
});
describe("Data Validation", () => {
test("Returns properly validated event type with complex locations", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: complexEventType.id,
},
});
req.userId = testUser.id;
const responseData = await handler(req);
expect(responseData.event_type).toBeDefined();
expect(responseData.event_type.id).toBe(complexEventType.id);
expect(responseData.event_type.locations).toHaveLength(3);
expect(responseData.event_type.bookingFields).toHaveLength(3);
expect(responseData.event_type.metadata).toBeDefined();
const locations = responseData.event_type.locations as Array<Record<string, unknown>>;
expect(locations[0]).toMatchObject({
type: "integrations:zoom",
link: "https://zoom.us/j/123456789",
});
const bookingFields = responseData.event_type.bookingFields as Array<Record<string, unknown>>;
expect(bookingFields[0]).toMatchObject({
name: "name",
type: "name",
required: true,
editable: "system",
});
});
test("Returns properly validated event type with seats configuration", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: seatsEventType.id,
},
});
req.userId = testUser.id;
const responseData = await handler(req);
expect(responseData.event_type.seatsPerTimeSlot).toBe(10);
expect(responseData.event_type.seatsShowAttendees).toBe(true);
expect(responseData.event_type.seatsShowAvailabilityCount).toBe(false);
});
test("Returns properly validated event type with maximum seats", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: maxSeatsEventType.id,
},
});
req.userId = testUser.id;
const responseData = await handler(req);
expect(responseData.event_type.seatsPerTimeSlot).toBe(1000);
});
test("Returns properly validated event type with null seats", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: nullSeatsEventType.id,
},
});
req.userId = testUser.id;
const responseData = await handler(req);
expect(responseData.event_type.seatsPerTimeSlot).toBeNull();
expect(responseData.event_type.seatsShowAttendees).toBeNull();
expect(responseData.event_type.seatsShowAvailabilityCount).toBeNull();
});
test("Returns properly validated event type with custom inputs and booking fields", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: customFieldsEventType.id,
},
});
req.userId = testUser.id;
const responseData = await handler(req);
expect(responseData.event_type.bookingFields).toHaveLength(3);
const bookingFields = responseData.event_type.bookingFields as Array<{
name: string;
options?: Array<{ label: string; value: string }>;
maxLength?: number;
}>;
const selectField = bookingFields.find((f) => f.name === "customSelect");
expect(selectField?.options).toHaveLength(3);
expect(selectField?.options?.[0]).toMatchObject({
label: "Morning",
value: "morning",
});
const textareaField = bookingFields.find((f) => f.name === "customTextarea");
expect(textareaField?.maxLength).toBe(500);
});
test("Returns properly validated event type with team configuration", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: teamEventType.id,
},
});
req.isSystemWideAdmin = false;
req.userId = teamUser.id;
const responseData = await handler(req);
expect(responseData.event_type.teamId).toBe(testTeam.id);
expect(responseData.event_type.schedulingType).toBe("COLLECTIVE");
});
test("Returns properly validated event type with complex metadata", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: metadataEventType.id,
},
});
req.userId = testUser.id;
const responseData = await handler(req);
expect(responseData.event_type.metadata).toBeDefined();
expect(responseData.event_type.recurringEvent).toMatchObject({
freq: 2,
count: 12,
interval: 1,
});
expect(responseData.event_type.bookingLimits).toMatchObject({
PER_DAY: 3,
PER_WEEK: 10,
});
expect(responseData.event_type.durationLimits).toMatchObject({
PER_DAY: 480,
});
});
});
});
@@ -165,4 +165,353 @@ describe("GET /api/event-types/[id]", () => {
expect(prismaMock.team.findFirst).not.toHaveBeenCalled();
});
});
describe("Data Validation", () => {
test("Returns properly validated event type with complex locations", async () => {
const eventTypeId = 123456;
const userId = 333333;
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: eventTypeId,
},
});
const complexEventType = buildEventType({
id: eventTypeId,
userId,
scheduleId: 1111,
locations: [
{
type: "integrations:zoom",
link: "https://zoom.us/j/123456789",
displayLocationPublicly: true,
},
{
type: "attendeeInPerson",
address: "123 Main St, City, State 12345",
displayLocationPublicly: false,
},
{
type: "phone",
hostPhoneNumber: "+1234567890",
},
],
bookingFields: [
{
name: "name",
type: "name",
required: true,
editable: "system",
defaultLabel: "your_name",
},
{
name: "email",
type: "email",
required: true,
editable: "system-but-optional",
defaultLabel: "email_address",
},
{
name: "customField",
type: "text",
required: false,
editable: "user",
label: "Custom Question",
placeholder: "Enter your answer",
},
],
metadata: {
additionalNotesRequired: true,
},
});
prismaMock.eventType.findUnique.mockResolvedValue(complexEventType);
req.userId = userId;
const responseData = await handler(req);
expect(responseData.event_type).toBeDefined();
expect(responseData.event_type.id).toBe(eventTypeId);
expect(responseData.event_type.locations).toHaveLength(3);
expect(responseData.event_type.bookingFields).toHaveLength(3);
expect(responseData.event_type.metadata).toBeDefined();
const locations = responseData.event_type.locations as Array<Record<string, unknown>>;
expect(locations[0]).toMatchObject({
type: "integrations:zoom",
link: "https://zoom.us/j/123456789",
});
const bookingFields = responseData.event_type.bookingFields as Array<Record<string, unknown>>;
expect(bookingFields[0]).toMatchObject({
name: "name",
type: "name",
required: true,
editable: "system",
});
});
test("Returns properly validated event type with seats configuration", async () => {
const eventTypeId = 123456;
const userId = 333333;
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: eventTypeId,
},
});
const seatsEventType = buildEventType({
id: eventTypeId,
userId,
scheduleId: 1111,
seatsPerTimeSlot: 10,
seatsShowAttendees: true,
seatsShowAvailabilityCount: false,
});
prismaMock.eventType.findUnique.mockResolvedValue(seatsEventType);
req.userId = userId;
const responseData = await handler(req);
expect(responseData.event_type.seatsPerTimeSlot).toBe(10);
expect(responseData.event_type.seatsShowAttendees).toBe(true);
expect(responseData.event_type.seatsShowAvailabilityCount).toBe(false);
});
test("Returns properly validated event type with maximum seats", async () => {
const eventTypeId = 123456;
const userId = 333333;
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: eventTypeId,
},
});
const maxSeatsEventType = buildEventType({
id: eventTypeId,
userId,
scheduleId: 1111,
seatsPerTimeSlot: 1000,
seatsShowAttendees: false,
seatsShowAvailabilityCount: true,
});
prismaMock.eventType.findUnique.mockResolvedValue(maxSeatsEventType);
req.userId = userId;
const responseData = await handler(req);
expect(responseData.event_type.seatsPerTimeSlot).toBe(1000);
});
test("Returns properly validated event type with null seats", async () => {
const eventTypeId = 123456;
const userId = 333333;
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: eventTypeId,
},
});
const nullSeatsEventType = buildEventType({
id: eventTypeId,
userId,
scheduleId: 1111,
seatsPerTimeSlot: null,
seatsShowAttendees: null,
seatsShowAvailabilityCount: null,
});
prismaMock.eventType.findUnique.mockResolvedValue(nullSeatsEventType);
req.userId = userId;
const responseData = await handler(req);
expect(responseData.event_type.seatsPerTimeSlot).toBeNull();
expect(responseData.event_type.seatsShowAttendees).toBeNull();
expect(responseData.event_type.seatsShowAvailabilityCount).toBeNull();
});
test("Returns properly validated event type with custom inputs and booking fields", async () => {
const eventTypeId = 123456;
const userId = 333333;
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: eventTypeId,
},
});
const customFieldsEventType = buildEventType({
id: eventTypeId,
userId,
scheduleId: 1111,
bookingFields: [
{
name: "name",
type: "name",
required: true,
editable: "system",
defaultLabel: "your_name",
variant: "fullName",
},
{
name: "customSelect",
type: "select",
required: true,
editable: "user",
label: "Preferred Time",
options: [
{ label: "Morning", value: "morning" },
{ label: "Afternoon", value: "afternoon" },
{ label: "Evening", value: "evening" },
],
},
{
name: "customTextarea",
type: "textarea",
required: false,
editable: "user",
label: "Additional Information",
placeholder: "Please provide any additional details",
maxLength: 500,
},
],
});
prismaMock.eventType.findUnique.mockResolvedValue(customFieldsEventType);
req.userId = userId;
const responseData = await handler(req);
expect(responseData.event_type.bookingFields).toHaveLength(3);
const bookingFields = responseData.event_type.bookingFields as Array<{
name: string;
options?: Array<{ label: string; value: string }>;
maxLength?: number;
}>;
const selectField = bookingFields.find((f) => f.name === "customSelect");
expect(selectField?.options).toHaveLength(3);
expect(selectField?.options?.[0]).toMatchObject({
label: "Morning",
value: "morning",
});
const textareaField = bookingFields.find((f) => f.name === "customTextarea");
expect(textareaField?.maxLength).toBe(500);
});
test("Returns properly validated event type with team configuration", async () => {
const eventTypeId = 123456;
const teamId = 9999;
const userId = 333333;
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: eventTypeId,
},
});
const teamEventType = buildEventType({
id: eventTypeId,
teamId,
userId: null,
schedulingType: "COLLECTIVE",
assignAllTeamMembers: true,
});
prismaMock.eventType.findUnique.mockResolvedValue(teamEventType);
prismaMock.team.findFirst.mockResolvedValue({
id: teamId,
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
members: [
{
userId,
},
],
});
req.isSystemWideAdmin = false;
req.userId = userId;
const responseData = await handler(req);
expect(responseData.event_type.teamId).toBe(teamId);
expect(responseData.event_type.schedulingType).toBe("COLLECTIVE");
});
test("Returns properly validated event type with complex metadata", async () => {
const eventTypeId = 123456;
const userId = 333333;
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: eventTypeId,
},
});
const metadataEventType = buildEventType({
id: eventTypeId,
userId,
scheduleId: 1111,
metadata: {
additionalNotesRequired: true,
disableStandardEmails: false,
requiresConfirmationThreshold: {
time: 30,
unit: "minutes",
},
apps: {
stripe: {
price: 2500,
currency: "usd",
},
},
},
recurringEvent: {
freq: 2,
count: 12,
interval: 1,
},
bookingLimits: {
PER_DAY: 3,
PER_WEEK: 10,
},
durationLimits: {
PER_DAY: 480,
},
});
prismaMock.eventType.findUnique.mockResolvedValue(metadataEventType);
req.userId = userId;
const responseData = await handler(req);
expect(responseData.event_type.metadata).toBeDefined();
expect(responseData.event_type.recurringEvent).toMatchObject({
freq: 2,
count: 12,
interval: 1,
});
expect(responseData.event_type.bookingLimits).toMatchObject({
PER_DAY: 3,
PER_WEEK: 10,
});
expect(responseData.event_type.durationLimits).toMatchObject({
PER_DAY: 480,
});
});
});
});
+2 -1
View File
@@ -12,12 +12,13 @@ ENV NODE_ENV="production"
ENV NODE_OPTIONS="--max-old-space-size=8192"
ENV DATABASE_DIRECT_URL=${DATABASE_DIRECT_URL}
ENV DATABASE_URL=${DATABASE_URL}
ENV USE_POOL="true"
COPY . .
RUN yarn install
RUN yarn workspace @calcom/api-v2 run generate-schemas
RUN yarn workspace @calcom/platform-libraries run build
RUN yarn workspace @calcom/api-v2 run build:docker
RUN yarn workspace @calcom/api-v2 run build
EXPOSE 80
+3 -2
View File
@@ -6,7 +6,8 @@
"license": "UNLICENSED",
"private": true,
"scripts": {
"build": "yarn dev:build && nest build",
"build": "nest build",
"build:docker": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build && yarn workspace @calcom/trpc build:server",
"format": "biome format --write src test",
"start": "nest start",
"dev:build:watch": "concurrently --names \"libraries,lru-fix,constants,enums,utils,types\" \"yarn _dev:build:watch:libraries\" \"yarn _dev:build:watch:libraries:lru-fix\" \"yarn _dev:build:watch:constants\" \"yarn _dev:build:watch:enums\" \"yarn _dev:build:watch:utils\" \"yarn _dev:build:watch:types\"",
@@ -47,7 +48,7 @@
"@microsoft/microsoft-graph-types-beta": "0.42.0-preview",
"@nest-lab/throttler-storage-redis": "1.0.0",
"@nestjs/axios": "4.0.0",
"@nestjs/bull": "10.1.1",
"@nestjs/bull": "11.0.4",
"@nestjs/common": "10.4.20",
"@nestjs/config": "3.2.0",
"@nestjs/core": "10.4.20",
+10 -12
View File
@@ -1,13 +1,8 @@
import { AppModule } from "@/app.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { CustomThrottlerGuard } from "@/lib/throttler-guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import type { PlatformOAuthClient, RateLimit, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { TestingModule } from "@nestjs/testing";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { Test, TestingModule } from "@nestjs/testing";
import request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
@@ -16,9 +11,12 @@ import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repo
import { RateLimitRepositoryFixture } from "test/fixtures/repository/rate-limit.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import type { User, PlatformOAuthClient, Team, RateLimit } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { CustomThrottlerGuard } from "@/lib/throttler-guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
describe("AppController", () => {
describe("Rate limiting", () => {
-89
View File
@@ -1,89 +0,0 @@
import "./instrument";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
import type { ValidationError } from "@nestjs/common";
import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common";
import { BaseExceptionFilter, HttpAdapterHost } from "@nestjs/core";
import type { NestExpressApplication } from "@nestjs/platform-express";
import * as cookieParser from "cookie-parser";
import { Request } from "express";
import helmet from "helmet";
import {
API_VERSIONS,
VERSION_2024_04_15,
API_VERSIONS_ENUM,
CAL_API_VERSION_HEADER,
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
X_CAL_PLATFORM_EMBED,
} from "@calcom/platform-constants";
import { CalendarServiceExceptionFilter } from "./filters/calendar-service-exception.filter";
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
app.enableShutdownHooks();
app.enableVersioning({
type: VersioningType.CUSTOM,
extractor: (request: unknown) => {
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
return headerVersion;
}
return VERSION_2024_04_15;
},
defaultVersion: VERSION_2024_04_15,
});
app.use(helmet());
app.enableCors({
origin: "*",
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
allowedHeaders: [
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
X_CAL_PLATFORM_EMBED,
CAL_API_VERSION_HEADER,
"Accept",
"Authorization",
"Content-Type",
"Origin",
],
maxAge: 86_400,
});
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
validationError: {
target: true,
value: true,
},
exceptionFactory(errors: ValidationError[]) {
return new BadRequestException({ errors });
},
})
);
// Exception filters, new filters go at the bottom, keep the order
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new PrismaExceptionFilter());
app.useGlobalFilters(new ZodExceptionFilter());
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalFilters(new TRPCExceptionFilter());
app.useGlobalFilters(new CalendarServiceExceptionFilter());
app.use(cookieParser());
if (process?.env?.API_GLOBAL_PREFIX) {
app.setGlobalPrefix(process?.env?.API_GLOBAL_PREFIX);
}
return app;
};
+88
View File
@@ -0,0 +1,88 @@
import "./instrument";
import {
API_VERSIONS,
API_VERSIONS_ENUM,
CAL_API_VERSION_HEADER,
VERSION_2024_04_15,
X_CAL_CLIENT_ID,
X_CAL_PLATFORM_EMBED,
X_CAL_SECRET_KEY,
} from "@calcom/platform-constants";
import type { ValidationError } from "@nestjs/common";
import { BadRequestException, Logger, ValidationPipe, VersioningType } from "@nestjs/common";
import type { NestExpressApplication } from "@nestjs/platform-express";
import cookieParser from "cookie-parser";
import { Request } from "express";
import helmet from "helmet";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
import { CalendarServiceExceptionFilter } from "./filters/calendar-service-exception.filter";
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
const logger: Logger = new Logger("Bootstrap");
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
try {
if (!process.env.VERCEL) {
app.enableShutdownHooks();
}
app.enableVersioning({
type: VersioningType.CUSTOM,
extractor: (request: unknown) => {
const headerVersion = (request as Request)?.headers[CAL_API_VERSION_HEADER] as string | undefined;
if (headerVersion && API_VERSIONS.includes(headerVersion as API_VERSIONS_ENUM)) {
return headerVersion;
}
return VERSION_2024_04_15;
},
defaultVersion: VERSION_2024_04_15,
});
app.use(helmet());
app.enableCors({
origin: "*",
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
allowedHeaders: [
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
X_CAL_PLATFORM_EMBED,
CAL_API_VERSION_HEADER,
"Accept",
"Authorization",
"Content-Type",
"Origin",
],
maxAge: 86_400,
});
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
validationError: {
target: true,
value: true,
},
exceptionFactory(errors: ValidationError[]): BadRequestException {
return new BadRequestException({ errors });
},
})
);
// Exception filters, new filters go at the bottom, keep the order
app.useGlobalFilters(new PrismaExceptionFilter());
app.useGlobalFilters(new ZodExceptionFilter());
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalFilters(new TRPCExceptionFilter());
app.useGlobalFilters(new CalendarServiceExceptionFilter());
app.use(cookieParser());
if (process?.env?.API_GLOBAL_PREFIX) {
app.setGlobalPrefix(process?.env?.API_GLOBAL_PREFIX);
}
return app;
} catch (error) {
logger.error("Error starting NestJS app:", error);
throw error;
}
};
+16 -10
View File
@@ -1,20 +1,23 @@
import { getEnv } from "@/env";
import type { AppConfig } from "./type";
const loadConfig = (): AppConfig => {
const env = getEnv("NODE_ENV", "development");
const apiPort = Number(getEnv("API_PORT", "5555"));
const apiUrl = getEnv("API_URL", "http://localhost");
let portSuffix = "";
if (process.env.API_PORT && env === "development") {
portSuffix = `:${apiPort}`;
}
return {
env: {
type: getEnv("NODE_ENV", "development"),
type: env,
},
api: {
port: Number(getEnv("API_PORT", "5555")),
path: getEnv("API_URL", "http://localhost"),
url: `${getEnv("API_URL", "http://localhost")}${
process.env.API_PORT && getEnv("NODE_ENV", "development") === "development"
? `:${Number(getEnv("API_PORT", "5555"))}`
: ""
}/v2`,
port: apiPort,
path: apiUrl,
url: `${apiUrl}${portSuffix}/v2`,
keyPrefix: getEnv("API_KEY_PREFIX", "cal_"),
licenseKey: getEnv("CALCOM_LICENSE_KEY", ""),
licenseKeyUrl: getEnv("GET_LICENSE_KEY_URL", "https://console.cal.com/api/license"),
@@ -27,6 +30,7 @@ const loadConfig = (): AppConfig => {
workerReadPoolMax: getEnv("DATABASE_READ_WORKER_POOL_MAX", 4),
workerWritePoolMax: getEnv("DATABASE_WRITE_WORKER_POOL_MAX", 6),
redisUrl: getEnv("REDIS_URL"),
usePool: getEnv("USE_POOL", "true") === "true",
},
next: {
authSecret: getEnv("NEXTAUTH_SECRET"),
@@ -40,7 +44,9 @@ const loadConfig = (): AppConfig => {
app: {
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
},
e2e: getEnv("IS_E2E", "false") === "true" ? true : false,
e2e: getEnv("IS_E2E", "false") === "true",
enableSlotsWorkers: getEnv("ENABLE_SLOTS_WORKERS", "true") === "true",
slotsWorkerPoolSize: Number(getEnv("SLOTS_WORKER_POOL_SIZE", "4")),
};
};
+3
View File
@@ -18,6 +18,7 @@ export type AppConfig = {
workerReadPoolMax: number;
workerWritePoolMax: number;
redisUrl: string;
usePool: boolean;
};
next: {
authSecret: string;
@@ -32,4 +33,6 @@ export type AppConfig = {
baseUrl: string;
};
e2e: boolean;
enableSlotsWorkers: boolean;
slotsWorkerPoolSize: number;
};
@@ -1,5 +1,20 @@
import { bootstrap } from "@/app";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { BookingResponse } from "@calcom/platform-libraries";
import { type RegularBookingCreateResult } from "@calcom/platform-libraries/bookings";
import type { ApiErrorResponse, ApiSuccessResponse } from "@calcom/platform-types";
import type { User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import 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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
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";
@@ -10,22 +25,6 @@ import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { 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";
describe("Bookings Endpoints 2024-04-15", () => {
describe("User Authenticated", () => {
@@ -349,6 +349,7 @@ export class BookingsController_2024_04_15 {
platformBookingLocation: bookingRequest.platformBookingLocation,
noEmail: bookingRequest.body.noEmail,
},
creationSource: "API_V2",
});
createdBookings.forEach(async (booking) => {
@@ -1,5 +1,22 @@
import { bootstrap } from "@/app";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
import {
GetBookingsDataEntry,
@@ -15,24 +32,6 @@ import {
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
const CLIENT_REDIRECT_URI = "http://localhost:4321";
@@ -212,10 +212,9 @@ export class BookingsController_2024_08_13 {
}
@Get("/:bookingUid/recordings")
// @Pbac(["booking.readRecordings"])
@Pbac(["booking.readRecordings"])
@Permissions([BOOKING_READ])
@UseGuards(BookingUidGuard)
// @UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
@UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Get all the recordings for the booking",
@@ -230,16 +229,13 @@ export class BookingsController_2024_08_13 {
return {
status: SUCCESS_STATUS,
data: recordings,
message:
"This endpoint will require authentication in a future release. Please update your integration to include valid credentials. See https://cal.com/docs/api-reference/v2/introduction#authentication for details.",
};
}
@Get("/:bookingUid/transcripts")
// @Pbac(["booking.readRecordings"])
@Pbac(["booking.readRecordings"])
@Permissions([BOOKING_READ])
@UseGuards(BookingUidGuard)
// @UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
@UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Get Cal Video real time transcript download links for the booking",
@@ -258,8 +254,6 @@ export class BookingsController_2024_08_13 {
return {
status: SUCCESS_STATUS,
data: transcripts ?? [],
message:
"This endpoint will require authentication in a future release. Please update your integration to include valid credentials. See https://cal.com/docs/api-reference/v2/introduction#authentication for details.",
};
}
@@ -280,7 +274,7 @@ export class BookingsController_2024_08_13 {
const { bookings, pagination } = await this.bookingsService.getBookings(queryParams, {
email: user.email,
id: user.id,
orgId: profile?.organizationId,
orgId: profile?.organizationId ?? undefined,
});
return {
@@ -397,18 +391,18 @@ export class BookingsController_2024_08_13 {
<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 markNoShow(
@Param("bookingUid") bookingUid: string,
@Body() body: MarkAbsentBookingInput_2024_08_13,
@GetUser() user: ApiAuthGuardUser
): Promise<MarkAbsentBookingOutput_2024_08_13> {
const booking = await this.bookingsService.markAbsent(bookingUid, user.id, body, user.uuid);
async markNoShow(
@Param("bookingUid") bookingUid: string,
@Body() body: MarkAbsentBookingInput_2024_08_13,
@GetUser() user: ApiAuthGuardUser
): Promise<MarkAbsentBookingOutput_2024_08_13> {
const booking = await this.bookingsService.markAbsent(bookingUid, user.id, body, user.uuid);
return {
status: SUCCESS_STATUS,
data: booking,
};
}
return {
status: SUCCESS_STATUS,
data: booking,
};
}
@Post("/:bookingUid/reassign")
@HttpCode(HttpStatus.OK)
@@ -1,25 +1,3 @@
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,
@@ -32,7 +10,28 @@ import {
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";
import type { Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import 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 { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
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";
const attendeeAddGuestsEmailSpy = jest
.spyOn(AttendeeAddGuestsEmail.prototype, "getHtml")
@@ -1,5 +1,32 @@
import { bootstrap } from "@/app";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
AttendeeCancelledEmail,
AttendeeRescheduledEmail,
AttendeeScheduledEmail,
OrganizerCancelledEmail,
OrganizerRescheduledEmail,
OrganizerScheduledEmail,
} from "@calcom/platform-libraries/emails";
import {
BookingOutput_2024_08_13,
CancelBookingInput_2024_08_13,
CreateBookingInput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import 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 { 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 { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
@@ -9,34 +36,6 @@ import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { 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 {
AttendeeScheduledEmail,
OrganizerScheduledEmail,
AttendeeRescheduledEmail,
OrganizerRescheduledEmail,
AttendeeCancelledEmail,
OrganizerCancelledEmail,
} from "@calcom/platform-libraries/emails";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
CancelBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { User, Team } from "@calcom/prisma/client";
jest.spyOn(AttendeeScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
return "<html><body>Mocked Email Content</body></html>";
@@ -177,6 +176,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -216,7 +216,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
expect(data.start).toEqual(body.start);
@@ -262,7 +262,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
@@ -1,16 +1,9 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type { Booking, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import 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";
@@ -19,9 +12,15 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type { Booking, User, PlatformOAuthClient, Team } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Booking access authorization", () => {
@@ -186,51 +185,51 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
// describe("GET /v2/bookings/:bookingUid/recordings - Authorization", () => {
// it("should allow booking organizer to access recordings", async () => {
// const calVideoService = app.get(CalVideoService);
// jest.spyOn(calVideoService, "getRecordings").mockResolvedValue([]);
describe("GET /v2/bookings/:bookingUid/recordings - Authorization", () => {
it("should allow booking organizer to access recordings", async () => {
const calVideoService = app.get(CalVideoService);
jest.spyOn(calVideoService, "getRecordings").mockResolvedValue([]);
// const response = await request(app.getHttpServer())
// .get(`/v2/bookings/${testBooking.uid}/recordings`)
// .set("Authorization", `Bearer ${ownerApiKey}`)
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
// .expect(200);
const response = await request(app.getHttpServer())
.get(`/v2/bookings/${testBooking.uid}/recordings`)
.set("Authorization", `Bearer ${ownerApiKey}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200);
// expect(response.body.status).toEqual(SUCCESS_STATUS);
// });
expect(response.body.status).toEqual(SUCCESS_STATUS);
});
// it("should return 403 when unauthorized user tries to access recordings", async () => {
// await request(app.getHttpServer())
// .get(`/v2/bookings/${testBooking.uid}/recordings`)
// .set("Authorization", `Bearer ${unauthorizedApiKey}`)
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
// .expect(403);
// });
// });
it("should return 403 when unauthorized user tries to access recordings", async () => {
await request(app.getHttpServer())
.get(`/v2/bookings/${testBooking.uid}/recordings`)
.set("Authorization", `Bearer ${unauthorizedApiKey}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(403);
});
});
// describe("GET /v2/bookings/:bookingUid/transcripts - Authorization", () => {
// it("should allow booking organizer to access transcripts", async () => {
// const calVideoService = app.get(CalVideoService);
// jest.spyOn(calVideoService, "getTranscripts").mockResolvedValue([]);
describe("GET /v2/bookings/:bookingUid/transcripts - Authorization", () => {
it("should allow booking organizer to access transcripts", async () => {
const calVideoService = app.get(CalVideoService);
jest.spyOn(calVideoService, "getTranscripts").mockResolvedValue([]);
// const response = await request(app.getHttpServer())
// .get(`/v2/bookings/${testBooking.uid}/transcripts`)
// .set("Authorization", `Bearer ${ownerApiKey}`)
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
// .expect(200);
const response = await request(app.getHttpServer())
.get(`/v2/bookings/${testBooking.uid}/transcripts`)
.set("Authorization", `Bearer ${ownerApiKey}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200);
// expect(response.body.status).toEqual(SUCCESS_STATUS);
// });
expect(response.body.status).toEqual(SUCCESS_STATUS);
});
// it("should return 403 when unauthorized user tries to access transcripts", async () => {
// await request(app.getHttpServer())
// .get(`/v2/bookings/${testBooking.uid}/transcripts`)
// .set("Authorization", `Bearer ${unauthorizedApiKey}`)
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
// .expect(403);
// });
// });
it("should return 403 when unauthorized user tries to access transcripts", async () => {
await request(app.getHttpServer())
.get(`/v2/bookings/${testBooking.uid}/transcripts`)
.set("Authorization", `Bearer ${unauthorizedApiKey}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(403);
});
});
afterAll(async () => {
await bookingsRepositoryFixture.deleteById(testBooking.id);
@@ -1,16 +1,15 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { 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 { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
BookingOutput_2024_08_13,
CreateBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import type { Booking, EventType, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import 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";
@@ -18,11 +17,15 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.
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 { 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";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
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";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Booking fields", () => {
@@ -775,7 +778,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
if (responseDataIsBooking(responseBody.data)) {
// eslint-disable-next-line
// @ts-ignore
// @ts-expect-error
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
expect(data.attendees[0].name).toEqual(`${splitName.firstName} ${splitName.lastName}`);
expect(data.attendees[0].bookingFieldsResponses.name).toEqual(splitName);
@@ -1197,4 +1200,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;
}
});
});
@@ -1,15 +1,9 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
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 { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type { Booking, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import 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";
@@ -17,9 +11,14 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.
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 { User, Booking, PlatformOAuthClient, Team } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
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";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Calendar Links", () => {
@@ -1,15 +1,10 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
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 { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import { BookingOutput_2024_08_13, GetBookingOutput_2024_08_13 } from "@calcom/platform-types";
import type { Booking, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import 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";
@@ -17,10 +12,14 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.
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 { BookingOutput_2024_08_13, GetBookingOutput_2024_08_13 } from "@calcom/platform-types";
import type { User, Booking, PlatformOAuthClient, Team } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
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";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Bookings confirmation", () => {
@@ -1,5 +1,35 @@
import { bootstrap } from "@/app";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
AttendeeCancelledEmail,
AttendeeDeclinedEmail,
AttendeeRequestEmail,
AttendeeRescheduledEmail,
AttendeeScheduledEmail,
OrganizerCancelledEmail,
OrganizerRequestEmail,
OrganizerRescheduledEmail,
OrganizerScheduledEmail,
} from "@calcom/platform-libraries/emails";
import type {
BookingOutput_2024_08_13,
CreateBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import 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 { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
@@ -8,37 +38,6 @@ import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15
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, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
OrganizerScheduledEmail,
AttendeeScheduledEmail,
OrganizerRescheduledEmail,
AttendeeRescheduledEmail,
OrganizerCancelledEmail,
AttendeeCancelledEmail,
AttendeeRequestEmail,
OrganizerRequestEmail,
AttendeeDeclinedEmail,
} from "@calcom/platform-libraries/emails";
import type {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import type { User, Team } from "@calcom/prisma/client";
jest
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
@@ -1,27 +1,3 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { 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";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
AttendeeCancelledEmail,
@@ -39,7 +15,30 @@ import type {
CreateBookingInput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { User, Team } from "@calcom/prisma/client";
import type { Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import 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";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
// Mock all email sending prototypes
jest
@@ -1,5 +1,32 @@
import { bootstrap } from "@/app";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
AttendeeCancelledEmail,
AttendeeRescheduledEmail,
AttendeeScheduledEmail,
OrganizerCancelledEmail,
OrganizerRescheduledEmail,
OrganizerScheduledEmail,
} from "@calcom/platform-libraries/emails";
import type {
BookingOutput_2024_08_13,
CancelBookingInput_2024_08_13,
CreateBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import 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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
@@ -10,34 +37,6 @@ import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
OrganizerScheduledEmail,
AttendeeScheduledEmail,
OrganizerRescheduledEmail,
AttendeeRescheduledEmail,
OrganizerCancelledEmail,
AttendeeCancelledEmail,
} from "@calcom/platform-libraries/emails";
import type {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
CancelBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { User, Team } from "@calcom/prisma/client";
jest
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
@@ -1,29 +1,3 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { Locales } from "@/lib/enums/locales";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import {
CreateManagedUserData,
CreateManagedUserOutput,
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import {
CAL_API_VERSION_HEADER,
SUCCESS_STATUS,
@@ -40,6 +14,31 @@ import {
GetBookingsOutput_2024_08_13,
} from "@calcom/platform-types";
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { Locales } from "@/lib/enums/locales";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import {
CreateManagedUserData,
CreateManagedUserOutput,
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UsersModule } from "@/modules/users/users.module";
const CLIENT_REDIRECT_URI = "http://localhost:4321";
@@ -356,7 +355,7 @@ describe("Managed user bookings 2024-08-13", () => {
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
@@ -369,7 +368,7 @@ describe("Managed user bookings 2024-08-13", () => {
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
@@ -408,7 +407,7 @@ describe("Managed user bookings 2024-08-13", () => {
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
@@ -421,7 +420,7 @@ describe("Managed user bookings 2024-08-13", () => {
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
@@ -435,7 +434,7 @@ describe("Managed user bookings 2024-08-13", () => {
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getThirdManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
@@ -473,7 +472,7 @@ describe("Managed user bookings 2024-08-13", () => {
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
@@ -486,7 +485,7 @@ describe("Managed user bookings 2024-08-13", () => {
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
@@ -525,7 +524,7 @@ describe("Managed user bookings 2024-08-13", () => {
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
@@ -538,7 +537,7 @@ describe("Managed user bookings 2024-08-13", () => {
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
@@ -552,7 +551,7 @@ describe("Managed user bookings 2024-08-13", () => {
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getThirdManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
@@ -568,7 +567,7 @@ describe("Managed user bookings 2024-08-13", () => {
const thirdManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const thirdManagedUserAttendeeBookings: BookingOutput_2024_08_13[] =
thirdManagedUserAttendeeBookingsResponseBody.data;
expect(thirdManagedUserBookingsCount).toEqual(2);
@@ -584,7 +583,7 @@ describe("Managed user bookings 2024-08-13", () => {
const thirdManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const thirdManagedUserAttendeeBookings: BookingOutput_2024_08_13[] =
thirdManagedUserAttendeeBookingsResponseBody.data;
expect(thirdManagedUserBookingsCount).toEqual(2);
@@ -600,7 +599,7 @@ describe("Managed user bookings 2024-08-13", () => {
const secondManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const secondManagedUserAttendeeBookings: BookingOutput_2024_08_13[] =
secondManagedUserAttendeeBookingsResponseBody.data;
expect(secondManagedUserBookingsCount).toEqual(4);
@@ -616,7 +615,7 @@ describe("Managed user bookings 2024-08-13", () => {
const secondManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const secondManagedUserAttendeeBookings: BookingOutput_2024_08_13[] =
secondManagedUserAttendeeBookingsResponseBody.data;
expect(secondManagedUserBookingsCount).toEqual(4);
@@ -624,6 +623,91 @@ describe("Managed user bookings 2024-08-13", () => {
});
});
describe("displayEmail fields", () => {
it("should return displayEmail without CUID suffix for host", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 7, 10, 0, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: "External Attendee",
email: "external-display-email-test@example.com",
timeZone: "Europe/Rome",
language: "en",
},
location: "https://meet.google.com/abc-def-ghi",
};
const response = await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingData = response.body.data;
// Host email should have CUID, displayEmail should not
expect(bookingData.hosts[0].email).toEqual(firstManagedUser.user.email);
expect(bookingData.hosts[0].displayEmail).toEqual(firstManagedUserEmail);
});
it("should return displayEmail without CUID suffix for attendee", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 7, 10, 30, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: secondManagedUser.user.name!,
email: secondManagedUser.user.email,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
location: "https://meet.google.com/abc-def-ghi",
};
const response = await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingData = response.body.data;
// Attendee email should have CUID, displayEmail should not
expect(bookingData.attendees[0].email).toEqual(secondManagedUser.user.email);
expect(bookingData.attendees[0].displayEmail).toEqual(secondManagedUserEmail);
// bookingFieldsResponses should also have displayEmail
expect(bookingData.bookingFieldsResponses.email).toEqual(secondManagedUser.user.email);
expect(bookingData.bookingFieldsResponses.displayEmail).toEqual(secondManagedUserEmail);
});
it("should return displayGuests without CUID suffix", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 7, 11, 0, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: "External Attendee",
email: "external-display-guests-test@example.com",
timeZone: "Europe/Rome",
language: "en",
},
guests: [secondManagedUser.user.email],
location: "https://meet.google.com/abc-def-ghi",
};
const response = await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingData = response.body.data;
// guests should have CUID, displayGuests should not
expect(bookingData.bookingFieldsResponses.guests).toContain(secondManagedUser.user.email);
expect(bookingData.bookingFieldsResponses.displayGuests).toContain(secondManagedUserEmail);
});
});
describe("booking confirmation by org admin", () => {
it("should allow org admin managed user to confirm booking using access token", async () => {
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
@@ -1,17 +1,10 @@
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";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
import type { Booking, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import 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";
@@ -23,10 +16,16 @@ 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 { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
import type { Booking, User, PlatformOAuthClient, Team } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
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";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Reassign bookings", () => {
@@ -492,6 +491,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
id: teamUser2.id,
name: teamUser2.name,
email: teamUser2.email,
displayEmail: teamUser2.email,
});
const reassigned = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid);
@@ -532,6 +532,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
id: teamUser1.id,
name: teamUser1.name,
email: teamUser1.email,
displayEmail: teamUser1.email,
});
const reassigned = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid);
@@ -664,7 +665,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(booking).toBeDefined();
expect(booking?.userId).toBeDefined();
const initialHostId = booking!.userId!;
const initialHostId = booking?.userId!;
rescheduleReasonBookingInitialHostId = initialHostId;
const expectedInitialTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${
@@ -706,7 +707,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(booking).toBeDefined();
expect(booking?.userId).toBeDefined();
const currentHostId = booking!.userId!;
const currentHostId = booking?.userId!;
const initialHostId = rescheduleReasonBookingInitialHostId;
const initialHostName = initialHostId === teamUser1.id ? teamUser1.name : teamUser2.name;
@@ -1,16 +1,15 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { 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 { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import { AttendeeCancelledEmail, OrganizerCancelledEmail } from "@calcom/platform-libraries/emails";
import type {
CancelBookingInput_2024_08_13,
CreateRecurringBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import 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";
@@ -18,15 +17,15 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.
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 { AttendeeCancelledEmail, OrganizerCancelledEmail } from "@calcom/platform-libraries/emails";
import type {
CreateRecurringBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
CancelBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { User, PlatformOAuthClient, Team } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
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";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Creating recurring bookings", () => {
@@ -185,6 +184,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -206,6 +206,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -262,6 +263,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -284,6 +286,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -304,6 +307,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(thirdBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -1,5 +1,21 @@
import { bootstrap } from "@/app";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type {
BookingOutput_2024_08_13,
CreateBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { EventType, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
@@ -8,23 +24,6 @@ import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import type { EventType, User } from "@calcom/prisma/client";
describe("Reschedule bookings 2024-08-13", () => {
let app: INestApplication;
@@ -1,5 +1,29 @@
import { bootstrap } from "@/app";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type {
CancelBookingInput_2024_08_13,
CancelSeatedBookingInput_2024_08_13,
CreateBookingInput_2024_08_13,
CreateSeatedBookingOutput_2024_08_13,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { DateTime } from "luxon";
import request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
@@ -8,31 +32,6 @@ import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15
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 { DateTime } from "luxon";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type {
CancelBookingInput_2024_08_13,
CancelSeatedBookingInput_2024_08_13,
CreateSeatedBookingOutput_2024_08_13,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
RescheduleSeatedBookingInput_2024_08_13,
CreateBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { Team, User } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Seated bookings", () => {
@@ -201,6 +200,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -276,6 +276,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstAttendee).toEqual({
name: createdSeatedBooking.attendees[0].name,
email: createdSeatedBooking.attendees[0].email,
displayEmail: createdSeatedBooking.attendees[0].displayEmail,
timeZone: createdSeatedBooking.attendees[0].timeZone,
language: createdSeatedBooking.attendees[0].language,
absent: false,
@@ -291,6 +292,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondAttendee).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -329,7 +331,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
const expected = structuredClone(createdSeatedBooking);
// note(Lauris): seatUid in get response resides only in each attendee object
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
delete expected.seatUid;
expect(data).toEqual(expected);
} else {
@@ -356,7 +358,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
const seatedBookingExpected = structuredClone(createdSeatedBooking);
// note(Lauris): seatUid in get response resides only in each attendee object
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
delete seatedBookingExpected.seatUid;
expect(seatedBooking).toEqual(seatedBookingExpected);
});
@@ -418,6 +420,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: attendee?.name,
email: attendee?.email,
displayEmail: attendee?.displayEmail,
timeZone: attendee?.timeZone,
language: attendee?.language,
absent: false,
@@ -841,6 +844,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -920,7 +924,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
function responseDataIsCreateSeatedBooking(data: any): data is CreateSeatedBookingOutput_2024_08_13 {
return data.hasOwnProperty("seatUid");
return Object.hasOwn(data, "seatUid");
}
function responseDataIsGetSeatedBooking(data: any): data is GetSeatedBookingOutput_2024_08_13 {
@@ -1,17 +1,17 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type {
BookingOutput_2024_08_13,
CreateBookingInput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
RecurringBookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import request from "supertest";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
@@ -23,17 +23,16 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
RecurringBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { User, PlatformOAuthClient, Team } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Team bookings", () => {
@@ -519,6 +518,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -573,6 +573,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: "919876543210@sms.cal.com",
displayEmail: "919876543210@sms.cal.com",
phoneNumber: body.attendee.phoneNumber,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
@@ -629,6 +630,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -636,6 +638,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[1]).toEqual({
name: teamUser2.name,
email: teamUser2.email,
displayEmail: teamUser2.email,
timeZone: teamUser2.timeZone,
language: teamUser2.locale,
absent: false,
@@ -690,6 +693,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -744,6 +748,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -927,6 +932,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: phoneBasedBooking.attendees[0].name,
email: phoneBasedBooking.attendees[0].email,
displayEmail: phoneBasedBooking.attendees[0].displayEmail,
phoneNumber: phoneBasedBooking.attendees[0].phoneNumber,
timeZone: phoneBasedBooking.attendees[0].timeZone,
language: phoneBasedBooking.attendees[0].language,
@@ -1010,6 +1016,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -1,16 +1,17 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { UpdateBookingLocationOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/update-location.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 {
CAL_API_VERSION_HEADER,
ERROR_STATUS,
SUCCESS_STATUS,
VERSION_2024_08_13,
} from "@calcom/platform-constants";
import type { BookingOutput_2024_08_13 } from "@calcom/platform-types";
import { UpdateBookingLocationInput_2024_08_13 } from "@calcom/platform-types";
import type { Team } from "@calcom/prisma/client";
import { Booking } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import 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";
@@ -18,17 +19,15 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import {
CAL_API_VERSION_HEADER,
ERROR_STATUS,
SUCCESS_STATUS,
VERSION_2024_08_13,
} from "@calcom/platform-constants";
import { UpdateBookingLocationInput_2024_08_13 } from "@calcom/platform-types";
import type { BookingOutput_2024_08_13 } from "@calcom/platform-types";
import { Booking } from "@calcom/prisma/client";
import type { Team } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { UpdateBookingLocationOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/update-location.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";
type TestUser = {
id: number;
@@ -1,5 +1,42 @@
import { bootstrap } from "@/app";
import {
CAL_API_VERSION_HEADER,
SUCCESS_STATUS,
VERSION_2024_06_14,
VERSION_2024_08_13,
X_CAL_CLIENT_ID,
} from "@calcom/platform-constants";
import { EventManager } from "@calcom/platform-libraries/event-types";
import type {
BookingOutput_2024_08_13,
CancelBookingInput_2024_08_13,
CreateBookingInput_2024_08_13,
CreateEventTypeInput_2024_06_14,
CreateRecurringBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
MarkAbsentBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { FAILED_EVENT_TYPE_IDENTIFICATION_ERROR_MESSAGE } from "@calcom/platform-types";
import type { Booking, EventType, PlatformOAuthClient, Team, User, Workflow } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { advanceTo, clear } from "jest-date-mock";
import 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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { WorkflowRepositoryFixture } from "test/fixtures/repository/workflow.repository.fixture";
import { WorkflowReminderRepositoryFixture } from "test/fixtures/repository/workflow-reminder.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
@@ -12,44 +49,6 @@ import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15
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 { advanceTo, clear } from "jest-date-mock";
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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { WorkflowReminderRepositoryFixture } from "test/fixtures/repository/workflow-reminder.repository.fixture";
import { WorkflowRepositoryFixture } from "test/fixtures/repository/workflow.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import {
CAL_API_VERSION_HEADER,
SUCCESS_STATUS,
VERSION_2024_06_14,
VERSION_2024_08_13,
X_CAL_CLIENT_ID,
} from "@calcom/platform-constants";
import { EventManager } from "@calcom/platform-libraries/event-types";
import { FAILED_EVENT_TYPE_IDENTIFICATION_ERROR_MESSAGE } from "@calcom/platform-types";
import type {
CreateEventTypeInput_2024_06_14,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
CreateRecurringBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
MarkAbsentBookingInput_2024_08_13,
CancelBookingInput_2024_08_13,
} from "@calcom/platform-types";
import type { EventType, User, Workflow, Booking, PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
describe("User bookings", () => {
@@ -483,6 +482,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -493,8 +493,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.bookingFieldsResponses).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
...body.bookingFieldsResponses,
guests: body.guests,
displayGuests: body.guests,
location: {
optionValue: googleMeetUrl,
value: "link",
@@ -566,6 +568,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(firstBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -588,6 +591,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(secondBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -609,6 +613,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(thirdBooking.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -1518,7 +1523,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const data: RecurringBookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
@@ -1598,7 +1603,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const data: RecurringBookingOutput_2024_08_13 = responseBody.data;
const booking = createdRecurringBooking[1];
expect(data.absentHost).toEqual(true);
@@ -1633,7 +1638,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const data: RecurringBookingOutput_2024_08_13 = responseBody.data;
const booking = createdRecurringBooking[2];
@@ -1713,7 +1718,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
@@ -2077,6 +2082,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
@@ -2087,8 +2093,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.bookingFieldsResponses).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
...body.bookingFieldsResponses,
guests: body.guests,
displayGuests: body.guests,
location: {
optionValue: googleMeetUrl,
value: "link",
@@ -2527,6 +2535,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
phoneNumber: body.attendee.phoneNumber,
@@ -2535,6 +2544,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.bookingFieldsResponses).toEqual({
name: body.attendee.name,
email: body.attendee.email,
displayEmail: body.attendee.email,
attendeePhoneNumber: body.attendee.phoneNumber,
smsReminderNumber: body.attendee.phoneNumber,
});
@@ -3131,24 +3141,22 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
const calVideoService = app.get(CalVideoService);
jest
.spyOn(calVideoService, "getVideoSessions")
.mockResolvedValue([
{
id: mockSessions[0].id,
room: mockSessions[0].room,
startTime: mockSessions[0].start_time,
duration: mockSessions[0].duration,
ongoing: mockSessions[0].ongoing,
maxParticipants: mockSessions[0].max_participants,
participants: mockSessions[0].participants.map((p) => ({
userId: p.user_id,
userName: p.user_name,
joinTime: p.join_time,
duration: p.duration,
})),
},
]);
jest.spyOn(calVideoService, "getVideoSessions").mockResolvedValue([
{
id: mockSessions[0].id,
room: mockSessions[0].room,
startTime: mockSessions[0].start_time,
duration: mockSessions[0].duration,
ongoing: mockSessions[0].ongoing,
maxParticipants: mockSessions[0].max_participants,
participants: mockSessions[0].participants.map((p) => ({
userId: p.user_id,
userName: p.user_name,
joinTime: p.join_time,
duration: p.duration,
})),
},
]);
const response = await request(app.getHttpServer())
.get(`/v2/bookings/${booking.uid}/conferencing-sessions`)
@@ -1,16 +1,15 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { 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 { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type {
BookingOutput_2024_08_13,
CreateBookingInput_2024_08_13,
CreateRecurringBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import type { EventType, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import 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";
@@ -18,15 +17,15 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import type {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
CreateRecurringBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import type { PlatformOAuthClient, Team, EventType, User } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
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";
describe("Bookings Endpoints 2024-08-13", () => {
describe("User bookings", () => {
@@ -33,6 +33,7 @@ export class BookingGuestsService_2024_08_13 {
ctx: { user },
input: { bookingId: booking.id, guests: input.guests },
emailsEnabled,
actionSource: "API_V2",
});
if (res.message === "Guests added") {
return await this.bookingsService.getBooking(bookingUid, user);
@@ -37,7 +37,6 @@ import { BadRequestException } from "@nestjs/common";
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 {
@@ -69,6 +68,7 @@ import {
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";
import { makeUserActor } from "@calcom/platform-libraries/bookings";
type CreatedBooking = {
hosts: { id: number }[];
@@ -115,7 +115,7 @@ export class BookingsService_2024_08_13 {
private readonly recurringBookingService: RecurringBookingService,
private readonly instantBookingCreateService: InstantBookingCreateService,
private readonly eventTypeAccessService: EventTypeAccessService
) {}
) { }
async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) {
let bookingTeamEventType = false;
@@ -321,8 +321,7 @@ export class BookingsService_2024_08_13 {
const allowedOptionValues = eventTypeBookingField.options.map((opt) => opt.value);
if (!this.isValidSingleOptionValue(submittedValue, allowedOptionValues)) {
throw new BadRequestException(
`Invalid option '${submittedValue}' for booking field '${
eventTypeBookingField.name
`Invalid option '${submittedValue}' for booking field '${eventTypeBookingField.name
}'. Allowed options are: ${allowedOptionValues.join(", ")}.`
);
}
@@ -336,8 +335,7 @@ export class BookingsService_2024_08_13 {
const allowedOptionValues = eventTypeBookingField.options.map((opt) => opt.value);
if (!this.areValidMultipleOptionValues(submittedValues, allowedOptionValues)) {
throw new BadRequestException(
`One or more invalid options for booking field '${
eventTypeBookingField.name
`One or more invalid options for booking field '${eventTypeBookingField.name
}'. Allowed options are: ${allowedOptionValues.join(", ")}.`
);
}
@@ -351,8 +349,7 @@ export class BookingsService_2024_08_13 {
const allowedOptionValues = eventTypeBookingField.options.map((opt) => opt.value);
if (!this.areValidMultipleOptionValues(submittedValues, allowedOptionValues)) {
throw new BadRequestException(
`One or more invalid options for booking field '${
eventTypeBookingField.name
`One or more invalid options for booking field '${eventTypeBookingField.name
}'. Allowed options are: ${allowedOptionValues.join(", ")}.`
);
}
@@ -367,8 +364,7 @@ export class BookingsService_2024_08_13 {
const allowedOptionValues = eventTypeBookingField.options.map((opt) => opt.value);
if (!this.isValidSingleOptionValue(submittedValue, allowedOptionValues)) {
throw new BadRequestException(
`Invalid option '${submittedValue}' for booking field '${
eventTypeBookingField.name
`Invalid option '${submittedValue}' for booking field '${eventTypeBookingField.name
}'. Allowed options are: ${allowedOptionValues.join(", ")}.`
);
}
@@ -464,6 +460,7 @@ export class BookingsService_2024_08_13 {
noEmail: bookingRequest.noEmail,
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
},
creationSource: "API_V2",
});
const ids = bookings.map((booking) => booking.id || 0);
return this.outputService.getOutputRecurringBookings(ids);
@@ -488,6 +485,7 @@ export class BookingsService_2024_08_13 {
platformBookingLocation: bookingRequest.platformBookingLocation,
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
},
creationSource: "API_V2",
});
return this.outputService.getOutputCreateRecurringSeatedBookings(
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" })),
@@ -921,40 +919,40 @@ export class BookingsService_2024_08_13 {
return await this.getBooking(recurringBookingUid, authUser);
}
async markAbsent(
bookingUid: string,
bookingOwnerId: number,
body: MarkAbsentBookingInput_2024_08_13,
userUuid?: string
) {
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
async markAbsent(
bookingUid: string,
bookingOwnerId: number,
body: MarkAbsentBookingInput_2024_08_13,
userUuid?: string
) {
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
if (!bookingBefore) {
throw new NotFoundException(`Booking with uid=${bookingUid} not found.`);
}
if (!bookingBefore) {
throw new NotFoundException(`Booking with uid=${bookingUid} not found.`);
}
const nowUtc = DateTime.utc();
const bookingStartTimeUtc = DateTime.fromJSDate(bookingBefore.startTime, { zone: "utc" });
const nowUtc = DateTime.utc();
const bookingStartTimeUtc = DateTime.fromJSDate(bookingBefore.startTime, { zone: "utc" });
if (nowUtc < bookingStartTimeUtc) {
throw new BadRequestException(
`Bookings can only be marked as absent after their scheduled start time. Current time in UTC+0: ${nowUtc.toISO()}, Booking start time in UTC+0: ${bookingStartTimeUtc.toISO()}`
);
}
if (nowUtc < bookingStartTimeUtc) {
throw new BadRequestException(
`Bookings can only be marked as absent after their scheduled start time. Current time in UTC+0: ${nowUtc.toISO()}, Booking start time in UTC+0: ${bookingStartTimeUtc.toISO()}`
);
}
const platformClientParams = bookingBefore?.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
: undefined;
const platformClientParams = bookingBefore?.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
: undefined;
await handleMarkNoShow({
bookingUid,
attendees: bodyTransformed.attendees,
noShowHost: bodyTransformed.noShowHost,
userId: bookingOwnerId,
userUuid,
platformClientParams,
});
await handleMarkNoShow({
bookingUid,
attendees: bodyTransformed.attendees,
noShowHost: bodyTransformed.noShowHost,
userId: bookingOwnerId,
userUuid,
platformClientParams,
});
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
@@ -1146,6 +1144,8 @@ export class BookingsService_2024_08_13 {
recurringEventId: booking.recurringEventId ?? undefined,
emailsEnabled,
platformClientParams,
actionSource: "API_V2",
actor: makeUserActor(requestUser.uuid),
},
});
@@ -1179,6 +1179,8 @@ export class BookingsService_2024_08_13 {
reason,
emailsEnabled,
platformClientParams,
actionSource: "API_V2",
actor: makeUserActor(requestUser.uuid),
},
});
@@ -93,6 +93,10 @@ type DatabaseMetadata = z.infer<typeof bookingMetadataSchema>;
export class OutputBookingsService_2024_08_13 {
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
private getDisplayEmail(email: string): string {
return email.replace(/\+[a-zA-Z0-9]{25}/, "");
}
async getOutputBooking(databaseBooking: DatabaseBooking) {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
@@ -127,6 +131,7 @@ export class OutputBookingsService_2024_08_13 {
attendees: databaseBooking.attendees.map((attendee) => ({
name: attendee.name,
email: attendee.email,
displayEmail: this.getDisplayEmail(attendee.email),
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
@@ -149,6 +154,25 @@ export class OutputBookingsService_2024_08_13 {
// note(Lauris): I don't know why plainToClass erases bookings responses and metadata so attaching manually
bookingTransformed.bookingFieldsResponses = bookingResponses;
bookingTransformed.metadata = this.getUserDefinedMetadata(metadata);
if (
bookingTransformed.bookingFieldsResponses?.email &&
typeof bookingTransformed.bookingFieldsResponses.email === "string"
) {
bookingTransformed.bookingFieldsResponses.displayEmail = this.getDisplayEmail(
bookingTransformed.bookingFieldsResponses.email
);
}
if (
bookingTransformed.bookingFieldsResponses?.guests &&
Array.isArray(bookingTransformed.bookingFieldsResponses.guests)
) {
bookingTransformed.bookingFieldsResponses.displayGuests = bookingTransformed.bookingFieldsResponses.guests.map(
(guest: string) => this.getDisplayEmail(guest)
);
}
return bookingTransformed;
}
@@ -192,12 +216,14 @@ export class OutputBookingsService_2024_08_13 {
id: "unknown",
name: "unknown",
email: "unknown",
displayEmail: "unknown",
username: "unknown",
};
}
return {
...user,
displayEmail: this.getDisplayEmail(user.email),
username: user.username || "unknown",
};
}
@@ -249,6 +275,7 @@ export class OutputBookingsService_2024_08_13 {
attendees: databaseBooking.attendees.map((attendee) => ({
name: attendee.name,
email: attendee.email,
displayEmail: this.getDisplayEmail(attendee.email),
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
@@ -272,6 +299,25 @@ export class OutputBookingsService_2024_08_13 {
// note(Lauris): I don't know why plainToClass erases bookings responses and metadata so attaching manually
bookingTransformed.bookingFieldsResponses = bookingResponses;
bookingTransformed.metadata = this.getUserDefinedMetadata(metadata);
if (
bookingTransformed.bookingFieldsResponses?.email &&
typeof bookingTransformed.bookingFieldsResponses.email === "string"
) {
bookingTransformed.bookingFieldsResponses.displayEmail = this.getDisplayEmail(
bookingTransformed.bookingFieldsResponses.email
);
}
if (
bookingTransformed.bookingFieldsResponses?.guests &&
Array.isArray(bookingTransformed.bookingFieldsResponses.guests)
) {
bookingTransformed.bookingFieldsResponses.displayGuests = bookingTransformed.bookingFieldsResponses.guests.map(
(guest: string) => this.getDisplayEmail(guest)
);
}
return bookingTransformed;
}
@@ -337,6 +383,7 @@ export class OutputBookingsService_2024_08_13 {
const attendeeData = {
name: attendee.name,
email: attendee.email,
displayEmail: this.getDisplayEmail(attendee.email),
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
@@ -465,6 +512,7 @@ export class OutputBookingsService_2024_08_13 {
const attendeeData = {
name: attendee.name,
email: attendee.email,
displayEmail: this.getDisplayEmail(attendee.email),
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
@@ -497,6 +545,9 @@ export class OutputBookingsService_2024_08_13 {
id: databaseBooking?.user?.id || 0,
name: databaseBooking?.user?.name || "unknown",
email: databaseBooking?.user?.email || "unknown",
displayEmail: databaseBooking?.user?.email
? this.getDisplayEmail(databaseBooking.user.email)
: "unknown",
},
};
}
@@ -1,5 +1,29 @@
import { bootstrap } from "@/app";
import {
GOOGLE_CALENDAR,
GOOGLE_CALENDAR_ID,
GOOGLE_CALENDAR_TYPE,
OFFICE_365_CALENDAR,
OFFICE_365_CALENDAR_ID,
OFFICE_365_CALENDAR_TYPE,
SUCCESS_STATUS,
} from "@calcom/platform-constants";
import { ICS_CALENDAR, ICS_CALENDAR_TYPE } from "@calcom/platform-constants/apps";
import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
import type { Credential, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.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 { CalendarsServiceMock } from "test/mocks/calendars-service-mock";
import { IcsCalendarServiceMock } from "test/mocks/ics-calendar-service-mock";
import { randomString } from "test/utils/randomString";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CreateIcsFeedOutput, CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output";
import { ConnectedCalendarsData } from "@/ee/calendars/outputs/connected-calendars.output";
import { DeletedCalendarCredentialsOutputResponseDto } from "@/ee/calendars/outputs/delete-calendar-credentials.output";
@@ -9,30 +33,6 @@ import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.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 { CalendarsServiceMock } from "test/mocks/calendars-service-mock";
import { IcsCalendarServiceMock } from "test/mocks/ics-calendar-service-mock";
import { randomString } from "test/utils/randomString";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import {
GOOGLE_CALENDAR,
OFFICE_365_CALENDAR,
GOOGLE_CALENDAR_TYPE,
GOOGLE_CALENDAR_ID,
} from "@calcom/platform-constants";
import { OFFICE_365_CALENDAR_ID, OFFICE_365_CALENDAR_TYPE } from "@calcom/platform-constants";
import { ICS_CALENDAR, ICS_CALENDAR_TYPE } from "@calcom/platform-constants/apps";
import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
import type { PlatformOAuthClient, Team, User, Credential } from "@calcom/prisma/client";
const CLIENT_REDIRECT_URI = "http://localhost:5555";
@@ -1,30 +1,28 @@
import { bootstrap } from "@/app";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { CreatePrivateLinkInput } from "@calcom/platform-types";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { CreatePrivateLinkInput } from "@calcom/platform-types";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { randomString } from "test/utils/randomString";
describe("Event Types Private Links Endpoints", () => {
let app: INestApplication;
let oAuthClient: any;
let _oAuthClient: any;
let organization: any;
let userRepositoryFixture: UserRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
@@ -61,7 +59,7 @@ describe("Event Types Private Links Endpoints", () => {
name: `private-links-organization-${randomString()}`,
slug: `private-links-org-slug-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
_oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
name: `private-links-user-${randomString()}`,
@@ -174,5 +172,3 @@ describe("Event Types Private Links Endpoints", () => {
await app.close();
});
});
@@ -1,43 +1,42 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { Editable } from "@/ee/event-types/event-types_2024_04_15//inputs/enums/editable";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
import { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output";
import { GetEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output";
import { GetEventTypesPublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import {
CAL_API_VERSION_HEADER,
SUCCESS_STATUS,
VERSION_2024_04_15,
VERSION_2024_06_11,
} from "@calcom/platform-constants";
import {
EventTypesByViewer,
EventTypesPublic,
eventTypeBookingFields,
eventTypeLocations,
} from "@calcom/platform-libraries/event-types";
import type { ApiSuccessResponse } from "@calcom/platform-types";
import type { EventType, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import request from "supertest";
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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import {
SUCCESS_STATUS,
VERSION_2024_06_11,
VERSION_2024_04_15,
CAL_API_VERSION_HEADER,
} from "@calcom/platform-constants";
import {
EventTypesByViewer,
eventTypeBookingFields,
eventTypeLocations,
EventTypesPublic,
} from "@calcom/platform-libraries/event-types";
import type { ApiSuccessResponse } from "@calcom/platform-types";
import type { EventType, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { Editable } from "@/ee/event-types/event-types_2024_04_15//inputs/enums/editable";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
import { GetEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output";
import { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output";
import { GetEventTypesPublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
describe("Event types Endpoints", () => {
describe("Not authenticated", () => {
@@ -429,12 +428,12 @@ describe("Event types Endpoints", () => {
await teamRepositoryFixture.delete(organization.id);
try {
await eventTypesRepositoryFixture.delete(eventType.id);
} catch (e) {
} catch (_e) {
// Event type might have been deleted by the test
}
try {
await userRepositoryFixture.delete(user.id);
} catch (e) {
} catch (_e) {
// User might have been deleted by the test
}
await app.close();
@@ -1,16 +1,32 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
import {
BookerLayoutsInputEnum_2024_06_14,
BookingWindowPeriodInputTypeEnum_2024_06_14,
ConfirmationPolicyEnum,
FrequencyInput,
NoticeThresholdUnitEnum,
} from "@calcom/platform-enums";
import { SchedulingType } from "@calcom/platform-libraries";
import {
type ApiSuccessResponse,
BaseConfirmationPolicy_2024_06_14,
type CreateEventTypeInput_2024_06_14,
type EventTypeOutput_2024_06_14,
type GuestsDefaultFieldOutput_2024_06_14,
type NameDefaultFieldInput_2024_06_14,
type NotesDefaultFieldInput_2024_06_14,
type SplitNameDefaultFieldOutput_2024_06_14,
supportedIntegrations,
TeamEventTypeOutput_2024_06_14,
type UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { FAILED_RECURRING_EVENT_TYPE_WITH_BOOKER_LIMITS_ERROR_MESSAGE } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/validators/CantHaveRecurrenceAndBookerActiveBookingsLimit";
import { REQUIRES_AT_LEAST_ONE_PROPERTY_ERROR } from "@calcom/platform-types/utils/RequiresOneOfPropertiesWhenNotDisabled";
import type { EventType, PlatformOAuthClient, Schedule, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
@@ -21,32 +37,15 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
import {
BookingWindowPeriodInputTypeEnum_2024_06_14,
BookerLayoutsInputEnum_2024_06_14,
ConfirmationPolicyEnum,
NoticeThresholdUnitEnum,
FrequencyInput,
} from "@calcom/platform-enums";
import { SchedulingType } from "@calcom/platform-libraries";
import {
BaseConfirmationPolicy_2024_06_14,
TeamEventTypeOutput_2024_06_14,
supportedIntegrations,
type ApiSuccessResponse,
type CreateEventTypeInput_2024_06_14,
type EventTypeOutput_2024_06_14,
type GuestsDefaultFieldOutput_2024_06_14,
type NameDefaultFieldInput_2024_06_14,
type NotesDefaultFieldInput_2024_06_14,
type SplitNameDefaultFieldOutput_2024_06_14,
type UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { FAILED_RECURRING_EVENT_TYPE_WITH_BOOKER_LIMITS_ERROR_MESSAGE } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/validators/CantHaveRecurrenceAndBookerActiveBookingsLimit";
import { REQUIRES_AT_LEAST_ONE_PROPERTY_ERROR } from "@calcom/platform-types/utils/RequiresOneOfPropertiesWhenNotDisabled";
import type { PlatformOAuthClient, Team, User, Schedule, EventType } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
if (a.slug < b.slug) return -1;
@@ -636,6 +635,7 @@ describe("Event types Endpoints", () => {
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
expect(createdEventType.bookingRequiresAuthentication).toEqual(true);
expect(createdEventType.bookingUrl).toContain(`/${user.username}/${body.slug}`);
expect(createdEventType.disableCancelling).toEqual({ disabled: true });
expect(createdEventType.disableRescheduling).toEqual({ disabled: true });
expect(createdEventType.calVideoSettings?.sendTranscriptionEmails).toEqual(true);
@@ -732,6 +732,7 @@ describe("Event types Endpoints", () => {
);
expect(fetchedEventType?.color).toEqual(eventType.color);
expect(fetchedEventType?.hidden).toEqual(false);
expect(fetchedEventType?.bookingUrl).toContain(`/${user.username}/${eventType.slug}`);
expect(fetchedEventType?.disableCancelling).toEqual(eventType.disableCancelling);
expect(fetchedEventType?.disableRescheduling).toEqual(eventType.disableRescheduling);
expect(fetchedEventType?.calVideoSettings?.sendTranscriptionEmails).toEqual(
@@ -1552,6 +1553,7 @@ describe("Event types Endpoints", () => {
eventType.lockTimeZoneToggleOnBookingPage
);
expect(fetchedEventType.color).toEqual(eventType.color);
expect(fetchedEventType.bookingUrl).toContain(`/${user.username}/${eventType.slug}`);
});
it("system admin can access another user's event type by id", async () => {
@@ -2678,12 +2680,19 @@ describe("Event types Endpoints", () => {
expect(createdEventType.title).toEqual(body.title);
secondCreatedEventType = responseBody.data;
const { id, title, slug, ...restFirst } = firstCreatedEventType;
const { id: id2, title: title2, slug: slug2, ...restSecond } = secondCreatedEventType;
const { id, title, slug, bookingUrl, ...restFirst } = firstCreatedEventType;
const {
id: id2,
title: title2,
slug: slug2,
bookingUrl: bookingUrl2,
...restSecond
} = secondCreatedEventType;
expect(restFirst).toEqual(restSecond);
expect(id2).not.toEqual(id);
expect(title2).not.toEqual(title);
expect(slug2).not.toEqual(slug);
expect(bookingUrl2).not.toEqual(bookingUrl);
});
});
@@ -1,14 +1,16 @@
import type { SortOrderType } from "@calcom/platform-types";
import type { Prisma } from "@calcom/prisma/client";
import { Injectable } from "@nestjs/common";
import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import type { SortOrderType } from "@calcom/platform-types";
import type { Prisma } from "@calcom/prisma/client";
@Injectable()
export class EventTypesRepository_2024_06_14 {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
constructor(
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService
) {}
async createUserEventType(
userId: number,
@@ -58,13 +60,59 @@ export class EventTypesRepository_2024_06_14 {
});
}
private readonly usersInclude = {
select: {
id: true,
name: true,
username: true,
isPlatformManaged: true,
avatarUrl: true,
brandColor: true,
darkBrandColor: true,
weekStart: true,
metadata: true,
organizationId: true,
organization: {
select: { slug: true },
},
movedToProfile: {
select: {
id: true,
username: true,
organizationId: true,
organization: {
select: {
id: true,
slug: true,
isPlatform: true,
},
},
},
},
profiles: {
select: {
id: true,
username: true,
organizationId: true,
organization: {
select: {
id: true,
slug: true,
isPlatform: true,
},
},
},
},
},
};
async getUserEventType(userId: number, eventTypeId: number) {
return this.dbRead.prisma.eventType.findFirst({
where: {
id: eventTypeId,
userId,
},
include: { users: true, schedule: true, destinationCalendar: true },
include: { users: this.usersInclude, schedule: true, destinationCalendar: true },
});
}
@@ -74,7 +122,7 @@ export class EventTypesRepository_2024_06_14 {
userId,
},
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
include: { users: true, schedule: true, destinationCalendar: true },
include: { users: this.usersInclude, schedule: true, destinationCalendar: true },
});
}
@@ -85,14 +133,19 @@ export class EventTypesRepository_2024_06_14 {
hidden: false,
},
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
include: { users: true, schedule: true, destinationCalendar: true },
include: { users: this.usersInclude, schedule: true, destinationCalendar: true },
});
}
async getEventTypeById(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { users: true, schedule: true, destinationCalendar: true, calVideoSettings: true },
include: {
users: this.usersInclude,
schedule: true,
destinationCalendar: true,
calVideoSettings: true,
},
});
}
@@ -100,7 +153,7 @@ export class EventTypesRepository_2024_06_14 {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: {
users: true,
users: this.usersInclude,
schedule: true,
destinationCalendar: true,
calVideoSettings: true,
@@ -140,7 +193,7 @@ export class EventTypesRepository_2024_06_14 {
slug: slug,
},
},
include: { users: true, schedule: true, destinationCalendar: true },
include: { users: this.usersInclude, schedule: true, destinationCalendar: true },
});
}
@@ -0,0 +1,281 @@
import { ConfigService } from "@nestjs/config";
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
jest.mock("@calcom/platform-libraries/organizations", () => ({
getBookerBaseUrlSync: jest.fn((slug: string | null) => {
if (!slug) return "https://cal.com";
return `https://${slug}.cal.com`;
}),
}));
describe("OutputEventTypesService_2024_06_14", () => {
let service: OutputEventTypesService_2024_06_14;
let configService: jest.Mocked<ConfigService>;
let usersService: UsersService;
beforeEach(() => {
configService = {
get: jest.fn((key: string, defaultValue?: string) => {
if (key === "app.baseUrl") return "https://cal.com";
return defaultValue;
}),
} as any;
const usersRepository = {} as UsersRepository;
usersService = new UsersService(usersRepository);
service = new OutputEventTypesService_2024_06_14(configService, usersService);
});
describe("buildBookingUrl", () => {
it("should return correct URL for user without organization", () => {
const user = {
id: 1,
name: "John Doe",
username: "john-doe",
avatarUrl: null,
brandColor: null,
darkBrandColor: null,
weekStart: "Monday",
metadata: {},
organizationId: null,
organization: null,
movedToProfile: null,
profiles: [],
};
const slug = "30min";
const result = service.buildBookingUrl(user, slug);
expect(result).toBe("https://cal.com/john-doe/30min");
});
it("should use profile username for org users", () => {
const user = {
id: 1,
name: "Owner",
username: "owner1-acme",
avatarUrl: null,
brandColor: null,
darkBrandColor: null,
weekStart: "Monday",
metadata: {},
organizationId: 1,
organization: { slug: "acme" },
movedToProfile: null,
profiles: [
{
id: 100,
username: "owner1",
organizationId: 1,
organization: { id: 1, slug: "acme", isPlatform: false },
},
],
};
const slug = "30min";
const result = service.buildBookingUrl(user, slug);
expect(result).toBe("https://acme.cal.com/owner1/30min");
});
it("should use cal.com for non-managed users in platform orgs", () => {
const user = {
id: 1,
name: "Dhairyashil Shinde",
username: "dhairyashil10101010-gmail-com",
avatarUrl: null,
brandColor: null,
darkBrandColor: null,
weekStart: "Monday",
metadata: {},
organizationId: 1,
organization: { slug: "gmail-platform-9041df0e" },
movedToProfile: null,
profiles: [
{
id: 100,
username: "dhairyashil",
organizationId: 1,
organization: { id: 1, slug: "gmail-platform-9041df0e", isPlatform: true },
},
],
};
const slug = "secret";
const result = service.buildBookingUrl(user, slug);
expect(result).toBe("https://cal.com/dhairyashil/secret");
});
it("should return empty string for managed users", () => {
const user = {
id: 1,
name: "Managed User",
username: "managed-user-abc123",
isPlatformManaged: true,
avatarUrl: null,
brandColor: null,
darkBrandColor: null,
weekStart: "Monday",
metadata: {},
organizationId: 1,
organization: { slug: "platform-org" },
movedToProfile: null,
profiles: [],
};
const slug = "30min";
const result = service.buildBookingUrl(user, slug);
expect(result).toBe("");
});
it("should fall back to user username when profile has no username", () => {
const user = {
id: 1,
name: "Keith",
username: "keith",
avatarUrl: null,
brandColor: null,
darkBrandColor: null,
weekStart: "Monday",
metadata: {},
organizationId: 2,
organization: { slug: "i" },
movedToProfile: null,
profiles: [
{
id: 200,
username: null,
organizationId: 2,
organization: { id: 2, slug: "i", isPlatform: false },
},
],
};
const slug = "30min";
const result = service.buildBookingUrl(user, slug);
expect(result).toBe("https://i.cal.com/keith/30min");
});
it("should return empty string when user is undefined", () => {
const user = undefined;
const slug = "30min";
const result = service.buildBookingUrl(user, slug);
expect(result).toBe("");
});
it("should handle organization with null slug", () => {
const user = {
id: 1,
name: "User",
username: "user",
avatarUrl: null,
brandColor: null,
darkBrandColor: null,
weekStart: "Monday",
metadata: {},
organizationId: 3,
organization: { slug: null },
movedToProfile: null,
profiles: [
{
id: 300,
username: "user",
organizationId: 3,
organization: { id: 3, slug: null, isPlatform: false },
},
],
};
const slug = "consultation";
const result = service.buildBookingUrl(user, slug);
expect(result).toBe("https://cal.com/user/consultation");
});
it("should handle base URL with trailing slash", () => {
const { getBookerBaseUrlSync } = require("@calcom/platform-libraries/organizations");
getBookerBaseUrlSync.mockReturnValueOnce("https://acme.cal.com/");
const user = {
id: 1,
name: "John",
username: "john-acme",
avatarUrl: null,
brandColor: null,
darkBrandColor: null,
weekStart: "Monday",
metadata: {},
organizationId: 1,
organization: { slug: "acme" },
movedToProfile: null,
profiles: [
{
id: 400,
username: "john",
organizationId: 1,
organization: { id: 1, slug: "acme", isPlatform: false },
},
],
};
const slug = "30min";
const result = service.buildBookingUrl(user, slug);
expect(result).toBe("https://acme.cal.com/john/30min");
});
it("should return empty string when username is empty", () => {
const user = {
id: 1,
name: "User",
username: "",
avatarUrl: null,
brandColor: null,
darkBrandColor: null,
weekStart: "Monday",
metadata: {},
organizationId: null,
organization: null,
movedToProfile: null,
profiles: [],
};
const slug = "30min";
const result = service.buildBookingUrl(user, slug);
expect(result).toBe("");
});
it("should return empty string when username is null", () => {
const user = {
id: 1,
name: "User",
username: null,
avatarUrl: null,
brandColor: null,
darkBrandColor: null,
weekStart: "Monday",
metadata: {},
organizationId: null,
organization: null,
movedToProfile: null,
profiles: [],
};
const slug = "30min";
const result = service.buildBookingUrl(user, slug);
expect(result).toBe("");
});
});
});
@@ -1,41 +1,66 @@
import {
transformLocationsInternalToApi,
transformBookingFieldsInternalToApi,
InternalLocationSchema,
SystemField,
CustomField,
transformIntervalLimitsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformRecurrenceInternalToApi,
transformBookerLayoutsInternalToApi,
transformRequiresConfirmationInternalToApi,
transformEventTypeColorsInternalToApi,
transformSeatsInternalToApi,
InternalLocation,
BookingFieldSchema,
} from "@/ee/event-types/event-types_2024_06_14/transformers";
import { Injectable } from "@nestjs/common";
import {
userMetadata,
getBookingFieldsWithSystemFields,
parseBookingLimit,
parseRecurringEvent,
getBookingFieldsWithSystemFields,
userMetadata,
} from "@calcom/platform-libraries";
import { EventTypeMetaDataSchema, parseEventTypeColor } from "@calcom/platform-libraries/event-types";
import { getBookerBaseUrlSync } from "@calcom/platform-libraries/organizations";
import type {
TransformFutureBookingsLimitSchema_2024_06_14,
BookerLayoutsTransformedSchema,
NoticeThresholdTransformedSchema,
EventTypeOutput_2024_06_14,
OutputUnknownLocation_2024_06_14,
OutputUnknownBookingField_2024_06_14,
NoticeThresholdTransformedSchema,
OutputBookingField_2024_06_14,
OutputUnknownBookingField_2024_06_14,
OutputUnknownLocation_2024_06_14,
TransformFutureBookingsLimitSchema_2024_06_14,
} from "@calcom/platform-types";
import type { EventType, User, Schedule, DestinationCalendar, CalVideoSettings } from "@calcom/prisma/client";
import type {
CalVideoSettings,
DestinationCalendar,
EventType,
Prisma,
Schedule,
Team,
} from "@calcom/prisma/client";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import {
BookingFieldSchema,
CustomField,
InternalLocation,
InternalLocationSchema,
SystemField,
transformBookerLayoutsInternalToApi,
transformBookingFieldsInternalToApi,
transformEventTypeColorsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformIntervalLimitsInternalToApi,
transformLocationsInternalToApi,
transformRecurrenceInternalToApi,
transformRequiresConfirmationInternalToApi,
transformSeatsInternalToApi,
} from "@/ee/event-types/event-types_2024_06_14/transformers";
import { ProfileMinimal, UsersService } from "@/modules/users/services/users.service";
type EventTypeUser = {
id: number;
name: string | null;
username: string | null;
isPlatformManaged?: boolean;
avatarUrl: string | null;
brandColor: string | null;
darkBrandColor: string | null;
weekStart: string;
metadata: Prisma.JsonValue;
organizationId: number | null;
organization?: { slug: string | null; isPlatform?: boolean } | null;
movedToProfile?: ProfileMinimal | null;
profiles?: ProfileMinimal[];
};
type EventTypeRelations = {
users: User[];
users: EventTypeUser[];
schedule: Schedule | null;
destinationCalendar?: DestinationCalendar | null;
calVideoSettings?: CalVideoSettings | null;
@@ -106,6 +131,11 @@ type Input = Pick<
@Injectable()
export class OutputEventTypesService_2024_06_14 {
constructor(
private readonly configService: ConfigService,
private readonly usersService: UsersService
) {}
getResponseEventType(
ownerId: number,
databaseEventType: Input,
@@ -184,6 +214,7 @@ export class OutputEventTypesService_2024_06_14 {
} as TransformFutureBookingsLimitSchema_2024_06_14);
const destinationCalendar = this.transformDestinationCalendar(databaseEventType.destinationCalendar);
const bookerActiveBookingsLimit = this.transformBookerActiveBookingsLimit(databaseEventType);
const bookingUrl = this.buildBookingUrl(databaseEventType.users[0], slug);
const disableReschedulingOutput = this.transformDisableRescheduling(
disableRescheduling,
minimumRescheduleNotice
@@ -239,6 +270,7 @@ export class OutputEventTypesService_2024_06_14 {
hidden,
bookingRequiresAuthentication,
bookerActiveBookingsLimit,
bookingUrl,
disableCancelling: disableCancellingOutput,
disableRescheduling: disableReschedulingOutput,
interfaceLanguage,
@@ -342,7 +374,7 @@ export class OutputEventTypesService_2024_06_14 {
return EventTypeMetaDataSchema.parse(metadata);
}
transformUsers(users: User[]) {
transformUsers(users: EventTypeUser[]) {
return users.map((user) => {
const metadata = user.metadata ? userMetadata.parse(user.metadata) : {};
return {
@@ -405,6 +437,33 @@ export class OutputEventTypesService_2024_06_14 {
});
}
buildBookingUrl(user: EventTypeUser | undefined, slug: string): string {
if (!user) {
return "";
}
// Managed users don't have public booking pages
if (user.isPlatformManaged) {
return "";
}
const profile = this.usersService.getUserMainProfile(user);
const username = profile?.username ?? user.username;
if (!username) {
return "";
}
const org = profile?.organization;
// Don't use org subdomain for platform organizations - they don't have public-facing subdomains
const orgSlug = org && !org.isPlatform ? org.slug : null;
const webAppUrl = this.configService.get<string>("app.baseUrl", "https://app.cal.com");
const baseUrl = orgSlug ? getBookerBaseUrlSync(orgSlug) : webAppUrl;
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
return `${normalizedBaseUrl}/${username}/${slug}`;
}
getResponseEventTypesWithoutHiddenFields(
eventTypes: EventTypeOutput_2024_06_14[]
): EventTypeOutput_2024_06_14[] {
@@ -1,23 +1,22 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import type { Credential, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.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 { CalendarsServiceMock } from "test/mocks/calendars-service-mock";
import type { PlatformOAuthClient, Team, User, Credential } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
const CLIENT_REDIRECT_URI = "http://localhost:5555";
+12 -13
View File
@@ -1,25 +1,24 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { UsersModule } from "@/modules/users/users.module";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ApiSuccessResponse, UserResponse } from "@calcom/platform-types";
import type { Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import request from "supertest";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { UserResponse, ApiSuccessResponse } from "@calcom/platform-types";
import type { User, Team } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { UsersModule } from "@/modules/users/users.module";
describe("Me Endpoints", () => {
describe("User Authentication", () => {
@@ -1,5 +1,16 @@
import { bootstrap } from "@/app";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_04_15 } from "@calcom/platform-constants";
import type { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
import type { User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { CreateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/create-schedule.output";
import { GetSchedulesOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-schedules.output";
@@ -9,18 +20,6 @@ import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_04_15 } from "@calcom/platform-constants";
import type { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
import type { User } from "@calcom/prisma/client";
describe("Schedules Endpoints", () => {
describe("User Authentication", () => {
@@ -214,7 +213,7 @@ describe("Schedules Endpoints", () => {
await userRepositoryFixture.deleteByEmail(user.email);
try {
await scheduleRepositoryFixture.deleteById(createdSchedule.id);
} catch (e) {}
} catch (_e) {}
await app.close();
});
@@ -1,18 +1,3 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_11 } from "@calcom/platform-constants";
import type {
CreateScheduleInput_2024_06_11,
@@ -20,10 +5,24 @@ import type {
GetScheduleOutput_2024_06_11,
GetSchedulesOutput_2024_06_11,
ScheduleOutput_2024_06_11,
UpdateScheduleOutput_2024_06_11,
UpdateScheduleInput_2024_06_11,
UpdateScheduleOutput_2024_06_11,
} from "@calcom/platform-types";
import type { User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
describe("Schedules Endpoints", () => {
describe("User Authentication", () => {
@@ -259,7 +258,7 @@ describe("Schedules Endpoints", () => {
await userRepositoryFixture.deleteByEmail(user.email);
try {
await scheduleRepositoryFixture.deleteById(createdSchedule.id);
} catch (e) {}
} catch (_e) {}
await app.close();
});
@@ -4,18 +4,34 @@ import { Injectable } from "@nestjs/common";
import type { WeekDay } from "@calcom/platform-types";
import type { Availability, Schedule } from "@calcom/prisma/client";
type DatabaseSchedule = Schedule & { availability: Availability[] };
@Injectable()
export class OutputSchedulesService_2024_06_11 {
constructor(private readonly usersRepository: UsersRepository) {}
async getResponseSchedule(databaseSchedule: Schedule & { availability: Availability[] }) {
if (!databaseSchedule.timeZone) {
databaseSchedule.timeZone = "Europe/London";
}
async getResponseSchedules(databaseSchedules: DatabaseSchedule[]) {
if (databaseSchedules.length === 0) return [];
const userIds = [...new Set(databaseSchedules.map((schedule) => schedule.userId))];
const defaultScheduleIds = await this.usersRepository.getUsersScheduleDefaultIds(userIds);
return databaseSchedules.map((schedule) =>
this.transformScheduleToOutput(schedule, defaultScheduleIds.get(schedule.userId) ?? null)
);
}
async getResponseSchedule(databaseSchedule: DatabaseSchedule) {
const ownerDefaultScheduleId = await this.usersRepository.getUserScheduleDefaultId(
databaseSchedule.userId
);
return this.transformScheduleToOutput(databaseSchedule, ownerDefaultScheduleId);
}
private transformScheduleToOutput(
databaseSchedule: DatabaseSchedule,
ownerDefaultScheduleId: number | null
) {
const timeZone = databaseSchedule.timeZone || "Europe/London";
const createdScheduleAvailabilities = databaseSchedule.availability.filter(
(availability) => !!availability.days.length
@@ -26,7 +42,7 @@ export class OutputSchedulesService_2024_06_11 {
id: databaseSchedule.id,
ownerId: databaseSchedule.userId,
name: databaseSchedule.name,
timeZone: databaseSchedule.timeZone,
timeZone,
availability: createdScheduleAvailabilities.map((availability) => ({
days: availability.days.map(transformNumberToDay),
startTime: this.padHoursMinutesWithZeros(
@@ -54,13 +70,9 @@ export class OutputSchedulesService_2024_06_11 {
};
}
padHoursMinutesWithZeros(hhMM: string) {
private padHoursMinutesWithZeros(hhMM: string) {
const [hours, minutes] = hhMM.split(":");
const formattedHours = hours.padStart(2, "0");
const formattedMinutes = minutes.padStart(2, "0");
return `${formattedHours}:${formattedMinutes}`;
return `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`;
}
}
+3
View File
@@ -38,6 +38,9 @@ export type Environment = {
DATABASE_WRITE_POOL_MAX: number;
DATABASE_READ_WORKER_POOL_MAX: number;
DATABASE_WRITE_WORKER_POOL_MAX: number;
ENABLE_SLOTS_WORKERS: string;
SLOTS_WORKER_POOL_SIZE: string;
USE_POOL: string;
};
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
@@ -14,6 +14,7 @@ import { BusyTimesService } from "@/lib/services/busy-times.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
import { OrgMembershipLookupService } from "@/lib/services/org-membership-lookup.service";
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
import { PrismaWorkerModule } from "@/modules/prisma/prisma-worker.module";
@@ -42,6 +43,7 @@ import { Module } from "@nestjs/common";
FilterHostsService,
QualifiedHostsService,
NoSlotsNotificationService,
OrgMembershipLookupService,
],
exports: [AvailableSlotsService],
})
@@ -1,10 +1,30 @@
import { RegularBookingModule } from "@/lib/modules/regular-booking.module";
import { RecurringBookingService } from "@/lib/services/recurring-booking.service";
import { Module } from "@nestjs/common";
import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service";
import { Logger } from "@/lib/logger.bridge";
import { Scope } from "@nestjs/common";
import { BookingAuditProducerService } from "@/lib/services/booking-audit-producer.service";
import { HashedLinkService } from "@/lib/services/hashed-link.service";
import { TaskerService } from "@/lib/services/tasker.service";
@Module({
imports: [RegularBookingModule],
providers: [RecurringBookingService],
providers: [
RecurringBookingService,
BookingEventHandlerService,
/** Required by BookingEventHandlerService - Starts **/
HashedLinkService,
{
provide: Logger,
useFactory: () => {
return new Logger();
},
scope: Scope.TRANSIENT,
},
BookingAuditProducerService,
TaskerService,
/** Required by BookingEventHandlerService - Ends **/
],
exports: [RecurringBookingService],
})
export class RecurringBookingModule {}
export class RecurringBookingModule { }
@@ -10,6 +10,7 @@ import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"
import { BusyTimesService } from "@/lib/services/busy-times.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
import { OrgMembershipLookupService } from "@/lib/services/org-membership-lookup.service";
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable } from "@nestjs/common";
@@ -35,7 +36,8 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
checkBookingLimitsService: CheckBookingLimitsService,
userAvailabilityService: UserAvailabilityService,
busyTimesService: BusyTimesService,
noSlotsNotificationService: NoSlotsNotificationService
noSlotsNotificationService: NoSlotsNotificationService,
orgMembershipLookupService: OrgMembershipLookupService
) {
super({
oooRepo: oooRepoDependency,
@@ -53,6 +55,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
qualifiedHostsService,
featuresRepo: featuresRepository,
noSlotsNotificationService,
orgMembershipLookup: orgMembershipLookupService,
});
}
}
@@ -0,0 +1,21 @@
import { Injectable } from "@nestjs/common";
import {
type OrgMembershipLookup,
ProfileRepository,
} from "@calcom/platform-libraries";
/**
* NestJS service that implements OrgMembershipLookup interface.
* Wraps ProfileRepository's static method for DI compatibility.
*/
@Injectable()
export class OrgMembershipLookupService implements OrgMembershipLookup {
async findFirstOrganizationIdForUser({
userId,
}: {
userId: number;
}): Promise<number | null> {
return ProfileRepository.findFirstOrganizationIdForUser({ userId });
}
}
@@ -1,13 +1,18 @@
import { RegularBookingService } from "@/lib/services/regular-booking.service";
import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service";
import { Injectable } from "@nestjs/common";
import { RecurringBookingService as BaseRecurringBookingService } from "@calcom/platform-libraries/bookings";
@Injectable()
export class RecurringBookingService extends BaseRecurringBookingService {
constructor(regularBookingService: RegularBookingService) {
constructor(
regularBookingService: RegularBookingService,
bookingEventHandler: BookingEventHandlerService
) {
super({
regularBookingService,
bookingEventHandler,
});
}
}
+97 -24
View File
@@ -1,46 +1,119 @@
import type { AppConfig } from "@/config/type";
import "dotenv/config";
import { IncomingMessage, Server, ServerResponse } from "node:http";
import { Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { NestFactory } from "@nestjs/core";
import type { NestExpressApplication } from "@nestjs/platform-express";
import "dotenv/config";
import type { Express, Request, Response } from "express";
import { WinstonModule } from "nest-winston";
import * as qs from "qs";
import qs from "qs";
import type { AppConfig } from "@/config/type";
import { bootstrap } from "./app";
import { AppModule } from "./app.module";
import { bootstrap } from "./bootstrap";
import { loggerConfig } from "./lib/logger";
import { generateSwaggerForApp } from "./swagger/generate-swagger";
run().catch((error: Error) => {
console.error("Failed to start Cal Platform API", { error: error.stack });
process.exit(1);
});
const logger: Logger = new Logger("App");
async function run() {
const app = await createNestApp();
const logger = new Logger("App");
/**
* Singleton Class to manage the NestJS App instance.
* Ensures we only initialize the app once per container lifecycle.
*/
class NestServer {
private static server: Express; // The underlying Express instance
try {
bootstrap(app);
const port = app.get(ConfigService<AppConfig, true>).get("api.port", { infer: true });
generateSwaggerForApp(app);
await app.listen(port);
logger.log(`Application started on port: ${port}`);
} catch (error) {
console.error(error);
logger.error("Application crashed", {
error,
});
private constructor() {}
/**
* Returns the cached server instance.
* If it doesn't exist, it creates, bootstraps, and initializes it.
*/
public static async getInstance(): Promise<Express> {
if (!NestServer.server) {
const app = await createNestApp();
// Execute bootstrap (Pipes, Interceptors, CORS, etc.)
bootstrap(app);
// Initialize the app (connects to DB, resolves modules)
await app.init();
// extract the Express instance to pass to Vercel
NestServer.server = app.getHttpAdapter().getInstance();
}
return NestServer.server;
}
}
export async function createNestApp() {
// -----------------------------------------------------------------------------
// LOCAL DEVELOPMENT STARTUP
// -----------------------------------------------------------------------------
if (!process.env.VERCEL) {
run().catch((error: Error) => {
logger.error("Failed to start Cal Platform API", { error: error.stack });
process.exit(1);
});
}
async function run(): Promise<void> {
const app = await createNestApp();
try {
bootstrap(app);
const config = app.get(ConfigService<AppConfig, true>);
const port = config.get("api.port", { infer: true });
if (config.get("env.type", { infer: true }) === "development") {
const { generateSwaggerForApp } = await import("./swagger/generate-swagger");
generateSwaggerForApp(app);
}
await app.listen(port);
logger.log(`Application started locally on port: ${port}`);
} catch (error) {
logger.error("Application crashed during local startup", { error });
}
}
// -----------------------------------------------------------------------------
// VERCEL SERVERLESS HANDLER
// -----------------------------------------------------------------------------
export default async (req: Request, res: Response): Promise<void> => {
try {
const server = await NestServer.getInstance();
// Vercel/AWS specific: Re-parse query strings to support array formats
// (e.g., ?ids[]=1&ids[]=2) which Vercel's native parser might simplify.
if (req.url) {
const [_path, queryString] = req.url.split("?");
if (queryString) {
req.query = qs.parse(queryString, { arrayLimit: 1000 });
}
}
// Delegate request to the cached Express instance
return server(req, res);
} catch (error) {
logger.error("Critical: Failed to initialize NestJS Serverless instance", error);
res.statusCode = 500;
res.end("Internal Server Error: Initialization Failed");
}
};
// -----------------------------------------------------------------------------
// APP FACTORY
// -----------------------------------------------------------------------------
export async function createNestApp(): Promise<
NestExpressApplication<Server<typeof IncomingMessage, typeof ServerResponse>>
> {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
logger: WinstonModule.createLogger(loggerConfig()),
// Preserved as requested:
bodyParser: false,
});
// Custom query parser configuration for the underlying Express app
app.set("query parser", (str: string) => qs.parse(str, { arrayLimit: 1000 }));
return app;
@@ -1,10 +1,10 @@
import { Injectable, NestMiddleware } from "@nestjs/common";
import * as bodyParser from "body-parser";
import { json } from "body-parser";
import type { Request, Response } from "express";
@Injectable()
export class JsonBodyMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => any) {
bodyParser.json()(req, res, next);
use(req: Request, res: Response, next: () => any): void {
json()(req, res, next);
}
}
@@ -1,10 +1,10 @@
import { Injectable, NestMiddleware } from "@nestjs/common";
import * as bodyParser from "body-parser";
import { raw } from "body-parser";
import type { Request, Response } from "express";
@Injectable()
export class RawBodyMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => any) {
bodyParser.raw({ type: "*/*" })(req, res, next);
use(req: Request, res: Response, next: () => any): void {
raw({ type: "*/*" })(req, res, next);
}
}
@@ -1,33 +1,30 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
import { AuthModule } from "@/modules/auth/auth.module";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { generateSecret } from "@calcom/platform-libraries";
import type { Membership, Team, User } from "@calcom/prisma/client";
import { AccessScope, OAuthClientType } from "@calcom/prisma/enums";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test, TestingModule } from "@nestjs/testing";
import * as nextAuthJwt from "next-auth/jwt";
import * as request from "supertest";
import { getToken } from "next-auth/jwt";
import request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuth2ClientRepositoryFixture } from "test/fixtures/repository/oauth2-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { generateSecret } from "@calcom/platform-libraries";
import type { Membership, Team, User } from "@calcom/prisma/client";
import { AccessScope, OAuthClientType } from "@calcom/prisma/enums";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
import { AuthModule } from "@/modules/auth/auth.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
// Mock next-auth/jwt getToken for ApiAuthStrategy NEXT_AUTH authentication
jest.mock("next-auth/jwt", () => ({
getToken: jest.fn(),
}));
const mockGetToken = nextAuthJwt.getToken as jest.MockedFunction<typeof nextAuthJwt.getToken>;
const mockGetToken = getToken as jest.MockedFunction<typeof getToken>;
describe("OAuth2 Controller Endpoints", () => {
describe("User Not Authenticated", () => {
@@ -140,12 +137,12 @@ describe("OAuth2 Controller Endpoints", () => {
.get(`/api/v2/auth/oauth2/clients/${testClientId}`)
.expect(200);
expect(response.body.status).toBe("success");
expect(response.body.data.id).toBe(testClientId);
expect(response.body.data.name).toBe("Test OAuth Client");
expect(response.body.data.redirectUri).toBe(testRedirectUri);
expect(response.body.data.type).toBe(OAuthClientType.CONFIDENTIAL);
expect(response.body.data.clientSecret).toBeUndefined();
expect(response.body.status).toBe("success");
expect(response.body.data.id).toBe(testClientId);
expect(response.body.data.name).toBe("Test OAuth Client");
expect(response.body.data.redirectUri).toBe(testRedirectUri);
expect(response.body.data.type).toBe(OAuthClientType.CONFIDENTIAL);
expect(response.body.data.clientSecret).toBeUndefined();
});
it("should return 404 for non-existent client ID", async () => {
@@ -1,16 +1,9 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { StripeService } from "@/modules/stripe/stripe.service";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { UserWithProfile } from "@/modules/users/users.repository";
import type { PlatformBilling, Team } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import Stripe from "stripe";
import * as request from "supertest";
import request from "supertest";
import { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
@@ -18,8 +11,14 @@ import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repo
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import type { Team, PlatformBilling } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { StripeService } from "@/modules/stripe/stripe.service";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { UserWithProfile } from "@/modules/users/users.repository";
describe("Platform Billing Controller (e2e)", () => {
let app: INestApplication;
@@ -126,7 +125,7 @@ describe("Platform Billing Controller (e2e)", () => {
};
},
},
} as unknown as Stripe)
}) as unknown as Stripe
);
return request(app.getHttpServer())
@@ -170,7 +169,7 @@ describe("Platform Billing Controller (e2e)", () => {
};
},
},
} as unknown as Stripe)
}) as unknown as Stripe
);
return request(app.getHttpServer())
.post("/v2/billing/webhook")
@@ -199,7 +198,7 @@ describe("Platform Billing Controller (e2e)", () => {
};
},
},
} as unknown as Stripe)
}) as unknown as Stripe
);
return request(app.getHttpServer())
@@ -232,7 +231,7 @@ describe("Platform Billing Controller (e2e)", () => {
};
},
},
} as unknown as Stripe)
}) as unknown as Stripe
);
return request(app.getHttpServer())
@@ -1,18 +1,3 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { ConferencingAppsOutputDto } from "@/modules/conferencing/outputs/get-conferencing-apps.output";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import {
GOOGLE_CALENDAR_ID,
GOOGLE_CALENDAR_TYPE,
@@ -22,6 +7,20 @@ import {
} from "@calcom/platform-constants";
import type { ApiSuccessResponse } from "@calcom/platform-types";
import type { User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { ConferencingAppsOutputDto } from "@/modules/conferencing/outputs/get-conferencing-apps.output";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
describe("Conferencing Endpoints", () => {
describe("conferencing controller e2e tests", () => {
@@ -1,5 +1,17 @@
import { bootstrap } from "@/app";
import { APPLE_CALENDAR_ID, APPLE_CALENDAR_TYPE, SUCCESS_STATUS } from "@calcom/platform-constants";
import type { Credential, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.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 { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
@@ -7,20 +19,6 @@ import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.
import { DestinationCalendarsOutputResponseDto } from "@/modules/destination-calendars/outputs/destination-calendars.output";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.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 { APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { PlatformOAuthClient, Team, User, Credential } from "@calcom/prisma/client";
const CLIENT_REDIRECT_URI = "http://localhost:5555";
@@ -1,5 +1,15 @@
import { bootstrap } from "@/app";
import type { EventType, Webhook } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
@@ -10,17 +20,6 @@ import {
EventTypeWebhooksOutputResponseDto,
} from "@/modules/webhooks/outputs/event-type-webhook.output";
import { DeleteManyWebhooksOutputResponseDto } from "@/modules/webhooks/outputs/webhook.output";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import type { EventType, Webhook } from "@calcom/prisma/client";
describe("EventTypes WebhooksController (e2e)", () => {
let app: INestApplication;
@@ -1,5 +1,22 @@
import { bootstrap } from "@/app";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { slugify } from "@calcom/platform-libraries";
import type { ApiSuccessResponse } from "@calcom/platform-types";
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_04_15/constants/constants";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
@@ -12,24 +29,6 @@ import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { slugify } from "@calcom/platform-libraries";
import type { ApiSuccessResponse } from "@calcom/platform-types";
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
const CLIENT_REDIRECT_URI = "http://localhost:4321";
@@ -1,18 +1,8 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { UserWithProfile } from "@/modules/users/users.repository";
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
import {
OAuthClientWebhooksOutputResponseDto,
OAuthClientWebhookOutputResponseDto,
} from "@/modules/webhooks/outputs/oauth-client-webhook.output";
import type { PlatformOAuthClient, Team, Webhook } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import request from "supertest";
import { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
@@ -22,8 +12,17 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import type { PlatformOAuthClient, Team, Webhook } from "@calcom/prisma/client";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { UserWithProfile } from "@/modules/users/users.repository";
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
import {
OAuthClientWebhookOutputResponseDto,
OAuthClientWebhooksOutputResponseDto,
} from "@/modules/webhooks/outputs/oauth-client-webhook.output";
describe("OAuth client WebhooksController (e2e)", () => {
let app: INestApplication;
@@ -1,25 +1,3 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { AuthModule } from "@/modules/auth/auth.module";
import { ApiAuthStrategy } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
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 { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { ApiAuthMockStrategy } from "test/mocks/api-auth-mock.strategy";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import {
APPS_READ,
APPS_WRITE,
@@ -34,11 +12,32 @@ import {
SUCCESS_STATUS,
} from "@calcom/platform-constants";
import type {
ApiSuccessResponse,
CreateOAuthClientInput,
UpdateOAuthClientInput,
ApiSuccessResponse,
} from "@calcom/platform-types";
import type { Membership, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { ApiAuthMockStrategy } from "test/mocks/api-auth-mock.strategy";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { AuthModule } from "@/modules/auth/auth.module";
import { ApiAuthStrategy } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
describe("OAuth Clients Endpoints", () => {
describe("User Not Authenticated", () => {
@@ -1,5 +1,18 @@
import { bootstrap } from "@/app";
import { X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test, TestingModule } from "@nestjs/testing";
import request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withNextAuth } from "test/utils/withNextAuth";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { ZodExceptionFilter } from "@/filters/zod-exception.filter";
@@ -10,20 +23,6 @@ import { ExchangeAuthorizationCodeInput } from "@/modules/oauth-clients/inputs/e
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
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, TestingModule } from "@nestjs/testing";
import * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withNextAuth } from "test/utils/withNextAuth";
import { X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
describe("OAuthFlow Endpoints", () => {
describe("User Not Authenticated", () => {

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