diff --git a/.changeset/config.json b/.changeset/config.json index c12b327eb70862..28258ad3febb01 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", - "changelog": ["@changesets/changelog-github", { "repo": "calcom/cal.com" }], + "changelog": ["@changesets/changelog-github", { "repo": "calcom/cal.diy" }], "commit": false, "fixed": [], "linked": [], diff --git a/.env.appStore.example b/.env.appStore.example index 4d41267d730b72..466b4ab4809b79 100644 --- a/.env.appStore.example +++ b/.env.appStore.example @@ -24,8 +24,8 @@ # ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️ # - BASECAMP -# Used to enable Basecamp integration with Cal.com -# @see https://github.com/calcom/cal.com#obtaining-basecamp-client-id-and-secret +# Used to enable Basecamp integration with Cal.diy +# @see https://github.com/calcom/cal.diy#obtaining-basecamp-client-id-and-secret BASECAMP3_CLIENT_ID= BASECAMP3_CLIENT_SECRET= BASECAMP3_USER_AGENT= @@ -34,7 +34,7 @@ BASECAMP3_USER_AGENT= # Enables Cal Video. to get your key # 1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information # 2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab. -# @see https://github.com/calcom/cal.com#obtaining-daily-api-credentials +# @see https://github.com/calcom/cal.diy#obtaining-daily-api-credentials DAILY_API_KEY= DAILY_SCALE_PLAN='' @@ -43,7 +43,7 @@ DAILY_MEETING_ENDED_WEBHOOK_SECRET='' # - GOOGLE CALENDAR/MEET/LOGIN # Needed to enable Google Calendar integration and Login with Google -# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials +# @see https://github.com/calcom/cal.diy#obtaining-the-google-api-credentials GOOGLE_API_CREDENTIALS= # To enable Login with Google you need to: @@ -55,18 +55,18 @@ GOOGLE_LOGIN_ENABLED=false # - HUBSPOT # Used for the HubSpot integration -# @see https://github.com/calcom/cal.com/#obtaining-hubspot-client-id-and-secret +# @see https://github.com/calcom/cal.diy/#obtaining-hubspot-client-id-and-secret HUBSPOT_CLIENT_ID="" HUBSPOT_CLIENT_SECRET="" # - OFFICE 365 # Used for the Office 365 / Outlook.com Calendar / MS Teams integration -# @see https://github.com/calcom/cal.com/#Obtaining-Microsoft-Graph-Client-ID-and-Secret +# @see https://github.com/calcom/cal.diy/#Obtaining-Microsoft-Graph-Client-ID-and-Secret MS_GRAPH_CLIENT_ID= MS_GRAPH_CLIENT_SECRET= # - SLACK -# @see https://github.com/calcom/cal.com/#obtaining-slack-client-id-and-secret-and-signing-secret +# @see https://github.com/calcom/cal.diy/#obtaining-slack-client-id-and-secret-and-signing-secret SLACK_SIGNING_SECRET= SLACK_CLIENT_ID= SLACK_CLIENT_SECRET= @@ -87,7 +87,7 @@ TANDEM_BASE_URL="https://tandem.chat" # - ZOOM # Used for the Zoom integration -# @see https://github.com/calcom/cal.com/#obtaining-zoom-client-id-and-secret +# @see https://github.com/calcom/cal.diy/#obtaining-zoom-client-id-and-secret ZOOM_CLIENT_ID= ZOOM_CLIENT_SECRET= @@ -98,7 +98,7 @@ GIPHY_API_KEY= # - VITAL # Used for the vital integration -# @see https://github.com/calcom/cal.com/#obtaining-vital-api-keys +# @see https://github.com/calcom/cal.diy/#obtaining-vital-api-keys VITAL_API_KEY= VITAL_WEBHOOK_SECRET= # "sandbox" | "prod" | "production" | "development" @@ -108,7 +108,7 @@ VITAL_REGION="us" # - ZAPIER # Used for the Zapier integration -# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/zapier/README.md +# @see https://github.com/calcom/cal.diy/blob/main/packages/app-store/zapier/README.md ZAPIER_INVITE_LINK="" # - LARK diff --git a/.env.example b/.env.example index 5baff90ab3e3aa..93583231249e36 100644 --- a/.env.example +++ b/.env.example @@ -1,29 +1,16 @@ # ********** INDEX ********** # -# - LICENSE (DEPRECATED) +# - LICENSE # - DATABASE # - SHARED # - NEXTAUTH # - E-MAIL SETTINGS # - ORGANIZATIONS -# - LICENSE (DEPRECATED) ************************************************************************************ -# https://github.com/calcom/cal.com/blob/main/LICENSE -# -# Summary of terms: -# - The codebase has to stay open source, whether it was modified or not -# - You can not repackage or sell the codebase -# - Acquire a commercial license to remove these terms by visiting: cal.com/sales -# - -# To enable enterprise-only features please add your environment variable to the .env file then make your way to /auth/setup to select your license and follow instructions. - -CALCOM_LICENSE_KEY= -# Signature token for the Cal.com License API (used for self-hosted integrations) -# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key -CAL_SIGNATURE_TOKEN= -# The route to the Cal.com License API -CALCOM_PRIVATE_API_ROUTE="https://goblin.cal.com" +# - LICENSE ************************************************************************************ +# This project is licensed under the MIT License. +# @see https://github.com/calcom/cal.diy/blob/main/LICENSE +# Cal.diy is fully open source — no license key is required. # *********************************************************************************************************** # - DATABASE ************************************************************************************************ @@ -44,15 +31,13 @@ NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000' NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js' # To enable SAML login, set both these variables -# @see https://github.com/calcom/cal.com/tree/main/packages/features/ee#setting-up-saml-login +# @see https://github.com/calcom/cal.diy/tree/main/packages/features/ee#setting-up-saml-login # SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml" SAML_DATABASE_URL= # SAML_ADMINS='pro@example.com' SAML_ADMINS= # NEXT_PUBLIC_HOSTED_CAL_FEATURES=1 NEXT_PUBLIC_HOSTED_CAL_FEATURES= -# For additional security set to a random secret and use that value as the client_secret during the OAuth 2.0 flow. -SAML_CLIENT_SECRET_VERIFIER= # If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line. # @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js @@ -60,7 +45,7 @@ SAML_CLIENT_SECRET_VERIFIER= PGSSLMODE= # Define which hostnames are expected for the app to work on -ALLOWED_HOSTNAMES='"cal.com","cal.dev","cal-staging.com","cal.community","cal.local:3000","localhost:3000"' +ALLOWED_HOSTNAMES='"cal.local:3000","localhost:3000"' # Reserved orgs subdomains for our own usage RESERVED_SUBDOMAINS='"app","auth","docs","design","console","go","status","api","saml","www","matrix","developer","cal","my","team","support","security","blog","learn","admin"' @@ -150,7 +135,7 @@ GOOGLE_LOGIN_ENABLED=false # - GOOGLE CALENDAR/MEET/LOGIN # Needed to enable Google Calendar integration and Login with Google -# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials +# @see https://github.com/calcom/cal.diy#obtaining-the-google-api-credentials GOOGLE_API_CREDENTIALS= # Token to verify incoming webhooks from Google Calendar GOOGLE_WEBHOOK_TOKEN= @@ -196,17 +181,6 @@ FORMBRICKS_FEEDBACK_SURVEY_ID= AVATARAPI_USERNAME= AVATARAPI_PASSWORD= -# Twilio -# Used to send SMS reminders in workflows -TWILIO_SID= -TWILIO_TOKEN= -TWILIO_MESSAGING_SID= -TWILIO_PHONE_NUMBER= -TWILIO_WHATSAPP_PHONE_NUMBER= -TWILIO_WHATSAPP_REMINDER_CONTENT_SID= -TWILIO_WHATSAPP_CANCELLED_CONTENT_SID= -TWILIO_WHATSAPP_RESCHEDULED_CONTENT_SID= -TWILIO_WHATSAPP_COMPLETED_CONTENT_SID= # For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters) NEXT_PUBLIC_SENDER_ID= TWILIO_VERIFY_SID= @@ -246,7 +220,7 @@ API_KEY_PREFIX=cal_ # allow access to the nodemailer transports from the .env file. E-mail templates are accessible within lib/emails/ # Configures the global From: header whilst sending emails. EMAIL_FROM='notifications@yourselfhostedcal.com' -EMAIL_FROM_NAME='Cal.com' +EMAIL_FROM_NAME='Cal.diy' # Configure SMTP settings (@see https://nodemailer.com/smtp/). # Configuration to receive emails locally (mailhog) @@ -290,9 +264,6 @@ CLOUDFLARE_TURNSTILE_SECRET= # 0 = false, 1=true NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER= -# Set the following value to true if you wish to enable Team Impersonation -NEXT_PUBLIC_TEAM_IMPERSONATION=false - # Close.com internal CRM CLOSECOM_CLIENT_ID= CLOSECOM_CLIENT_SECRET= @@ -301,8 +272,8 @@ CLOSECOM_CLIENT_SECRET= SENDGRID_SYNC_API_KEY= # Change your Brand -NEXT_PUBLIC_APP_NAME="Cal.com" -NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com" +NEXT_PUBLIC_APP_NAME="Cal.diy" +NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.diy" NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc." # Set this to true in to disable new signups # NEXT_PUBLIC_DISABLE_SIGNUP=true @@ -362,7 +333,7 @@ E2E_TEST_APPLE_CALENDAR_EMAIL="" E2E_TEST_APPLE_CALENDAR_PASSWORD="" # - CALCOM QA ACCOUNT -# Used for E2E tests on Cal.com that require 3rd party integrations +# Used for E2E tests on Cal.diy that require 3rd party integrations E2E_TEST_CALCOM_QA_EMAIL="qa@example.com" # Replace with your own password E2E_TEST_CALCOM_QA_PASSWORD="password" @@ -370,7 +341,7 @@ E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS= E2E_TEST_CALCOM_GCAL_KEYS= # - APP CREDENTIAL SYNC *********************************************************************************** -# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations +# Used for self-hosters that are implementing Cal.diy into their applications that already have certain integrations # Under settings/admin/apps ensure that all app secrets are set the same as the parent application # You can use: `openssl rand -base64 32` to generate one CALCOM_CREDENTIAL_SYNC_SECRET="" @@ -378,24 +349,11 @@ CALCOM_CREDENTIAL_SYNC_SECRET="" CALCOM_CREDENTIAL_SYNC_HEADER_NAME="calcom-credential-sync-secret" # This the endpoint from which the token is fetched CALCOM_CREDENTIAL_SYNC_ENDPOINT="" -# Key should match on Cal.com and your application +# Key should match on Cal.diy and your application # must be 24 bytes for AES256 encryption algorithm # You can use: `openssl rand -base64 24` to generate one CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY="" -# - OIDC E2E TEST ******************************************************************************************* - -# Ensure this ADMIN EMAIL is present in the SAML_ADMINS list -E2E_TEST_SAML_ADMIN_EMAIL= -E2E_TEST_SAML_ADMIN_PASSWORD= - -E2E_TEST_OIDC_CLIENT_ID= -E2E_TEST_OIDC_CLIENT_SECRET= -E2E_TEST_OIDC_PROVIDER_DOMAIN= - -E2E_TEST_OIDC_USER_EMAIL= -E2E_TEST_OIDC_USER_PASSWORD= - # *********************************************************************************************************** # api v2 @@ -468,7 +426,7 @@ LINGO_DOT_DEV_API_KEY= DIRECTORY_IDS_TO_LOG= -# Set this when Cal.com is used to serve only one organization's booking pages +# Set this when Cal.diy is used to serve only one organization's booking pages # Read more about it in the README.md NEXT_PUBLIC_SINGLE_ORG_SLUG= @@ -523,10 +481,3 @@ 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= \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 674deef6205dd5..b27ab6ad19f6f2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Questions - url: https://github.com/calcom/cal.com/discussions - about: Need help selfhosting or ask a general question about the project? Open a discussion + url: https://github.com/calcom/cal.diy/discussions + about: Need help self-hosting or have a general question about Cal.diy? Open a discussion diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index a071344ccd1e59..e3029227ef4987 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -55,4 +55,4 @@ assignees: "" - If this issue has a `🚨 needs approval` label, don't start coding yet. Wait until a core member approves feature request by removing this label, then you can start coding. - For clarity: Non-core member issues automatically get the `🚨 needs approval` label. - Your feature ideas are invaluable to us! However, they undergo review to ensure alignment with the product's direction. - - Follow Best Practices lined out in our [Contributor Docs](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md) + - Follow Best Practices lined out in our [Contributor Docs](https://github.com/calcom/cal.diy/blob/main/CONTRIBUTING.md) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 995996b7d599cb..43f7554eed07e2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,10 @@ ## What does this PR do? - + - Fixes #XXXX (GitHub issue number) -- Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description) ## Visual Demo (For contributors especially) @@ -22,7 +23,7 @@ A visual demonstration is strongly recommended, for both the original and new ch ## Mandatory Tasks (DO NOT REMOVE) - [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected). -- [ ] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox. +- [ ] I have updated the developer docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox. - [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works. ## How should this be tested? @@ -38,7 +39,7 @@ A visual demonstration is strongly recommended, for both the original and new ch -- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md) +- I haven't read the [contributing guide](https://github.com/calcom/cal.diy/blob/main/CONTRIBUTING.md) - My code doesn't follow the style guidelines of this project - I haven't commented my code, particularly in hard-to-understand areas - I haven't checked if my changes generate no new warnings diff --git a/.github/actions/docker-build-and-test/action.yml b/.github/actions/docker-build-and-test/action.yml index 0e476393775798..3654aa47c42143 100644 --- a/.github/actions/docker-build-and-test/action.yml +++ b/.github/actions/docker-build-and-test/action.yml @@ -62,8 +62,8 @@ runs: with: images: | docker.io/calendso/calendso - docker.io/calcom/cal.com - ghcr.io/calcom/cal.com + docker.io/calcom/cal.diy + ghcr.io/calcom/cal.diy flavor: | latest=${{ !github.event.release.prerelease && inputs.use-as-latest == 'true' }} suffix=${{ inputs.platform-suffix }} @@ -91,7 +91,7 @@ runs: docker compose logs database - name: Set up Docker Buildx - uses: useblacksmith/setup-docker-builder@v1 + uses: docker/setup-buildx-action@v3 with: driver-opts: | network=container:database @@ -100,7 +100,7 @@ runs: - name: Build image id: docker_build - uses: useblacksmith/build-push-action@v2 + uses: docker/build-push-action@v6 with: context: ./ file: ./Dockerfile @@ -159,7 +159,7 @@ runs: - name: Push image id: docker_push - uses: useblacksmith/build-push-action@v2 + uses: docker/build-push-action@v6 if: ${{ inputs.push-image == 'true' && !github.event.release.prerelease }} with: context: ./ diff --git a/.github/oasdiff-err-ignore.txt b/.github/oasdiff-err-ignore.txt index 01c10fab7a5baa..3e5bb3c8c1021c 100644 --- a/.github/oasdiff-err-ignore.txt +++ b/.github/oasdiff-err-ignore.txt @@ -15,6 +15,760 @@ GET /v2/oauth-clients/{clientId}/webhooks/{webhookId} added the new path request DELETE /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId' GET /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId' GET /v2/webhooks/{webhookId} added the new path request parameter 'webhookId' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links added the new required 'header' request parameter 'cal-api-version' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links added the new required 'header' request parameter 'cal-api-version' +DELETE /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId} added the new required 'header' request parameter 'cal-api-version' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId} added the new required 'header' request parameter 'cal-api-version' +GET /v2/event-types/{eventTypeId}/private-links added the new required 'header' request parameter 'cal-api-version' +POST /v2/event-types/{eventTypeId}/private-links added the new required 'header' request parameter 'cal-api-version' +DELETE /v2/event-types/{eventTypeId}/private-links/{linkId} added the new required 'header' request parameter 'cal-api-version' +PATCH /v2/event-types/{eventTypeId}/private-links/{linkId} added the new required 'header' request parameter 'cal-api-version' +POST /v2/event-types the 'lengthInMinutesOptions/items/' request property type/format changed from 'string'/'' to 'number'/'' +PATCH /v2/event-types/{eventTypeId} the 'lengthInMinutesOptions/items/' request property type/format changed from 'string'/'' to 'number'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'lengthInMinutesOptions/items/' request property type/format changed from 'string'/'' to 'number'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'lengthInMinutesOptions/items/' request property type/format changed from 'string'/'' to 'number'/'' +POST /v2/teams/{teamId}/event-types the 'lengthInMinutesOptions/items/' request property type/format changed from 'string'/'' to 'number'/'' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'lengthInMinutesOptions/items/' request property type/format changed from 'string'/'' to 'number'/'' +POST /v2/event-types removed the request property 'bookingLimitsCount/oneOf[#/components/schemas/BaseBookingLimitsCount_2024_06_14]/disabled' +PATCH /v2/event-types/{eventTypeId} removed the request property 'bookingLimitsCount/oneOf[#/components/schemas/BaseBookingLimitsCount_2024_06_14]/disabled' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types removed the request property 'bookingLimitsCount/oneOf[#/components/schemas/BaseBookingLimitsCount_2024_06_14]/disabled' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} removed the request property 'bookingLimitsCount/oneOf[#/components/schemas/BaseBookingLimitsCount_2024_06_14]/disabled' +POST /v2/teams/{teamId}/event-types removed the request property 'bookingLimitsCount/oneOf[#/components/schemas/BaseBookingLimitsCount_2024_06_14]/disabled' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} removed the request property 'bookingLimitsCount/oneOf[#/components/schemas/BaseBookingLimitsCount_2024_06_14]/disabled' +GET /v2/calendars/busy-times added the new required 'query' request parameter 'calendarsToLoad' +GET /v2/calendars/busy-times the 'query' request parameter 'dateFrom' became required +GET /v2/calendars/busy-times the 'query' request parameter 'dateTo' became required +GET /v2/calendars/busy-times deleted the 'query' request parameter 'credentialId' +GET /v2/calendars/busy-times deleted the 'query' request parameter 'externalId' +GET /v2/auth/oauth2/clients/{clientId} removed the required property 'data/redirect_uri' from the response with the '200' status +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +POST /v2/event-types the request property 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became not nullable +POST /v2/event-types the 'bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required' request property type/format changed from 'boolean'/'' to 'object'/'' +POST /v2/event-types the 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' request property type/format changed from 'string'/'' to 'object'/'' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '201' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the request property 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became not nullable +PATCH /v2/event-types/{eventTypeId} the 'bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required' request property type/format changed from 'boolean'/'' to 'object'/'' +PATCH /v2/event-types/{eventTypeId} the 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' request property type/format changed from 'string'/'' to 'object'/'' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the request property 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became not nullable +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required' request property type/format changed from 'boolean'/'' to 'object'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' request property type/format changed from 'string'/'' to 'object'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '201' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the request property 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became not nullable +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required' request property type/format changed from 'boolean'/'' to 'object'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' request property type/format changed from 'string'/'' to 'object'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/create-phone-call the 'enabled' request property type/format changed from 'boolean'/'' to 'object'/'' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +POST /v2/teams/{teamId}/event-types the request property 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became not nullable +POST /v2/teams/{teamId}/event-types the 'bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required' request property type/format changed from 'boolean'/'' to 'object'/'' +POST /v2/teams/{teamId}/event-types the 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' request property type/format changed from 'string'/'' to 'object'/'' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '201' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the request property 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became not nullable +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required' request property type/format changed from 'boolean'/'' to 'object'/'' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' request property type/format changed from 'string'/'' to 'object'/'' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +POST /v2/teams/{teamId}/event-types/{eventTypeId}/create-phone-call the 'enabled' request property type/format changed from 'boolean'/'' to 'object'/'' + +GET /v2/event-types the 'data/items/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/event-types the 'data/items/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/event-types the 'data/items/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/event-types the 'data/items/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/event-types the 'data/items/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/event-types the 'data/items/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +POST /v2/event-types the 'data/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/event-types the 'data/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/event-types the 'data/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/event-types the 'data/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/event-types the 'data/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/event-types the 'data/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingLimitsCount' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingLimitsDuration' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/confirmationPolicy' response's property type/format changed from 'object'/'' to ''/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/forwardParamsSuccessRedirect' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/ownerId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/parentEventTypeId' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/seatsPerTimeSlot' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/slotInterval' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/successRedirectUrl' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/event-types added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/items/bookingLimitsCount' response property 'oneOf' list for the response status '200' +GET /v2/event-types added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/items/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +GET /v2/event-types added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/items/confirmationPolicy' response property 'oneOf' list for the response status '200' +POST /v2/event-types added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/bookingLimitsCount' response property 'oneOf' list for the response status '201' +POST /v2/event-types added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/bookingLimitsDuration' response property 'oneOf' list for the response status '201' +POST /v2/event-types added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/confirmationPolicy' response property 'oneOf' list for the response status '201' +GET /v2/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingLimitsCount' response property 'oneOf' list for the response status '200' +GET /v2/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsCount' response property 'oneOf' list for the response status '200' +GET /v2/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +GET /v2/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +GET /v2/event-types/{eventTypeId} added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/confirmationPolicy' response property 'oneOf' list for the response status '200' +GET /v2/event-types/{eventTypeId} added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/confirmationPolicy' response property 'oneOf' list for the response status '200' +PATCH /v2/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/bookingLimitsCount' response property 'oneOf' list for the response status '200' +PATCH /v2/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +PATCH /v2/event-types/{eventTypeId} added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/confirmationPolicy' response property 'oneOf' list for the response status '200' +GET /v2/organizations/{orgId}/teams/event-types added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/items/bookingLimitsCount' response property 'oneOf' list for the response status '200' +GET /v2/organizations/{orgId}/teams/event-types added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/items/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +GET /v2/organizations/{orgId}/teams/event-types added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/items/confirmationPolicy' response property 'oneOf' list for the response status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/items/bookingLimitsCount' response property 'oneOf' list for the response status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/items/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/items/confirmationPolicy' response property 'oneOf' list for the response status '200' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsCount' response property 'oneOf' list for the response status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[subschema #2]/items/bookingLimitsCount' response property 'oneOf' list for the response status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsDuration' response property 'oneOf' list for the response status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[subschema #2]/items/bookingLimitsDuration' response property 'oneOf' list for the response status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/confirmationPolicy' response property 'oneOf' list for the response status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[subschema #2]/items/confirmationPolicy' response property 'oneOf' list for the response status '201' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/bookingLimitsCount' response property 'oneOf' list for the response status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/confirmationPolicy' response property 'oneOf' list for the response status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsCount' response property 'oneOf' list for the response status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[subschema #2]/items/bookingLimitsCount' response property 'oneOf' list for the response status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[subschema #2]/items/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/confirmationPolicy' response property 'oneOf' list for the response status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[subschema #2]/items/confirmationPolicy' response property 'oneOf' list for the response status '200' +GET /v2/teams/{teamId}/event-types added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/items/bookingLimitsCount' response property 'oneOf' list for the response status '200' +GET /v2/teams/{teamId}/event-types added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/items/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +GET /v2/teams/{teamId}/event-types added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/items/confirmationPolicy' response property 'oneOf' list for the response status '200' +POST /v2/teams/{teamId}/event-types added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsCount' response property 'oneOf' list for the response status '201' +POST /v2/teams/{teamId}/event-types added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[subschema #2]/items/bookingLimitsCount' response property 'oneOf' list for the response status '201' +POST /v2/teams/{teamId}/event-types added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsDuration' response property 'oneOf' list for the response status '201' +POST /v2/teams/{teamId}/event-types added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[subschema #2]/items/bookingLimitsDuration' response property 'oneOf' list for the response status '201' +POST /v2/teams/{teamId}/event-types added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/confirmationPolicy' response property 'oneOf' list for the response status '201' +POST /v2/teams/{teamId}/event-types added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[subschema #2]/items/confirmationPolicy' response property 'oneOf' list for the response status '201' +GET /v2/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/bookingLimitsCount' response property 'oneOf' list for the response status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/confirmationPolicy' response property 'oneOf' list for the response status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsCount' response property 'oneOf' list for the response status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsCount_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[subschema #2]/items/bookingLimitsCount' response property 'oneOf' list for the response status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseBookingLimitsDuration_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[subschema #2]/items/bookingLimitsDuration' response property 'oneOf' list for the response status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/confirmationPolicy' response property 'oneOf' list for the response status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} added '#/components/schemas/BaseConfirmationPolicy_2024_06_14, #/components/schemas/Disabled_2024_06_14' to the 'data/oneOf[subschema #2]/items/confirmationPolicy' response property 'oneOf' list for the response status '200' +GET /v2/event-types the response property 'data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types the 'data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +POST /v2/event-types the 'bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required' request property type/format changed from 'object'/'' to 'boolean'/'' +POST /v2/event-types the 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' request property type/format changed from 'object'/'' to 'string'/'' +POST /v2/event-types the response property 'data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/event-types the 'data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +GET /v2/event-types/{eventTypeId} the response property 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +GET /v2/event-types/{eventTypeId} the response property 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required' request property type/format changed from 'object'/'' to 'boolean'/'' +PATCH /v2/event-types/{eventTypeId} the 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' request property type/format changed from 'object'/'' to 'string'/'' +PATCH /v2/event-types/{eventTypeId} the response property 'data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the response property 'data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the response property 'data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required' request property type/format changed from 'object'/'' to 'boolean'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' request property type/format changed from 'object'/'' to 'string'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the response property 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the response property 'data/oneOf[subschema #2]/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the response property 'data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required' request property type/format changed from 'object'/'' to 'boolean'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' request property type/format changed from 'object'/'' to 'string'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the response property 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the response property 'data/oneOf[subschema #2]/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/create-phone-call the 'enabled' request property type/format changed from 'object'/'' to 'boolean'/'' +GET /v2/teams/{teamId}/event-types the response property 'data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +POST /v2/teams/{teamId}/event-types the 'bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required' request property type/format changed from 'object'/'' to 'boolean'/'' +POST /v2/teams/{teamId}/event-types the 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' request property type/format changed from 'object'/'' to 'string'/'' +POST /v2/teams/{teamId}/event-types the response property 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '201' +POST /v2/teams/{teamId}/event-types the response property 'data/oneOf[subschema #2]/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the response property 'data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required' request property type/format changed from 'object'/'' to 'boolean'/'' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' request property type/format changed from 'object'/'' to 'string'/'' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the response property 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the response property 'data/oneOf[subschema #2]/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' became nullable for the status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +POST /v2/teams/{teamId}/event-types/{eventTypeId}/create-phone-call the 'enabled' request property type/format changed from 'object'/'' to 'boolean'/'' + +# PR #584: Fix DestinationCalendar.id (object -> number) and PlatformOAuthClientDto.logo (object -> string) +GET /v2/calendars the 'data/destinationCalendar/id' response's property type/format changed from 'object'/'' to 'number'/'' for status '200' +GET /v2/oauth-clients the 'data/items/logo' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +DELETE /v2/oauth-clients/{clientId} the 'data/logo' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/oauth-clients/{clientId} the 'data/logo' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/oauth-clients/{clientId} the 'data/logo' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows the 'data/items/createdAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows the 'data/items/steps/items/includeCalendarEvent' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows the 'data/items/updatedAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows the 'steps/items/oneOf[#/components/schemas/WorkflowEmailAddressStepDto]/includeCalendarEvent' request property type/format changed from 'object'/'' to 'boolean'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows the 'steps/items/oneOf[#/components/schemas/WorkflowEmailAttendeeStepDto]/includeCalendarEvent' request property type/format changed from 'object'/'' to 'boolean'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows the 'steps/items/oneOf[#/components/schemas/WorkflowEmailHostStepDto]/includeCalendarEvent' request property type/format changed from 'object'/'' to 'boolean'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows the 'trigger/oneOf[#/components/schemas/OnAfterCalVideoGuestsNoShowTriggerDto]/offset/allOf[#/components/schemas/WorkflowTriggerOffsetDto]/unit' request property type/format changed from 'object'/'' to 'string'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows the 'trigger/oneOf[#/components/schemas/OnAfterCalVideoHostsNoShowTriggerDto]/offset/allOf[#/components/schemas/WorkflowTriggerOffsetDto]/unit' request property type/format changed from 'object'/'' to 'string'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows the 'trigger/oneOf[#/components/schemas/OnAfterEventTriggerDto]/offset/allOf[#/components/schemas/WorkflowTriggerOffsetDto]/unit' request property type/format changed from 'object'/'' to 'string'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows the 'trigger/oneOf[#/components/schemas/OnBeforeEventTriggerDto]/offset/allOf[#/components/schemas/WorkflowTriggerOffsetDto]/unit' request property type/format changed from 'object'/'' to 'string'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows the 'data/items/createdAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows the 'data/items/steps/items/includeCalendarEvent' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows the 'data/items/updatedAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'data/items/createdAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'data/items/steps/items/includeCalendarEvent' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'data/items/updatedAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'steps/items/oneOf[#/components/schemas/WorkflowEmailAddressStepDto]/includeCalendarEvent' request property type/format changed from 'object'/'' to 'boolean'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'steps/items/oneOf[#/components/schemas/WorkflowEmailAttendeeStepDto]/includeCalendarEvent' request property type/format changed from 'object'/'' to 'boolean'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'trigger/oneOf[#/components/schemas/OnFormSubmittedNoEventTriggerDto]/offset/allOf[#/components/schemas/WorkflowTriggerOffsetDto]/unit' request property type/format changed from 'object'/'' to 'string'/'' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'data/items/createdAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'data/items/steps/items/includeCalendarEvent' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'data/items/updatedAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'data/items/createdAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'data/items/steps/items/includeCalendarEvent' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'data/items/updatedAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'steps/items/oneOf[#/components/schemas/UpdateEmailAddressWorkflowStepDto]/includeCalendarEvent' request property type/format changed from 'object'/'' to 'boolean'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'steps/items/oneOf[#/components/schemas/UpdateEmailAttendeeWorkflowStepDto]/includeCalendarEvent' request property type/format changed from 'object'/'' to 'boolean'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'steps/items/oneOf[#/components/schemas/UpdateEmailHostWorkflowStepDto]/includeCalendarEvent' request property type/format changed from 'object'/'' to 'boolean'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'trigger/oneOf[#/components/schemas/OnAfterCalVideoGuestsNoShowTriggerDto]/offset/allOf[#/components/schemas/WorkflowTriggerOffsetDto]/unit' request property type/format changed from 'object'/'' to 'string'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'trigger/oneOf[#/components/schemas/OnAfterCalVideoHostsNoShowTriggerDto]/offset/allOf[#/components/schemas/WorkflowTriggerOffsetDto]/unit' request property type/format changed from 'object'/'' to 'string'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'trigger/oneOf[#/components/schemas/OnAfterEventTriggerDto]/offset/allOf[#/components/schemas/WorkflowTriggerOffsetDto]/unit' request property type/format changed from 'object'/'' to 'string'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'trigger/oneOf[#/components/schemas/OnBeforeEventTriggerDto]/offset/allOf[#/components/schemas/WorkflowTriggerOffsetDto]/unit' request property type/format changed from 'object'/'' to 'string'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'data/items/createdAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'data/items/steps/items/includeCalendarEvent' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'data/items/updatedAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'data/items/createdAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'data/items/steps/items/includeCalendarEvent' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'data/items/updatedAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'steps/items/oneOf[#/components/schemas/UpdateEmailAddressWorkflowStepDto]/includeCalendarEvent' request property type/format changed from 'object'/'' to 'boolean'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'steps/items/oneOf[#/components/schemas/UpdateEmailAttendeeWorkflowStepDto]/includeCalendarEvent' request property type/format changed from 'object'/'' to 'boolean'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'trigger/oneOf[#/components/schemas/OnFormSubmittedNoEventTriggerDto]/offset/allOf[#/components/schemas/WorkflowTriggerOffsetDto]/unit' request property type/format changed from 'object'/'' to 'string'/'' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'data/items/createdAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'data/items/steps/items/includeCalendarEvent' response's property type/format changed from 'object'/'' to 'boolean'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'data/items/updatedAt' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/stripe/check the 'status' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/stripe/check the 'status' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' GET /v2/event-types/{eventTypeId}/webhooks the 'data/items/triggers/items/' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' POST /v2/event-types/{eventTypeId}/webhooks removed the enum value 'AFTER_GUESTS_CAL_VIDEO_NO_SHOW' of the request property 'triggers' POST /v2/event-types/{eventTypeId}/webhooks removed the enum value 'AFTER_HOSTS_CAL_VIDEO_NO_SHOW' of the request property 'triggers' @@ -512,3 +1266,1293 @@ GET /v2/verified-resources/phones/{id} removed the required property 'data/readO GET /v2/verified-resources/phones/{id} removed the required property 'data/schedule' from the response with the '200' status GET /v2/verified-resources/phones/{id} removed the required property 'data/timeZone' from the response with the '200' status GET /v2/verified-resources/phones/{id} removed the required property 'data/workingHours' from the response with the '200' status +GET /v2/bookings/{bookingUid}/calendar-links the 'status' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid}/references the 'status' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings/{bookingUid}/references the 'status' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/event-types the 'data/items/users/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +POST /v2/event-types the 'data/users/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '201' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/users/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/organizations/{orgId}/attributes/slugs/{attributeSlug}/options/assigned the 'data/items/assignedUserIds/items/' response's property type/format changed from 'string'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/attributes/{attributeId}/options/assigned the 'data/items/assignedUserIds/items/' response's property type/format changed from 'string'/'' to 'number'/'' for status '200' +GET /v2/organizations/{orgId}/teams/event-types the 'data/items/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/items/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '201' +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/teams/{teamId}/event-types the 'data/items/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '201' +POST /v2/teams/{teamId}/event-types the 'data/oneOf[subschema #2]/items/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '201' +GET /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} the 'data/oneOf[subschema #2]/items/hosts/items/' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/calendars the 'data/connectedCalendars/items/integration/locationOption' response's property type/format changed from 'object'/'' to ''/'' for status '200' +GET /v2/oauth-clients/{clientId}/users the 'data/items/createdDate' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/oauth-clients/{clientId}/users the 'data/user/createdDate' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +DELETE /v2/oauth-clients/{clientId}/users/{userId} the 'data/createdDate' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/oauth-clients/{clientId}/users/{userId} the 'data/createdDate' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/oauth-clients/{clientId}/users/{userId} the 'data/createdDate' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/routing-forms the response property 'data/items/fields' became optional for the status '200' +GET /v2/organizations/{orgId}/routing-forms the response property 'data/items/routes' became optional for the status '200' +GET /v2/organizations/{orgId}/routing-forms the response property 'data/items/settings' became optional for the status '200' +GET /v2/organizations/{orgId}/routing-forms the 'data/items/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/routing-forms the 'data/items/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/routing-forms the response property 'data/items/fields' became optional for the status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/routing-forms the response property 'data/items/routes' became optional for the status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/routing-forms the response property 'data/items/settings' became optional for the status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/routing-forms the 'data/items/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/routing-forms the 'data/items/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows the 'data/items/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows the 'data/items/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows the 'data/items/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows the 'data/items/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'data/items/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'data/items/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'data/items/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form the 'data/items/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'data/items/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'data/items/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'data/items/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} the 'data/items/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'data/items/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'data/items/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'data/items/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form the 'data/items/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +DELETE /v2/organizations/{orgId}/roles/{roleId} added the new 'organization.customDomain.create' enum value to the 'data/permissions/items/' response property for the response status '200' +DELETE /v2/organizations/{orgId}/roles/{roleId} added the new 'organization.customDomain.delete' enum value to the 'data/permissions/items/' response property for the response status '200' +DELETE /v2/organizations/{orgId}/roles/{roleId} added the new 'organization.customDomain.read' enum value to the 'data/permissions/items/' response property for the response status '200' +DELETE /v2/organizations/{orgId}/roles/{roleId} added the new 'organization.customDomain.update' enum value to the 'data/permissions/items/' response property for the response status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/roles added the new 'organization.customDomain.create' enum value to the 'data/items/permissions/items/' response property for the response status '200' +GET /v2/organizations/{orgId}/roles added the new 'organization.customDomain.delete' enum value to the 'data/items/permissions/items/' response property for the response status '200' +GET /v2/organizations/{orgId}/roles added the new 'organization.customDomain.read' enum value to the 'data/items/permissions/items/' response property for the response status '200' +GET /v2/organizations/{orgId}/roles added the new 'organization.customDomain.update' enum value to the 'data/items/permissions/items/' response property for the response status '200' +GET /v2/organizations/{orgId}/roles/{roleId} added the new 'organization.customDomain.create' enum value to the 'data/permissions/items/' response property for the response status '200' +GET /v2/organizations/{orgId}/roles/{roleId} added the new 'organization.customDomain.delete' enum value to the 'data/permissions/items/' response property for the response status '200' +GET /v2/organizations/{orgId}/roles/{roleId} added the new 'organization.customDomain.read' enum value to the 'data/permissions/items/' response property for the response status '200' +GET /v2/organizations/{orgId}/roles/{roleId} added the new 'organization.customDomain.update' enum value to the 'data/permissions/items/' response property for the response status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/organizations/{orgId}/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +GET /v2/teams/{teamId}/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/organizations/{orgId}/roles/{roleId} added the new 'organization.customDomain.create' enum value to the 'data/permissions/items/' response property for the response status '200' +PATCH /v2/organizations/{orgId}/roles/{roleId} added the new 'organization.customDomain.delete' enum value to the 'data/permissions/items/' response property for the response status '200' +PATCH /v2/organizations/{orgId}/roles/{roleId} added the new 'organization.customDomain.read' enum value to the 'data/permissions/items/' response property for the response status '200' +PATCH /v2/organizations/{orgId}/roles/{roleId} added the new 'organization.customDomain.update' enum value to the 'data/permissions/items/' response property for the response status '200' +PATCH /v2/slots/reservations/{uid} the 'data/reservationUntil' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/slots/reservations/{uid} the 'data/slotEnd' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/slots/reservations/{uid} the 'data/slotStart' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +PATCH /v2/slots/reservations/{uid} the 'slotStart' request property type/format changed from 'string'/'' to 'string'/'date-time' +POST /v2/bookings the '/oneOf[#/components/schemas/CreateBookingInput_2024_08_13]/start' request property type/format changed from 'string'/'' to 'string'/'date-time' +POST /v2/bookings the '/oneOf[#/components/schemas/CreateInstantBookingInput_2024_08_13]/start' request property type/format changed from 'string'/'' to 'string'/'date-time' +POST /v2/bookings the '/oneOf[#/components/schemas/CreateRecurringBookingInput_2024_08_13]/start' request property type/format changed from 'string'/'' to 'string'/'date-time' +POST /v2/bookings the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '200' +POST /v2/bookings/{bookingUid}/reschedule the '/oneOf[#/components/schemas/RescheduleBookingInput_2024_08_13]/start' request property type/format changed from 'string'/'' to 'string'/'date-time' +POST /v2/bookings/{bookingUid}/reschedule the '/oneOf[#/components/schemas/RescheduleSeatedBookingInput_2024_08_13]/start' request property type/format changed from 'string'/'' to 'string'/'date-time' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/organizations/{orgId}/roles added the new 'organization.customDomain.create' enum value to the 'data/permissions/items/' response property for the response status '201' +POST /v2/organizations/{orgId}/roles added the new 'organization.customDomain.delete' enum value to the 'data/permissions/items/' response property for the response status '201' +POST /v2/organizations/{orgId}/roles added the new 'organization.customDomain.read' enum value to the 'data/permissions/items/' response property for the response status '201' +POST /v2/organizations/{orgId}/roles added the new 'organization.customDomain.update' enum value to the 'data/permissions/items/' response property for the response status '201' +POST /v2/slots/reservations the 'data/reservationUntil' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/slots/reservations the 'data/slotEnd' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/slots/reservations the 'data/slotStart' response's property type/format changed from 'string'/'' to 'string'/'date-time' for status '201' +POST /v2/slots/reservations the 'slotStart' request property type/format changed from 'string'/'' to 'string'/'date-time' +POST /v2/teams/{teamId}/event-types/{eventTypeId}/create-phone-call deleted the 'path' request parameter 'orgId' + +# Cal.diy-specific breaking changes (intentional divergence from cal.com) +DELETE /v2/me/ooo/{oooId} api path removed without deprecation +DELETE /v2/notifications/subscriptions/app-push api path removed without deprecation +DELETE /v2/oauth-clients/{clientId} the 'data/logo' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +DELETE /v2/oauth-clients/{clientId}/users/{userId} the 'data/createdDate' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +DELETE /v2/organizations/{orgId}/attributes/options/{userId}/{attributeOptionId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/attributes/{attributeId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/attributes/{attributeId}/options/{optionId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/memberships/{membershipId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/organizations/{managedOrganizationId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/roles/{roleId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/roles/{roleId}/permissions api path removed without deprecation +DELETE /v2/organizations/{orgId}/roles/{roleId}/permissions/{permission} api path removed without deprecation +DELETE /v2/organizations/{orgId}/teams/{teamId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/teams/{teamId}/conferencing/{app}/disconnect api path removed without deprecation +DELETE /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/teams/{teamId}/memberships/{membershipId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/teams/{teamId}/roles/{roleId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/teams/{teamId}/roles/{roleId}/permissions api path removed without deprecation +DELETE /v2/organizations/{orgId}/teams/{teamId}/roles/{roleId}/permissions/{permission} api path removed without deprecation +DELETE /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form api path removed without deprecation +DELETE /v2/organizations/{orgId}/users/{userId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/users/{userId}/ooo/{oooId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/users/{userId}/schedules/{scheduleId} api path removed without deprecation +DELETE /v2/organizations/{orgId}/webhooks/{webhookId} api path removed without deprecation +DELETE /v2/teams/{teamId} api path removed without deprecation +DELETE /v2/teams/{teamId}/event-types/{eventTypeId} api path removed without deprecation +DELETE /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks api path removed without deprecation +DELETE /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks/{webhookId} api path removed without deprecation +DELETE /v2/teams/{teamId}/memberships/{membershipId} api path removed without deprecation +DELETE /v2/teams/{teamId}/users/{userId}/ooo/{oooId} api path removed without deprecation +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings the 'data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/by-seat/{seatUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid} the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/bookings/{bookingUid}/calendar-links the 'status' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/bookings/{bookingUid}/references the 'status' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/calendars the 'data/connectedCalendars/items/integration/locationOption' response's property type/format changed from ''/'' to 'object'/'' for status '200' +GET /v2/calendars the 'data/destinationCalendar/id' response's property type/format changed from 'number'/'' to 'object'/'' for status '200' +GET /v2/calendars/busy-times added the new required 'query' request parameter 'credentialId' +GET /v2/calendars/busy-times added the new required 'query' request parameter 'externalId' +GET /v2/credits/available api path removed without deprecation +GET /v2/event-types removed the required property 'data/items/users/items/avatarUrl' from the response with the '200' status +GET /v2/event-types removed the required property 'data/items/users/items/brandColor' from the response with the '200' status +GET /v2/event-types removed the required property 'data/items/users/items/darkBrandColor' from the response with the '200' status +GET /v2/event-types removed the required property 'data/items/users/items/id' from the response with the '200' status +GET /v2/event-types removed the required property 'data/items/users/items/metadata' from the response with the '200' status +GET /v2/event-types removed the required property 'data/items/users/items/name' from the response with the '200' status +GET /v2/event-types removed the required property 'data/items/users/items/username' from the response with the '200' status +GET /v2/event-types removed the required property 'data/items/users/items/weekStart' from the response with the '200' status +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingLimitsCount' response's property type/format changed from ''/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/bookingLimitsDuration' response's property type/format changed from ''/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/confirmationPolicy' response's property type/format changed from ''/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/forwardParamsSuccessRedirect' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/seatsPerTimeSlot' response's property type/format changed from 'number'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/slotInterval' response's property type/format changed from 'number'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/successRedirectUrl' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/event-types the 'data/items/users/items/' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/event-types/{eventTypeId} removed the required property 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/avatarUrl' from the response with the '200' status +GET /v2/event-types/{eventTypeId} removed the required property 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/brandColor' from the response with the '200' status +GET /v2/event-types/{eventTypeId} removed the required property 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/darkBrandColor' from the response with the '200' status +GET /v2/event-types/{eventTypeId} removed the required property 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/id' from the response with the '200' status +GET /v2/event-types/{eventTypeId} removed the required property 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/metadata' from the response with the '200' status +GET /v2/event-types/{eventTypeId} removed the required property 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/name' from the response with the '200' status +GET /v2/event-types/{eventTypeId} removed the required property 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/username' from the response with the '200' status +GET /v2/event-types/{eventTypeId} removed the required property 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/weekStart' from the response with the '200' status +GET /v2/event-types/{eventTypeId} removed the required property 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/name' from the response with the '200' status +GET /v2/event-types/{eventTypeId} removed the required property 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/userId' from the response with the '200' status +GET /v2/event-types/{eventTypeId} removed the required property 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/username' from the response with the '200' status +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingLimitsCount' response's property type/format changed from ''/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingLimitsDuration' response's property type/format changed from ''/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/confirmationPolicy' response's property type/format changed from ''/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/forwardParamsSuccessRedirect' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/seatsPerTimeSlot' response's property type/format changed from 'number'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/slotInterval' response's property type/format changed from 'number'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/successRedirectUrl' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsCount' response's property type/format changed from ''/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsDuration' response's property type/format changed from ''/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/confirmationPolicy' response's property type/format changed from ''/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/forwardParamsSuccessRedirect' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/ownerId' response's property type/format changed from 'number'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/parentEventTypeId' response's property type/format changed from 'number'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/seatsPerTimeSlot' response's property type/format changed from 'number'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/slotInterval' response's property type/format changed from 'number'/'' to 'object'/'' for status '200' +GET /v2/event-types/{eventTypeId} the 'data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/successRedirectUrl' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/me removed the required property 'data/locale' from the response with the '200' status +GET /v2/me/ooo api path removed without deprecation +GET /v2/oauth-clients the 'data/items/logo' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/oauth-clients/{clientId} the 'data/logo' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/oauth-clients/{clientId}/users the 'data/items/createdDate' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/oauth-clients/{clientId}/users/{userId} the 'data/createdDate' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +GET /v2/organizations/{orgId}/attributes api path removed without deprecation +GET /v2/organizations/{orgId}/attributes/options/{userId} api path removed without deprecation +GET /v2/organizations/{orgId}/attributes/slugs/{attributeSlug}/options/assigned api path removed without deprecation +GET /v2/organizations/{orgId}/attributes/{attributeId} api path removed without deprecation +GET /v2/organizations/{orgId}/attributes/{attributeId}/options api path removed without deprecation +GET /v2/organizations/{orgId}/attributes/{attributeId}/options/assigned api path removed without deprecation +GET /v2/organizations/{orgId}/bookings api path removed without deprecation +GET /v2/organizations/{orgId}/memberships api path removed without deprecation +GET /v2/organizations/{orgId}/memberships/{membershipId} api path removed without deprecation +GET /v2/organizations/{orgId}/ooo api path removed without deprecation +GET /v2/organizations/{orgId}/organizations api path removed without deprecation +GET /v2/organizations/{orgId}/organizations/{managedOrganizationId} api path removed without deprecation +GET /v2/organizations/{orgId}/roles api path removed without deprecation +GET /v2/organizations/{orgId}/roles/{roleId} api path removed without deprecation +GET /v2/organizations/{orgId}/roles/{roleId}/permissions api path removed without deprecation +GET /v2/organizations/{orgId}/routing-forms api path removed without deprecation +GET /v2/organizations/{orgId}/routing-forms/{routingFormId}/responses api path removed without deprecation +GET /v2/organizations/{orgId}/schedules api path removed without deprecation +GET /v2/organizations/{orgId}/teams api path removed without deprecation +GET /v2/organizations/{orgId}/teams/event-types api path removed without deprecation +GET /v2/organizations/{orgId}/teams/me api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId} api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/bookings api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/bookings/{bookingUid}/references api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/conferencing api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/conferencing/default api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/conferencing/{app}/oauth/auth-url api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/conferencing/{app}/oauth/callback api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/event-types api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/memberships api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/memberships/{membershipId} api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/roles api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/roles/{roleId} api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/roles/{roleId}/permissions api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/routing-forms api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/schedules api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/stripe/check api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/stripe/connect api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/stripe/save api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/users/{userId}/schedules api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/{id} api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/{id} api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/workflows api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} api path removed without deprecation +GET /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form api path removed without deprecation +GET /v2/organizations/{orgId}/users api path removed without deprecation +GET /v2/organizations/{orgId}/users/{userId}/bookings api path removed without deprecation +GET /v2/organizations/{orgId}/users/{userId}/ooo api path removed without deprecation +GET /v2/organizations/{orgId}/users/{userId}/schedules api path removed without deprecation +GET /v2/organizations/{orgId}/users/{userId}/schedules/{scheduleId} api path removed without deprecation +GET /v2/organizations/{orgId}/webhooks api path removed without deprecation +GET /v2/organizations/{orgId}/webhooks/{webhookId} api path removed without deprecation +GET /v2/stripe/check the 'status' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +GET /v2/teams api path removed without deprecation +GET /v2/teams/{teamId} api path removed without deprecation +GET /v2/teams/{teamId}/bookings api path removed without deprecation +GET /v2/teams/{teamId}/event-types api path removed without deprecation +GET /v2/teams/{teamId}/event-types/{eventTypeId} api path removed without deprecation +GET /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks api path removed without deprecation +GET /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks/{webhookId} api path removed without deprecation +GET /v2/teams/{teamId}/memberships api path removed without deprecation +GET /v2/teams/{teamId}/memberships/{membershipId} api path removed without deprecation +GET /v2/teams/{teamId}/schedules api path removed without deprecation +GET /v2/teams/{teamId}/users/{userId}/ooo api path removed without deprecation +GET /v2/teams/{teamId}/verified-resources/emails api path removed without deprecation +GET /v2/teams/{teamId}/verified-resources/emails/{id} api path removed without deprecation +GET /v2/teams/{teamId}/verified-resources/phones api path removed without deprecation +GET /v2/teams/{teamId}/verified-resources/phones/{id} api path removed without deprecation +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/bookings/{bookingUid}/location the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} removed '#/components/schemas/SplitNameDefaultFieldInput_2024_06_14' from the 'bookingFields/items/' request property 'oneOf' list +PATCH /v2/event-types/{eventTypeId} removed the required property 'data/users/items/avatarUrl' from the response with the '200' status +PATCH /v2/event-types/{eventTypeId} removed the required property 'data/users/items/brandColor' from the response with the '200' status +PATCH /v2/event-types/{eventTypeId} removed the required property 'data/users/items/darkBrandColor' from the response with the '200' status +PATCH /v2/event-types/{eventTypeId} removed the required property 'data/users/items/id' from the response with the '200' status +PATCH /v2/event-types/{eventTypeId} removed the required property 'data/users/items/metadata' from the response with the '200' status +PATCH /v2/event-types/{eventTypeId} removed the required property 'data/users/items/name' from the response with the '200' status +PATCH /v2/event-types/{eventTypeId} removed the required property 'data/users/items/username' from the response with the '200' status +PATCH /v2/event-types/{eventTypeId} removed the required property 'data/users/items/weekStart' from the response with the '200' status +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingLimitsCount' response's property type/format changed from ''/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/bookingLimitsDuration' response's property type/format changed from ''/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/confirmationPolicy' response's property type/format changed from ''/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/forwardParamsSuccessRedirect' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/seatsPerTimeSlot' response's property type/format changed from 'number'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/slotInterval' response's property type/format changed from 'number'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/successRedirectUrl' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'data/users/items/' response's property type/format changed from 'object'/'' to 'string'/'' for status '200' +PATCH /v2/event-types/{eventTypeId} the 'lengthInMinutesOptions/items/' request property type/format changed from 'number'/'' to 'string'/'' +PATCH /v2/event-types/{eventTypeId}/webhooks/{webhookId} removed the enum value 'ROUTING_FORM_FALLBACK_HIT' of the request property 'triggers/items/' +PATCH /v2/me removed the required property 'data/locale' from the response with the '200' status +PATCH /v2/me/ooo/{oooId} api path removed without deprecation +PATCH /v2/oauth-clients/{clientId} the 'data/logo' response's property type/format changed from 'string'/'' to 'object'/'' for status '200' +PATCH /v2/oauth-clients/{clientId}/users/{userId} the 'data/createdDate' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/oauth-clients/{clientId}/webhooks/{webhookId} removed the enum value 'ROUTING_FORM_FALLBACK_HIT' of the request property 'triggers/items/' +PATCH /v2/organizations/{orgId}/attributes/{attributeId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/attributes/{attributeId}/options/{optionId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/delegation-credentials/{credentialId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/memberships/{membershipId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/organizations/{managedOrganizationId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/roles/{roleId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/routing-forms/{routingFormId}/responses/{responseId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/teams/{teamId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/teams/{teamId}/memberships/{membershipId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/teams/{teamId}/roles/{roleId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses/{responseId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/teams/{teamId}/workflows/{workflowId}/routing-form api path removed without deprecation +PATCH /v2/organizations/{orgId}/users/{userId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/users/{userId}/ooo/{oooId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/users/{userId}/schedules/{scheduleId} api path removed without deprecation +PATCH /v2/organizations/{orgId}/webhooks/{webhookId} api path removed without deprecation +PATCH /v2/schedules/{scheduleId} removed the enum value 'Friday' of the request property 'availability/items/days' +PATCH /v2/schedules/{scheduleId} removed the enum value 'Monday' of the request property 'availability/items/days' +PATCH /v2/schedules/{scheduleId} removed the enum value 'Saturday' of the request property 'availability/items/days' +PATCH /v2/schedules/{scheduleId} removed the enum value 'Sunday' of the request property 'availability/items/days' +PATCH /v2/schedules/{scheduleId} removed the enum value 'Thursday' of the request property 'availability/items/days' +PATCH /v2/schedules/{scheduleId} removed the enum value 'Tuesday' of the request property 'availability/items/days' +PATCH /v2/schedules/{scheduleId} removed the enum value 'Wednesday' of the request property 'availability/items/days' +PATCH /v2/slots/reservations/{uid} the 'data/reservationUntil' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/slots/reservations/{uid} the 'data/slotEnd' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/slots/reservations/{uid} the 'data/slotStart' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +PATCH /v2/slots/reservations/{uid} the 'slotStart' request property type/format changed from 'string'/'date-time' to 'string'/'' +PATCH /v2/teams/{teamId} api path removed without deprecation +PATCH /v2/teams/{teamId}/event-types/{eventTypeId} api path removed without deprecation +PATCH /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks/{webhookId} api path removed without deprecation +PATCH /v2/teams/{teamId}/memberships/{membershipId} api path removed without deprecation +PATCH /v2/teams/{teamId}/users/{userId}/ooo/{oooId} api path removed without deprecation +PATCH /v2/webhooks/{webhookId} removed the enum value 'ROUTING_FORM_FALLBACK_HIT' of the request property 'triggers/items/' +POST /v2/auth/oauth2/token removed the required property 'scope' from the response with the '200' status +POST /v2/bookings removed '#/components/schemas/CreateInstantBookingInput_2024_08_13' from the request body 'oneOf' list +POST /v2/bookings the '/oneOf[#/components/schemas/CreateBookingInput_2024_08_13]/start' request property type/format changed from 'string'/'date-time' to 'string'/'' +POST /v2/bookings the '/oneOf[#/components/schemas/CreateRecurringBookingInput_2024_08_13]/start' request property type/format changed from 'string'/'date-time' to 'string'/'' +POST /v2/bookings the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings the request property '/oneOf[#/components/schemas/CreateBookingInput_2024_08_13]/routing/allOf[#/components/schemas/Routing]/responseId' became not nullable +POST /v2/bookings the request property '/oneOf[#/components/schemas/CreateBookingInput_2024_08_13]/routing/allOf[#/components/schemas/Routing]/responseId' became required +POST /v2/bookings the request property '/oneOf[#/components/schemas/CreateRecurringBookingInput_2024_08_13]/routing/allOf[#/components/schemas/Routing]/responseId' became not nullable +POST /v2/bookings the request property '/oneOf[#/components/schemas/CreateRecurringBookingInput_2024_08_13]/routing/allOf[#/components/schemas/Routing]/responseId' became required +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/cancel the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/confirm the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/decline the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/guests the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/mark-absent the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '200' +POST /v2/bookings/{bookingUid}/request-reschedule api path removed without deprecation +POST /v2/bookings/{bookingUid}/reschedule the '/oneOf[#/components/schemas/RescheduleBookingInput_2024_08_13]/start' request property type/format changed from 'string'/'date-time' to 'string'/'' +POST /v2/bookings/{bookingUid}/reschedule the '/oneOf[#/components/schemas/RescheduleSeatedBookingInput_2024_08_13]/start' request property type/format changed from 'string'/'date-time' to 'string'/'' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/bookings/{bookingUid}/reschedule the 'data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/credits/charge api path removed without deprecation +POST /v2/event-types removed '#/components/schemas/SplitNameDefaultFieldInput_2024_06_14' from the 'bookingFields/items/' request property 'oneOf' list +POST /v2/event-types removed the required property 'data/users/items/avatarUrl' from the response with the '201' status +POST /v2/event-types removed the required property 'data/users/items/brandColor' from the response with the '201' status +POST /v2/event-types removed the required property 'data/users/items/darkBrandColor' from the response with the '201' status +POST /v2/event-types removed the required property 'data/users/items/id' from the response with the '201' status +POST /v2/event-types removed the required property 'data/users/items/metadata' from the response with the '201' status +POST /v2/event-types removed the required property 'data/users/items/name' from the response with the '201' status +POST /v2/event-types removed the required property 'data/users/items/username' from the response with the '201' status +POST /v2/event-types removed the required property 'data/users/items/weekStart' from the response with the '201' status +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingLimitsCount' response's property type/format changed from ''/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/bookingLimitsDuration' response's property type/format changed from ''/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/confirmationPolicy' response's property type/format changed from ''/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/forwardParamsSuccessRedirect' response's property type/format changed from 'boolean'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/seatsPerTimeSlot' response's property type/format changed from 'number'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/slotInterval' response's property type/format changed from 'number'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/successRedirectUrl' response's property type/format changed from 'string'/'' to 'object'/'' for status '201' +POST /v2/event-types the 'data/users/items/' response's property type/format changed from 'object'/'' to 'string'/'' for status '201' +POST /v2/event-types the 'lengthInMinutesOptions/items/' request property type/format changed from 'number'/'' to 'string'/'' +POST /v2/event-types/{eventTypeId}/webhooks removed the enum value 'ROUTING_FORM_FALLBACK_HIT' of the request property 'triggers/items/' +POST /v2/me/ooo api path removed without deprecation +POST /v2/notifications/subscriptions/app-push api path removed without deprecation +POST /v2/oauth-clients/{clientId}/users the 'data/user/createdDate' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/oauth-clients/{clientId}/webhooks removed the enum value 'ROUTING_FORM_FALLBACK_HIT' of the request property 'triggers/items/' +POST /v2/organizations/{orgId}/attributes api path removed without deprecation +POST /v2/organizations/{orgId}/attributes/options/{userId} api path removed without deprecation +POST /v2/organizations/{orgId}/attributes/{attributeId}/options api path removed without deprecation +POST /v2/organizations/{orgId}/bookings/block api path removed without deprecation +POST /v2/organizations/{orgId}/bookings/report api path removed without deprecation +POST /v2/organizations/{orgId}/delegation-credentials api path removed without deprecation +POST /v2/organizations/{orgId}/memberships api path removed without deprecation +POST /v2/organizations/{orgId}/organizations api path removed without deprecation +POST /v2/organizations/{orgId}/roles api path removed without deprecation +POST /v2/organizations/{orgId}/roles/{roleId}/permissions api path removed without deprecation +POST /v2/organizations/{orgId}/routing-forms/{routingFormId}/responses api path removed without deprecation +POST /v2/organizations/{orgId}/teams api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/conferencing/{app}/connect api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/conferencing/{app}/default api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/event-types api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/create-phone-call api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/invite api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/memberships api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/roles api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/roles/{roleId}/permissions api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/verification-code/request api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/verification-code/verify api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/verification-code/request api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/verification-code/verify api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/workflows api path removed without deprecation +POST /v2/organizations/{orgId}/teams/{teamId}/workflows/routing-form api path removed without deprecation +POST /v2/organizations/{orgId}/users api path removed without deprecation +POST /v2/organizations/{orgId}/users/{userId}/ooo api path removed without deprecation +POST /v2/organizations/{orgId}/users/{userId}/schedules api path removed without deprecation +POST /v2/organizations/{orgId}/webhooks api path removed without deprecation +POST /v2/routing-forms/{routingFormId}/calculate-slots api path removed without deprecation +POST /v2/schedules removed the enum value 'Friday' of the request property 'availability/items/days' +POST /v2/schedules removed the enum value 'Monday' of the request property 'availability/items/days' +POST /v2/schedules removed the enum value 'Saturday' of the request property 'availability/items/days' +POST /v2/schedules removed the enum value 'Sunday' of the request property 'availability/items/days' +POST /v2/schedules removed the enum value 'Thursday' of the request property 'availability/items/days' +POST /v2/schedules removed the enum value 'Tuesday' of the request property 'availability/items/days' +POST /v2/schedules removed the enum value 'Wednesday' of the request property 'availability/items/days' +POST /v2/slots/reservations the 'data/reservationUntil' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/slots/reservations the 'data/slotEnd' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/slots/reservations the 'data/slotStart' response's property type/format changed from 'string'/'date-time' to 'string'/'' for status '201' +POST /v2/slots/reservations the 'slotStart' request property type/format changed from 'string'/'date-time' to 'string'/'' +POST /v2/teams api path removed without deprecation +POST /v2/teams/{teamId}/event-types api path removed without deprecation +POST /v2/teams/{teamId}/event-types/{eventTypeId}/create-phone-call api path removed without deprecation +POST /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks api path removed without deprecation +POST /v2/teams/{teamId}/invite api path removed without deprecation +POST /v2/teams/{teamId}/memberships api path removed without deprecation +POST /v2/teams/{teamId}/users/{userId}/ooo api path removed without deprecation +POST /v2/teams/{teamId}/verified-resources/emails/verification-code/request api path removed without deprecation +POST /v2/teams/{teamId}/verified-resources/emails/verification-code/verify api path removed without deprecation +POST /v2/teams/{teamId}/verified-resources/phones/verification-code/request api path removed without deprecation +POST /v2/teams/{teamId}/verified-resources/phones/verification-code/verify api path removed without deprecation +POST /v2/webhooks removed the enum value 'ROUTING_FORM_FALLBACK_HIT' of the request property 'triggers/items/' +PUT /v2/organizations/{orgId}/roles/{roleId}/permissions api path removed without deprecation +PUT /v2/organizations/{orgId}/teams/{teamId}/roles/{roleId}/permissions api path removed without deprecation + +POST /v2/auth/oauth2/token removed the required property `scope` from the response with the `200` status +GET /v2/bookings the `data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings the `data/items/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings removed `#/components/schemas/CreateInstantBookingInput_2024_08_13` from the request body `oneOf` list +POST /v2/bookings the request property `/oneOf[#/components/schemas/CreateBookingInput_2024_08_13]/routing/allOf[#/components/schemas/Routing]/responseId` became not nullable +POST /v2/bookings the request property `/oneOf[#/components/schemas/CreateRecurringBookingInput_2024_08_13]/routing/allOf[#/components/schemas/Routing]/responseId` became not nullable +POST /v2/bookings the request property `/oneOf[#/components/schemas/CreateBookingInput_2024_08_13]/routing/allOf[#/components/schemas/Routing]/responseId` became required +POST /v2/bookings the request property `/oneOf[#/components/schemas/CreateRecurringBookingInput_2024_08_13]/routing/allOf[#/components/schemas/Routing]/responseId` became required +POST /v2/bookings the `/oneOf[#/components/schemas/CreateBookingInput_2024_08_13]/start` request property type/format changed from `string`/`date-time` to `string`/`` +POST /v2/bookings the `/oneOf[#/components/schemas/CreateRecurringBookingInput_2024_08_13]/start` request property type/format changed from `string`/`date-time` to `string`/`` +POST /v2/bookings added `subschema #2, subschema #4` to the `data` response property `oneOf` list for the response status `201` +POST /v2/bookings the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings the `data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings the `data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings the `data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings the `data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +GET /v2/bookings/by-seat/{seatUid} added `subschema #3, subschema #6` to the `data` response property `oneOf` list for the response status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/by-seat/{seatUid} the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} added `subschema #3, subschema #6` to the `data` response property `oneOf` list for the response status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid} the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid}/calendar-links the `status` response's property type/format changed from `string`/`` to `object`/`` for 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}/cancel the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/cancel the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for 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}/confirm the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/confirm the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for 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}/decline the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/decline the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests added `subschema #3, subschema #6` to the `data` response property `oneOf` list for the response status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/guests the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location added `subschema #3, subschema #6` to the `data` response property `oneOf` list for the response status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/GetSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/bookings/{bookingUid}/location the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/mark-absent the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/mark-absent the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/mark-absent the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/mark-absent the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/mark-absent the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/mark-absent the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/mark-absent the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/bookings/{bookingUid}/mark-absent the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/bookings/{bookingUid}/references the `status` response's property type/format changed from `string`/`` to `object`/`` for status `200` +POST /v2/bookings/{bookingUid}/reschedule the `/oneOf[#/components/schemas/RescheduleBookingInput_2024_08_13]/start` request property type/format changed from `string`/`date-time` to `string`/`` +POST /v2/bookings/{bookingUid}/reschedule the `/oneOf[#/components/schemas/RescheduleSeatedBookingInput_2024_08_13]/start` request property type/format changed from `string`/`date-time` to `string`/`` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/BookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/CreateSeatedBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/createdAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/end` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/start` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/bookings/{bookingUid}/reschedule the `data/oneOf[#/components/schemas/RecurringBookingOutput_2024_08_13]/updatedAt` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +GET /v2/calendars the `data/connectedCalendars/items/integration/locationOption` response's property type/format changed from ``/`` to `object`/`` for status `200` +GET /v2/calendars the `data/destinationCalendar/id` response's property type/format changed from `number`/`` to `object`/`` for status `200` +GET /v2/calendars/busy-times added the new required `query` request parameter `credentialId` +GET /v2/calendars/busy-times added the new required `query` request parameter `externalId` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingLimitsCount` response's property type/format changed from ``/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/bookingLimitsDuration` response's property type/format changed from ``/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit` response's property type/format changed from `string`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/confirmationPolicy` response's property type/format changed from ``/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/forwardParamsSuccessRedirect` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/seatsPerTimeSlot` response's property type/format changed from `number`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/slotInterval` response's property type/format changed from `number`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/successRedirectUrl` response's property type/format changed from `string`/`` to `object`/`` for status `200` +GET /v2/event-types the `data/items/users/items/` response's property type/format changed from `object`/`` to `string`/`` for status `200` +GET /v2/event-types removed the required property `data/items/users/items/avatarUrl` from the response with the `200` status +GET /v2/event-types removed the required property `data/items/users/items/brandColor` from the response with the `200` status +GET /v2/event-types removed the required property `data/items/users/items/darkBrandColor` from the response with the `200` status +GET /v2/event-types removed the required property `data/items/users/items/id` from the response with the `200` status +GET /v2/event-types removed the required property `data/items/users/items/metadata` from the response with the `200` status +GET /v2/event-types removed the required property `data/items/users/items/name` from the response with the `200` status +GET /v2/event-types removed the required property `data/items/users/items/username` from the response with the `200` status +GET /v2/event-types removed the required property `data/items/users/items/weekStart` from the response with the `200` status +POST /v2/event-types the request property `calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit` became not nullable +POST /v2/event-types removed `#/components/schemas/SplitNameDefaultFieldInput_2024_06_14` from the `bookingFields/items/` request property `oneOf` list +POST /v2/event-types the `bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required` request property type/format changed from `boolean`/`` to `object`/`` +POST /v2/event-types the `calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit` request property type/format changed from `string`/`` to `object`/`` +POST /v2/event-types the `lengthInMinutesOptions/items/` request property type/format changed from `number`/`` to `string`/`` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingLimitsCount` response's property type/format changed from ``/`` to `object`/`` for status `201` +POST /v2/event-types the `data/bookingLimitsDuration` response's property type/format changed from ``/`` to `object`/`` for status `201` +POST /v2/event-types the `data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit` response's property type/format changed from `string`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/confirmationPolicy` response's property type/format changed from ``/`` to `object`/`` for status `201` +POST /v2/event-types the `data/forwardParamsSuccessRedirect` response's property type/format changed from `boolean`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/seatsPerTimeSlot` response's property type/format changed from `number`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/slotInterval` response's property type/format changed from `number`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/successRedirectUrl` response's property type/format changed from `string`/`` to `object`/`` for status `201` +POST /v2/event-types the `data/users/items/` response's property type/format changed from `object`/`` to `string`/`` for status `201` +POST /v2/event-types removed the required property `data/users/items/avatarUrl` from the response with the `201` status +POST /v2/event-types removed the required property `data/users/items/brandColor` from the response with the `201` status +POST /v2/event-types removed the required property `data/users/items/darkBrandColor` from the response with the `201` status +POST /v2/event-types removed the required property `data/users/items/id` from the response with the `201` status +POST /v2/event-types removed the required property `data/users/items/metadata` from the response with the `201` status +POST /v2/event-types removed the required property `data/users/items/name` from the response with the `201` status +POST /v2/event-types removed the required property `data/users/items/username` from the response with the `201` status +POST /v2/event-types removed the required property `data/users/items/weekStart` from the response with the `201` status +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingLimitsCount` response's property type/format changed from ``/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/bookingLimitsDuration` response's property type/format changed from ``/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit` response's property type/format changed from `string`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/confirmationPolicy` response's property type/format changed from ``/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/forwardParamsSuccessRedirect` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/seatsPerTimeSlot` response's property type/format changed from `number`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/slotInterval` response's property type/format changed from `number`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/successRedirectUrl` response's property type/format changed from `string`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/` response's property type/format changed from `object`/`` to `string`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsCount` response's property type/format changed from ``/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/bookingLimitsDuration` response's property type/format changed from ``/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit` response's property type/format changed from `string`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/confirmationPolicy` response's property type/format changed from ``/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/forwardParamsSuccessRedirect` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/` response's property type/format changed from `object`/`` to `string`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/ownerId` response's property type/format changed from `number`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/parentEventTypeId` response's property type/format changed from `number`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/seatsPerTimeSlot` response's property type/format changed from `number`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/slotInterval` response's property type/format changed from `number`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} the `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/successRedirectUrl` response's property type/format changed from `string`/`` to `object`/`` for status `200` +GET /v2/event-types/{eventTypeId} removed the required property `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/avatarUrl` from the response with the `200` status +GET /v2/event-types/{eventTypeId} removed the required property `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/brandColor` from the response with the `200` status +GET /v2/event-types/{eventTypeId} removed the required property `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/darkBrandColor` from the response with the `200` status +GET /v2/event-types/{eventTypeId} removed the required property `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/id` from the response with the `200` status +GET /v2/event-types/{eventTypeId} removed the required property `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/metadata` from the response with the `200` status +GET /v2/event-types/{eventTypeId} removed the required property `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/name` from the response with the `200` status +GET /v2/event-types/{eventTypeId} removed the required property `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/username` from the response with the `200` status +GET /v2/event-types/{eventTypeId} removed the required property `data/oneOf[#/components/schemas/EventTypeOutput_2024_06_14]/users/items/weekStart` from the response with the `200` status +GET /v2/event-types/{eventTypeId} removed the required property `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/name` from the response with the `200` status +GET /v2/event-types/{eventTypeId} removed the required property `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/userId` from the response with the `200` status +GET /v2/event-types/{eventTypeId} removed the required property `data/oneOf[#/components/schemas/TeamEventTypeOutput_2024_06_14]/hosts/items/username` from the response with the `200` status +PATCH /v2/event-types/{eventTypeId} the request property `calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit` became not nullable +PATCH /v2/event-types/{eventTypeId} removed `#/components/schemas/SplitNameDefaultFieldInput_2024_06_14` from the `bookingFields/items/` request property `oneOf` list +PATCH /v2/event-types/{eventTypeId} the `bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldInput_2024_06_14]/required` request property type/format changed from `boolean`/`` to `object`/`` +PATCH /v2/event-types/{eventTypeId} the `calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit` request property type/format changed from `string`/`` to `object`/`` +PATCH /v2/event-types/{eventTypeId} the `lengthInMinutesOptions/items/` request property type/format changed from `number`/`` to `string`/`` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/AddressFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/BooleanFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/CheckboxGroupFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/EmailDefaultFieldOutput_2024_06_14]/required` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/GuestsDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/LocationDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/MultiEmailFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/MultiSelectFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/NameDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/NotesDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/NumberFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/PhoneFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/RadioGroupFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/SelectFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/TextAreaFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/TextFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/TitleDefaultFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingFields/items/oneOf[#/components/schemas/UrlFieldOutput_2024_06_14]/isDefault` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingLimitsCount` response's property type/format changed from ``/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/bookingLimitsDuration` response's property type/format changed from ``/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/calVideoSettings/allOf[#/components/schemas/CalVideoSettings]/redirectUrlOnExit` response's property type/format changed from `string`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/confirmationPolicy` response's property type/format changed from ``/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/forwardParamsSuccessRedirect` response's property type/format changed from `boolean`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/seatsPerTimeSlot` response's property type/format changed from `number`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/slotInterval` response's property type/format changed from `number`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/successRedirectUrl` response's property type/format changed from `string`/`` to `object`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} the `data/users/items/` response's property type/format changed from `object`/`` to `string`/`` for status `200` +PATCH /v2/event-types/{eventTypeId} removed the required property `data/users/items/avatarUrl` from the response with the `200` status +PATCH /v2/event-types/{eventTypeId} removed the required property `data/users/items/brandColor` from the response with the `200` status +PATCH /v2/event-types/{eventTypeId} removed the required property `data/users/items/darkBrandColor` from the response with the `200` status +PATCH /v2/event-types/{eventTypeId} removed the required property `data/users/items/id` from the response with the `200` status +PATCH /v2/event-types/{eventTypeId} removed the required property `data/users/items/metadata` from the response with the `200` status +PATCH /v2/event-types/{eventTypeId} removed the required property `data/users/items/name` from the response with the `200` status +PATCH /v2/event-types/{eventTypeId} removed the required property `data/users/items/username` from the response with the `200` status +PATCH /v2/event-types/{eventTypeId} removed the required property `data/users/items/weekStart` from the response with the `200` status +POST /v2/event-types/{eventTypeId}/webhooks removed the enum value `ROUTING_FORM_FALLBACK_HIT` of the request property `triggers/items/` +PATCH /v2/event-types/{eventTypeId}/webhooks/{webhookId} removed the enum value `ROUTING_FORM_FALLBACK_HIT` of the request property `triggers/items/` +GET /v2/me removed the required property `data/locale` from the response with the `200` status +PATCH /v2/me removed the required property `data/locale` from the response with the `200` status +GET /v2/oauth-clients the `data/items/logo` response's property type/format changed from `string`/`` to `object`/`` for status `200` +DELETE /v2/oauth-clients/{clientId} the `data/logo` response's property type/format changed from `string`/`` to `object`/`` for status `200` +GET /v2/oauth-clients/{clientId} the `data/logo` response's property type/format changed from `string`/`` to `object`/`` for status `200` +PATCH /v2/oauth-clients/{clientId} the `data/logo` response's property type/format changed from `string`/`` to `object`/`` for status `200` +GET /v2/oauth-clients/{clientId}/users the `data/items/createdDate` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/oauth-clients/{clientId}/users the `data/user/createdDate` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +DELETE /v2/oauth-clients/{clientId}/users/{userId} the `data/createdDate` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/oauth-clients/{clientId}/users/{userId} the `data/createdDate` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/oauth-clients/{clientId}/users/{userId} the `data/createdDate` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +POST /v2/oauth-clients/{clientId}/webhooks removed the enum value `ROUTING_FORM_FALLBACK_HIT` of the request property `triggers/items/` +PATCH /v2/oauth-clients/{clientId}/webhooks/{webhookId} removed the enum value `ROUTING_FORM_FALLBACK_HIT` of the request property `triggers/items/` +POST /v2/schedules removed the enum value `Friday` of the request property `availability/items/days` +POST /v2/schedules removed the enum value `Monday` of the request property `availability/items/days` +POST /v2/schedules removed the enum value `Saturday` of the request property `availability/items/days` +POST /v2/schedules removed the enum value `Sunday` of the request property `availability/items/days` +POST /v2/schedules removed the enum value `Thursday` of the request property `availability/items/days` +POST /v2/schedules removed the enum value `Tuesday` of the request property `availability/items/days` +POST /v2/schedules removed the enum value `Wednesday` of the request property `availability/items/days` +PATCH /v2/schedules/{scheduleId} removed the enum value `Friday` of the request property `availability/items/days` +PATCH /v2/schedules/{scheduleId} removed the enum value `Monday` of the request property `availability/items/days` +PATCH /v2/schedules/{scheduleId} removed the enum value `Saturday` of the request property `availability/items/days` +PATCH /v2/schedules/{scheduleId} removed the enum value `Sunday` of the request property `availability/items/days` +PATCH /v2/schedules/{scheduleId} removed the enum value `Thursday` of the request property `availability/items/days` +PATCH /v2/schedules/{scheduleId} removed the enum value `Tuesday` of the request property `availability/items/days` +PATCH /v2/schedules/{scheduleId} removed the enum value `Wednesday` of the request property `availability/items/days` +POST /v2/slots/reservations the `slotStart` request property type/format changed from `string`/`date-time` to `string`/`` +POST /v2/slots/reservations the `data/reservationUntil` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/slots/reservations the `data/slotEnd` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +POST /v2/slots/reservations the `data/slotStart` response's property type/format changed from `string`/`date-time` to `string`/`` for status `201` +PATCH /v2/slots/reservations/{uid} the `slotStart` request property type/format changed from `string`/`date-time` to `string`/`` +PATCH /v2/slots/reservations/{uid} the `data/reservationUntil` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/slots/reservations/{uid} the `data/slotEnd` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +PATCH /v2/slots/reservations/{uid} the `data/slotStart` response's property type/format changed from `string`/`date-time` to `string`/`` for status `200` +GET /v2/stripe/check the `status` response's property type/format changed from `string`/`` to `object`/`` for status `200` +POST /v2/webhooks removed the enum value `ROUTING_FORM_FALLBACK_HIT` of the request property `triggers/items/` +PATCH /v2/webhooks/{webhookId} removed the enum value `ROUTING_FORM_FALLBACK_HIT` of the request property `triggers/items/` diff --git a/.github/workflows/all-checks.yml b/.github/workflows/all-checks.yml index 7fbcb4197c6d9c..6459fe9b31bd27 100644 --- a/.github/workflows/all-checks.yml +++ b/.github/workflows/all-checks.yml @@ -29,11 +29,6 @@ jobs: uses: ./.github/workflows/api-v2-unit-tests.yml secrets: inherit - build-api-v1: - name: Production builds - uses: ./.github/workflows/api-v1-production-build.yml - secrets: inherit - build-api-v2: name: Production builds uses: ./.github/workflows/api-v2-production-build.yml @@ -51,38 +46,38 @@ jobs: integration-test: name: Tests - needs: [lint, build, build-api-v1, build-api-v2] + needs: [lint, build, build-api-v2] uses: ./.github/workflows/integration-tests.yml secrets: inherit e2e: name: Tests - needs: [lint, build, build-api-v1, build-api-v2] + needs: [lint, build, build-api-v2] uses: ./.github/workflows/e2e.yml secrets: inherit e2e-app-store: name: Tests - needs: [lint, build, build-api-v1, build-api-v2] + needs: [lint, build, build-api-v2] uses: ./.github/workflows/e2e-app-store.yml secrets: inherit e2e-embed: name: Tests - needs: [lint, build, build-api-v1, build-api-v2] + needs: [lint, build, build-api-v2] uses: ./.github/workflows/e2e-embed.yml secrets: inherit e2e-embed-react: name: Tests - needs: [lint, build, build-api-v1, build-api-v2] + needs: [lint, build, build-api-v2] uses: ./.github/workflows/e2e-embed-react.yml secrets: inherit required: - needs: [lint, type-check, unit-test, api-v2-unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store] + needs: [lint, type-check, unit-test, api-v2-unit-test, integration-test, build, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store] if: always() - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - name: fail if conditional jobs failed if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled') diff --git a/.github/workflows/api-v1-production-build.yml b/.github/workflows/api-v1-production-build.yml deleted file mode 100644 index 4b394599e9f6da..00000000000000 --- a/.github/workflows/api-v1-production-build.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Production Builds - -on: - workflow_call: - -permissions: - contents: read - -env: - ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }} - CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }} - DATABASE_URL: ${{ secrets.CI_DATABASE_URL }} - DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }} - E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }} - E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }} - E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }} - E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }} - E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} - E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }} - GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }} - GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }} - NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }} - NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }} - NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }} - NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }} - NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }} - NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }} - NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }} - NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }} - NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }} - PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }} - PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }} - SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }} - SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }} - STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} - STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }} - STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }} - SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }} - SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }} - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - -jobs: - build: - name: Build API v1 - runs-on: blacksmith-4vcpu-ubuntu-2404 - timeout-minutes: 30 - services: - postgres: - image: postgres:18 - credentials: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: calendso - options: >- - --health-cmd "pg_isready -h 127.0.0.1 -U postgres" - --health-interval 2s - --health-timeout 3s - --health-retries 15 - --health-start-period 2s - ports: - - 5432:5432 - steps: - - uses: actions/checkout@v4 - 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 - uses: actions/cache@v4 - id: cache-api-v1-build - env: - cache-name: api-v1-build - key-1: ${{ hashFiles('yarn.lock') }} - key-2: ${{ hashFiles('apps/api/v1/**.[jt]s', 'apps/api/v1/**.[jt]sx', '!**/node_modules') }} - with: - path: | - ${{ github.workspace }}/apps/api/v1/.next - **/.turbo/** - **/dist/** - key: ${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }} - - name: Log Cache Hit - if: steps.cache-api-v1-build.outputs.cache-hit == 'true' - run: echo "Cache hit for API v1 build. Skipping build." - - name: Run build - if: steps.cache-api-v1-build.outputs.cache-hit != 'true' - run: | - export NODE_OPTIONS="--max_old_space_size=8192" - yarn turbo run build --filter=@calcom/api... - shell: bash diff --git a/.github/workflows/api-v2-production-build.yml b/.github/workflows/api-v2-production-build.yml index 0611f100e862c1..e287800d364305 100644 --- a/.github/workflows/api-v2-production-build.yml +++ b/.github/workflows/api-v2-production-build.yml @@ -14,7 +14,7 @@ jobs: name: Build API v2 permissions: contents: read - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 30 services: postgres: diff --git a/.github/workflows/api-v2-unit-tests.yml b/.github/workflows/api-v2-unit-tests.yml index 8eb08509caf126..d567973aa2967b 100644 --- a/.github/workflows/api-v2-unit-tests.yml +++ b/.github/workflows/api-v2-unit-tests.yml @@ -9,7 +9,7 @@ jobs: test: name: API v2 Unit timeout-minutes: 20 - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/atoms-production-build.yml b/.github/workflows/atoms-production-build.yml index 3552cb5a7551fd..b3098d0e10dbd8 100644 --- a/.github/workflows/atoms-production-build.yml +++ b/.github/workflows/atoms-production-build.yml @@ -8,7 +8,7 @@ jobs: name: Build Atoms permissions: contents: read - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/cache-clean.yml b/.github/workflows/cache-clean.yml index 121c5b935ea6d9..f550ba484840c6 100644 --- a/.github/workflows/cache-clean.yml +++ b/.github/workflows/cache-clean.yml @@ -6,7 +6,7 @@ on: jobs: cleanup: - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v4 diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml index 685646b1542b86..990deccf741ba3 100644 --- a/.github/workflows/changesets.yml +++ b/.github/workflows/changesets.yml @@ -13,7 +13,7 @@ permissions: jobs: release: - if: github.repository == 'calcom/cal.com' # prevent this action from running on forks + if: github.repository == 'calcom/cal.diy' # prevent this action from running on forks name: Release runs-on: ubuntu-latest steps: diff --git a/.github/workflows/check-api-v2-breaking-changes.yml b/.github/workflows/check-api-v2-breaking-changes.yml deleted file mode 100644 index 739b6d413cbe99..00000000000000 --- a/.github/workflows/check-api-v2-breaking-changes.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Check API v2 breaking changes - -on: - workflow_call: - -permissions: - contents: read - -env: - API_KEY_PREFIX: ${{ secrets.CI_API_KEY_PREFIX }} - API_PORT: ${{ vars.CI_API_V2_PORT }} - API_URL: "http://localhost" - CALCOM_LICENSE_KEY: ${{ secrets.CI_CALCOM_LICENSE_KEY }} - DATABASE_URL: ${{ secrets.CI_DATABASE_URL }} - DATABASE_READ_URL: ${{ secrets.CI_DATABASE_URL }} - DATABASE_WRITE_URL: ${{ secrets.CI_DATABASE_URL }} - JWT_SECRET: ${{ secrets.CI_JWT_SECRET }} - NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }} - NODE_ENV: ${{ vars.CI_NODE_ENV }} - NODE_OPTIONS: --max-old-space-size=4096 - REDIS_URL: "redis://localhost:6379" - STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} - STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }} - WEB_APP_URL: "https://app.cal.com" - -jobs: - check-api-v2-breaking-changes: - name: Check API v2 breaking changes - timeout-minutes: 10 - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - uses: actions/checkout@v4 - with: - sparse-checkout: .github - - uses: ./.github/actions/cache-checkout - - uses: ./.github/actions/yarn-install - - - name: Build platform packages - run: | - yarn turbo run build \ - --filter=@calcom/platform-constants \ - --filter=@calcom/platform-enums \ - --filter=@calcom/platform-utils \ - --filter=@calcom/platform-types \ - --filter=@calcom/platform-libraries \ - --filter=@calcom/trpc - - - name: Generate Swagger - working-directory: apps/api/v2 - run: | - yarn prisma generate - yarn generate-swagger - - - name: Check API v2 breaking changes - run: | - docker run --rm \ - -v "$PWD:/work" -w /work \ - tufin/oasdiff:latest \ - breaking \ - https://raw.githubusercontent.com/calcom/cal.com/refs/heads/main/docs/api-reference/v2/openapi.json \ - docs/api-reference/v2/openapi.json \ - --fail-on ERR \ - --err-ignore .github/oasdiff-err-ignore.txt diff --git a/.github/workflows/check-prisma-migrations.yml b/.github/workflows/check-prisma-migrations.yml index bddb9fd84ee4b2..52e8dab2c773b0 100644 --- a/.github/workflows/check-prisma-migrations.yml +++ b/.github/workflows/check-prisma-migrations.yml @@ -8,7 +8,7 @@ env: jobs: check-prisma-migrations: name: Check migrations match schema - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest services: postgres: image: postgres:18 diff --git a/.github/workflows/check-types.yml b/.github/workflows/check-types.yml index 30288425371460..b10b5d71b2fbd9 100644 --- a/.github/workflows/check-types.yml +++ b/.github/workflows/check-types.yml @@ -9,7 +9,7 @@ permissions: contents: read jobs: check-types: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/delete-blacksmith-cache.yml b/.github/workflows/delete-blacksmith-cache.yml deleted file mode 100644 index 5a84df5f482dcc..00000000000000 --- a/.github/workflows/delete-blacksmith-cache.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Delete Blacksmith Cache -on: - workflow_dispatch: - inputs: - cache_key: - description: "Blacksmith Cache Key to Delete" - required: true - type: string - pull_request: - types: [closed] - -jobs: - manually-delete-blacksmith-cache: - if: github.event_name == 'workflow_dispatch' - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - name: Checkout - uses: actions/checkout@v4 - - uses: useblacksmith/cache-delete@v1 - with: - key: ${{ inputs.cache_key }} - - delete-cache-build-on-pr-close: - if: github.event_name == 'pull_request' && github.event.action == 'closed' - runs-on: blacksmith-2vcpu-ubuntu-2404 - env: - CACHE_NAME: prod-build - steps: - - name: Delete cache-build cache - uses: useblacksmith/cache-delete@v1 - 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" diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml deleted file mode 100644 index 3b573b7a8a5f78..00000000000000 --- a/.github/workflows/docs-build.yml +++ /dev/null @@ -1,38 +0,0 @@ -# This is just to test this file -name: Build - -on: - workflow_call: - -jobs: - build: - name: Build Docs - permissions: - contents: read - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: .github - - uses: ./.github/actions/cache-checkout - - name: Cache Docs build - uses: actions/cache@v4 - id: cache-docs-build - env: - cache-name: docs-build - key-1: ${{ hashFiles('yarn.lock') }} - key-2: ${{ hashFiles('docs/**.*', '!**/node_modules') }} - with: - path: | - **/docs/** - key: ${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }} - - name: Run build - if: steps.cache-docs-build.outputs.cache-hit != 'true' - working-directory: docs - run: | - export NODE_OPTIONS="--max_old_space_size=8192" - npm install -g mintlify@4.2.87 - mintlify dev & - sleep 5 # Let it run for 5 seconds - kill $! - shell: bash diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index 196fbac2727900..8ac3f3cf24e903 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -91,10 +91,9 @@ jobs: # Write to the specific paths echo "$CONTENT" > apps/web/trigger.version.ts echo "$CONTENT" > apps/api/v2/trigger.version.ts - echo "$CONTENT" > apps/api/v1/trigger.version.js # Force add to git since these are likely in .gitignore - git add -f apps/web/trigger.version.ts apps/api/v2/trigger.version.ts apps/api/v1/trigger.version.js + git add -f apps/web/trigger.version.ts apps/api/v2/trigger.version.ts # 4. Commit everything together - name: Commit changes diff --git a/.github/workflows/e2e-api-v2.yml b/.github/workflows/e2e-api-v2.yml index cbee6ae91adf39..cfa810d7cd1ed6 100644 --- a/.github/workflows/e2e-api-v2.yml +++ b/.github/workflows/e2e-api-v2.yml @@ -33,7 +33,7 @@ jobs: e2e: timeout-minutes: 20 name: E2E API v2 (${{ matrix.shard }}/${{ strategy.job-total }}) - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest services: postgres: image: postgres:18 diff --git a/.github/workflows/e2e-app-store.yml b/.github/workflows/e2e-app-store.yml index a8422e68d0460a..6caa7bc1b1a729 100644 --- a/.github/workflows/e2e-app-store.yml +++ b/.github/workflows/e2e-app-store.yml @@ -45,7 +45,7 @@ jobs: e2e-app-store: timeout-minutes: 20 name: E2E App Store - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest services: postgres: image: postgres:18 diff --git a/.github/workflows/e2e-atoms.yml b/.github/workflows/e2e-atoms.yml index 90485e8539dc29..e694acea444fa3 100644 --- a/.github/workflows/e2e-atoms.yml +++ b/.github/workflows/e2e-atoms.yml @@ -22,7 +22,7 @@ jobs: e2e-atoms: timeout-minutes: 15 name: E2E Atoms - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: docker/login-action@v3 with: diff --git a/.github/workflows/e2e-embed-react.yml b/.github/workflows/e2e-embed-react.yml index d76a18aafd1ceb..a097116dd49411 100644 --- a/.github/workflows/e2e-embed-react.yml +++ b/.github/workflows/e2e-embed-react.yml @@ -45,7 +45,7 @@ jobs: e2e-embed: timeout-minutes: 20 name: E2E Embed React - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest services: postgres: image: postgres:18 diff --git a/.github/workflows/e2e-embed.yml b/.github/workflows/e2e-embed.yml index 12682bc6144dbf..2c9ca42ab63cd0 100644 --- a/.github/workflows/e2e-embed.yml +++ b/.github/workflows/e2e-embed.yml @@ -45,7 +45,7 @@ jobs: e2e-embed: timeout-minutes: 20 name: E2E Embed Core - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest services: postgres: image: postgres:18 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 1dc4b10e2c00c8..1c19c446a399a9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -45,7 +45,7 @@ jobs: e2e: timeout-minutes: 20 name: E2E (${{ matrix.shard }}/${{ strategy.job-total }}) - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest services: postgres: image: postgres:18 diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml index 62824f4417aa36..b44abad56b635f 100644 --- a/.github/workflows/i18n.yml +++ b/.github/workflows/i18n.yml @@ -11,7 +11,7 @@ jobs: i18n: name: Run i18n if: ${{ secrets.CI_LINGO_DOT_DEV_API_KEY != '' }} - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest permissions: actions: write contents: write diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index a67e2e998492c8..02b548529c0bf2 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -45,7 +45,7 @@ jobs: integration: timeout-minutes: 20 name: Integration - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest services: postgres: image: postgres:18 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c709b17c7bcb2c..5c9af507afc78b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,7 @@ permissions: contents: read jobs: lint: - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/nextjs-bundle-analysis-annotation.yml b/.github/workflows/nextjs-bundle-analysis-annotation.yml index fa686e10c70f89..cacdc2a1df1580 100644 --- a/.github/workflows/nextjs-bundle-analysis-annotation.yml +++ b/.github/workflows/nextjs-bundle-analysis-annotation.yml @@ -12,7 +12,7 @@ permissions: jobs: annotate: if: always() - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/yarn-install diff --git a/.github/workflows/nextjs-bundle-analysis.yml b/.github/workflows/nextjs-bundle-analysis.yml index d79c6bf2c2ccea..c45fe54b93c860 100644 --- a/.github/workflows/nextjs-bundle-analysis.yml +++ b/.github/workflows/nextjs-bundle-analysis.yml @@ -49,7 +49,7 @@ env: jobs: analyze: if: always() - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 @@ -72,7 +72,11 @@ jobs: path: apps/web/.next/analyze/__bundle_analysis.json - name: Delete cached build - uses: useblacksmith/cache-delete@v1 - with: - key: prod-build-main- - prefix: "true" + continue-on-error: true + run: | + gh extension install actions/gh-actions-cache + for key in $(gh actions-cache list -R ${{ github.repository }} --key "prod-build-main-" --limit 100 | cut -f1); do + gh actions-cache delete "$key" -R ${{ github.repository }} --confirm + done + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/pr-welcome-bot.yml b/.github/workflows/pr-welcome-bot.yml new file mode 100644 index 00000000000000..215ed8ad2270ec --- /dev/null +++ b/.github/workflows/pr-welcome-bot.yml @@ -0,0 +1,48 @@ +name: "PR Welcome Bot" + +on: + pull_request_target: + types: + - opened + +permissions: + pull-requests: write + +jobs: + welcome: + name: Welcome new contributors + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + + // Skip PRs from org members and bots + if ( + ["MEMBER", "OWNER", "COLLABORATOR"].includes(pr.author_association) || + pr.user.type === "Bot" + ) { + console.log(`Skipping — author association: ${pr.author_association}, type: ${pr.user.type}`); + return; + } + + const body = [ + `Welcome to **Cal.diy**, @${pr.user.login}! Thanks for opening this pull request.`, + "", + "A few things to keep in mind:", + "", + "- **This is Cal.diy, not Cal.com.** Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will **not** be deployed to the Cal.com production app.", + "- Please review our [Contributing Guidelines](https://github.com/calcom/cal.diy/blob/main/CONTRIBUTING.md) if you haven't already.", + "- Make sure your PR title follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format.", + "", + "A maintainer will review your PR soon. Thanks for contributing!", + ].join("\n"); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body, + }); diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 3a109bf8fcb9be..a9c7ffe832538d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,7 +23,7 @@ jobs: # This MUST run before any job that checks out PR code and executes it with secrets trust-check: name: Trust Check - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest permissions: pull-requests: read actions: read @@ -162,7 +162,7 @@ jobs: name: Prepare needs: [trust-check] if: needs.trust-check.outputs.is-trusted == 'true' - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest permissions: pull-requests: read outputs: @@ -217,7 +217,6 @@ jobs: - "packages/platform-libraries/**" - "packages/trpc/**" - "packages/prisma/schema.prisma" - - "docs/api-reference/v2/**" has-prisma-changes: - "packages/prisma/schema.prisma" - "packages/prisma/migrations/**" @@ -345,13 +344,6 @@ jobs: DB_CACHE_HIT: ${{ needs.prepare.outputs.db-cache-hit }} secrets: inherit - build-api-v1: - name: Production builds - needs: [prepare, setup-db] - if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }} - uses: ./.github/workflows/api-v1-production-build.yml - secrets: inherit - build-api-v2: name: Production builds needs: [prepare] @@ -366,13 +358,6 @@ jobs: uses: ./.github/workflows/atoms-production-build.yml secrets: inherit - build-docs: - name: Production builds - needs: [prepare] - if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }} - uses: ./.github/workflows/docs-build.yml - secrets: inherit - build: name: Production builds needs: [prepare] @@ -394,13 +379,6 @@ jobs: uses: ./.github/workflows/e2e.yml secrets: inherit - check-api-v2-breaking-changes: - name: Check API v2 breaking changes - needs: [prepare] - if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-api-v2-changes == 'true' }} - uses: ./.github/workflows/check-api-v2-breaking-changes.yml - secrets: inherit - e2e-api-v2: name: Tests needs: [prepare, setup-db] @@ -445,14 +423,11 @@ jobs: unit-test, api-v2-unit-test, security-audit, - check-api-v2-breaking-changes, check-prisma-migrations, integration-test, build, - build-api-v1, build-api-v2, build-atoms, - build-docs, setup-db, e2e, e2e-api-v2, @@ -461,7 +436,7 @@ jobs: e2e-app-store, ] if: always() - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - name: Fail if trust-check did not succeed run: | @@ -485,13 +460,10 @@ jobs: needs.unit-test.result != 'success' || needs.api-v2-unit-test.result != 'success' || needs.security-audit.result != 'success' || - (needs.prepare.outputs.has-api-v2-changes == 'true' && needs.check-api-v2-breaking-changes.result != 'success') || (needs.prepare.outputs.has-prisma-changes == 'true' && needs.check-prisma-migrations.result != 'success') || needs.build.result != 'success' || - needs.build-api-v1.result != 'success' || needs.build-api-v2.result != 'success' || needs.build-atoms.result != 'success' || - needs.build-docs.result != 'success' || needs.setup-db.result != 'success' || needs.integration-test.result != 'success' || needs.e2e.result != 'success' || diff --git a/.github/workflows/production-build-without-database.yml b/.github/workflows/production-build-without-database.yml index 58f6575521b3d8..ece78b0b0a00d4 100644 --- a/.github/workflows/production-build-without-database.yml +++ b/.github/workflows/production-build-without-database.yml @@ -39,7 +39,7 @@ env: jobs: build: name: Build Web App - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 @@ -66,8 +66,12 @@ jobs: lookup-only: true - name: Delete old production build caches for this branch if: steps.cache-check.outputs.cache-hit != 'true' - uses: useblacksmith/cache-delete@v1 - with: - key: prod-build-${{ github.head_ref || github.ref_name }} - prefix: "true" + continue-on-error: true + run: | + gh extension install actions/gh-actions-cache + for key in $(gh actions-cache list -R ${{ github.repository }} --key "prod-build-${{ github.head_ref || github.ref_name }}" --limit 100 | cut -f1); do + gh actions-cache delete "$key" -R ${{ github.repository }} --confirm + done + env: + GH_TOKEN: ${{ github.token }} - uses: ./.github/actions/cache-build diff --git a/.github/workflows/release-docker.yaml b/.github/workflows/release-docker.yaml index 8b062b7ee2861d..e9012e3ebcb0a6 100644 --- a/.github/workflows/release-docker.yaml +++ b/.github/workflows/release-docker.yaml @@ -32,7 +32,7 @@ on: jobs: prepare: name: "Prepare Release" - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest outputs: release_tag: ${{ steps.determine-tag.outputs.release_tag }} checkout_ref: ${{ steps.determine-tag.outputs.checkout_ref }} @@ -81,7 +81,7 @@ jobs: release-amd64: name: "Release AMD64" needs: prepare - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest env: RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} steps: @@ -106,7 +106,7 @@ jobs: use-as-latest: "true" - name: Notify Slack on Success - if: success() + if: success() && secrets.CI_SLACK_WEBHOOK_URL != '' uses: slackapi/slack-github-action@v1.24.0 with: payload: | @@ -117,7 +117,7 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }} - name: Notify Slack on Failure - if: failure() + if: failure() && secrets.CI_SLACK_WEBHOOK_URL != '' uses: slackapi/slack-github-action@v1.24.0 with: payload: | @@ -154,7 +154,7 @@ jobs: push-image: ${{ (github.event_name == 'push' || inputs.PUSH_IMAGE) && 'true' || 'false' }} - name: Notify Slack on Success - if: success() + if: success() && secrets.CI_SLACK_WEBHOOK_URL != '' uses: slackapi/slack-github-action@v1.24.0 with: payload: | @@ -165,7 +165,7 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }} - name: Notify Slack on Failure - if: failure() + if: failure() && secrets.CI_SLACK_WEBHOOK_URL != '' uses: slackapi/slack-github-action@v1.24.0 with: payload: | diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index c59d452a074127..0df84f682a2664 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -6,7 +6,7 @@ permissions: contents: read jobs: audit: - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/setup-db.yml b/.github/workflows/setup-db.yml index 22d41cb5c14451..3148c3ddeade58 100644 --- a/.github/workflows/setup-db.yml +++ b/.github/workflows/setup-db.yml @@ -26,7 +26,7 @@ env: jobs: setup-db: name: Setup Database - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 15 services: postgres: diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index b1df3dc09edede..4af892454ed094 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -9,7 +9,7 @@ jobs: test: name: Unit timeout-minutes: 20 - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: diff --git a/.snaplet/transform.ts b/.snaplet/transform.ts index a49e9dc6086e9c..639d0040e14cba 100644 --- a/.snaplet/transform.ts +++ b/.snaplet/transform.ts @@ -52,7 +52,7 @@ export default defineConfig({ access_token: c.uuid(row.access_token), expires_at: c.int(row.expires_at, { min: 0, - max: Math.pow(4, 8) - 1, + max: 4 ** 8 - 1, }), token_type: c.uuid(row.token_type), id_token: c.uuid(row.id_token), @@ -148,7 +148,7 @@ export default defineConfig({ return { amount: c.int(row.amount, { min: 0, - max: Math.pow(4, 8) - 1, + max: 4 ** 8 - 1, }), currency: c.sentence(row.currency), data: { @@ -188,7 +188,7 @@ export default defineConfig({ id: c .int(row.id, { min: 1, - max: Math.pow(4, 8) - 1, + max: 4 ** 8 - 1, }) .toString(), identifier: c.uuid(row.identifier), @@ -215,24 +215,6 @@ export default defineConfig({ payload: c.password(row.payload), }; }, - WorkflowStep({ row }) { - return { - sendTo: c.oneOf(row.sendTo, [ - "Man", - "Woman", - "Transgender", - "Non-binary/non-conforming", - "Not specified", - ]), - sender: c.oneOf(row.sender, [ - "Man", - "Woman", - "Transgender", - "Non-binary/non-conforming", - "Not specified", - ]), - }; - }, users: ({ row }) => row.role !== "ADMIN" ? { diff --git a/.well-known/security.txt b/.well-known/security.txt index 6db124150ebb7f..fa3d8e1bd1c3f3 100644 --- a/.well-known/security.txt +++ b/.well-known/security.txt @@ -1,4 +1,4 @@ Contact: security@cal.com Preferred-Languages: en Canonical: https://cal.com/.well-known/security.txt -Policy: https://github.com/calcom/cal.com/security/policy +Policy: https://github.com/calcom/cal.diy/security/policy diff --git a/AGENTS.md b/AGENTS.md index 5032e5b61725d9..064ffc3290b797 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ -# Cal.com Development Guide for AI Agents +# Cal.diy Development Guide for AI Agents -You are a senior Cal.com engineer working in a Yarn/Turbo monorepo. You prioritize type safety, security, and small, reviewable diffs. +You are a senior Cal.diy engineer working in a Yarn/Turbo monorepo. You prioritize type safety, security, and small, reviewable diffs. ## Do diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0402b9f215e14..da3a67fedb3877 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,6 @@ -# Contributing to Cal.com +# Contributing to Cal.diy + +> **Important:** Cal.diy is a community-driven, open-source fork of Cal.com. Contributions made here **do not** get merged into Cal.com's production service — Cal.com is now closed-source. This repo is maintained independently by the community under the MIT license. Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. @@ -6,7 +8,7 @@ Contributions are what make the open source community such an amazing place to l ### 👥 Prevent Work Duplication -Before submitting a new issue or PR, check if it already exists in the [Issues](https://github.com/calcom/cal.com/issues) or [Pull Requests](https://github.com/calcom/cal.com/pulls). +Before submitting a new issue or PR, check if it already exists in the [Issues](https://github.com/calcom/cal.diy/issues) or [Pull Requests](https://github.com/calcom/cal.diy/pulls). ### ✅ Work Only on Approved Issues @@ -64,7 +66,7 @@ Write with the future in mind. If there are trade-offs, edge cases, or temporary Minor improvements, non-core feature requests - + @@ -72,7 +74,7 @@ Write with the future in mind. If there are trade-offs, edge cases, or temporary Confusing UX (but still functional) - + @@ -80,7 +82,7 @@ Write with the future in mind. If there are trade-offs, edge cases, or temporary Core Features (Booking page, availability, timezone calculation) - + @@ -88,7 +90,7 @@ Write with the future in mind. If there are trade-offs, edge cases, or temporary Core Bugs (Login, Booking page, Emails not working) - + @@ -148,7 +150,7 @@ export class HashedLinkService { ... } ## Developing -[See README](https://github.com/calcom/cal.com#development) +[See README](https://github.com/calcom/cal.diy#development) ## Building @@ -166,7 +168,7 @@ More info on how to add new tests coming soon. ### Running Tests -[See README](https://github.com/calcom/cal.com#e2e-testing) +[See README](https://github.com/calcom/cal.diy#e2e-testing) #### Resolving Issues diff --git a/LICENSE b/LICENSE index 6520d20afc0bc1..98b51fbaac9145 100644 --- a/LICENSE +++ b/LICENSE @@ -1,670 +1,21 @@ -Copyright (c) 2020-present Cal.com, Inc. - -Portions of this software are licensed as follows: - -* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee and -https://github.com/calcom/cal.com/tree/main/apps/api/v2/src/ee directory of this repository (Commercial License) is licensed under the license defined in "ee/LICENSE". -* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component. -* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below. - - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. +MIT License - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. +Copyright (c) 2020-present Cal.com, Inc. - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PERMISSIONS.md b/PERMISSIONS.md index 3efd4b9862604e..f45c14d146cc6d 100644 --- a/PERMISSIONS.md +++ b/PERMISSIONS.md @@ -1,4 +1,4 @@ -# Cal.com Permission Documentation +# Cal.diy Permission Documentation This document maps existing role-based permission checks to the new PBAC (Permission-Based Access Control) system's permission strings in the format `resource.action`. diff --git a/README.md b/README.md index 9fe2b7643b1b3b..76ec30e722e31a 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,61 @@

- + Logo -

Cal.com

+

Cal.diy

- The open-source Calendly successor. + The community-driven, open-source scheduling platform.
- Learn more » + GitHub

- Discussions - · - Website - · - Issues - · - Roadmap + Discussions + · + Issues + · + Contributing

- Product Hunt - Checkly QA - Uptime - Github Stars - Ask DeepWiki - Hacker News - License - Commits-per-month - Pricing - Jitsu Tracked - Checkly Availability - - - - - - + License + Github Stars + Commits-per-month + Docker Pulls + +

-## About the Project +## About Cal.diy -booking-screen +booking-screen -# Scheduling infrastructure for absolutely everyone +**Cal.diy** is the community-driven, fully open-source scheduling platform — a fork of [Cal.com](https://cal.com) with all enterprise/commercial code removed. -The open source Calendly successor. You are in charge -of your own data, workflow, and appearance. +Cal.diy is **100% MIT-licensed** with no proprietary "Enterprise Edition" features. It's designed for individuals and self-hosters who want full control over their scheduling infrastructure without any commercial dependencies. -Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes, and even calls with our families. However, most tools are very limited in terms of control and customization. +### What's different from Cal.com? -That's where Cal.com comes in. Self-hosted or hosted by us. White-label by design. API-driven and ready to be deployed on your own domain. Full control of your events and data. +- **No enterprise features** — Teams, Organizations, Insights, Workflows, SSO/SAML, and other EE-only features have been removed +- **No license key required** — Everything works out of the box, no Cal.com account or license needed +- **100% open source** — The entire codebase is licensed under MIT, no "Open Core" split +- **Community-maintained** — Contributions are welcome and go directly into this project (see [CONTRIBUTING.md](./CONTRIBUTING.md)) -## Recognition - -#### [Hacker News](https://news.ycombinator.com/from?site=cal.com) - - - Featured on Hacker News - - - - Featured on Hacker News - - -#### [Product Hunt](https://producthunt.com/products/cal-com?utm_source=badge-top-post-badge&utm_medium=badge) - -Cal.com - The open source Calendly alternative | Product Hunt Cal.com - The open source Calendly alternative | Product Hunt Cal.com - The open source Calendly alternative | Product Hunt - -This project is tested with browserstack +> **Note:** Cal.diy is a self-hosted project. There is no hosted/managed version. You run it on your own infrastructure. ### Built With -- [Next.js](https://nextjs.org/?ref=cal.com) -- [tRPC](https://trpc.io/?ref=cal.com) -- [React.js](https://reactjs.org/?ref=cal.com) -- [Tailwind CSS](https://tailwindcss.com/?ref=cal.com) -- [Prisma.io](https://prisma.io/?ref=cal.com) -- [Daily.co](https://go.cal.com/daily) - -## Contact us - -Meet our sales team for any commercial inquiries. - -Book us with Cal.com - -## Stay Up-to-Date - -Cal.com officially launched as v.1.0 on the 15th of September 2021 and we've come a long way so far. Watch **releases** of this repository to be notified of future updates: - -![cal-star-github](https://user-images.githubusercontent.com/8019099/154853944-a9e3c999-3da3-4048-b149-b4f73893c6fb.gif) +- [Next.js](https://nextjs.org/) +- [tRPC](https://trpc.io/) +- [React.js](https://reactjs.org/) +- [Tailwind CSS](https://tailwindcss.com/) +- [Prisma.io](https://prisma.io/) +- [Daily.co](https://daily.co/) @@ -112,7 +65,7 @@ To get a local copy up and running, please follow these simple steps. ### Prerequisites -Here is what you need to be able to run Cal.com. +Here is what you need to be able to run Cal.diy. - Node.js (Version: >=18.x) - PostgreSQL (Version: >=13.x) @@ -124,19 +77,18 @@ Here is what you need to be able to run Cal.com. ### Setup -1. Clone the repo (or fork https://github.com/calcom/cal.com/fork). The code is licensed under [AGPLv3](https://github.com/calcom/cal.com/blob/main/LICENSE), which requires you to provide source code to users who interact with the software over a network. For commercial use without these requirements, [acquire a commercial license](https://cal.com/sales) +1. Clone the repo (or fork https://github.com/calcom/cal.diy/fork) ```sh - git clone https://github.com/calcom/cal.com.git + git clone https://github.com/calcom/cal.diy.git ``` - > If you are on Windows, run the following command on `gitbash` with admin privileges:
> `git clone -c core.symlinks=true https://github.com/calcom/cal.com.git`
- > See [docs](https://cal.com/docs/how-to-guides/how-to-troubleshoot-symbolic-link-issues-on-windows#enable-symbolic-links) for more details. + > If you are on Windows, run the following command on `gitbash` with admin privileges:
> `git clone -c core.symlinks=true https://github.com/calcom/cal.diy.git`
2. Go to the project folder ```sh - cd cal.com + cd cal.diy ``` 3. Install packages with yarn @@ -150,7 +102,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 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file. - + > **Windows users:** Replace the `packages/prisma/.env` symlink with a real copy to avoid a Prisma error (`unexpected character / in variable name`): > > ```sh @@ -198,7 +150,7 @@ You can use any of these credentials to sign in at [http://localhost:3000](http: #### Development tip -1. Add `export NODE_OPTIONS=“--max-old-space-size=16384”` to your shell script to increase the memory limit for the node process. Alternatively, you can run this in your terminal before running the app. Replace 16384 with the amount of RAM you want to allocate to the node process. +1. Add `export NODE_OPTIONS="--max-old-space-size=16384"` to your shell script to increase the memory limit for the node process. Alternatively, you can run this in your terminal before running the app. Replace 16384 with the amount of RAM you want to allocate to the node process. 2. Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\ Where {level} can be one of the following: @@ -230,7 +182,7 @@ for Logger level to be set at info, for example. 2. This will open a fully configured workspace in your browser with all the necessary dependencies already installed. -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/calcom/cal.com) +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/calcom/cal.diy) #### Manual setup @@ -393,63 +345,15 @@ Executable doesn't exist at /Users/alice/Library/Caches/ms-playwright/chromium-1 1. Enjoy the new version. -## AI-Assisted Development - -This repository includes configuration for AI coding assistants. All AI configuration lives in the `agents/` directory as a single source of truth. - -### Structure - -``` -agents/ -├── rules/ # Modular engineering rules -├── skills/ # Reusable skills/prompts -├── commands.md # Command reference -└── knowledge-base.md # Domain knowledge - -AGENTS.md # Main agent instructions -``` - -### Tool Configuration - -We use symlinks to share configuration across tools: - -``` -.claude/ -├── rules -> ../agents/rules -└── skills -> ../agents/skills - -.cursor/ -├── rules -> ../agents/rules -└── skills -> ../agents/skills -``` - -### Using Other Tools - -If you prefer other AI tools (Windsurf, Goose, OpenCode, etc.), you can create your own dot folders and exclude them from git: - -```bash -# Add to .git/info/exclude (local only, not committed) -.windsurf/ -.goose/ -.opencode/ -``` - -This keeps the repository clean while allowing personal tool preferences. - ## Deployment ### Docker -**Official support**: Our team will begin to officially support the Dockerfile and docker-compose resources in this -repository. - -**Important**: Cal.com will **not** be supporting installations that use these Docker resources. While we provide and maintain the Docker configurations, support for Docker-based installations is the responsibility of the user. +The Docker image can be found on DockerHub at [https://hub.docker.com/r/calcom/cal.diy](https://hub.docker.com/r/calcom/cal.diy). -This image can be found on DockerHub at [https://hub.docker.com/r/calcom/cal.com](https://hub.docker.com/r/calcom/cal.com). - -**Note for ARM Users**: Use the {version}-arm suffix for pulling images. Example: `docker pull calcom/cal.com:v5.6.19-arm`. +**Note for ARM Users**: Use the {version}-arm suffix for pulling images. Example: `docker pull calcom/cal.diy:v5.6.19-arm`. #### Requirements @@ -457,20 +361,18 @@ Make sure you have `docker` & `docker compose` installed on the server / system. Note: `docker compose` without the hyphen is now the primary method of using docker-compose, per the Docker documentation. -#### (Most users) Running Cal.com with Docker Compose - -If you are evaluating Cal.com or running with minimal to no modifications, this option is for you. +#### Running Cal.diy with Docker Compose -1. Clone calcom/cal.com +1. Clone the repository ```bash - git clone --recursive https://github.com/calcom/cal.com.git + git clone --recursive https://github.com/calcom/cal.diy.git ``` 2. Change into the directory ```bash - cd cal.com + cd cal.diy ``` 3. Prepare your configuration: Rename `.env.example` to `.env` and then update `.env` @@ -537,25 +439,21 @@ If you are evaluating Cal.com or running with minimal to no modifications, this docker compose pull ``` - This will use the default image locations as specified by `image:` in the docker-compose.yaml file. - - Note: To aid with support, by default Scarf.sh is used as registry proxy for download metrics. +5. Start Cal.diy via docker compose -5. Start Cal.com via docker compose - - (Most basic users, and for First Run) To run the complete stack, which includes a local Postgres database, Cal.com web app, and Prisma Studio: + To run the complete stack, which includes a local Postgres database, Cal.diy web app, and Prisma Studio: ```bash docker compose up -d ``` - To run Cal.com web app and Prisma Studio against a remote database, ensure that DATABASE_URL is configured for an available database and run: + To run Cal.diy web app and Prisma Studio against a remote database, ensure that DATABASE_URL is configured for an available database and run: ```bash docker compose up -d calcom studio ``` - To run only the Cal.com web app, ensure that DATABASE_URL is configured for an available database and run: + To run only the Cal.diy web app, ensure that DATABASE_URL is configured for an available database and run: ```bash docker compose up -d calcom @@ -563,13 +461,13 @@ If you are evaluating Cal.com or running with minimal to no modifications, this **Note: to run in attached mode for debugging, remove `-d` from your desired run command.** -6. Open a browser to [http://localhost:3000](http://localhost:3000), or your defined NEXT_PUBLIC_WEBAPP_URL. The first time you run Cal.com, a setup wizard will initialize. Define your first user, and you're ready to go! +6. Open a browser to [http://localhost:3000](http://localhost:3000), or your defined NEXT_PUBLIC_WEBAPP_URL. The first time you run Cal.diy, a setup wizard will initialize. Define your first user, and you're ready to go! **Note for first-time setup (Calendar integration)**: During the setup wizard, you may encounter a "Connect your Calendar" step that appears to be required. If you do not wish to connect a calendar at this time, you can skip this step by navigating directly to the dashboard at `/event-types`. Calendar integrations can be added later from the Settings > Integrations page. -#### Updating Cal.com +#### Updating Cal.diy -1. Stop the Cal.com stack +1. Stop the Cal.diy stack ```bash docker compose down @@ -582,31 +480,31 @@ If you are evaluating Cal.com or running with minimal to no modifications, this ``` 3. Update env vars as necessary. -4. Re-start the Cal.com stack +4. Re-start the Cal.diy stack ```bash docker compose up -d ``` -#### (Advanced users) Build and Run Cal.com +#### Building from source with Docker -1. Clone calcom/cal.com. +1. Clone the repository ```bash - git clone https://github.com/calcom/cal.com.git + git clone https://github.com/calcom/cal.diy.git ``` 2. Change into the directory ```bash - cd cal.com + cd cal.diy ``` 3. Rename `.env.example` to `.env` and then update `.env` For configuration options see [Build-time variables](#build-time-variables) below. Update the appropriate values in your .env file, then proceed. -4. Build the Cal.com docker image: +4. Build the Cal.diy docker image: Note: Due to application configuration requirements, an available database is currently required during the build process. @@ -618,27 +516,27 @@ If you are evaluating Cal.com or running with minimal to no modifications, this docker compose up -d database ``` -5. Build Cal.com via docker compose (DOCKER_BUILDKIT=0 must be provided to allow a network bridge to be used at build time. This requirement will be removed in the future) +5. Build Cal.diy via docker compose (DOCKER_BUILDKIT=0 must be provided to allow a network bridge to be used at build time. This requirement will be removed in the future) ```bash DOCKER_BUILDKIT=0 docker compose build calcom ``` -6. Start Cal.com via docker compose +6. Start Cal.diy via docker compose - (Most basic users, and for First Run) To run the complete stack, which includes a local Postgres database, Cal.com web app, and Prisma Studio: + To run the complete stack, which includes a local Postgres database, Cal.diy web app, and Prisma Studio: ```bash docker compose up -d ``` - To run Cal.com web app and Prisma Studio against a remote database, ensure that DATABASE_URL is configured for an available database and run: + To run Cal.diy web app and Prisma Studio against a remote database, ensure that DATABASE_URL is configured for an available database and run: ```bash docker compose up -d calcom studio ``` - To run only the Cal.com web app, ensure that DATABASE_URL is configured for an available database and run: + To run only the Cal.diy web app, ensure that DATABASE_URL is configured for an available database and run: ```bash docker compose up -d calcom @@ -646,7 +544,7 @@ If you are evaluating Cal.com or running with minimal to no modifications, this **Note: to run in attached mode for debugging, remove `-d` from your desired run command.** -7. Open a browser to [http://localhost:3000](http://localhost:3000), or your defined NEXT_PUBLIC_WEBAPP_URL. The first time you run Cal.com, a setup wizard will initialize. Define your first user, and you're ready to go! +7. Open a browser to [http://localhost:3000](http://localhost:3000), or your defined NEXT_PUBLIC_WEBAPP_URL. The first time you run Cal.diy, a setup wizard will initialize. Define your first user, and you're ready to go! #### Configuration @@ -654,35 +552,28 @@ If you are evaluating Cal.com or running with minimal to no modifications, this These variables must also be provided at runtime -| Variable | Description | Required | Default | -| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------- | -| DATABASE_URL | database url with credentials - if using a connection pooler, this setting should point there | required | `postgresql://unicorn_user:magical_password@database:5432/calendso` | -| CALCOM_LICENSE_KEY | Enterprise License Key | optional | | -| NEXT_PUBLIC_WEBAPP_URL | Base URL of the site. NOTE: if this value differs from the value used at build-time, there will be a slight delay during container start (to update the statically built files). | optional | `http://localhost:3000` | -| NEXTAUTH_URL | Location of the auth server. By default, this is the Cal.com docker instance itself. | optional | `{NEXT_PUBLIC_WEBAPP_URL}/api/auth` | -| NEXTAUTH_SECRET | Cookie encryption key. Must match build variable. Generate with: `openssl rand -base64 32` | required | `secret` | -| CALENDSO_ENCRYPTION_KEY | Authentication encryption key (32 bytes for AES256). Must match build variable. Generate with: `openssl rand -base64 24` | required | `secret` | +| Variable | Description | Required | Default | +| --- | --- | --- | --- | +| DATABASE_URL | database url with credentials - if using a connection pooler, this setting should point there | required | `postgresql://unicorn_user:magical_password@database:5432/calendso` | +| NEXT_PUBLIC_WEBAPP_URL | Base URL of the site. NOTE: if this value differs from the value used at build-time, there will be a slight delay during container start (to update the statically built files). | optional | `http://localhost:3000` | +| NEXTAUTH_URL | Location of the auth server. By default, this is the Cal.diy docker instance itself. | optional | `{NEXT_PUBLIC_WEBAPP_URL}/api/auth` | +| NEXTAUTH_SECRET | Cookie encryption key. Must match build variable. Generate with: `openssl rand -base64 32` | required | `secret` | +| CALENDSO_ENCRYPTION_KEY | Authentication encryption key (32 bytes for AES256). Must match build variable. Generate with: `openssl rand -base64 24` | required | `secret` | ##### Build-time variables If building the image yourself, these variables must be provided at the time of the docker build, and can be provided by updating the .env file. Currently, if you require changes to these variables, you must follow the instructions to build and publish your own image. -Updating these variables is not required for evaluation, but is required for running in production. Instructions for generating variables can be found in the [Cal.com instructions](https://github.com/calcom/cal.com) - -| Variable | Description | Required | Default | -| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------- | -| DATABASE_URL | database url with credentials - if using a connection pooler, this setting should point there | required | `postgresql://unicorn_user:magical_password@database:5432/calendso` | -| MAX_OLD_SPACE_SIZE | Needed for Nodejs/NPM build options | required | 4096 | -| NEXT_PUBLIC_LICENSE_CONSENT | license consent - true/false | required | | -| NEXTAUTH_SECRET | Cookie encryption key | required | `secret` | -| CALENDSO_ENCRYPTION_KEY | Authentication encryption key | required | `secret` | -| NEXT_PUBLIC_WEBAPP_URL | Base URL injected into static files | optional | `http://localhost:3000` | -| NEXT_PUBLIC_WEBSITE_TERMS_URL | custom URL for terms and conditions website | optional | `https://cal.com/terms` | -| NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL | custom URL for privacy policy website | optional | `https://cal.com/privacy` | -| NEXT_PUBLIC_API_V2_URL | URL for the v2 API, only required for custom integrations or custom booking experiences using [Cal.com Platform](https://cal.com/platform) | optional | | -| CALCOM_TELEMETRY_DISABLED | Allow Cal.com to collect anonymous usage data (set to `1` to disable) | optional | | -| NEXT_PUBLIC_SINGLE_ORG_SLUG | Required if ORGANIZATIONS_ENABLED is true | optional | | -| ORGANIZATIONS_ENABLED | Used for Enterprise or Organizations plan | optional | | +| Variable | Description | Required | Default | +| --- | --- | --- | --- | +| DATABASE_URL | database url with credentials - if using a connection pooler, this setting should point there | required | `postgresql://unicorn_user:magical_password@database:5432/calendso` | +| MAX_OLD_SPACE_SIZE | Needed for Nodejs/NPM build options | required | 4096 | +| NEXTAUTH_SECRET | Cookie encryption key | required | `secret` | +| CALENDSO_ENCRYPTION_KEY | Authentication encryption key | required | `secret` | +| NEXT_PUBLIC_WEBAPP_URL | Base URL injected into static files | optional | `http://localhost:3000` | +| NEXT_PUBLIC_WEBSITE_TERMS_URL | custom URL for terms and conditions website | optional | | +| NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL | custom URL for privacy policy website | optional | | +| CALCOM_TELEMETRY_DISABLED | Allow Cal.diy to collect anonymous usage data (set to `1` to disable) | optional | | #### Troubleshooting @@ -720,25 +611,23 @@ docker-calcom-1 | @calcom/web:start: message: 'request to http://testing.loca docker-calcom-1 | @calcom/web:start: } ``` - - ### Railway [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/cal) -You can deploy Cal.com on [Railway](https://railway.app) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal.com on their platform. +You can deploy Cal.diy on [Railway](https://railway.app). The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying on their platform. ### Northflank [![Deploy on Northflank](https://assets.northflank.com/deploy_to_northflank_smm_36700fb050.svg)](https://northflank.com/stacks/deploy-calcom) -You can deploy Cal.com on [Northflank](https://northflank.com) using the button above. The team at Northflank also have a [detailed blog post](https://northflank.com/guides/deploy-calcom-with-northflank) on deploying Cal.com on their platform. +You can deploy Cal.diy on [Northflank](https://northflank.com). The team at Northflank also have a [detailed blog post](https://northflank.com/guides/deploy-calcom-with-northflank) on deploying on their platform. ### Vercel Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan. -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.diy&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.diy%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.diy&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F) ### Render @@ -748,98 +637,18 @@ Currently Vercel Pro Plan is required to be able to Deploy this application with [![Deploy on Elestio](https://elest.io/images/logos/deploy-to-elestio-btn.png)](https://elest.io/open-source/cal.com) - - -## Roadmap - -Cal.com Roadmap - -See the [roadmap project](https://cal.com/roadmap) for a list of proposed features (and known issues). You can change the view to see planned tagged releases. - ## License -Cal.com, Inc. is a commercial open source company, which means some parts of this open source repository require a commercial license. The concept is called "Open Core" where the core technology (99%) is fully open source, licensed under [AGPLv3](https://opensource.org/license/agpl-v3) and the last 1% is covered under a commercial license (["/ee" Enterprise Edition](https://github.com/calcom/cal.com/tree/main/packages/features/ee)) which we believe is entirely relevant for larger organisations that require enterprise features. Enterprise features are built by the core engineering team of Cal.com, Inc. which is hired in full-time. Find their compensation on https://cal.com/open. - -> [!NOTE] -> Our philosophy is simple, all "Singleplayer APIs" are open-source under AGPLv3. All commercial "Multiplayer APIs" are under a commercial license. - -| | AGPLv3 | EE | -| --------------------------------- | ------ | --- | -| Self-host for commercial purposes | ✅ | ✅ | -| Clone privately | ✅ | ✅ | -| Fork publicly | ✅ | ✅ | -| Requires CLA | ✅ | ✅ | -|  Official Support | ❌  | ✅ | -| Derivative work privately | ❌ | ✅ | -|  SSO | ❌ | ✅ | -| Admin Panel | ❌ | ✅ | -| Impersonation | ❌ | ✅ | -| Managed Event Types | ❌ | ✅ | -| Organizations | ❌ | ✅ | -| Payments | ❌ | ✅ | -| Platform | ❌ | ✅ | -| Teams | ❌ | ✅ | -| Users | ❌ | ✅ | -| Video | ❌ | ✅ | -| Workflows | ❌ | ✅ | - -> [!TIP] -> We work closely with the community and always invite feedback about what should be open and what is fine to be commercial. This list is not set and stone and we have moved things from commercial to open in the past. Please open a [discussion](https://github.com/calcom/cal.com/discussions) if you feel like something is wrong. - -## Repo Activity - - - -## Contributing -We ❤️ contributions! Whether it’s fixing a typo, improving documentation, or building new features, your help makes Cal.com better. - -- Check out our [Contributing Guide](./CONTRIBUTING.md) for detailed steps. -- Join the discussion on [GitHub Discussions](https://github.com/calcom/cal.com/discussions) or our community channels. -- Please follow our coding standards and commit message conventions to keep the project consistent. - -Even small improvements matter — thank you for helping us grow! - -### Good First Issues - -We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process. +Cal.diy is fully open source, licensed under the [MIT License](https://opensource.org/license/mit). - - -### Bounties - - - - - Bounties of cal - - - - - -### Contributors - - - - - - - -### Translations - -Don't code but still want to contribute? Join our [Discussions](https://github.com/calcom/cal.com/discussions) and join the [#Translate channel](https://github.com/calcom/cal.com/discussions/categories/translations) and let us know what language you want to translate. - -![ar translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ar&style=flat&logo=crowdin&query=%24.progress.0.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![bg translation](https://img.shields.io/badge/dynamic/json?color=blue&label=bg&style=flat&logo=crowdin&query=%24.progress.1.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![cs translation](https://img.shields.io/badge/dynamic/json?color=blue&label=cs&style=flat&logo=crowdin&query=%24.progress.2.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![de translation](https://img.shields.io/badge/dynamic/json?color=blue&label=de&style=flat&logo=crowdin&query=%24.progress.3.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![el translation](https://img.shields.io/badge/dynamic/json?color=blue&label=el&style=flat&logo=crowdin&query=%24.progress.4.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![en translation](https://img.shields.io/badge/dynamic/json?color=blue&label=en&style=flat&logo=crowdin&query=%24.progress.5.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![es translation](https://img.shields.io/badge/dynamic/json?color=blue&label=es&style=flat&logo=crowdin&query=%24.progress.6.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![es-419 translation](https://img.shields.io/badge/dynamic/json?color=blue&label=es-419&style=flat&logo=crowdin&query=%24.progress.7.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![fr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=fr&style=flat&logo=crowdin&query=%24.progress.8.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![he translation](https://img.shields.io/badge/dynamic/json?color=blue&label=he&style=flat&logo=crowdin&query=%24.progress.9.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![hu translation](https://img.shields.io/badge/dynamic/json?color=blue&label=hu&style=flat&logo=crowdin&query=%24.progress.10.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![it translation](https://img.shields.io/badge/dynamic/json?color=blue&label=it&style=flat&logo=crowdin&query=%24.progress.11.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![ja translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ja&style=flat&logo=crowdin&query=%24.progress.12.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![ko translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ko&style=flat&logo=crowdin&query=%24.progress.13.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![nl translation](https://img.shields.io/badge/dynamic/json?color=blue&label=nl&style=flat&logo=crowdin&query=%24.progress.14.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![no translation](https://img.shields.io/badge/dynamic/json?color=blue&label=no&style=flat&logo=crowdin&query=%24.progress.15.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![pl translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pl&style=flat&logo=crowdin&query=%24.progress.16.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![pt translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pt&style=flat&logo=crowdin&query=%24.progress.17.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![pt-BR translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pt-BR&style=flat&logo=crowdin&query=%24.progress.18.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![ro translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ro&style=flat&logo=crowdin&query=%24.progress.19.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&logo=crowdin&query=%24.progress.20.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![sr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=sr&style=flat&logo=crowdin&query=%24.progress.21.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![sv translation](https://img.shields.io/badge/dynamic/json?color=blue&label=sv&style=flat&logo=crowdin&query=%24.progress.22.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![tr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=tr&style=flat&logo=crowdin&query=%24.progress.23.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![uk translation](https://img.shields.io/badge/dynamic/json?color=blue&label=uk&style=flat&logo=crowdin&query=%24.progress.24.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![vi translation](https://img.shields.io/badge/dynamic/json?color=blue&label=vi&style=flat&logo=crowdin&query=%24.progress.25.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![zh-CN translation](https://img.shields.io/badge/dynamic/json?color=blue&label=zh-CN&style=flat&logo=crowdin&query=%24.progress.26.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![zh-TW translation](https://img.shields.io/badge/dynamic/json?color=blue&label=zh-TW&style=flat&logo=crowdin&query=%24.progress.27.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) +Unlike Cal.com's "Open Core" model, Cal.diy has **no commercial/enterprise code**. The entire codebase is available under the same open-source license. ## Enabling Content Security Policy - Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported. -## Single Org Mode - -Refer to docs [here](./docs/self-hosting/guides/organization/single-organization-setup) for a detailed documentation with screenshots. - ## Integrations ### Obtaining the Google API Credentials @@ -852,11 +661,11 @@ Refer to docs [here](./docs/self-hosting/guides/organization/single-organization 6. In the third page (Test Users), add the Google account(s) you'll be using. Make sure the details are correct on the last page of the wizard and your consent screen will be configured. 7. Now select [Credentials](https://console.cloud.google.com/apis/credentials) from the side pane and then select Create Credentials. Select the OAuth Client ID option. 8. Select Web Application as the Application Type. -9. Under Authorized redirect URI's, select Add URI and then add the URI `/api/integrations/googlecalendar/callback` and `/api/auth/callback/google` replacing Cal.com URL with the URI at which your application runs. +9. Under Authorized redirect URI's, select Add URI and then add the URI `/api/integrations/googlecalendar/callback` and `/api/auth/callback/google` replacing Cal.diy URL with the URI at which your application runs. 10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs. 11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the `.env` file as the value for `GOOGLE_API_CREDENTIALS` key. -#### _Adding google calendar to Cal.com App Store_ +#### _Adding google calendar to Cal.diy App Store_ After adding Google credentials, you can now Google Calendar App to the app store. You can repopulate the App store by running @@ -870,7 +679,7 @@ You will need to complete a few more steps to activate Google Calendar App. Make sure to complete section "Obtaining the Google API Credentials". After that do the following -1. Add extra redirect URL `/api/auth/callback/google` +1. Add extra redirect URL `/api/auth/callback/google` 1. Under 'OAuth consent screen', click "PUBLISH APP" ### Obtaining Microsoft Graph Client ID and Secret @@ -878,7 +687,7 @@ following 1. Open [Azure App Registration](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) and select New registration 2. Name your application 3. Set **Who can use this application or access this API?** to **Accounts in any organizational directory (Any Azure AD directory - Multitenant)** -4. Set the **Web** redirect URI to `/api/integrations/office365calendar/callback` replacing Cal.com URL with the URI at which your application runs. +4. Set the **Web** redirect URI to `/api/integrations/office365calendar/callback` replacing Cal.diy URL with the URI at which your application runs. 5. Use **Application (client) ID** as the **MS_GRAPH_CLIENT_ID** attribute value in .env 6. Click **Certificates & secrets** create a new client secret and use the value as the **MS_GRAPH_CLIENT_SECRET** attribute @@ -891,17 +700,17 @@ following 5. Choose "User-managed app" for "Select how the app is managed". 6. De-select the option to publish the app on the Zoom App Marketplace, if asked. 7. Now copy the Client ID and Client Secret to your `.env` file into the `ZOOM_CLIENT_ID` and `ZOOM_CLIENT_SECRET` fields. -8. Set the "OAuth Redirect URL" under "OAuth Information" as `/api/integrations/zoomvideo/callback` replacing Cal.com URL with the URI at which your application runs. +8. Set the "OAuth Redirect URL" under "OAuth Information" as `/api/integrations/zoomvideo/callback` replacing Cal.diy URL with the URI at which your application runs. 9. Also add the redirect URL given above as an allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form. 10. You don't need to provide basic information about your app. Instead click on "Scopes" and then on "+ Add Scopes". On the left, 1. click the category "Meeting" and check the scope `meeting:write:meeting`. 2. click the category "User" and check the scope `user:read:settings`. 11. Click "Done". -12. You're good to go. Now you can easily add your Zoom integration in the Cal.com settings. +12. You're good to go. Now you can easily add your Zoom integration in the Cal.diy settings. ### Obtaining Daily API Credentials -1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information +1. Open [Daily.co](https://daily.co/) and create an account. 2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab. 3. Copy your API key. 4. Now paste the API key to your `.env` file into the `DAILY_API_KEY` field in your `.env` file. @@ -913,10 +722,9 @@ following 2. Register a new application by clicking the Register one now link. 3. Fill in your company details. 4. Select Basecamp 4 as the product to integrate with. -5. Set the Redirect URL for OAuth `/api/integrations/basecamp3/callback` replacing Cal.com URL with the URI at which your application runs. +5. Set the Redirect URL for OAuth `/api/integrations/basecamp3/callback` replacing Cal.diy URL with the URI at which your application runs. 6. Click on done and copy the Client ID and secret into the `BASECAMP3_CLIENT_ID` and `BASECAMP3_CLIENT_SECRET` fields. 7. Set the `BASECAMP3_CLIENT_SECRET` env variable to `{your_domain} ({support_email})`. - For example, `Cal.com (support@cal.com)`. ### Obtaining HubSpot Client ID and Secret @@ -926,10 +734,10 @@ following 4. Fill in any information you want in the "App info" tab 5. Go to tab "Auth" 6. Now copy the Client ID and Client Secret to your `.env` file into the `HUBSPOT_CLIENT_ID` and `HUBSPOT_CLIENT_SECRET` fields. -7. Set the Redirect URL for OAuth `/api/integrations/hubspot/callback` replacing Cal.com URL with the URI at which your application runs. +7. Set the Redirect URL for OAuth `/api/integrations/hubspot/callback` replacing Cal.diy URL with the URI at which your application runs. 8. In the "Scopes" section at the bottom of the page, make sure you select "Read" and "Write" for scopes called `crm.objects.contacts` and `crm.lists`. 9. Click the "Save" button at the bottom footer. -10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts. +10. You're good to go. Now you can see any booking in Cal.diy created as a meeting in HubSpot for your contacts. ### Obtaining Webex Client ID and Secret @@ -943,10 +751,10 @@ following 4. Fill in any information you want in the "Client Details" tab 5. Go to tab "Client Secret" tab. 6. Now copy the Client ID and Client Secret to your `.env` file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields. -7. Set the Redirect URL for OAuth `/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs. +7. Set the Redirect URL for OAuth `/api/integrations/zohocrm/callback` replacing Cal.diy URL with the URI at which your application runs. 8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers. 9. Click the "Save"/ "UPDATE" button at the bottom footer. -10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings. +10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.diy settings. ### Obtaining Zoho Calendar Client ID and Secret @@ -962,66 +770,55 @@ following ### Rate Limiting with Unkey -Cal.com uses [Unkey](https://unkey.com) for rate limiting. This is an optional feature and is not required for testing or self-hosting. +Cal.diy uses [Unkey](https://unkey.com) for rate limiting. This is an optional feature and is not required for self-hosting. If you want to enable rate limiting: 1. Sign up for an account at [unkey.com](https://unkey.com) -2. Create a Root key with permissions for +2. Create a Root key with permissions for `ratelimit.create_namespace` and `ratelimit.limit` 3. Copy the root key to your `.env` file into the `UNKEY_ROOT_KEY` field -Note: If you don't configure Unkey, Cal.com will work normally without rate limiting enabled. +Note: If you don't configure Unkey, Cal.diy will work normally without rate limiting enabled. -## Workflows +## Contributing -### Setting up SendGrid for Email reminders +We welcome contributions! Whether it's fixing a typo, improving documentation, or building new features, your help makes Cal.diy better. -1. Create a SendGrid account (https://signup.sendgrid.com/) -2. Go to Settings -> API keys and create an API key -3. Copy API key to your `.env` file into the `SENDGRID_API_KEY` field -4. Go to Settings -> Sender Authentication and verify a single sender -5. Copy the verified E-Mail to your `.env` file into the `SENDGRID_EMAIL` field -6. Add your custom sender name to the `.env` file into the `NEXT_PUBLIC_SENDGRID_SENDER_NAME` field (fallback is Cal.com) +> **Important:** Cal.diy is a community fork. Contributions to this repo do **not** flow to Cal.com's production platform. See [CONTRIBUTING.md](./CONTRIBUTING.md) for details. -### Setting up Twilio for SMS reminders +- Check out our [Contributing Guide](./CONTRIBUTING.md) for detailed steps. +- Join the discussion on [GitHub Discussions](https://github.com/calcom/cal.diy/discussions). +- Please follow our coding standards and commit message conventions to keep the project consistent. -1. Create a Twilio account (https://twilio.com/try-twilio) -2. Click ‘Get a Twilio phone number’ -3. Copy Account SID to your `.env` file into the `TWILIO_SID` field -4. Copy Auth Token to your `.env` file into the `TWILIO_TOKEN` field -5. Copy your Twilio phone number to your `.env` file into the `TWILIO_PHONE_NUMBER` field -6. Add your own sender ID to the `.env` file into the `NEXT_PUBLIC_SENDER_ID` field (fallback is Cal.com) -7. Create a messaging service (Develop -> Messaging -> Services) -8. Choose any name for the messaging service -9. Click 'Add Senders' -10. Choose phone number as sender type -11. Add the listed phone number -12. Leave all other fields as they are -13. Complete setup and click ‘View my new Messaging Service’ -14. Copy Messaging Service SID to your `.env` file into the `TWILIO_MESSAGING_SID` field -15. Create a verify service -16. Copy Verify Service SID to your `.env` file into the `TWILIO_VERIFY_SID` field +Even small improvements matter — thank you for helping us grow! -## Changesets +### Good First Issues -We use changesets to generate changelogs and publish public packages (packages with `private: true` are ignored). +We have a list of [help wanted](https://github.com/calcom/cal.diy/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process. -An example of good readme is [atoms readme](https://github.com/calcom/cal.com/blob/main/packages/platform/atoms/README.md). Every public package must: + + +### Contributors + + + + -1. Follow semantic versioning when using changesets. -2. Mark breaking changes using `❗️Breaking change` + + +### Translations + +Don't code but still want to contribute? Join our [Discussions](https://github.com/calcom/cal.diy/discussions) and help translate Cal.diy into your language. ## Acknowledgements -Special thanks to these amazing projects which help power Cal.com: +Cal.diy is built on the foundation created by [Cal.com](https://cal.com) and the many contributors to the original project. Special thanks to: -- [Vercel](https://vercel.com/?utm_source=calend-so&utm_campaign=oss) +- [Vercel](https://vercel.com/) - [Next.js](https://nextjs.org/) - [Day.js](https://day.js.org/) - [Tailwind CSS](https://tailwindcss.com/) - [Prisma](https://prisma.io/) - -Cal.com is an [open startup](https://cal.com/open) and [Jitsu](https://github.com/jitsucom/jitsu) (an open-source Segment alternative) helps us to track most of the usage metrics. diff --git a/SECURITY.md b/SECURITY.md index 8dead5a02798e4..689fbea92814d4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ Contact: [security@cal.com](mailto:security@cal.com) Based on [https://supabase.com/.well-known/security.txt](https://supabase.com/.well-known/security.txt) -At Cal.com, we consider the security of our systems a top priority. But no +At Cal.diy, we consider the security of our systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present. diff --git a/__checks__/organization.spec.ts b/__checks__/organization.spec.ts deleted file mode 100644 index 95af227fe001e5..00000000000000 --- a/__checks__/organization.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("Org", () => { - // Because these pages involve next.config.js rewrites, it's better to test them on production - test.describe("Embeds - i.cal.com", () => { - test("Org Profile Page should be embeddable", async ({ page }) => { - const response = await page.goto("https://i.cal.com/embed"); - expect(response?.status()).toBe(200); - await page.screenshot({ path: "screenshot.jpg" }); - const body = await response?.text(); - await expectPageToBeRenderedWithEmbedSsr(body); - }); - - test("Org User(Rick) Page should be embeddable", async ({ page }) => { - const response = await page.goto("https://i.cal.com/team-rick/embed"); - expect(response?.status()).toBe(200); - await expect(page.locator("text=Used by Checkly")).toBeVisible(); - const body = await response?.text(); - await expectPageToBeRenderedWithEmbedSsr(body); - }); - - test("Org User Event(/team-rick/test-event) Page should be embeddable", async ({ page }) => { - const response = await page.goto("https://i.cal.com/team-rick/test-event/embed"); - expect(response?.status()).toBe(200); - await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible(); - await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible(); - const body = await response?.text(); - await expectPageToBeRenderedWithEmbedSsr(body); - }); - - test("Org Team Profile(/sales) page should be embeddable", async ({ page }) => { - const response = await page.goto("https://i.cal.com/sales/embed"); - expect(response?.status()).toBe(200); - await expect(page.locator("text=Cal.com Sales")).toBeVisible(); - const body = await response?.text(); - await expectPageToBeRenderedWithEmbedSsr(body); - }); - - test("Org Team Event page(/sales/hippa) should be embeddable", async ({ page }) => { - const response = await page.goto("https://i.cal.com/sales/hipaa/embed"); - expect(response?.status()).toBe(200); - await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible(); - await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible(); - const body = await response?.text(); - await expectPageToBeRenderedWithEmbedSsr(body); - }); - }); - - test.describe("Dynamic Group Booking", () => { - test("Dynamic Group booking link should load", async ({ page }) => { - const users = [ - { - username: "peer", - name: "Peer Richelsen", - }, - { - username: "bailey", - name: "Bailey Pumfleet", - }, - ]; - const response = await page.goto(`http://i.cal.com/${users[0].username}+${users[1].username}`); - expect(response?.status()).toBe(200); - expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Group Meeting"); - - expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain( - "Join us for a meeting with multiple people" - ); - expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(2); - }); - }); - - test("Organization Homepage - Has Engineering and Marketing Teams", async ({ page }) => { - const response = await page.goto("https://i.cal.com"); - expect(response?.status()).toBe(200); - // Somehow there are two Cal.com text momentarily, but shouldn't be the concern of this check - await expect(page.locator("text=Cal.com").first()).toBeVisible(); - await expect(page.locator("text=Engineering")).toBeVisible(); - await expect(page.locator("text=Marketing")).toBeVisible(); - }); - - test.describe("Browse the Engineering Team", async () => { - test("By User Navigation", async ({ page }) => { - const response = await page.goto("https://i.cal.com"); - await page.waitForLoadState("networkidle"); - expect(response?.status()).toBe(200); - await page.click('text="Engineering"'); - await expect(page.locator("text=Cal.com Engineering")).toBeVisible(); - }); - - test("By /team/engineering", async ({ page }) => { - await page.goto("https://i.cal.com/team/engineering"); - await expect(page.locator("text=Cal.com Engineering")).toBeVisible(); - }); - - test("By /engineering", async ({ page }) => { - await page.goto("https://i.cal.com/engineering"); - await expect(page.locator("text=Cal.com Engineering")).toBeVisible(); - }); - }); -}); - -// This ensures that the route is actually mapped to a page that is using withEmbedSsr -async function expectPageToBeRenderedWithEmbedSsr(responseText: string | undefined) { - expect(responseText).toContain('\\"isEmbed\\":true'); -} diff --git a/agents/README.md b/agents/README.md index acd66c6f1f6360..5fb9e10ec1aeb4 100644 --- a/agents/README.md +++ b/agents/README.md @@ -1,4 +1,4 @@ -# Cal.com Agent Documentation Index +# Cal.diy Agent Documentation Index - **[../AGENTS.md](../AGENTS.md)** - Main guide (structure, tech stack, commands, examples) - **[commands.md](commands.md)** - Command reference diff --git a/agents/knowledge-base.md b/agents/knowledge-base.md index ac6b851ce4c5ad..ec970777ea4797 100644 --- a/agents/knowledge-base.md +++ b/agents/knowledge-base.md @@ -1,6 +1,6 @@ # Knowledge Base - Domain & Product-Specific Information -This file contains domain knowledge about the Cal.com product and codebase. For coding guidelines and rules, see [`rules/`](rules/). +This file contains domain knowledge about the Cal.diy product and codebase. For coding guidelines and rules, see [`rules/`](rules/). ## When working with managed event types @@ -52,9 +52,9 @@ Control logging verbosity by setting `NEXT_PUBLIC_LOGGER_LEVEL` in .env: - 5: error - 6: fatal -### Cal.com Event Identification +### Cal.diy Event Identification -Cal.com events in Google Calendar can be identified by checking if the iCalUID ends with `@Cal.com` (e.g., `2GBXSdEixretciJfKVmYN8@Cal.com`). This identifier is used to distinguish Cal.com bookings from other calendar events for data storage and privacy purposes. +Cal.diy events in Google Calendar can be identified by checking if the iCalUID ends with `@Cal.diy` (e.g., `2GBXSdEixretciJfKVmYN8@Cal.diy`). This identifier is used to distinguish Cal.diy bookings from other calendar events for data storage and privacy purposes. ### UI Component Locations @@ -87,7 +87,7 @@ To make persistent changes to API documentation, use NestJS decorators (`@ApiQue ### Workflows vs Webhooks -Workflows and webhooks are two completely separate features in Cal.com with different implementations and file structures: +Workflows and webhooks are two completely separate features in Cal.diy with different implementations and file structures: - Workflow constants: `packages/features/ee/workflows/lib/constants.ts` - NOT in the webhooks directory diff --git a/agents/rules/README.md b/agents/rules/README.md index a0a70d9f9b4a75..d7bf907275fd92 100644 --- a/agents/rules/README.md +++ b/agents/rules/README.md @@ -1,6 +1,6 @@ -# Cal.com Engineering Rules +# Cal.diy Engineering Rules -This directory contains modular, machine-readable engineering rules derived from [Cal.com's Engineering Standards for 2026 and Beyond](https://cal.com/blog/engineering-in-2026-and-beyond). +This directory contains modular, machine-readable engineering rules derived from [Cal.diy's Engineering Standards for 2026 and Beyond](https://cal.com/blog/engineering-in-2026-and-beyond). ## Structure diff --git a/agents/rules/api-no-breaking-changes.md b/agents/rules/api-no-breaking-changes.md index 2f7203f9512c67..d327fc9bb116d0 100644 --- a/agents/rules/api-no-breaking-changes.md +++ b/agents/rules/api-no-breaking-changes.md @@ -57,4 +57,4 @@ interface BookingResponse { - Give users ample time to migrate (minimum 6 months for public APIs) - Document exactly what changed and why -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/api-thin-controllers.md b/agents/rules/api-thin-controllers.md index 34956c841f6d41..61a804b545efd4 100644 --- a/agents/rules/api-thin-controllers.md +++ b/agents/rules/api-thin-controllers.md @@ -70,4 +70,4 @@ export async function POST(request: Request) { **The principle:** We must detach HTTP technology from our application. The way we transfer data between client and server (whether REST, tRPC, etc.) should not influence how our core application works. HTTP is a delivery mechanism, not an architectural driver. -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/architecture-feature-boundaries.md b/agents/rules/architecture-feature-boundaries.md index 52613fcd18ec64..6d517bb0cc0a36 100644 --- a/agents/rules/architecture-feature-boundaries.md +++ b/agents/rules/architecture-feature-boundaries.md @@ -39,4 +39,4 @@ Domain boundaries are enforced automatically through linting. If `packages/featu - Easier testing: Test the entire feature as a unit with all pieces in one place - Clearer dependencies: When you see `import { getAvailability } from '@calcom/features/availability'`, you know exactly which feature you're depending on -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/architecture-vertical-slices.md b/agents/rules/architecture-vertical-slices.md index d5237f8beb20be..bd80ab280ee6a5 100644 --- a/agents/rules/architecture-vertical-slices.md +++ b/agents/rules/architecture-vertical-slices.md @@ -58,4 +58,4 @@ Each feature folder is a self-contained vertical slice that includes: - Teams can work on different features without conflicts - Features are loosely coupled and can evolve independently -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/ci-check-failures.md b/agents/rules/ci-check-failures.md index 5858b1bd656108..7a540965c0744d 100644 --- a/agents/rules/ci-check-failures.md +++ b/agents/rules/ci-check-failures.md @@ -9,7 +9,7 @@ tags: ci, debugging, workflow ## What to Focus On -When reviewing CI check failures in Cal.com: +When reviewing CI check failures in Cal.diy: 1. **E2E tests can be flaky** and may fail intermittently 2. **Focus only on CI failures that are directly related to your code changes** diff --git a/agents/rules/ci-type-check-first.md b/agents/rules/ci-type-check-first.md index 68e0dc0c782934..25a749299b2ca6 100644 --- a/agents/rules/ci-type-check-first.md +++ b/agents/rules/ci-type-check-first.md @@ -9,7 +9,7 @@ tags: ci, typescript, type-check, workflow ## Priority Order -When working on the Cal.com repository, prioritize fixing type issues before addressing failing tests. +When working on the Cal.diy repository, prioritize fixing type issues before addressing failing tests. 1. Run `yarn type-check:ci --force` first 2. Fix all TypeScript errors diff --git a/agents/rules/culture-accountability.md b/agents/rules/culture-accountability.md index 31e979bab84bd8..ccd2a2eec6090b 100644 --- a/agents/rules/culture-accountability.md +++ b/agents/rules/culture-accountability.md @@ -42,4 +42,4 @@ We hold each other accountable for quality. Cutting corners might feel faster in - Focus on the code, not the person - Remember that quality standards protect everyone -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/culture-leverage-ai.md b/agents/rules/culture-leverage-ai.md index fd03fa7eb774bb..c13b1d172eb287 100644 --- a/agents/rules/culture-leverage-ai.md +++ b/agents/rules/culture-leverage-ai.md @@ -72,4 +72,4 @@ describe("calculateOverlap", () => { - Checks are fast and useful - AI helps ensure comprehensive coverage -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/data-dto-boundaries.md b/agents/rules/data-dto-boundaries.md index ab3f11b92c6e48..6cd9d17d393b3f 100644 --- a/agents/rules/data-dto-boundaries.md +++ b/agents/rules/data-dto-boundaries.md @@ -93,4 +93,4 @@ import { BookingStatus } from "@calcom/prisma/client"; Yes, this requires more code. Yes, it's worth it. Explicit boundaries prevent the architectural erosion that creates long-term maintenance nightmares. -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/data-prefer-select-over-include.md b/agents/rules/data-prefer-select-over-include.md index 5b752c8a3e7d41..89d2e9668c5d88 100644 --- a/agents/rules/data-prefer-select-over-include.md +++ b/agents/rules/data-prefer-select-over-include.md @@ -46,4 +46,4 @@ const booking = await prisma.booking.findFirst({ **Exception:** Use `include` only when you genuinely need all fields from a relation, which is rare. -Reference: [Cal.com Engineering Standards](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Standards](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/data-prisma-feature-flags.md b/agents/rules/data-prisma-feature-flags.md index bd3551794663fe..243533b488cd4d 100644 --- a/agents/rules/data-prisma-feature-flags.md +++ b/agents/rules/data-prisma-feature-flags.md @@ -9,7 +9,7 @@ tags: prisma, feature-flags, migrations ## Creating Feature Flag Migrations -To seed new feature flags in Cal.com, create a Prisma migration: +To seed new feature flags in Cal.diy, create a Prisma migration: ```bash yarn prisma migrate dev --create-only --name seed_[feature_name]_feature diff --git a/agents/rules/data-prisma-migrations.md b/agents/rules/data-prisma-migrations.md index b2d15236fdaa04..ea86e86f90c16b 100644 --- a/agents/rules/data-prisma-migrations.md +++ b/agents/rules/data-prisma-migrations.md @@ -9,7 +9,7 @@ tags: prisma, database, migrations, schema ## After Schema Changes -After making changes to the Prisma schema in Cal.com and creating migrations, you need to run: +After making changes to the Prisma schema in Cal.diy and creating migrations, you need to run: ```bash yarn prisma generate diff --git a/agents/rules/data-repository-methods.md b/agents/rules/data-repository-methods.md index 3db70f081f5f31..b382ff2ee08786 100644 --- a/agents/rules/data-repository-methods.md +++ b/agents/rules/data-repository-methods.md @@ -197,4 +197,4 @@ class BookingRepository { - Keep methods generic and reusable: `findByUserIdIncludeAttendees` not `findBookingsForReporting` - No business logic in repositories - that belongs in Services -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/data-repository-pattern.md b/agents/rules/data-repository-pattern.md index 62ed42e2218715..6140c87e6f7cd2 100644 --- a/agents/rules/data-repository-pattern.md +++ b/agents/rules/data-repository-pattern.md @@ -62,4 +62,4 @@ If we ever switch from Prisma to Drizzle or another ORM, the only changes requir - DI container wiring for new repositories - Nothing else in the codebase should care or change -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/patterns-app-store.md b/agents/rules/patterns-app-store.md index e0b4a6c81da966..a20b0d065699bd 100644 --- a/agents/rules/patterns-app-store.md +++ b/agents/rules/patterns-app-store.md @@ -9,7 +9,7 @@ tags: app-store, integrations, generated-files ## Generated Files -The Cal.com repository uses generated files (`*.generated.ts`) for app-store integrations. These files are created by the app-store-cli tool. +The Cal.diy repository uses generated files (`*.generated.ts`) for app-store integrations. These files are created by the app-store-cli tool. **Do not manually modify** `*.generated.ts` files. If you need structural changes to how integrations are imported or used, update the CLI code that generates these files. diff --git a/agents/rules/patterns-dependency-injection.md b/agents/rules/patterns-dependency-injection.md index 8b809d61cb5bf2..bfbc93bc0c35e3 100644 --- a/agents/rules/patterns-dependency-injection.md +++ b/agents/rules/patterns-dependency-injection.md @@ -54,7 +54,7 @@ class BookingService { - **Caching Proxies**: Wrap repositories or services to add caching behavior transparently - **Decorators**: Add cross-cutting concerns (logging, metrics) without polluting domain logic -## Cal.com's Type-Safe DI with moduleLoader +## Cal.diy's Type-Safe DI with moduleLoader We use `@evyweb/ioctopus` to manage service and repository dependencies. The **moduleLoader pattern** provides type-safe dependency injection, ensuring that if a service adds a new dependency, TypeScript will catch missing dependencies at build time rather than runtime. @@ -277,4 +277,4 @@ export function getMyService(): MyService { - **Automatic dependency resolution**: The moduleLoader automatically loads all dependencies recursively - **Self-documenting**: Each module declares its own dependencies explicitly -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/patterns-factory-pattern.md b/agents/rules/patterns-factory-pattern.md index 65a3fcfc985795..e6ade05fa7a502 100644 --- a/agents/rules/patterns-factory-pattern.md +++ b/agents/rules/patterns-factory-pattern.md @@ -84,4 +84,4 @@ class TeamBillingService extends BillingService { - Prefer polymorphism over conditionals - Watch for if statement accumulation during code review -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/patterns-workflow-triggers.md b/agents/rules/patterns-workflow-triggers.md deleted file mode 100644 index 38a68c53e90266..00000000000000 --- a/agents/rules/patterns-workflow-triggers.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Workflow Trigger Implementation -impact: MEDIUM -impactDescription: Consistent workflow patterns ensure reliable automation -tags: workflows, triggers, automation ---- - -# Workflow Trigger Implementation - -## Using scheduleWorkflowReminders - -To trigger workflows in Cal.com, use the `scheduleWorkflowReminders` function. This is the standard approach used throughout the codebase. - -Before implementing new workflow triggers, examine existing implementations in the codebase to understand the pattern. The function: -- Filters workflows by trigger type -- Processes each workflow step - -Key locations where this is used: -- Booking handlers -- Confirmation processes -- Other booking-related events - -## Adding New Workflow Triggers - -1. Check `packages/prisma/schema.prisma` for existing webhooks and workflow trigger enums as reference -2. Add the same enums to workflows (only when asked specifically) -3. Add enums to `packages/features/ee/workflows/lib/constants.ts` for UI display -4. Add translations to `en/locale.json` using the format `{enum}_trigger` (all lowercase) - -Webhook triggers serve as the reference implementation pattern for workflow triggers. - -## Workflows vs Webhooks - -Workflows and webhooks are two completely separate features in Cal.com with different implementations and file structures: - -- Workflow constants: `packages/features/ee/workflows/lib/constants.ts` -- NOT in the webhooks directory - -When working on workflow triggers, do not reference or use webhook trigger implementations - they are distinct systems. diff --git a/agents/rules/performance-avoid-quadratic.md b/agents/rules/performance-avoid-quadratic.md index d7fa3f2da0e03e..780f7209ae91b4 100644 --- a/agents/rules/performance-avoid-quadratic.md +++ b/agents/rules/performance-avoid-quadratic.md @@ -51,4 +51,4 @@ const available = availableSlots.filter(slot => { - **Hash maps/sets**: Use for O(1) lookups instead of `.find` or `.includes` on arrays - **Interval trees**: For scheduling, availability, and range queries -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/performance-dayjs-usage.md b/agents/rules/performance-dayjs-usage.md index 4563b50eb69f01..578182bbe53934 100644 --- a/agents/rules/performance-dayjs-usage.md +++ b/agents/rules/performance-dayjs-usage.md @@ -50,4 +50,4 @@ new Intl.DateTimeFormat(language).format(date); - Date formatting without timezone concerns - Performance-critical loops over dates -Reference: [Cal.com Engineering Standards](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Standards](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/performance-scheduling-complexity.md b/agents/rules/performance-scheduling-complexity.md index 1e5afd1829facb..a40b1cf04f185d 100644 --- a/agents/rules/performance-scheduling-complexity.md +++ b/agents/rules/performance-scheduling-complexity.md @@ -55,4 +55,4 @@ async function precomputeTeamAvailability(teamId: number) { This is why performance isn't just a nice-to-have in scheduling software. It's the foundation that determines whether your system can scale to enterprise needs. -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/quality-avoid-barrel-imports.md b/agents/rules/quality-avoid-barrel-imports.md index 68289a8cc994f3..b8aebd874a79af 100644 --- a/agents/rules/quality-avoid-barrel-imports.md +++ b/agents/rules/quality-avoid-barrel-imports.md @@ -32,4 +32,4 @@ import { UserService } from "./services/UserService"; import { Button } from "@calcom/ui/components/button"; ``` -Reference: [Cal.com Engineering Standards](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Standards](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/quality-code-review.md b/agents/rules/quality-code-review.md index 62bf70e1d652fb..de84d48a26d63f 100644 --- a/agents/rules/quality-code-review.md +++ b/agents/rules/quality-code-review.md @@ -26,7 +26,7 @@ Focus on providing a clear summary of what the PR is doing and its core function - Does the code do what it claims to do? - Are there any obvious bugs or edge cases? -- Does it follow Cal.com coding standards? +- Does it follow Cal.diy coding standards? - Is the change appropriately scoped? ## What to Skip (Unless Asked) diff --git a/agents/rules/quality-imports.md b/agents/rules/quality-imports.md index df55be8f105d86..767c538ff8dffc 100644 --- a/agents/rules/quality-imports.md +++ b/agents/rules/quality-imports.md @@ -9,7 +9,7 @@ tags: imports, exports, modules, app-store ## Named vs Default Exports -When working with imports in the Cal.com codebase, particularly in app-store integrations, pay attention to whether modules use named exports or default exports. +When working with imports in the Cal.diy codebase, particularly in app-store integrations, pay attention to whether modules use named exports or default exports. Many services like VideoApiAdapter, CalendarService, and PaymentService are exported as named exports, but the actual export name may differ from the generic service type. @@ -26,7 +26,7 @@ import CalendarService from "./applecalendar/lib/CalendarService"; ## Generated Files -When fixing imports in Cal.com's generated files (like `packages/app-store/apps.browser-*.generated.tsx`), always check the actual exports in the source files first. +When fixing imports in Cal.diy's generated files (like `packages/app-store/apps.browser-*.generated.tsx`), always check the actual exports in the source files first. For EventTypeAppCardInterface components, they likely use named exports rather than default exports, requiring: diff --git a/agents/rules/quality-no-followup-prs.md b/agents/rules/quality-no-followup-prs.md index 1976a6a97db63f..3a32d2a40435a3 100644 --- a/agents/rules/quality-no-followup-prs.md +++ b/agents/rules/quality-no-followup-prs.md @@ -37,4 +37,4 @@ const processedResult = processData(userData); **The principle:** The "slowness" of doing things right is an investment, not a cost. Code that's architected correctly from the start doesn't need massive refactors later. -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/quality-simplicity.md b/agents/rules/quality-simplicity.md index f001bcc6068d6d..68e1e41b39bf01 100644 --- a/agents/rules/quality-simplicity.md +++ b/agents/rules/quality-simplicity.md @@ -40,4 +40,4 @@ for (const item of data) { **Important note:** Simple doesn't mean lacking in features. Just because our goal is to create simple systems, this doesn't mean they should feel anemic and lacking obvious functionality. -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/quality-thorough-code-review.md b/agents/rules/quality-thorough-code-review.md index f2f5c62403ced8..7873762a33ccfd 100644 --- a/agents/rules/quality-thorough-code-review.md +++ b/agents/rules/quality-thorough-code-review.md @@ -37,4 +37,4 @@ Every nitpick matters. Every pattern violation matters. Address them before merg - If someone wants to commit untested code, push back - If someone suggests copying and pasting instead of creating a proper abstraction, call it out respectfully -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/testing-coverage-requirements.md b/agents/rules/testing-coverage-requirements.md index 06bd13ee91c4aa..acccadf451ee04 100644 --- a/agents/rules/testing-coverage-requirements.md +++ b/agents/rules/testing-coverage-requirements.md @@ -64,4 +64,4 @@ Yes, we know coverage doesn't guarantee perfect tests. We know you can write mea **Leverage AI for test generation:** AI can quickly and intelligently build comprehensive test suites. Manual testing is more and more a thing of the past. -Reference: [Cal.com Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) +Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond) diff --git a/agents/rules/testing-incremental.md b/agents/rules/testing-incremental.md index 7d9f5937b74da0..3b1e9e73c61454 100644 --- a/agents/rules/testing-incremental.md +++ b/agents/rules/testing-incremental.md @@ -9,7 +9,7 @@ tags: testing, debugging, workflow ## One File at a Time -When fixing failing tests in the Cal.com repository, take an incremental approach by addressing one file at a time rather than attempting to fix all issues simultaneously. +When fixing failing tests in the Cal.diy repository, take an incremental approach by addressing one file at a time rather than attempting to fix all issues simultaneously. This methodical approach makes it easier to identify and resolve specific issues without getting overwhelmed by the complexity of multiple failing tests across different files. diff --git a/agents/rules/testing-mocking.md b/agents/rules/testing-mocking.md index 67b40a02f91c5b..9ede706e17869e 100644 --- a/agents/rules/testing-mocking.md +++ b/agents/rules/testing-mocking.md @@ -9,13 +9,13 @@ tags: testing, mocking, calendar, app-store ## Calendar Service Mocks -When mocking calendar services in Cal.com test files, implement the `Calendar` interface rather than adding individual properties from each specific calendar service type (like `FeishuCalendarService`). +When mocking calendar services in Cal.diy test files, implement the `Calendar` interface rather than adding individual properties from each specific calendar service type (like `FeishuCalendarService`). Since all calendar services implement the `Calendar` interface and are stored in a map, the mock service should also implement this interface to ensure type compatibility. ## App-Store Integration Mocks -When mocking app-store resources in Cal.com tests, prefer implementing simpler mock designs that directly implement the required interfaces rather than trying to match complex deep mock structures created with `mockDeep`. +When mocking app-store resources in Cal.diy tests, prefer implementing simpler mock designs that directly implement the required interfaces rather than trying to match complex deep mock structures created with `mockDeep`. This approach is more maintainable and helps resolve type compatibility issues. diff --git a/agents/rules/testing-timezone.md b/agents/rules/testing-timezone.md index 33074d4d61888b..7a1e7d5665a9c4 100644 --- a/agents/rules/testing-timezone.md +++ b/agents/rules/testing-timezone.md @@ -9,7 +9,7 @@ tags: testing, timezone, consistency ## Always Use TZ=UTC -When running tests in the Cal.com repository, use the `TZ=UTC` environment variable: +When running tests in the Cal.diy repository, use the `TZ=UTC` environment variable: ```bash TZ=UTC yarn test diff --git a/agents/skills/calcom-api/SKILL.md b/agents/skills/calcom-api/SKILL.md index 5f65fb4e1247cc..ba5818aaa5d066 100644 --- a/agents/skills/calcom-api/SKILL.md +++ b/agents/skills/calcom-api/SKILL.md @@ -1,9 +1,9 @@ --- name: calcom-api -description: Interact with the Cal.com API v2 to manage scheduling, bookings, event types, availability, and calendars. Use this skill when building integrations that need to create or manage bookings, check availability, configure event types, or sync calendars with Cal.com's scheduling infrastructure. +description: Interact with the Cal.diy API v2 to manage scheduling, bookings, event types, availability, and calendars. Use this skill when building integrations that need to create or manage bookings, check availability, configure event types, or sync calendars with Cal.diy's scheduling infrastructure. env: CAL_API_KEY: - description: "Cal.com API key (prefixed with cal_live_ or cal_test_). Required for all API requests." + description: "Cal.diy API key (prefixed with cal_live_ or cal_test_). Required for all API requests." required: true CAL_CLIENT_ID: description: "OAuth client ID for platform integrations managing users on behalf of others. Sent as x-cal-client-id header." @@ -16,9 +16,9 @@ env: required: false --- -# Cal.com API v2 +# Cal.diy API v2 -This skill provides guidance for AI agents to interact with the Cal.com API v2, enabling scheduling automation, booking management, and calendar integrations. +This skill provides guidance for AI agents to interact with the Cal.diy API v2, enabling scheduling automation, booking management, and calendar integrations. ## Base URL @@ -31,7 +31,7 @@ https://api.cal.com/v2 | Environment Variable | Required | Description | |---------------------|----------|-------------| -| `CAL_API_KEY` | Yes | Cal.com API key (prefixed with `cal_live_` or `cal_test_`). Used as Bearer token for all API requests. Generate from Settings > Developer > API Keys. | +| `CAL_API_KEY` | Yes | Cal.diy API key (prefixed with `cal_live_` or `cal_test_`). Used as Bearer token for all API requests. Generate from Settings > Developer > API Keys. | | `CAL_CLIENT_ID` | No | OAuth client ID for platform integrations that manage users on behalf of others. Sent as `x-cal-client-id` header. | | `CAL_SECRET_KEY` | No | OAuth client secret for platform integrations. Sent as `x-cal-secret-key` header. | | `CAL_WEBHOOK_SECRET` | No | Secret for verifying webhook payload signatures via the `X-Cal-Signature-256` header. | diff --git a/agents/skills/calcom-api/references/authentication.md b/agents/skills/calcom-api/references/authentication.md index 8a44675c605f0a..da3dbf604c021a 100644 --- a/agents/skills/calcom-api/references/authentication.md +++ b/agents/skills/calcom-api/references/authentication.md @@ -1,10 +1,10 @@ # Authentication API Reference -Detailed documentation for authentication methods in the Cal.com API v2. +Detailed documentation for authentication methods in the Cal.diy API v2. ## Authentication Methods -Cal.com API v2 supports two authentication methods: +Cal.diy API v2 supports two authentication methods: 1. **API Key Authentication** - For direct API access 2. **OAuth/Platform Authentication** - For platform integrations managing users on behalf of others @@ -15,7 +15,7 @@ The primary authentication method for most API consumers. ### Obtaining an API Key -1. Log in to your Cal.com account +1. Log in to your Cal.diy account 2. Navigate to Settings > Developer > API Keys 3. Click "Create new API key" 4. Copy and securely store the generated key @@ -31,7 +31,7 @@ Authorization: Bearer cal_live_abc123xyz... ### API Key Format -All Cal.com API keys are prefixed with `cal_`: +All Cal.diy API keys are prefixed with `cal_`: - `cal_live_...` - Production API keys - `cal_test_...` - Test/sandbox API keys (if available) @@ -186,7 +186,7 @@ Common causes: 4. **Use minimal permissions**: Request only the scopes/permissions your application needs -5. **Monitor API usage**: Check your Cal.com dashboard for unusual activity +5. **Monitor API usage**: Check your Cal.diy dashboard for unusual activity 6. **Secure transmission**: Always use HTTPS for API requests diff --git a/agents/skills/calcom-api/references/bookings.md b/agents/skills/calcom-api/references/bookings.md index c76be873841eb2..ca23900119d474 100644 --- a/agents/skills/calcom-api/references/bookings.md +++ b/agents/skills/calcom-api/references/bookings.md @@ -1,6 +1,6 @@ # Bookings API Reference -Detailed documentation for booking-related endpoints in the Cal.com API v2. +Detailed documentation for booking-related endpoints in the Cal.diy API v2. ## Endpoints Overview diff --git a/agents/skills/calcom-api/references/calendars.md b/agents/skills/calcom-api/references/calendars.md index a297548bbf8453..9ab5fe5d291997 100644 --- a/agents/skills/calcom-api/references/calendars.md +++ b/agents/skills/calcom-api/references/calendars.md @@ -1,6 +1,6 @@ # Calendars API Reference -Detailed documentation for calendar integration endpoints in the Cal.com API v2. +Detailed documentation for calendar integration endpoints in the Cal.diy API v2. ## Endpoints Overview @@ -284,10 +284,10 @@ GET /v2/calendars/{calendar}/events/{eventUid} POST /v2/calendars/google-calendar/connect 2. User selects which calendars to check for conflicts - (Done via Cal.com dashboard) + (Done via Cal.diy dashboard) 3. User sets destination calendar for new bookings - (Done via Cal.com dashboard) + (Done via Cal.diy dashboard) 4. When checking slots: - API fetches busy times from all selected calendars @@ -298,9 +298,9 @@ GET /v2/calendars/{calendar}/events/{eventUid} - Confirmation emails sent to attendees ``` -### Cal.com Event Identification +### Cal.diy Event Identification -Cal.com events in external calendars can be identified by their iCalUID ending with `@Cal.com` (e.g., `2GBXSdEixretciJfKVmYN8@Cal.com`). +Cal.diy events in external calendars can be identified by their iCalUID ending with `@Cal.diy` (e.g., `2GBXSdEixretciJfKVmYN8@Cal.diy`). ## Team Calendar Integration diff --git a/agents/skills/calcom-api/references/event-types.md b/agents/skills/calcom-api/references/event-types.md index 2189bda954bca9..0ee12c41dcaeae 100644 --- a/agents/skills/calcom-api/references/event-types.md +++ b/agents/skills/calcom-api/references/event-types.md @@ -1,6 +1,6 @@ # Event Types API Reference -Detailed documentation for event type endpoints in the Cal.com API v2. +Detailed documentation for event type endpoints in the Cal.diy API v2. ## Endpoints Overview diff --git a/agents/skills/calcom-api/references/schedules.md b/agents/skills/calcom-api/references/schedules.md index 9868df60c2b1c1..60b3d5fe081add 100644 --- a/agents/skills/calcom-api/references/schedules.md +++ b/agents/skills/calcom-api/references/schedules.md @@ -1,6 +1,6 @@ # Schedules API Reference -Detailed documentation for schedule management endpoints in the Cal.com API v2. +Detailed documentation for schedule management endpoints in the Cal.diy API v2. ## Endpoints Overview diff --git a/agents/skills/calcom-api/references/slots-availability.md b/agents/skills/calcom-api/references/slots-availability.md index e304df56a240d9..5549dcfe040bfa 100644 --- a/agents/skills/calcom-api/references/slots-availability.md +++ b/agents/skills/calcom-api/references/slots-availability.md @@ -1,6 +1,6 @@ # Slots and Availability API Reference -Detailed documentation for checking availability and managing slots in the Cal.com API v2. +Detailed documentation for checking availability and managing slots in the Cal.diy API v2. ## Endpoints Overview diff --git a/agents/skills/calcom-api/references/webhooks.md b/agents/skills/calcom-api/references/webhooks.md index 2c0b8c0a76c88b..0e7b16ef4c5f25 100644 --- a/agents/skills/calcom-api/references/webhooks.md +++ b/agents/skills/calcom-api/references/webhooks.md @@ -1,6 +1,6 @@ # Webhooks API Reference -Detailed documentation for webhook management endpoints in the Cal.com API v2. +Detailed documentation for webhook management endpoints in the Cal.diy API v2. ## Endpoints Overview diff --git a/app.json b/app.json index ff45e67a923b1c..fa6d1d853f987c 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,7 @@ { - "name": "Cal.com", + "name": "Cal.diy", "description": "Open Source Scheduling", - "repository": "https://github.com/calcom/cal.com", + "repository": "https://github.com/calcom/cal.diy", "logo": "https://cal.com/android-chrome-512x512.png", "keywords": ["react", "typescript", "node", "nextjs", "prisma", "postgres", "trpc"], "addons": [ diff --git a/apps/api/v1/.env.example b/apps/api/v1/.env.example deleted file mode 100644 index d8d98b68b0de24..00000000000000 --- a/apps/api/v1/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -API_KEY_PREFIX=cal_ -DATABASE_URL="postgresql://postgres:@localhost:5450/calendso" -NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000 - -# Get CALCOM_LICENSE_KEY here: https://go.cal.com/get-license -CALCOM_LICENSE_KEY="" - -NEXT_PUBLIC_API_V2_ROOT_URL=http://localhost:5555 diff --git a/apps/api/v1/.gitignore b/apps/api/v1/.gitignore deleted file mode 100644 index a2c49f1abbb02e..00000000000000 --- a/apps/api/v1/.gitignore +++ /dev/null @@ -1,81 +0,0 @@ -# .env file -.env - -# dependencies -node_modules -.pnp -.pnp.js - -# testing -coverage -/test-results/ -playwright/videos -playwright/screenshots -playwright/artifacts -playwright/results -playwright/reports/* - -# next.js -.next/ -out/ -build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local -.env.* -!.env.example - -# vercel -.vercel - -# Webstorm -.idea - -### VisualStudioCode template -.vscode/ -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Typescript -tsconfig.tsbuildinfo - -# turbo -.turbo - -# Prisma-Zod -packages/prisma/zod/*.ts - -# Builds -dist - -# Linting -lint-results - -# Yarn -yarn-error.log - -.turbo -.next -.husky -.vscode -.env \ No newline at end of file diff --git a/apps/api/v1/.gitkeep b/apps/api/v1/.gitkeep deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/apps/api/v1/LICENSE b/apps/api/v1/LICENSE deleted file mode 100644 index a8c6744758303a..00000000000000 --- a/apps/api/v1/LICENSE +++ /dev/null @@ -1,42 +0,0 @@ -The Cal.com Commercial License (the “Commercial License”) -Copyright (c) 2020-present Cal.com, Inc - -With regard to the Cal.com Software: - -This software and associated documentation files (the "Software") may only be -used in production, if you (and any entity that you represent) have agreed to, -and are in compliance with, the Cal.com Subscription Terms available -at https://cal.com/terms, or other agreements governing -the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"), -and otherwise have a valid Cal.com Enterprise Edition subscription ("Commercial Subscription") -for the correct number of hosts as defined in the "Commercial Terms ("Hosts"). Subject to the foregoing sentence, -you are free to modify this Software and publish patches to the Software. You agree -that Cal.com and/or its licensors (as applicable) retain all right, title and interest in -and to all such modifications and/or patches, and all such modifications and/or -patches may only be used, copied, modified, displayed, distributed, or otherwise -exploited with a valid Commercial Subscription for the correct number of hosts. -Notwithstanding the foregoing, you may copy and modify the Software for development -and testing purposes, without requiring a subscription. You agree that Cal.com and/or -its licensors (as applicable) retain all right, title and interest in and to all such -modifications. You are not granted any other rights beyond what is expressly stated herein. -Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, -and/or sell the Software. - -This Commercial License applies only to the part of this Software that is not distributed under -the AGPLv3 license. Any part of this Software distributed under the MIT license or which -is served client-side as an image, font, cascading stylesheet (CSS), file which produces -or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or -in part, is copyrighted under the AGPLv3 license. The full text of this Commercial License shall -be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -For all third party components incorporated into the Cal.com Software, those -components are licensed under the original license provided by the owner of the -applicable component. diff --git a/apps/api/v1/README.md b/apps/api/v1/README.md deleted file mode 100644 index 071d58879d0b1b..00000000000000 --- a/apps/api/v1/README.md +++ /dev/null @@ -1,215 +0,0 @@ - - - -# Commercial Cal.com Public API - -Welcome to the Public API ("/apps/api") of the Cal.com. - -This is the public REST api for cal.com. -It exposes CRUD Endpoints of all our most important resources. -And it makes it easy for anyone to integrate with Cal.com at the application programming level. - -## Stack - -- NextJS -- TypeScript -- Prisma - -## Development - -### Setup - -1. Clone the main repo (NOT THIS ONE) - - ```sh - git clone --recurse-submodules -j8 https://github.com/calcom/cal.com.git - ``` - -1. Go to the project folder - - ```sh - cd cal.com - ``` - -1. Copy `apps/api/.env.example` to `apps/api/.env` - - ```sh - cp apps/api/.env.example apps/api/.env - cp .env.example .env - ``` - -1. Install packages with yarn - - ```sh - yarn - ``` - -1. Start developing - - ```sh - yarn workspace @calcom/api dev - ``` - -1. Open [http://localhost:3002](http://localhost:3002) with your browser to see the result. - -## API Authentication (API Keys) - -The API requires a valid apiKey query param to be passed: -You can generate them at - -For example: - -```sh -GET https://api.cal.com/v1/users?apiKey={INSERT_YOUR_CAL.COM_API_KEY_HERE} -``` - -API Keys optionally may have expiry dates, if they are expired they won't work. If you create an apiKey without a userId relation, it won't work either for now as it relies on it to establish the current authenticated user. - -In the future we might add support for header Bearer Auth if we need to or if our customers require it. - -## Middlewares - -We don't use the new NextJS 12 Beta Middlewares, mainly because they run on the edge, and are not able to call prisma from api endpoints. We use instead a very nifty library called next-api-middleware that let's us use a similar approach building our own middlewares and applying them as we see fit. - -- withMiddleware() requires some default middlewares (verifyApiKey, etc...) - -## Next.config.js - -### Redirects - -Since this is an API only project, we don't want to have to type /api/ in all the routes, and so redirect all traffic to api, so a call to `api.cal.com/v1` will resolve to `api.cal.com/api/v1` - -Likewise, v1 is added as param query called version to final /api call so we don't duplicate endpoints in the future for versioning if needed. - -### Transpiling locally shared monorepo modules - -We're calling several packages from monorepo, this need to be transpiled before building since are not available as regular npm packages. That's what withTM does. - -```js - "@calcom/app-store", - "@calcom/prisma", - "@calcom/lib", - "@calcom/features", -``` - -## API Endpoint Validation - -We validate that only the supported methods are accepted at each endpoint, so in - -- **/endpoint**: you can only [GET] (all) and [POST] (create new) -- **/endpoint/id**: you can read create and edit [GET, PATCH, DELETE] - -### Zod Validations - -The API uses `zod` library like our main web repo. It validates that either GET query parameters or POST body content's are valid and up to our spec. It gives errors when parsing result's with schemas and failing validation. - -We use it in several ways, but mainly, we first import the auto-generated schema from @calcom/prisma for each model, which lives in `lib/validations/` - -We have some shared validations which several resources require, like baseApiParams which parses apiKey in all requests, or querIdAsString or TransformParseInt which deal with the id's coming from req.query. - -- **[*]BaseBodyParams** that omits any values from the model that are too sensitive or we don't want to pick when creating a new resource like id, userId, etc.. (those are gotten from context or elsewhere) - -- **[*]Public** that also omits any values that we don't want to expose when returning the model as a response, which we parse against before returning all resources. - -- **[*]BodyParams** which merges both `[*]BaseBodyParams.merge([*]RequiredParams);` - -### Next Validations - -[Next-Validations Docs](https://next-validations.productsway.com/) -[Next-Validations Repo](https://github.com/jellydn/next-validations) -We also use this useful helper library that let us wrap our endpoints in a validate HOC that checks the req against our validation schema built out with zod for either query and / or body's requests. - -## Testing with Jest + node-mocks-http - -We aim to provide a fully tested API for our peace of mind, this is accomplished by using jest + node-mocks-http - -## Endpoints matrix - -| resource | get [id] | get all | create | edit | delete | -| --------------------- | -------- | ------- | ------ | ---- | ------ | -| attendees | ✅ | ✅ | ✅ | ✅ | ✅ | -| availabilities | ✅ | ✅ | ✅ | ✅ | ✅ | -| booking-references | ✅ | ✅ | ✅ | ✅ | ✅ | -| event-references | ✅ | ✅ | ✅ | ✅ | ✅ | -| destination-calendars | ✅ | ✅ | ✅ | ✅ | ✅ | -| custom-inputs | ✅ | ✅ | ✅ | ✅ | ✅ | -| event-types | ✅ | ✅ | ✅ | ✅ | ✅ | -| memberships | ✅ | ✅ | ✅ | ✅ | ✅ | -| payments | ✅ | ✅ | ❌ | ❌ | ❌ | -| schedules | ✅ | ✅ | ✅ | ✅ | ✅ | -| selected-calendars | ✅ | ✅ | ✅ | ✅ | ✅ | -| teams | ✅ | ✅ | ✅ | ✅ | ✅ | -| users | ✅ | 👤[1] | ✅ | ✅ | ✅ | - -## Models from database that are not exposed - -mostly because they're deemed too sensitive can be revisited if needed. Also they are expected to be used via cal's webapp. - -- [ ] Api Keys -- [ ] Credentials -- [ ] Webhooks -- [ ] ResetPasswordRequest -- [ ] VerificationToken -- [ ] ReminderMail - -## Documentation (OpenAPI) - -You will see that each endpoint has a comment at the top with the annotation `@swagger` with the documentation of the endpoint, **please update it if you change the code!** This is what auto-generates the OpenAPI spec by collecting the YAML in each endpoint and parsing it in /docs alongside the json-schema (auto-generated from prisma package, not added to code but manually for now, need to fix later) - -### @calcom/apps/swagger - -The documentation of the API lives inside the code, and it's auto-generated, the only endpoints that return without a valid apiKey are the homepage, with a JSON message redirecting you to the docs. and the /docs endpoint, which returns the OpenAPI 3.0 JSON Spec. Which SwaggerUi then consumes and generates the docs on. - -## Deployment - -`scripts/vercel-deploy.sh` -The API is deployed to vercel.com, it uses a similar deployment script to website or webapp, and requires transpilation of several shared packages that are part of our turborepo ["app-store", "prisma", "lib", "ee"] -in order to build and deploy properly. - -## Envirorment variables - -### Required - -DATABASE_URL=DATABASE_URL="postgresql://postgres:@localhost:5450/calendso" - -## Optional - -API*KEY_PREFIX=cal*# This can be changed per envirorment so cal*test* for staging for example. - -> If you're self-hosting under our commercial license, you can use any prefix you want for api keys. either leave the default cal\_ (not providing any envirorment variable) or modify it - -**Ensure that while testing swagger, API project should be run in production mode** -We make sure of this by not using next in dev, but next build && next start, if you want hot module reloading and such when developing, please use yarn run next directly on apps/api. - -See . Here in dev mode OPTIONS method is hardcoded to return only GET and OPTIONS as allowed method. Running in Production mode would cause this file to be not used. This is hot-reloading logic only. -To remove this limitation, we need to ensure that on local endpoints are requested by swagger at /api/v1 and not /v1 - -## How to deploy - -We recommend deploying API in vercel. - -There's some settings that you'll need to setup. - -Under Vercel > Your API Project > Settings - -In General > Build & Development Settings -BUILD COMMAND: `yarn turbo run build --filter=@calcom/api --no-deps` -OUTPUT DIRECTORY: `apps/api/.next` - -In Git > Ignored Build Step - -Add this command: `./scripts/vercel-deploy.sh` - -See `scripts/vercel-deploy.sh` for more info on how the deployment is done. - -> _❗ IMPORTANT: If you're forking the API repo you will need to update the URLs in both the main repo [`.gitmodules`](https://github.com/calcom/cal.com/blob/main/.gitmodules#L7) and this repo [`./scripts/vercel-deploy.sh`](https://github.com/calcom/api/blob/main/scripts/vercel-deploy.sh#L3) ❗_ - -## Environment variables - -Lastly API requires an env var for `DATABASE_URL` and `CALCOM_LICENSE_KEY` diff --git a/apps/api/v1/instrumentation.ts b/apps/api/v1/instrumentation.ts deleted file mode 100644 index 79040c9dbb19cb..00000000000000 --- a/apps/api/v1/instrumentation.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as Sentry from "@sentry/nextjs"; - -export function register() { - if (process.env.NEXT_RUNTIME === "nodejs") { - Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - }); - } - - if (process.env.NEXT_RUNTIME === "edge") { - Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - }); - } -} diff --git a/apps/api/v1/lib/constants.ts b/apps/api/v1/lib/constants.ts deleted file mode 100644 index fb04db4a1c49fc..00000000000000 --- a/apps/api/v1/lib/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const PRISMA_CLIENT_CACHING_TIME = 1000 * 60 * 60 * 24; // one day in ms diff --git a/apps/api/v1/lib/helpers/addRequestid.ts b/apps/api/v1/lib/helpers/addRequestid.ts deleted file mode 100644 index 92d2d6919a4486..00000000000000 --- a/apps/api/v1/lib/helpers/addRequestid.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { nanoid } from "nanoid"; -import type { NextMiddleware } from "next-api-middleware"; - -export const addRequestId: NextMiddleware = async (_req, res, next) => { - // Apply header with unique ID to every request - res.setHeader("Calcom-Response-ID", nanoid()); - // Add all headers here instead of next.config.js as it is throwing error( Cannot set headers after they are sent to the client) for OPTIONS method - // It is known to happen only in Dev Mode. - res.setHeader("Access-Control-Allow-Credentials", "true"); - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS, PATCH, DELETE, POST, PUT"); - res.setHeader( - "Access-Control-Allow-Headers", - "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Content-Type, api_key, Authorization" - ); - - // Ensure all OPTIONS request are automatically successful. Headers are already set above. - if (_req.method === "OPTIONS") { - res.status(200).end(); - return; - } - // Let remaining middleware and API route execute - await next(); -}; diff --git a/apps/api/v1/lib/helpers/captureErrors.ts b/apps/api/v1/lib/helpers/captureErrors.ts deleted file mode 100644 index 216276f3fdd640..00000000000000 --- a/apps/api/v1/lib/helpers/captureErrors.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { captureException as SentryCaptureException } from "@sentry/nextjs"; -import type { NextMiddleware } from "next-api-middleware"; - -export const captureErrors: NextMiddleware = async (_req, res, next) => { - try { - // Catch any errors that are thrown in remaining - // middleware and the API route handler - await next(); - } catch (error) { - console.error(error); - SentryCaptureException(error); - res.status(500).json({ message: "Something went wrong" }); - } -}; diff --git a/apps/api/v1/lib/helpers/captureUserId.ts b/apps/api/v1/lib/helpers/captureUserId.ts deleted file mode 100644 index 0256bb8345740f..00000000000000 --- a/apps/api/v1/lib/helpers/captureUserId.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { setUser as SentrySetUser } from "@sentry/nextjs"; -import type { NextMiddleware } from "next-api-middleware"; - -export const captureUserId: NextMiddleware = async (req, res, next) => { - if (req.userId) SentrySetUser({ id: req.userId }); - - await next(); -}; diff --git a/apps/api/v1/lib/helpers/extendRequest.ts b/apps/api/v1/lib/helpers/extendRequest.ts deleted file mode 100644 index d6fff1cbd0375b..00000000000000 --- a/apps/api/v1/lib/helpers/extendRequest.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { NextMiddleware } from "next-api-middleware"; - -export const extendRequest: NextMiddleware = async (req, res, next) => { - req.pagination = { - take: 100, - skip: 0, - }; - await next(); -}; diff --git a/apps/api/v1/lib/helpers/httpMethods.ts b/apps/api/v1/lib/helpers/httpMethods.ts deleted file mode 100644 index 8003a71984bbfe..00000000000000 --- a/apps/api/v1/lib/helpers/httpMethods.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { NextMiddleware } from "next-api-middleware"; - -export const httpMethod = (allowedHttpMethod: "GET" | "POST" | "PATCH" | "DELETE"): NextMiddleware => { - return async function (req, res, next) { - if (req.method === allowedHttpMethod || req.method == "OPTIONS") { - await next(); - } else { - res.status(405).json({ message: `Only ${allowedHttpMethod} Method allowed` }); - res.end(); - } - }; -}; -// Made this so we can support several HTTP Methods in one route and use it there. -// Could be further extracted into a third function or refactored into one. -// that checks if it's just a string or an array and apply the correct logic to both cases. -export const httpMethods = (allowedHttpMethod: string[]): NextMiddleware => { - return async function (req, res, next) { - if (allowedHttpMethod.some((method) => method === req.method || req.method == "OPTIONS")) { - await next(); - } else { - res.status(405).json({ message: `Only ${allowedHttpMethod} Method allowed` }); - res.end(); - } - }; -}; - -export const HTTP_POST = httpMethod("POST"); -export const HTTP_GET = httpMethod("GET"); -export const HTTP_PATCH = httpMethod("PATCH"); -export const HTTP_DELETE = httpMethod("DELETE"); -export const HTTP_GET_DELETE_PATCH = httpMethods(["GET", "DELETE", "PATCH"]); -export const HTTP_GET_OR_POST = httpMethods(["GET", "POST"]); diff --git a/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts b/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts deleted file mode 100644 index 656f69a7baa5f9..00000000000000 --- a/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { handleAutoLock } from "@calcom/features/ee/api-keys/lib/autoLock"; -import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; -import { HttpError } from "@calcom/lib/http-error"; -import type { RatelimitResponse } from "@unkey/ratelimit"; -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, expect, it, vi } from "vitest"; -import { rateLimitApiKey } from "~/lib/helpers/rateLimitApiKey"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -const testUserId = 123; - -vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({ - checkRateLimitAndThrowError: vi.fn(), -})); - -vi.mock("@calcom/features/ee/api-keys/lib/autoLock", () => ({ - handleAutoLock: vi.fn(), -})); - -vi.mock("@calcom/prisma", () => ({ - default: {}, - prisma: {}, -})); - -describe("rateLimitApiKey middleware", () => { - it("should return 401 if no apiKey is provided", async () => { - const { req, res } = createMocks({ - method: "GET", - query: {}, - userId: testUserId, - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await rateLimitApiKey(req, res, vi.fn() as any); - - expect(res._getStatusCode()).toBe(401); - expect(res._getJSONData()).toEqual({ message: "No apiKey provided" }); - }); - - it("should call checkRateLimitAndThrowError with correct parameters", async () => { - const { req, res } = createMocks({ - method: "GET", - query: { apiKey: "test-key" }, - userId: testUserId, - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (checkRateLimitAndThrowError as any).mockResolvedValueOnce({ - limit: 100, - remaining: 99, - reset: Date.now(), - }); - - // @ts-expect-error weird typing between middleware and createMocks - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await rateLimitApiKey(req, res, vi.fn() as any); - - expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({ - identifier: testUserId.toString(), - rateLimitingType: "api", - onRateLimiterResponse: expect.any(Function), - }); - }); - - it("should set rate limit headers correctly", async () => { - const { req, res } = createMocks({ - method: "GET", - query: { apiKey: "test-key" }, - userId: testUserId, - }); - - const rateLimiterResponse: RatelimitResponse = { - limit: 100, - remaining: 99, - reset: Date.now(), - success: true, - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (checkRateLimitAndThrowError as any).mockImplementationOnce( - ({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => { - onRateLimiterResponse(rateLimiterResponse); - } - ); - // @ts-expect-error weird typing between middleware and createMocks - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await rateLimitApiKey(req, res, vi.fn() as any); - - expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit); - expect(res.getHeader("X-RateLimit-Remaining")).toBe(rateLimiterResponse.remaining); - expect(res.getHeader("X-RateLimit-Reset")).toBe(rateLimiterResponse.reset); - }); - - it("should return 429 if rate limit is exceeded", async () => { - const { req, res } = createMocks({ - method: "GET", - query: { apiKey: "test-key" }, - userId: testUserId, - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded")); - - // @ts-expect-error weird typing between middleware and createMocks - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await rateLimitApiKey(req, res, vi.fn() as any); - - expect(res._getStatusCode()).toBe(429); - expect(res._getJSONData()).toEqual({ message: "Rate limit exceeded" }); - }); - - it("should lock API key when rate limit is repeatedly exceeded", async () => { - const { req, res } = createMocks({ - method: "GET", - query: { apiKey: "test-key" }, - userId: testUserId, - }); - - const rateLimiterResponse: RatelimitResponse = { - success: false, - remaining: 0, - limit: 100, - reset: Date.now(), - }; - - // Mock rate limiter to trigger the onRateLimiterResponse callback - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (checkRateLimitAndThrowError as any).mockImplementationOnce( - ({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => { - onRateLimiterResponse(rateLimiterResponse); - } - ); - - // Mock handleAutoLock to indicate the key was locked - vi.mocked(handleAutoLock).mockResolvedValueOnce(true); - - // @ts-expect-error weird typing between middleware and createMocks - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await rateLimitApiKey(req, res, vi.fn() as any); - - expect(handleAutoLock).toHaveBeenCalledWith({ - identifier: testUserId.toString(), - identifierType: "userId", - rateLimitResponse: rateLimiterResponse, - }); - - expect(res._getStatusCode()).toBe(429); - expect(res._getJSONData()).toEqual({ message: "Too many requests" }); - }); - - it("should handle API key not found error during auto-lock", async () => { - const { req, res } = createMocks({ - method: "GET", - query: { apiKey: "test-key" }, - userId: testUserId, - }); - - const rateLimiterResponse: RatelimitResponse = { - success: false, - remaining: 0, - limit: 100, - reset: Date.now(), - }; - - // Mock rate limiter to trigger the onRateLimiterResponse callback - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (checkRateLimitAndThrowError as any).mockImplementationOnce( - ({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => { - onRateLimiterResponse(rateLimiterResponse); - } - ); - - // Mock handleAutoLock to throw a "No user found" error - vi.mocked(handleAutoLock).mockRejectedValueOnce(new Error("No user found for this API key.")); - - // @ts-expect-error weird typing between middleware and createMocks - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await rateLimitApiKey(req, res, vi.fn() as any); - - expect(handleAutoLock).toHaveBeenCalledWith({ - identifier: testUserId.toString(), - identifierType: "userId", - rateLimitResponse: rateLimiterResponse, - }); - - expect(res._getStatusCode()).toBe(401); - expect(res._getJSONData()).toEqual({ message: "No user found for this API key." }); - }); - - it("should continue if auto-lock returns false (not locked)", async () => { - const { req, res } = createMocks({ - method: "GET", - query: { apiKey: "test-key" }, - userId: testUserId, - }); - - const rateLimiterResponse: RatelimitResponse = { - success: false, - remaining: 0, - limit: 100, - reset: Date.now(), - }; - - // Mock rate limiter to trigger the onRateLimiterResponse callback - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (checkRateLimitAndThrowError as any).mockImplementationOnce( - ({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => { - onRateLimiterResponse(rateLimiterResponse); - } - ); - - // Mock handleAutoLock to indicate the key was not locked - vi.mocked(handleAutoLock).mockResolvedValueOnce(false); - - const next = vi.fn(); - // @ts-expect-error weird typing between middleware and createMocks - await rateLimitApiKey(req, res, next); - - expect(handleAutoLock).toHaveBeenCalledWith({ - identifier: testUserId.toString(), - identifierType: "userId", - rateLimitResponse: rateLimiterResponse, - }); - - // Verify headers were set but request continued - expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit); - expect(next).toHaveBeenCalled(); - }); - - it("should handle HttpError during rate limiting", async () => { - const { req, res } = createMocks({ - method: "GET", - query: { apiKey: "test-key" }, - userId: testUserId, - }); - - // Mock checkRateLimitAndThrowError to throw HttpError - vi.mocked(checkRateLimitAndThrowError).mockRejectedValueOnce( - new HttpError({ - statusCode: 429, - message: "Custom rate limit error", - }) - ); - - // @ts-expect-error weird typing between middleware and createMocks - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await rateLimitApiKey(req, res, vi.fn() as any); - - expect(res._getStatusCode()).toBe(429); - expect(res._getJSONData()).toEqual({ message: "Custom rate limit error" }); - }); -}); diff --git a/apps/api/v1/lib/helpers/rateLimitApiKey.ts b/apps/api/v1/lib/helpers/rateLimitApiKey.ts deleted file mode 100644 index 444f0f62614451..00000000000000 --- a/apps/api/v1/lib/helpers/rateLimitApiKey.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { NextMiddleware } from "next-api-middleware"; - -import { handleAutoLock } from "@calcom/features/ee/api-keys/lib/autoLock"; -import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; -import { HttpError } from "@calcom/lib/http-error"; - -export const rateLimitApiKey: NextMiddleware = async (req, res, next) => { - if (!req.userId) return res.status(401).json({ message: "No userId provided" }); - if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" }); - - // TODO: Add a way to add trusted api keys - try { - const identifier = req.userId.toString(); - await checkRateLimitAndThrowError({ - identifier, - rateLimitingType: "api", - onRateLimiterResponse: async (response) => { - res.setHeader("X-RateLimit-Limit", response.limit); - res.setHeader("X-RateLimit-Remaining", response.remaining); - res.setHeader("X-RateLimit-Reset", response.reset); - - try { - const didLock = await handleAutoLock({ - identifier, - identifierType: "userId", - rateLimitResponse: response, - }); - - if (didLock) { - return res.status(429).json({ message: "Too many requests" }); - } - } catch (error) { - if (error instanceof Error && error.message === "No user found for this API key.") { - return res.status(401).json({ message: error.message }); - } - throw error; - } - }, - }); - } catch (error) { - if (error instanceof HttpError) { - return res.status(error.statusCode).json({ message: error.message }); - } - return res.status(429).json({ message: "Rate limit exceeded" }); - } - - await next(); -}; diff --git a/apps/api/v1/lib/helpers/safeParseJSON.ts b/apps/api/v1/lib/helpers/safeParseJSON.ts deleted file mode 100644 index 3681e422739656..00000000000000 --- a/apps/api/v1/lib/helpers/safeParseJSON.ts +++ /dev/null @@ -1,14 +0,0 @@ -export default function parseJSONSafely(str: string) { - try { - return JSON.parse(str); - } catch (e) { - console.error((e as Error).message); - if ((e as Error).message.includes("Unexpected token")) { - return { - success: false, - message: `Invalid JSON in the body: ${(e as Error).message}`, - }; - } - return {}; - } -} diff --git a/apps/api/v1/lib/helpers/verifyApiKey.test.ts b/apps/api/v1/lib/helpers/verifyApiKey.test.ts deleted file mode 100644 index bd8c404a04b932..00000000000000 --- a/apps/api/v1/lib/helpers/verifyApiKey.test.ts +++ /dev/null @@ -1,262 +0,0 @@ -/** - * Unit Tests for verifyApiKey middleware - * - * These tests verify the middleware logic without touching the database. - * All dependencies (repositories, utilities) are mocked. - */ -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import type { ILicenseKeyService } from "@calcom/ee/common/server/LicenseKeyService"; -import LicenseKeyService, { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService"; -import { PrismaApiKeyRepository } from "@calcom/features/ee/api-keys/repositories/PrismaApiKeyRepository"; -import { ApiKeyService } from "@calcom/features/ee/api-keys/services/ApiKeyService"; -import type { IDeploymentRepository } from "@calcom/features/ee/deployment/repositories/IDeploymentRepository"; -import { UserPermissionRole } from "@calcom/prisma/enums"; - -import { isAdminGuard } from "../utils/isAdmin"; -import { isLockedOrBlocked } from "../utils/isLockedOrBlocked"; -import { ScopeOfAdmin } from "../utils/scopeOfAdmin"; -import { verifyApiKey } from "./verifyApiKey"; - -vi.mock("@calcom/features/ee/api-keys/services/ApiKeyService", () => ({ - ApiKeyService: vi.fn(), -})); - -vi.mock("@calcom/features/ee/api-keys/repositories/PrismaApiKeyRepository", () => ({ - PrismaApiKeyRepository: vi.fn(), -})); - -vi.mock("../utils/isAdmin", () => ({ - isAdminGuard: vi.fn(), -})); - -vi.mock("../utils/isLockedOrBlocked", () => ({ - isLockedOrBlocked: vi.fn(), -})); - -vi.mock("@calcom/lib/crypto", () => ({ - symmetricDecrypt: vi.fn().mockReturnValue("mocked-decrypted-value"), - symmetricEncrypt: vi.fn().mockReturnValue("mocked-encrypted-value"), -})); - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -beforeEach(() => { - vi.stubEnv("CALENDSO_ENCRYPTION_KEY", "22gfxhWUlcKliUeXcu8xNah2+HP/29ZX"); -}); - -afterEach(() => { - vi.resetAllMocks(); - vi.unstubAllEnvs(); -}); - -const mockDeploymentRepository: IDeploymentRepository = { - getLicenseKeyWithId: vi.fn().mockResolvedValue("mockLicenseKey"), - getSignatureToken: vi.fn().mockResolvedValue("mockSignatureToken"), -}; - -describe("Verify API key - Unit Tests", () => { - let service: ILicenseKeyService; - let mockApiKeyService: ApiKeyService; - - beforeEach(async () => { - service = await LicenseKeyService.create(mockDeploymentRepository); - vi.spyOn(service, "checkLicense"); - - vi.spyOn(LicenseKeySingleton, "getInstance").mockResolvedValue(service as LicenseKeyService); - - mockApiKeyService = { - verifyKeyByHashedKey: vi.fn(), - } as unknown as ApiKeyService; - - vi.mocked(ApiKeyService).mockImplementation(function() { return mockApiKeyService; }); - vi.mocked(PrismaApiKeyRepository).mockImplementation(function() { return {} as unknown as PrismaApiKeyRepository; }); - - vi.mocked(isAdminGuard).mockReset(); - vi.mocked(isLockedOrBlocked).mockReset(); - }); - - it("should throw an error if the api key is not valid", async () => { - const { req, res } = createMocks({ - method: "POST", - body: {}, - }); - - const middleware = { - fn: verifyApiKey, - }; - - vi.mocked(service.checkLicense).mockResolvedValue(false); - - const serverNext = vi.fn((next: void) => Promise.resolve(next)); - - const middlewareSpy = vi.spyOn(middleware, "fn"); - - await middleware.fn(req, res, serverNext); - - expect(middlewareSpy).toBeCalled(); - - expect(res.statusCode).toBe(401); - }); - - it("should throw an error if no api key is provided", async () => { - const { req, res } = createMocks({ - method: "POST", - body: {}, - }); - - const middleware = { - fn: verifyApiKey, - }; - - vi.mocked(service.checkLicense).mockResolvedValue(true); - - const serverNext = vi.fn((next: void) => Promise.resolve(next)); - - const middlewareSpy = vi.spyOn(middleware, "fn"); - - await middleware.fn(req, res, serverNext); - - expect(middlewareSpy).toBeCalled(); - - expect(res.statusCode).toBe(401); - }); - - it("should set correct permissions for system-wide admin", async () => { - const { req, res } = createMocks({ - method: "POST", - body: {}, - query: { - apiKey: "cal_test_key", - }, - }); - - vi.mocked(mockApiKeyService.verifyKeyByHashedKey).mockResolvedValue({ - valid: true, - userId: 1, - user: { - uuid: "test-uuid-1", - role: UserPermissionRole.ADMIN, - locked: false, - email: "admin@example.com", - }, - }); - - vi.mocked(isAdminGuard).mockResolvedValue({ - isAdmin: true, - scope: ScopeOfAdmin.SystemWide, - }); - - vi.mocked(isLockedOrBlocked).mockResolvedValue(false); - - const middleware = { - fn: verifyApiKey, - }; - - vi.mocked(service.checkLicense).mockResolvedValue(true); - - const serverNext = vi.fn((next: void) => Promise.resolve(next)); - - const middlewareSpy = vi.spyOn(middleware, "fn"); - - await middleware.fn(req, res, serverNext); - - expect(middlewareSpy).toBeCalled(); - - expect(req.isSystemWideAdmin).toBe(true); - expect(req.isOrganizationOwnerOrAdmin).toBe(false); - }); - - it("should set correct permissions for org-level admin", async () => { - const { req, res } = createMocks({ - method: "POST", - body: {}, - query: { - apiKey: "cal_test_key", - }, - }); - - vi.mocked(mockApiKeyService.verifyKeyByHashedKey).mockResolvedValue({ - valid: true, - userId: 2, - user: { - uuid: "test-uuid-2", - role: UserPermissionRole.USER, - locked: false, - email: "org-admin@acme.com", - }, - }); - - vi.mocked(isAdminGuard).mockResolvedValue({ - isAdmin: true, - scope: ScopeOfAdmin.OrgOwnerOrAdmin, - }); - - vi.mocked(isLockedOrBlocked).mockResolvedValue(false); - - const middleware = { - fn: verifyApiKey, - }; - - vi.mocked(service.checkLicense).mockResolvedValue(true); - - const serverNext = vi.fn((next: void) => Promise.resolve(next)); - - const middlewareSpy = vi.spyOn(middleware, "fn"); - - await middleware.fn(req, res, serverNext); - - expect(middlewareSpy).toBeCalled(); - - expect(req.isSystemWideAdmin).toBe(false); - expect(req.isOrganizationOwnerOrAdmin).toBe(true); - }); - - it("should return 403 if user is locked or blocked", async () => { - const { req, res } = createMocks({ - method: "POST", - body: {}, - query: { - apiKey: "cal_test_key", - }, - }); - - vi.mocked(mockApiKeyService.verifyKeyByHashedKey).mockResolvedValue({ - valid: true, - userId: 3, - user: { - uuid: "test-uuid-3", - role: UserPermissionRole.USER, - locked: true, - email: "locked@example.com", - }, - }); - - vi.mocked(isAdminGuard).mockResolvedValue({ - isAdmin: false, - scope: ScopeOfAdmin.SystemWide, - }); - - vi.mocked(isLockedOrBlocked).mockResolvedValue(true); - - const middleware = { - fn: verifyApiKey, - }; - - vi.mocked(service.checkLicense).mockResolvedValue(true); - - const serverNext = vi.fn((next: void) => Promise.resolve(next)); - const middlewareSpy = vi.spyOn(middleware, "fn"); - - await middleware.fn(req, res, serverNext); - - expect(middlewareSpy).toBeCalled(); - expect(res.statusCode).toBe(403); - expect(JSON.parse(res._getData())).toEqual({ error: "You are not authorized to perform this request." }); - expect(serverNext).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/api/v1/lib/helpers/verifyApiKey.ts b/apps/api/v1/lib/helpers/verifyApiKey.ts deleted file mode 100644 index b71cdcab3153d0..00000000000000 --- a/apps/api/v1/lib/helpers/verifyApiKey.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { NextMiddleware } from "next-api-middleware"; - -import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService"; -import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys"; -import { PrismaApiKeyRepository } from "@calcom/features/ee/api-keys/repositories/PrismaApiKeyRepository"; -import { ApiKeyService } from "@calcom/features/ee/api-keys/services/ApiKeyService"; -import { DeploymentRepository } from "@calcom/features/ee/deployment/repositories/DeploymentRepository"; -import { IS_PRODUCTION } from "@calcom/lib/constants"; -import { prisma } from "@calcom/prisma"; - -import { isAdminGuard } from "../utils/isAdmin"; -import { isLockedOrBlocked } from "../utils/isLockedOrBlocked"; -import { ScopeOfAdmin } from "../utils/scopeOfAdmin"; - -// This verifies the apiKey and sets the user if it is valid. -export const verifyApiKey: NextMiddleware = async (req, res, next) => { - const deploymentRepo = new DeploymentRepository(prisma); - const licenseKeyService = await LicenseKeySingleton.getInstance(deploymentRepo); - const hasValidLicense = await licenseKeyService.checkLicense(); - - if (!hasValidLicense && IS_PRODUCTION) { - return res.status(401).json({ message: "Invalid or missing CALCOM_LICENSE_KEY environment variable" }); - } - - if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" }); - - const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", ""); - const hashedKey = hashAPIKey(strippedApiKey); - - // Use service layer for API key verification - const apiKeyRepo = new PrismaApiKeyRepository(prisma); - const apiKeyService = new ApiKeyService({ apiKeyRepo }); - const result = await apiKeyService.verifyKeyByHashedKey(hashedKey); - - if (!result.valid) { - return res.status(401).json({ error: result.error }); - } - - // save the user id and uuid in the request for later use - req.userId = result.userId; - req.userUuid = result.user.uuid; - req.user = result.user; - - const { isAdmin, scope } = await isAdminGuard(req); - const userIsLockedOrBlocked = await isLockedOrBlocked(req); - - if (userIsLockedOrBlocked) - return res.status(403).json({ error: "You are not authorized to perform this request." }); - - req.isSystemWideAdmin = isAdmin && scope === ScopeOfAdmin.SystemWide; - req.isOrganizationOwnerOrAdmin = isAdmin && scope === ScopeOfAdmin.OrgOwnerOrAdmin; - - await next(); -}; diff --git a/apps/api/v1/lib/helpers/verifyCredentialSyncEnabled.ts b/apps/api/v1/lib/helpers/verifyCredentialSyncEnabled.ts deleted file mode 100644 index b7922ae54df1d4..00000000000000 --- a/apps/api/v1/lib/helpers/verifyCredentialSyncEnabled.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { NextMiddleware } from "next-api-middleware"; - -import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants"; - -export const verifyCredentialSyncEnabled: NextMiddleware = async (req, res, next) => { - const { isSystemWideAdmin } = req; - - if (!isSystemWideAdmin) { - return res.status(403).json({ error: "Only admin API keys can access credential syncing endpoints" }); - } - - if (!APP_CREDENTIAL_SHARING_ENABLED) { - return res.status(501).json({ error: "Credential syncing is not enabled" }); - } - - if ( - req.headers[process.env.CALCOM_CREDENTIAL_SYNC_HEADER_NAME || "calcom-credential-sync-secret"] !== - process.env.CALCOM_CREDENTIAL_SYNC_SECRET - ) { - return res.status(401).json({ message: "Invalid credential sync secret" }); - } - - await next(); -}; diff --git a/apps/api/v1/lib/helpers/withMiddleware.ts b/apps/api/v1/lib/helpers/withMiddleware.ts deleted file mode 100644 index ff409fba51c158..00000000000000 --- a/apps/api/v1/lib/helpers/withMiddleware.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { label } from "next-api-middleware"; - -import { addRequestId } from "./addRequestid"; -import { captureErrors } from "./captureErrors"; -import { captureUserId } from "./captureUserId"; -import { extendRequest } from "./extendRequest"; -import { - HTTP_DELETE, - HTTP_GET, - HTTP_GET_DELETE_PATCH, - HTTP_GET_OR_POST, - HTTP_PATCH, - HTTP_POST, -} from "./httpMethods"; -import { rateLimitApiKey } from "./rateLimitApiKey"; -import { verifyApiKey } from "./verifyApiKey"; -import { verifyCredentialSyncEnabled } from "./verifyCredentialSyncEnabled"; -import { withPagination } from "./withPagination"; - -const middleware = { - HTTP_GET_OR_POST, - HTTP_GET_DELETE_PATCH, - HTTP_GET, - HTTP_PATCH, - HTTP_POST, - HTTP_DELETE, - addRequestId, - verifyApiKey, - rateLimitApiKey, - extendRequest, - pagination: withPagination, - captureErrors, - captureUserId, - verifyCredentialSyncEnabled, -}; - -type Middleware = keyof typeof middleware; - -const middlewareOrder: Middleware[] = [ - "extendRequest", - "captureErrors", - "verifyApiKey", - "rateLimitApiKey", - "addRequestId", - "captureUserId", -]; - -const withMiddleware = label(middleware, middlewareOrder); - -export { middleware, middlewareOrder, withMiddleware }; diff --git a/apps/api/v1/lib/helpers/withPagination.ts b/apps/api/v1/lib/helpers/withPagination.ts deleted file mode 100644 index f29c08829b187c..00000000000000 --- a/apps/api/v1/lib/helpers/withPagination.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { NextMiddleware } from "next-api-middleware"; -import z from "zod"; - -const withPage = z.object({ - page: z.coerce.number().min(1).optional().default(1), - take: z.coerce.number().min(1).optional().default(10), -}); - -export const withPagination: NextMiddleware = async (req, _, next) => { - const { page, take } = withPage.parse(req.query); - const skip = (page - 1) * take; - req.pagination = { - take, - skip, - }; - await next(); -}; diff --git a/apps/api/v1/lib/selects/event-type.ts b/apps/api/v1/lib/selects/event-type.ts deleted file mode 100644 index 2da41fdb84c745..00000000000000 --- a/apps/api/v1/lib/selects/event-type.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { Prisma } from "@calcom/prisma/client"; - -export const eventTypeSelect = { - id: true, - title: true, - slug: true, - length: true, - hidden: true, - position: true, - userId: true, - teamId: true, - scheduleId: true, - eventName: true, - timeZone: true, - periodType: true, - periodStartDate: true, - periodEndDate: true, - periodDays: true, - periodCountCalendarDays: true, - requiresConfirmation: true, - recurringEvent: true, - disableGuests: true, - hideCalendarNotes: true, - minimumBookingNotice: true, - beforeEventBuffer: true, - afterEventBuffer: true, - schedulingType: true, - price: true, - currency: true, - slotInterval: true, - parentId: true, - successRedirectUrl: true, - description: true, - locations: true, - metadata: true, - seatsPerTimeSlot: true, - seatsShowAttendees: true, - seatsShowAvailabilityCount: true, - bookingFields: true, - bookingLimits: true, - onlyShowFirstAvailableSlot: true, - durationLimits: true, - customInputs: { - select: { id: true, label: true, required: true, options: true, type: true, placeholder: true }, - }, - hashedLink: { - select: { link: true }, - }, - team: { - select: { slug: true }, - }, - hosts: { - select: { - userId: true, - isFixed: true, - scheduleId: true, - }, - }, - owner: { - select: { username: true, id: true, timeZone: true }, - }, - children: { - select: { - id: true, - userId: true, - }, - }, -} satisfies Prisma.EventTypeSelect; diff --git a/apps/api/v1/lib/types.ts b/apps/api/v1/lib/types.ts deleted file mode 100644 index 3022c8c0c808c1..00000000000000 --- a/apps/api/v1/lib/types.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { EventLocationType } from "@calcom/app-store/locations"; -import type { - Attendee, - Availability, - Booking, - BookingReference, - Credential, - DestinationCalendar, - EventType, - EventTypeCustomInput, - Membership, - Payment, - ReminderMail, - Schedule, - SelectedCalendar, - Team, - User, - Webhook, -} from "@calcom/prisma/client"; - -// Base response, used for all responses -export type BaseResponse = { - message?: string; - error?: Error; -}; - -// User -export type UserResponse = BaseResponse & { - user?: Partial; -}; - -export type UsersResponse = BaseResponse & { - users?: Partial[]; -}; - -// Team -export type TeamResponse = BaseResponse & { - team?: Partial; - owner?: Partial; -}; -export type TeamsResponse = BaseResponse & { - teams?: Partial[]; -}; - -// SelectedCalendar -export type SelectedCalendarResponse = BaseResponse & { - selected_calendar?: Partial; -}; -export type SelectedCalendarsResponse = BaseResponse & { - selected_calendars?: Partial[]; -}; - -// Attendee -export type AttendeeResponse = BaseResponse & { - attendee?: Partial; -}; -// Grouping attendees in booking arrays for now, -// later might remove endpoint and move to booking endpoint altogether. -export type AttendeesResponse = BaseResponse & { - attendees?: Partial[]; -}; - -// Availability -export type AvailabilityResponse = BaseResponse & { - availability?: Partial; -}; -export type AvailabilitiesResponse = BaseResponse & { - availabilities?: Partial[]; -}; - -// BookingReference -export type BookingReferenceResponse = BaseResponse & { - booking_reference?: Partial; -}; -export type BookingReferencesResponse = BaseResponse & { - booking_references?: Partial[]; -}; - -// Booking -export type BookingResponse = BaseResponse & { - booking?: Partial; -}; -export type BookingsResponse = BaseResponse & { - bookings?: Partial[]; -}; - -// Credential -export type CredentialResponse = BaseResponse & { - credential?: Partial; -}; -export type CredentialsResponse = BaseResponse & { - credentials?: Partial[]; -}; - -// DestinationCalendar -export type DestinationCalendarResponse = BaseResponse & { - destination_calendar?: Partial; -}; -export type DestinationCalendarsResponse = BaseResponse & { - destination_calendars?: Partial[]; -}; - -// Membership -export type MembershipResponse = BaseResponse & { - membership?: Partial; -}; -export type MembershipsResponse = BaseResponse & { - memberships?: Partial[]; -}; - -// EventTypeCustomInput -export type EventTypeCustomInputResponse = BaseResponse & { - event_type_custom_input?: Partial; -}; -export type EventTypeCustomInputsResponse = BaseResponse & { - event_type_custom_inputs?: Partial[]; -}; -// From rrule https://jakubroztocil.github.io/rrule freq -export enum Frequency { - "YEARLY", - "MONTHLY", - "WEEKLY", - "DAILY", - "HOURLY", - "MINUTELY", - "SECONDLY", -} -interface EventTypeExtended extends Omit { - recurringEvent: { - dtstart?: Date | undefined; - interval?: number | undefined; - count?: number | undefined; - freq?: Frequency | undefined; - until?: Date | undefined; - tzid?: string | undefined; - } | null; - locations: - | { - link?: string | undefined; - address?: string | undefined; - hostPhoneNumber?: string | undefined; - type: EventLocationType; - }[] - | null - // eslint-disable-next-line @typescript-eslint/no-explicit-any - | any; -} - -// EventType -export type EventTypeResponse = BaseResponse & { - event_type?: Partial; -}; -export type EventTypesResponse = BaseResponse & { - event_types?: Partial[]; -}; - -// Payment -export type PaymentResponse = BaseResponse & { - payment?: Partial; -}; -export type PaymentsResponse = BaseResponse & { - payments?: Partial[]; -}; - -// Schedule -export type ScheduleResponse = BaseResponse & { - schedule?: Partial; -}; -export type SchedulesResponse = BaseResponse & { - schedules?: Partial[]; -}; - -// Webhook -export type WebhookResponse = BaseResponse & { - webhook?: Partial | null; -}; -export type WebhooksResponse = BaseResponse & { - webhooks?: Partial[]; -}; - -// ReminderMail -export type ReminderMailResponse = BaseResponse & { - reminder_mail?: Partial; -}; -export type ReminderMailsResponse = BaseResponse & { - reminder_mails?: Partial[]; -}; diff --git a/apps/api/v1/lib/utils/bookings/get/buildWhereClause.ts b/apps/api/v1/lib/utils/bookings/get/buildWhereClause.ts deleted file mode 100644 index ae6b0348b0f717..00000000000000 --- a/apps/api/v1/lib/utils/bookings/get/buildWhereClause.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Constructs the WHERE clause for Prisma booking findMany operation. - * - * @param userId - The ID of the user making the request. This is used to filter bookings where the user is either the host or an attendee. - * @param attendeeEmails - An array of emails provided in the request for filtering bookings by attendee emails, used in case of Admin calls. - * @param userIds - An array of user IDs to be included in the filter. Defaults to an empty array, and an array of user IDs in case of Admin call containing it. - * @param userEmails - An array of user emails to be included in the filter if it is an Admin call and contains userId in query parameter. Defaults to an empty array. - * - * @returns An object that represents the WHERE clause for the findMany/findUnique operation. - */ -export function buildWhereClause(userId: number | null, attendeeEmails: string[], userIds: number[] = []) { - const filterByAttendeeEmails = attendeeEmails.length > 0; - const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : !!userId ? { userId } : {}; - - let whereClause = {}; - - if (filterByAttendeeEmails) { - whereClause = { - AND: [ - userFilter, - { - attendees: { - some: { - email: { in: attendeeEmails }, - }, - }, - }, - ], - }; - } else { - whereClause = { - ...userFilter, - }; - } - - return whereClause; -} diff --git a/apps/api/v1/lib/utils/extractUserIdsFromQuery.ts b/apps/api/v1/lib/utils/extractUserIdsFromQuery.ts deleted file mode 100644 index 8faffc4b986054..00000000000000 --- a/apps/api/v1/lib/utils/extractUserIdsFromQuery.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; - -import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; - -export function extractUserIdsFromQuery({ isSystemWideAdmin, query }: NextApiRequest) { - /** Guard: Only admins can query other users */ - if (!isSystemWideAdmin) { - throw new HttpError({ statusCode: 401, message: "ADMIN required" }); - } - const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query); - return Array.isArray(userIdOrUserIds) ? userIdOrUserIds : [userIdOrUserIds]; -} diff --git a/apps/api/v1/lib/utils/isAdmin.ts b/apps/api/v1/lib/utils/isAdmin.ts deleted file mode 100644 index 1958ee055fcc9f..00000000000000 --- a/apps/api/v1/lib/utils/isAdmin.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { NextApiRequest } from "next"; - -import prisma from "@calcom/prisma"; -import { UserPermissionRole, MembershipRole } from "@calcom/prisma/enums"; - -import { ScopeOfAdmin } from "./scopeOfAdmin"; - -export const isAdminGuard = async (req: NextApiRequest) => { - const { user, userId } = req; - if (!user) return { isAdmin: false, scope: null }; - - const { role: userRole } = user; - if (userRole === UserPermissionRole.ADMIN) return { isAdmin: true, scope: ScopeOfAdmin.SystemWide }; - - const orgOwnerOrAdminMemberships = await prisma.membership.findMany({ - where: { - userId: userId, - accepted: true, - role: { - in: [MembershipRole.OWNER, MembershipRole.ADMIN], - }, - team: { - isOrganization: true, - organizationSettings: { - isAdminAPIEnabled: true, - }, - }, - }, - select: { - team: { - select: { - id: true, - isOrganization: true, - }, - }, - }, - }); - if (orgOwnerOrAdminMemberships.length > 0) return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin }; - - return { isAdmin: false, scope: null }; -}; diff --git a/apps/api/v1/lib/utils/isLockedOrBlocked.ts b/apps/api/v1/lib/utils/isLockedOrBlocked.ts deleted file mode 100644 index e0f3d423b00b35..00000000000000 --- a/apps/api/v1/lib/utils/isLockedOrBlocked.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { sentrySpan } from "@calcom/features/watchlist/lib/telemetry"; -import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller"; - -export async function isLockedOrBlocked(req: NextApiRequest) { - const user = req.user; - if (!user?.email) return false; - return ( - user.locked || - (await checkIfEmailIsBlockedInWatchlistController({ - email: user.email, - organizationId: null, - span: sentrySpan, - })) - ); -} diff --git a/apps/api/v1/lib/utils/isValidBase64Image.ts b/apps/api/v1/lib/utils/isValidBase64Image.ts deleted file mode 100644 index 8c69df55067390..00000000000000 --- a/apps/api/v1/lib/utils/isValidBase64Image.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function isValidBase64Image(input: string): boolean { - const regex = /^data:image\/[^;]+;base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; - return regex.test(input); -} diff --git a/apps/api/v1/lib/utils/retrieveScopedAccessibleUsers.ts b/apps/api/v1/lib/utils/retrieveScopedAccessibleUsers.ts deleted file mode 100644 index 08bfc37dd720e5..00000000000000 --- a/apps/api/v1/lib/utils/retrieveScopedAccessibleUsers.ts +++ /dev/null @@ -1,95 +0,0 @@ -import prisma from "@calcom/prisma"; -import { MembershipRole } from "@calcom/prisma/enums"; - -type AccessibleUsersType = { - memberUserIds: number[]; - adminUserId: number; -}; - -const getAllOrganizationMemberships = async ( - memberships: { - userId: number; - role: MembershipRole; - teamId: number; - }[], - orgId: number -) => { - return memberships.reduce((acc, membership) => { - if (membership.teamId === orgId) { - acc.push(membership.userId); - } - return acc; - }, []); -}; - -const getAllAdminMemberships = async (userId: number) => { - return await prisma.membership.findMany({ - where: { - userId: userId, - accepted: true, - role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] }, - }, - select: { - team: { - select: { - id: true, - isOrganization: true, - }, - }, - }, - }); -}; - -const getAllOrganizationMembers = async (organizationId: number) => { - return await prisma.membership.findMany({ - where: { - teamId: organizationId, - accepted: true, - }, - select: { - userId: true, - }, - }); -}; - -export const getAccessibleUsers = async ({ - memberUserIds, - adminUserId, -}: AccessibleUsersType): Promise => { - const orConditions = []; - if (memberUserIds.length > 0) { - orConditions.push({ userId: { in: memberUserIds } }); - } - orConditions.push({ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } }); - - const memberships = await prisma.membership.findMany({ - where: { - team: { - isOrganization: true, - }, - accepted: true, - OR: orConditions, - }, - select: { - userId: true, - role: true, - teamId: true, - }, - }); - - const orgId = memberships.find((membership) => membership.userId === adminUserId)?.teamId; - if (!orgId) return []; - - const allAccessibleMemberUserIds = await getAllOrganizationMemberships(memberships, orgId); - const accessibleUserIds = allAccessibleMemberUserIds.filter((userId) => userId !== adminUserId); - return accessibleUserIds; -}; - -export const retrieveOrgScopedAccessibleUsers = async ({ adminId }: { adminId: number }) => { - const adminMemberships = await getAllAdminMemberships(adminId); - const organizationId = adminMemberships.find((membership) => membership.team.isOrganization)?.team.id; - if (!organizationId) return []; - - const allMemberships = await getAllOrganizationMembers(organizationId); - return allMemberships.map((membership) => membership.userId); -}; diff --git a/apps/api/v1/lib/utils/scopeOfAdmin.ts b/apps/api/v1/lib/utils/scopeOfAdmin.ts deleted file mode 100644 index ed0985669962de..00000000000000 --- a/apps/api/v1/lib/utils/scopeOfAdmin.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const ScopeOfAdmin = { - SystemWide: "SystemWide", - OrgOwnerOrAdmin: "OrgOwnerOrAdmin", -} as const; diff --git a/apps/api/v1/lib/utils/stringifyISODate.ts b/apps/api/v1/lib/utils/stringifyISODate.ts deleted file mode 100644 index cf21cd804baa91..00000000000000 --- a/apps/api/v1/lib/utils/stringifyISODate.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const stringifyISODate = (date: Date | undefined): string => { - return `${date?.toISOString()}`; -}; -// TODO: create a function that takes an object and returns a stringified version of dates of it. diff --git a/apps/api/v1/lib/validations/api-key.ts b/apps/api/v1/lib/validations/api-key.ts deleted file mode 100644 index beb7081498c50c..00000000000000 --- a/apps/api/v1/lib/validations/api-key.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { z } from "zod"; - -import { ApiKeySchema as ApiKey } from "@calcom/prisma/zod/modelSchema/ApiKeySchema"; - -export const apiKeyCreateBodySchema = ApiKey.pick({ - note: true, - expiresAt: true, - userId: true, -}) - .partial({ userId: true }) - .merge(z.object({ neverExpires: z.boolean().optional() })) - .strict(); - -export const apiKeyEditBodySchema = ApiKey.pick({ - note: true, -}) - .partial() - .strict(); - -export const apiKeyPublicSchema = ApiKey.pick({ - id: true, - userId: true, - note: true, - createdAt: true, - expiresAt: true, - lastUsedAt: true, - /** We might never want to expose these. Leaving this a as reminder. */ - // hashedKey: true, -}); diff --git a/apps/api/v1/lib/validations/attendee.ts b/apps/api/v1/lib/validations/attendee.ts deleted file mode 100644 index f2e69cf1530ac5..00000000000000 --- a/apps/api/v1/lib/validations/attendee.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { z } from "zod"; - -import { emailSchema } from "@calcom/lib/emailSchema"; -import { AttendeeSchema } from "@calcom/prisma/zod/modelSchema/AttendeeSchema"; - -import { timeZone } from "~/lib/validations/shared/timeZone"; - -export const schemaAttendeeBaseBodyParams = AttendeeSchema.pick({ - bookingId: true, - email: true, - name: true, - timeZone: true, -}); - -const schemaAttendeeCreateParams = z - .object({ - bookingId: z.number().int(), - email: emailSchema, - name: z.string(), - timeZone: timeZone, - }) - .strict(); - -const schemaAttendeeEditParams = z - .object({ - name: z.string().optional(), - email: emailSchema.optional(), - timeZone: timeZone.optional(), - }) - .strict(); -export const schemaAttendeeEditBodyParams = schemaAttendeeBaseBodyParams.merge(schemaAttendeeEditParams); -export const schemaAttendeeCreateBodyParams = schemaAttendeeBaseBodyParams.merge(schemaAttendeeCreateParams); - -export const schemaAttendeeReadPublic = AttendeeSchema.pick({ - id: true, - bookingId: true, - name: true, - email: true, - timeZone: true, -}); diff --git a/apps/api/v1/lib/validations/availability.ts b/apps/api/v1/lib/validations/availability.ts deleted file mode 100644 index ffcd622730de43..00000000000000 --- a/apps/api/v1/lib/validations/availability.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { z } from "zod"; - -import { denullishShape } from "@calcom/prisma/zod-utils"; -import { AvailabilitySchema } from "@calcom/prisma/zod/modelSchema/AvailabilitySchema"; -import { ScheduleSchema } from "@calcom/prisma/zod/modelSchema/ScheduleSchema"; - -export const schemaAvailabilityBaseBodyParams = /** We make all these properties required */ denullishShape( - AvailabilitySchema.pick({ - /** We need to pass the schedule where this availability belongs to */ - scheduleId: true, - }) -); - -export const schemaAvailabilityReadPublic = AvailabilitySchema.pick({ - id: true, - startTime: true, - endTime: true, - date: true, - scheduleId: true, - days: true, - // eventTypeId: true /** @deprecated */, - // userId: true /** @deprecated */, -}).merge(z.object({ success: z.boolean().optional(), Schedule: ScheduleSchema.partial() }).partial()); - -const schemaAvailabilityCreateParams = z - .object({ - startTime: z.date().or(z.string()), - endTime: z.date().or(z.string()), - days: z.array(z.number()).optional(), - date: z.date().or(z.string()).optional(), - }) - .strict(); - -const schemaAvailabilityEditParams = z - .object({ - startTime: z.date().or(z.string()).optional(), - endTime: z.date().or(z.string()).optional(), - days: z.array(z.number()).optional(), - date: z.date().or(z.string()).optional(), - }) - .strict(); - -export const schemaAvailabilityEditBodyParams = schemaAvailabilityEditParams; - -export const schemaAvailabilityCreateBodyParams = schemaAvailabilityBaseBodyParams.merge( - schemaAvailabilityCreateParams -); - -export const schemaAvailabilityReadBodyParams = z - .object({ - userId: z.union([z.number(), z.array(z.number())]), - }) - .partial(); - -export const schemaSingleAvailabilityReadBodyParams = z.object({ - userId: z.number(), -}); diff --git a/apps/api/v1/lib/validations/booking-reference.ts b/apps/api/v1/lib/validations/booking-reference.ts deleted file mode 100644 index 64cef76c1411ba..00000000000000 --- a/apps/api/v1/lib/validations/booking-reference.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { denullishShape } from "@calcom/prisma/zod-utils"; -import { BookingReferenceSchema } from "@calcom/prisma/zod/modelSchema/BookingReferenceSchema"; - -export const schemaBookingReferenceBaseBodyParams = BookingReferenceSchema.pick({ - type: true, - bookingId: true, - uid: true, - meetingId: true, - meetingPassword: true, - meetingUrl: true, - deleted: true, -}).partial(); - -export const schemaBookingReferenceReadPublic = BookingReferenceSchema.pick({ - id: true, - type: true, - bookingId: true, - uid: true, - meetingId: true, - meetingPassword: true, - meetingUrl: true, - deleted: true, -}); - -export const schemaBookingCreateBodyParams = BookingReferenceSchema.omit({ id: true, bookingId: true }) - .merge(denullishShape(BookingReferenceSchema.pick({ bookingId: true }))) - .strict(); -export const schemaBookingEditBodyParams = schemaBookingCreateBodyParams.partial(); diff --git a/apps/api/v1/lib/validations/booking.ts b/apps/api/v1/lib/validations/booking.ts deleted file mode 100644 index 220f261b5cd7b8..00000000000000 --- a/apps/api/v1/lib/validations/booking.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { z } from "zod"; - -import { extendedBookingCreateBody } from "@calcom/features/bookings/lib/bookingCreateBodySchema"; -import { iso8601 } from "@calcom/prisma/zod-utils"; -import { AttendeeSchema } from "@calcom/prisma/zod/modelSchema/AttendeeSchema"; -import { BookingSchema as Booking } from "@calcom/prisma/zod/modelSchema/BookingSchema"; -import { EventTypeSchema } from "@calcom/prisma/zod/modelSchema/EventTypeSchema"; -import { PaymentSchema } from "@calcom/prisma/zod/modelSchema/PaymentSchema"; -import { TeamSchema } from "@calcom/prisma/zod/modelSchema/TeamSchema"; -import { UserSchema } from "@calcom/prisma/zod/modelSchema/UserSchema"; - -import { schemaQueryUserId } from "./shared/queryUserId"; - -const schemaBookingBaseBodyParams = Booking.pick({ - uid: true, - userId: true, - eventTypeId: true, - title: true, - description: true, - startTime: true, - endTime: true, - status: true, - rescheduledBy: true, - cancelledBy: true, - createdAt: true, -}).partial(); - -export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(schemaQueryUserId.partial()); - -export const schemaBookingGetParams = z.object({ - dateFrom: iso8601.optional(), - dateTo: iso8601.optional(), - order: z.enum(["asc", "desc"]).default("asc"), - sortBy: z.enum(["createdAt", "updatedAt"]).optional(), - status: z.enum(["upcoming"]).optional(), -}); - -export type Status = z.infer["status"]; - -export const bookingCancelSchema = z.object({ - id: z.number(), - allRemainingBookings: z.boolean().optional(), - cancelSubsequentBookings: z.boolean().optional(), - cancellationReason: z.string().optional().default("Not Provided"), - seatReferenceUid: z.string().optional(), - cancelledBy: z.string().email({ message: "Invalid email" }).optional(), - internalNote: z - .object({ - id: z.number(), - name: z.string(), - cancellationReason: z.string().optional().nullable(), - }) - .optional() - .nullable(), -}); - -const schemaBookingEditParams = z - .object({ - title: z.string().optional(), - startTime: iso8601.optional(), - endTime: iso8601.optional(), - cancelledBy: z.string().email({ message: "Invalid Email" }).optional(), - rescheduledBy: z.string().email({ message: "Invalid Email" }).optional(), - // Not supporting responses in edit as that might require re-triggering emails - // responses - }) - .strict(); - -export const schemaBookingEditBodyParams = schemaBookingBaseBodyParams - .merge(schemaBookingEditParams) - .omit({ uid: true }); - -const teamSchema = TeamSchema.pick({ - name: true, - slug: true, -}); - -export const schemaBookingReadPublic = Booking.extend({ - eventType: EventTypeSchema.pick({ - title: true, - slug: true, - }) - .merge( - z.object({ - team: teamSchema.nullish(), - }) - ) - .nullish(), - attendees: z - .array( - AttendeeSchema.pick({ - id: true, - email: true, - name: true, - timeZone: true, - locale: true, - }) - ) - .optional(), - user: UserSchema.pick({ - email: true, - name: true, - timeZone: true, - locale: true, - }).nullish(), - payment: z - .array( - PaymentSchema.pick({ - id: true, - success: true, - paymentOption: true, - }) - ) - .optional(), - responses: z.record(z.any()).nullable(), - // Override metadata to handle reassignment objects from Round Robin/Managed Events - // Safe to use z.any() here because: - // 1. API v1 POST only accepts z.record(z.string()) for metadata (user input restricted) - // 2. API v1 PATCH does not accept metadata changes at all - // 3. Complex metadata (objects) are only set by trusted internal features - metadata: z.record(z.any()).nullable(), -}).pick({ - id: true, - userId: true, - description: true, - eventTypeId: true, - uid: true, - title: true, - startTime: true, - endTime: true, - // Note: timeZone is not a field on Booking model - it's in nested attendees/user objects - attendees: true, - user: true, - eventType: true, - payment: true, - metadata: true, - status: true, - responses: true, - fromReschedule: true, - cancelledBy: true, - rescheduledBy: true, - createdAt: true, -}); - -export { - bookingCreateSchemaLegacyPropsForApi, - bookingCreateBodySchemaForApi, -} from "@calcom/features/bookings/lib/bookingCreateBodySchema"; diff --git a/apps/api/v1/lib/validations/connected-calendar.ts b/apps/api/v1/lib/validations/connected-calendar.ts deleted file mode 100644 index 470aea1bd3702e..00000000000000 --- a/apps/api/v1/lib/validations/connected-calendar.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { z } from "zod"; - -const CalendarSchema = z.object({ - externalId: z.string(), - name: z.string(), - primary: z.boolean(), - readOnly: z.boolean(), -}); - -const IntegrationSchema = z.object({ - name: z.string(), - appId: z.string(), - userId: z.number(), - integration: z.string(), - calendars: z.array(CalendarSchema), -}); - -export const schemaConnectedCalendarsReadPublic = z.array(IntegrationSchema); diff --git a/apps/api/v1/lib/validations/credential-sync.ts b/apps/api/v1/lib/validations/credential-sync.ts deleted file mode 100644 index 04e248c047025d..00000000000000 --- a/apps/api/v1/lib/validations/credential-sync.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { z } from "zod"; - -import { HttpError } from "@calcom/lib/http-error"; - -const userId = z.string().transform((val) => { - const userIdInt = parseInt(val); - - if (isNaN(userIdInt)) { - throw new HttpError({ message: "userId is not a valid number", statusCode: 400 }); - } - - return userIdInt; -}); -const appSlug = z.string(); -const credentialId = z.string().transform((val) => { - const credentialIdInt = parseInt(val); - - if (isNaN(credentialIdInt)) { - throw new HttpError({ message: "credentialId is not a valid number", statusCode: 400 }); - } - - return credentialIdInt; -}); -const encryptedKey = z.string(); - -export const schemaCredentialGetParams = z.object({ - userId, - appSlug: appSlug.optional(), -}); - -export const schemaCredentialPostParams = z.object({ - userId, - createSelectedCalendar: z - .string() - .optional() - .transform((val) => { - return val === "true"; - }), - createDestinationCalendar: z - .string() - .optional() - .transform((val) => { - return val === "true"; - }), -}); - -export const schemaCredentialPostBody = z.object({ - appSlug, - encryptedKey, -}); - -export const schemaCredentialPatchParams = z.object({ - userId, - credentialId, -}); - -export const schemaCredentialPatchBody = z.object({ - encryptedKey, -}); - -export const schemaCredentialDeleteParams = z.object({ - userId, - credentialId, -}); diff --git a/apps/api/v1/lib/validations/destination-calendar.ts b/apps/api/v1/lib/validations/destination-calendar.ts deleted file mode 100644 index 7bae6bc5b9b809..00000000000000 --- a/apps/api/v1/lib/validations/destination-calendar.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { z } from "zod"; - -import { DestinationCalendarSchema } from "@calcom/prisma/zod/modelSchema/DestinationCalendarSchema"; - -// Note: bookingId is not a field on DestinationCalendar model in Prisma schema -export const schemaDestinationCalendarBaseBodyParams = DestinationCalendarSchema.pick({ - integration: true, - externalId: true, - eventTypeId: true, - userId: true, -}).partial(); - -const schemaDestinationCalendarCreateParams = z - .object({ - integration: z.string(), - externalId: z.string(), - eventTypeId: z.number().optional(), - bookingId: z.number().optional(), - userId: z.number().optional(), - }) - .strict(); - -export const schemaDestinationCalendarCreateBodyParams = schemaDestinationCalendarBaseBodyParams.merge( - schemaDestinationCalendarCreateParams -); - -const schemaDestinationCalendarEditParams = z - .object({ - integration: z.string().optional(), - externalId: z.string().optional(), - eventTypeId: z.number().optional(), - bookingId: z.number().optional(), - userId: z.number().optional(), - }) - .strict(); - -export const schemaDestinationCalendarEditBodyParams = schemaDestinationCalendarBaseBodyParams.merge( - schemaDestinationCalendarEditParams -); - -// Note: bookingId is not a field on DestinationCalendar model in Prisma schema -export const schemaDestinationCalendarReadPublic = DestinationCalendarSchema.pick({ - id: true, - integration: true, - externalId: true, - eventTypeId: true, - userId: true, -}); diff --git a/apps/api/v1/lib/validations/event-type-custom-input.ts b/apps/api/v1/lib/validations/event-type-custom-input.ts deleted file mode 100644 index 1426e643c0a7eb..00000000000000 --- a/apps/api/v1/lib/validations/event-type-custom-input.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { EventTypeCustomInputSchema } from "@calcom/prisma/zod/modelSchema/EventTypeCustomInputSchema"; - -export const schemaEventTypeCustomInputBaseBodyParams = EventTypeCustomInputSchema.omit({ - id: true, -}); - -export const schemaEventTypeCustomInputPublic = EventTypeCustomInputSchema.omit({}); - -export const schemaEventTypeCustomInputBodyParams = schemaEventTypeCustomInputBaseBodyParams.strict(); - -export const schemaEventTypeCustomInputEditBodyParams = schemaEventTypeCustomInputBaseBodyParams - .partial() - .strict(); diff --git a/apps/api/v1/lib/validations/event-type.ts b/apps/api/v1/lib/validations/event-type.ts deleted file mode 100644 index 798ad2c442be7b..00000000000000 --- a/apps/api/v1/lib/validations/event-type.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { z } from "zod"; - -import { - MAX_SEATS_PER_TIME_SLOT, - MAX_EVENT_DURATION_MINUTES, - MIN_EVENT_DURATION_MINUTES, -} from "@calcom/lib/constants"; -import slugify from "@calcom/lib/slugify"; -import { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; -import { EventTypeSchema } from "@calcom/prisma/zod/modelSchema/EventTypeSchema"; -import { HostSchema } from "@calcom/prisma/zod/modelSchema/HostSchema"; - -import { Frequency } from "~/lib/types"; - -import { schemaQueryUserId } from "./shared/queryUserId"; -import { timeZone } from "./shared/timeZone"; - -const recurringEventInputSchema = z.object({ - dtstart: z.string().optional(), - interval: z.number().int().optional(), - count: z.number().int().optional(), - freq: z.nativeEnum(Frequency).optional(), - until: z.string().optional(), - tzid: timeZone.optional(), -}); - -const hostSchema = HostSchema.pick({ - isFixed: true, - userId: true, - scheduleId: true, -}); - -export const childrenSchema = z.object({ - id: z.number().int(), - userId: z.number().int(), -}); - -export const schemaEventTypeBaseBodyParams = EventTypeSchema.pick({ - title: true, - description: true, - slug: true, - length: true, - hidden: true, - position: true, - eventName: true, - timeZone: true, - schedulingType: true, - // START Limit future bookings - periodType: true, - periodStartDate: true, - periodEndDate: true, - periodDays: true, - periodCountCalendarDays: true, - // END Limit future bookings - requiresConfirmation: true, - disableGuests: true, - hideCalendarNotes: true, - minimumBookingNotice: true, - parentId: true, - beforeEventBuffer: true, - afterEventBuffer: true, - teamId: true, - price: true, - currency: true, - slotInterval: true, - successRedirectUrl: true, - locations: true, - bookingLimits: true, - onlyShowFirstAvailableSlot: true, - durationLimits: true, - assignAllTeamMembers: true, -}) - .merge( - z.object({ - children: z.array(childrenSchema).optional().default([]), - hosts: z.array(hostSchema).optional().default([]), - }) - ) - .partial() - .strict(); - -const schemaEventTypeCreateParams = z - .object({ - title: z.string(), - slug: z.string().transform((s) => slugify(s)), - description: z.string().optional().nullable(), - length: z.number().int().min(MIN_EVENT_DURATION_MINUTES).max(MAX_EVENT_DURATION_MINUTES), - metadata: z.any().optional(), - recurringEvent: recurringEventInputSchema.optional(), - seatsPerTimeSlot: z.number().optional(), - seatsShowAttendees: z.boolean().optional(), - seatsShowAvailabilityCount: z.boolean().optional(), - bookingFields: eventTypeBookingFields.optional(), - scheduleId: z.number().optional(), - parentId: z.number().optional(), - }) - .strict(); - -export const schemaEventTypeCreateBodyParams = schemaEventTypeBaseBodyParams - .merge(schemaEventTypeCreateParams) - .merge(schemaQueryUserId.partial()); - -const schemaEventTypeEditParams = z - .object({ - title: z.string().optional(), - slug: z - .string() - .transform((s) => slugify(s)) - .optional(), - length: z.number().int().optional(), - seatsPerTimeSlot: z.number().min(1).max(MAX_SEATS_PER_TIME_SLOT).nullable().optional(), - seatsShowAttendees: z.boolean().optional(), - seatsShowAvailabilityCount: z.boolean().optional(), - bookingFields: eventTypeBookingFields.optional(), - scheduleId: z.number().optional(), - }) - .strict(); - -export const schemaEventTypeEditBodyParams = schemaEventTypeBaseBodyParams.merge(schemaEventTypeEditParams); diff --git a/apps/api/v1/lib/validations/membership.ts b/apps/api/v1/lib/validations/membership.ts deleted file mode 100644 index 906a250dff960e..00000000000000 --- a/apps/api/v1/lib/validations/membership.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { z } from "zod"; - -import { MembershipRole } from "@calcom/prisma/enums"; -import { stringOrNumber } from "@calcom/prisma/zod-utils"; -import { MembershipSchema } from "@calcom/prisma/zod/modelSchema/MembershipSchema"; -import { TeamSchema } from "@calcom/prisma/zod/modelSchema/TeamSchema"; - -import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -export const schemaMembershipBaseBodyParams = MembershipSchema.omit({}); - -const schemaMembershipRequiredParams = z.object({ - teamId: z.number(), -}); - -export const membershipCreateBodySchema = MembershipSchema.omit({ - id: true, - createdAt: true, - updatedAt: true, -}) - .partial({ - accepted: true, - role: true, - disableImpersonation: true, - }) - .transform((v) => ({ - accepted: false, - role: MembershipRole.MEMBER, - disableImpersonation: false, - ...v, - })); - -export const membershipEditBodySchema = MembershipSchema.omit({ - /** To avoid complication, let's avoid updating these, instead you can delete and create a new invite */ - teamId: true, - userId: true, - id: true, - createdAt: true, - updatedAt: true, -}) - .partial({ - accepted: true, - role: true, - disableImpersonation: true, - }) - .strict(); - -export const schemaMembershipBodyParams = schemaMembershipBaseBodyParams.merge( - schemaMembershipRequiredParams -); - -export const schemaMembershipPublic = MembershipSchema.merge(z.object({ team: TeamSchema }).partial()); - -/** We extract userId and teamId from compound ID string */ -export const membershipIdSchema = schemaQueryIdAsString - // So we can query additional team data in memberships - .merge(z.object({ teamId: z.union([stringOrNumber, z.array(stringOrNumber)]) }).partial()) - .transform((v, ctx) => { - const [userIdStr, teamIdStr] = v.id.split("_"); - const userIdInt = schemaQueryIdParseInt.safeParse({ id: userIdStr }); - const teamIdInt = schemaQueryIdParseInt.safeParse({ id: teamIdStr }); - if (!userIdInt.success) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "userId is not a number" }); - return z.NEVER; - } - if (!teamIdInt.success) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "teamId is not a number " }); - return z.NEVER; - } - return { - userId: userIdInt.data.id, - teamId: teamIdInt.data.id, - }; - }); diff --git a/apps/api/v1/lib/validations/payment.ts b/apps/api/v1/lib/validations/payment.ts deleted file mode 100644 index b8d544730a565b..00000000000000 --- a/apps/api/v1/lib/validations/payment.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PaymentSchema } from "@calcom/prisma/zod/modelSchema/PaymentSchema"; - -export const schemaPaymentPublic = PaymentSchema.pick({ - id: true, - amount: true, - success: true, - refunded: true, - fee: true, - paymentOption: true, - currency: true, - bookingId: true, -}); diff --git a/apps/api/v1/lib/validations/reminder-mail.ts b/apps/api/v1/lib/validations/reminder-mail.ts deleted file mode 100644 index 4f8af42d77a800..00000000000000 --- a/apps/api/v1/lib/validations/reminder-mail.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { z } from "zod"; - -import { ReminderMailSchema } from "@calcom/prisma/zod/modelSchema/ReminderMailSchema"; - -export const schemaReminderMailBaseBodyParams = ReminderMailSchema.omit({ id: true }).partial(); - -export const schemaReminderMailPublic = ReminderMailSchema.omit({}); - -const schemaReminderMailRequiredParams = z.object({ - referenceId: z.number().int(), - reminderType: z.enum(["PENDING_BOOKING_CONFIRMATION"]), - elapsedMinutes: z.number().int(), -}); - -export const schemaReminderMailBodyParams = schemaReminderMailBaseBodyParams.merge( - schemaReminderMailRequiredParams -); diff --git a/apps/api/v1/lib/validations/schedule.ts b/apps/api/v1/lib/validations/schedule.ts deleted file mode 100644 index 25a11ab7aa83c4..00000000000000 --- a/apps/api/v1/lib/validations/schedule.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { z } from "zod"; - -import dayjs from "@calcom/dayjs"; -import { AvailabilitySchema } from "@calcom/prisma/zod/modelSchema/AvailabilitySchema"; -import { ScheduleSchema } from "@calcom/prisma/zod/modelSchema/ScheduleSchema"; - -import { timeZone } from "./shared/timeZone"; - -const schemaScheduleBaseBodyParams = ScheduleSchema.omit({ id: true, timeZone: true }).partial(); - -export const schemaSingleScheduleBodyParams = schemaScheduleBaseBodyParams.merge( - z.object({ userId: z.number().optional(), timeZone: timeZone.optional() }) -); - -export const schemaCreateScheduleBodyParams = schemaScheduleBaseBodyParams.merge( - z.object({ userId: z.number().optional(), name: z.string(), timeZone }) -); - -export const schemaSchedulePublic = z - .object({ id: z.number() }) - .merge(ScheduleSchema) - .merge( - z.object({ - availability: z - .array( - AvailabilitySchema.pick({ - id: true, - eventTypeId: true, - date: true, - days: true, - startTime: true, - endTime: true, - }) - ) - .transform((v) => - v.map((item) => ({ - ...item, - startTime: dayjs.utc(item.startTime).format("HH:mm:ss"), - endTime: dayjs.utc(item.endTime).format("HH:mm:ss"), - })) - ) - .optional(), - }) - ); diff --git a/apps/api/v1/lib/validations/selected-calendar.ts b/apps/api/v1/lib/validations/selected-calendar.ts deleted file mode 100644 index 1e77352bc84a01..00000000000000 --- a/apps/api/v1/lib/validations/selected-calendar.ts +++ /dev/null @@ -1,61 +0,0 @@ -import z from "zod"; - -import { SelectedCalendarSchema } from "@calcom/prisma/zod/modelSchema/SelectedCalendarSchema"; - -import { schemaQueryIdAsString } from "./shared/queryIdString"; -import { schemaQueryIdParseInt } from "./shared/queryIdTransformParseInt"; - -export const schemaSelectedCalendarBaseBodyParams = SelectedCalendarSchema; - -export const schemaSelectedCalendarPublic = SelectedCalendarSchema.omit({}); - -export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams - .pick({ - integration: true, - externalId: true, - userId: true, - }) - .partial({ - userId: true, - }); - -export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams - .omit({ - // id is decided by DB - id: true, - // No eventTypeId support in API v1 - eventTypeId: true, - }) - .partial(); - -export const selectedCalendarIdSchema = schemaQueryIdAsString.transform((v, ctx) => { - /** We can assume the first part is the userId since it's an integer */ - const [userIdStr, ...rest] = v.id.split("_"); - /** We can assume that the remainder is both the integration type and external id combined */ - const integration_externalId = rest.join("_"); - /** - * Since we only handle calendars here we can split by `_calendar_` and re add it later on. - * This handle special cases like `google_calendar_c_blabla@group.calendar.google.com` and - * `hubspot_other_calendar`. - **/ - const [_integration, externalId] = integration_externalId.split("_calendar_"); - const userIdInt = schemaQueryIdParseInt.safeParse({ id: userIdStr }); - if (!userIdInt.success) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "userId is not a number" }); - return z.NEVER; - } - if (!_integration) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Missing integration" }); - return z.NEVER; - } - if (!externalId) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Missing externalId" }); - return z.NEVER; - } - return { - userId: userIdInt.data.id, - /** We re-add the split `_calendar` string */ - integration: `${_integration}_calendar`, - externalId, - }; -}); diff --git a/apps/api/v1/lib/validations/shared/baseApiParams.ts b/apps/api/v1/lib/validations/shared/baseApiParams.ts deleted file mode 100644 index d0482d0d274ecb..00000000000000 --- a/apps/api/v1/lib/validations/shared/baseApiParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from "zod"; - -// Extracted out as utility function so can be reused -// at different endpoints that require this validation. -export const baseApiParams = z.object({ - // since we added apiKey as query param this is required by next-validations helper - // for query params to work properly and not fail. - apiKey: z.string().optional(), - // version required for supporting /v1/ redirect to query in api as *?version=1 - version: z.string().optional(), -}); diff --git a/apps/api/v1/lib/validations/shared/jsonSchema.ts b/apps/api/v1/lib/validations/shared/jsonSchema.ts deleted file mode 100644 index 423b28e95528db..00000000000000 --- a/apps/api/v1/lib/validations/shared/jsonSchema.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from "zod"; - -// Helper schema for JSON fields -type Literal = boolean | number | string | null; -type Json = Literal | { [key: string]: Json } | Json[]; -const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); -const jsonObjectSchema = z.record(z.lazy(() => jsonSchema)); -const jsonArraySchema = z.array(z.lazy(() => jsonSchema)); -export const jsonSchema: z.ZodSchema = z.lazy(() => - z.union([literalSchema, jsonObjectSchema, jsonArraySchema]) -); diff --git a/apps/api/v1/lib/validations/shared/queryAttendeeEmail.ts b/apps/api/v1/lib/validations/shared/queryAttendeeEmail.ts deleted file mode 100644 index c90abc5b421e88..00000000000000 --- a/apps/api/v1/lib/validations/shared/queryAttendeeEmail.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { withValidation } from "next-validations"; -import { z } from "zod"; - -import { emailSchema } from "@calcom/lib/emailSchema"; - -import { baseApiParams } from "./baseApiParams"; - -// Extracted out as utility function so can be reused -// at different endpoints that require this validation. -export const schemaQueryAttendeeEmail = baseApiParams.extend({ - attendeeEmail: emailSchema, -}); - -export const schemaQuerySingleOrMultipleAttendeeEmails = z.object({ - attendeeEmail: z.union([emailSchema, z.array(emailSchema)]).optional(), -}); - -export const withValidQueryAttendeeEmail = withValidation({ - schema: schemaQueryAttendeeEmail, - type: "Zod", - mode: "query", -}); diff --git a/apps/api/v1/lib/validations/shared/queryExpandRelations.ts b/apps/api/v1/lib/validations/shared/queryExpandRelations.ts deleted file mode 100644 index f6a5115deb9fcf..00000000000000 --- a/apps/api/v1/lib/validations/shared/queryExpandRelations.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from "zod"; - -const expandEnum = z.enum(["team"]); - -export const schemaQuerySingleOrMultipleExpand = z - .union([ - expandEnum, // Allow a single value from the enum - z.array(expandEnum).refine((arr) => new Set(arr).size === arr.length, { - message: "Array values must be unique", - }), // Allow an array of enum values, with uniqueness constraint - ]) - .optional(); diff --git a/apps/api/v1/lib/validations/shared/queryIdString.ts b/apps/api/v1/lib/validations/shared/queryIdString.ts deleted file mode 100644 index 97bdfa4b15c29d..00000000000000 --- a/apps/api/v1/lib/validations/shared/queryIdString.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { withValidation } from "next-validations"; -import { z } from "zod"; - -import { baseApiParams } from "./baseApiParams"; - -// Extracted out as utility function so can be reused -// at different endpoints that require this validation. -/** Used for UUID style id queries */ -export const schemaQueryIdAsString = baseApiParams - .extend({ - id: z.string(), - }) - .strict(); - -export const withValidQueryIdString = withValidation({ - schema: schemaQueryIdAsString, - type: "Zod", - mode: "query", -}); diff --git a/apps/api/v1/lib/validations/shared/queryIdTransformParseInt.ts b/apps/api/v1/lib/validations/shared/queryIdTransformParseInt.ts deleted file mode 100644 index ef6d811ea996c3..00000000000000 --- a/apps/api/v1/lib/validations/shared/queryIdTransformParseInt.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { withValidation } from "next-validations"; -import { z } from "zod"; - -import { baseApiParams } from "./baseApiParams"; - -// Extracted out as utility function so can be reused -// at different endpoints that require this validation. -export const schemaQueryIdParseInt = baseApiParams.extend({ - id: z.coerce.number(), -}); - -export const withValidQueryIdTransformParseInt = withValidation({ - schema: schemaQueryIdParseInt, - type: "Zod", - mode: "query", -}); - -export const getTranscriptFromRecordingId = schemaQueryIdParseInt.extend({ - recordingId: z.string(), -}); diff --git a/apps/api/v1/lib/validations/shared/querySlug.ts b/apps/api/v1/lib/validations/shared/querySlug.ts deleted file mode 100644 index e8fefc3fd0b7bd..00000000000000 --- a/apps/api/v1/lib/validations/shared/querySlug.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from "zod"; - -import { baseApiParams } from "./baseApiParams"; - -export const schemaQuerySlug = baseApiParams.extend({ - slug: z.string().optional(), -}); diff --git a/apps/api/v1/lib/validations/shared/queryTeamId.ts b/apps/api/v1/lib/validations/shared/queryTeamId.ts deleted file mode 100644 index f2a80361eb8ef8..00000000000000 --- a/apps/api/v1/lib/validations/shared/queryTeamId.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { withValidation } from "next-validations"; -import { z } from "zod"; - -import { baseApiParams } from "./baseApiParams"; - -// Extracted out as utility function so can be reused -// at different endpoints that require this validation. -export const schemaQueryTeamId = baseApiParams - .extend({ - teamId: z - .string() - .regex(/^\d+$/) - .transform((id) => parseInt(id)), - }) - .strict(); - -export const withValidQueryTeamId = withValidation({ - schema: schemaQueryTeamId, - type: "Zod", - mode: "query", -}); diff --git a/apps/api/v1/lib/validations/shared/queryUserEmail.ts b/apps/api/v1/lib/validations/shared/queryUserEmail.ts deleted file mode 100644 index 49f96f6354ba7b..00000000000000 --- a/apps/api/v1/lib/validations/shared/queryUserEmail.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { withValidation } from "next-validations"; -import { z } from "zod"; - -import { emailSchema } from "@calcom/lib/emailSchema"; - -import { baseApiParams } from "./baseApiParams"; - -// Extracted out as utility function so can be reused -// at different endpoints that require this validation. -export const schemaQueryUserEmail = baseApiParams.extend({ - email: emailSchema, -}); - -export const schemaQuerySingleOrMultipleUserEmails = z.object({ - email: z.union([emailSchema, z.array(emailSchema)]), -}); - -export const withValidQueryUserEmail = withValidation({ - schema: schemaQueryUserEmail, - type: "Zod", - mode: "query", -}); diff --git a/apps/api/v1/lib/validations/shared/queryUserId.ts b/apps/api/v1/lib/validations/shared/queryUserId.ts deleted file mode 100644 index f187d0f13413ff..00000000000000 --- a/apps/api/v1/lib/validations/shared/queryUserId.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { withValidation } from "next-validations"; -import { z } from "zod"; - -import { stringOrNumber } from "@calcom/prisma/zod-utils"; - -import { baseApiParams } from "./baseApiParams"; - -// Extracted out as utility function so can be reused -// at different endpoints that require this validation. -export const schemaQueryUserId = baseApiParams.extend({ - userId: stringOrNumber, -}); - -export const schemaQuerySingleOrMultipleUserIds = z.object({ - userId: z.union([stringOrNumber, z.array(stringOrNumber)]), -}); - -export const schemaQuerySingleOrMultipleTeamIds = z.object({ - teamId: z.union([stringOrNumber, z.array(stringOrNumber)]), -}); - -export const withValidQueryUserId = withValidation({ - schema: schemaQueryUserId, - type: "Zod", - mode: "query", -}); diff --git a/apps/api/v1/lib/validations/shared/timeZone.ts b/apps/api/v1/lib/validations/shared/timeZone.ts deleted file mode 100644 index 290bea55faf990..00000000000000 --- a/apps/api/v1/lib/validations/shared/timeZone.ts +++ /dev/null @@ -1,7 +0,0 @@ -import tzdata from "tzdata"; -import { z } from "zod"; - -// @note: This is a custom validation that checks if the timezone is valid and exists in the tzdb library -export const timeZone = z.string().refine((tz: string) => Object.keys(tzdata.zones).includes(tz), { - message: `Expected one of the following: ${Object.keys(tzdata.zones).join(", ")}`, -}); diff --git a/apps/api/v1/lib/validations/team.ts b/apps/api/v1/lib/validations/team.ts deleted file mode 100644 index a1666a638f9cdf..00000000000000 --- a/apps/api/v1/lib/validations/team.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { z } from "zod"; - -import { TeamSchema } from "@calcom/prisma/zod/modelSchema/TeamSchema"; - -export const schemaTeamBaseBodyParams = TeamSchema.omit({ id: true, createdAt: true }).partial({ - hideBranding: true, - metadata: true, - pendingPayment: true, - isOrganization: true, - isPlatform: true, - smsLockState: true, - smsLockReviewedByAdmin: true, - bookingLimits: true, - includeManagedEventsInLimits: true, -}); - -const schemaTeamRequiredParams = z.object({ - name: z.string().max(255), -}); - -export const schemaTeamBodyParams = schemaTeamBaseBodyParams.merge(schemaTeamRequiredParams).strict(); - -export const schemaTeamUpdateBodyParams = schemaTeamBodyParams.partial(); - -const schemaOwnerId = z.object({ - ownerId: z.number().optional(), -}); - -export const schemaTeamCreateBodyParams = schemaTeamBodyParams.merge(schemaOwnerId).strict(); - -export const schemaTeamReadPublic = TeamSchema.omit({}); - -export const schemaTeamsReadPublic = z.array(schemaTeamReadPublic); diff --git a/apps/api/v1/lib/validations/user.ts b/apps/api/v1/lib/validations/user.ts deleted file mode 100644 index d9996d36a86779..00000000000000 --- a/apps/api/v1/lib/validations/user.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { checkUsername } from "@calcom/features/profile/lib/checkUsername"; -import { emailSchema } from "@calcom/lib/emailSchema"; -import { UserSchema } from "@calcom/prisma/zod/modelSchema/UserSchema"; -import { iso8601 } from "@calcom/prisma/zod-utils"; -import { z } from "zod"; -import { isValidBase64Image } from "~/lib/utils/isValidBase64Image"; -import { timeZone } from "~/lib/validations/shared/timeZone"; - -// @note: These are the ONLY values allowed as weekStart. So user don't introduce bad data. -enum weekdays { - MONDAY = "Monday", - TUESDAY = "Tuesday", - WEDNESDAY = "Wednesday", - THURSDAY = "Thursday", - FRIDAY = "Friday", - SATURDAY = "Saturday", - SUNDAY = "Sunday", -} - -// @note: extracted from apps/web/next-i18next.config.js, update if new locales. -enum locales { - EN = "en", - FR = "fr", - IT = "it", - RU = "ru", - ES = "es", - DE = "de", - PT = "pt", - RO = "ro", - NL = "nl", - PT_BR = "pt-BR", - ES_419 = "es-419", - KO = "ko", - JA = "ja", - PL = "pl", - AR = "ar", - IW = "iw", - ZH_CN = "zh-CN", - ZH_TW = "zh-TW", - CS = "cs", - SR = "sr", - SV = "sv", - VI = "vi", - BN = "bn", -} -enum theme { - DARK = "dark", - LIGHT = "light", -} - -enum timeFormat { - TWELVE = 12, - TWENTY_FOUR = 24, -} - -const usernameSchema = z - .string() - .transform((v) => v.toLowerCase()) - // .refine(() => {}) - .superRefine(async (val, ctx) => { - if (val) { - const result = await checkUsername(val); - if (!result.available) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "already_in_use_error" }); - if (result.premium) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "premium_username" }); - } - }); - -// @note: These are the values that are editable via PATCH method on the user Model -export const schemaUserBaseBodyParams = UserSchema.pick({ - name: true, - email: true, - username: true, - bio: true, - timeZone: true, - weekStart: true, - theme: true, - appTheme: true, - defaultScheduleId: true, - locale: true, - hideBranding: true, - timeFormat: true, - brandColor: true, - darkBrandColor: true, - allowDynamicBooking: true, - role: true, - // @note: disallowing avatar changes via API for now. We can add it later if needed. User should upload image via UI. - // avatar: true, -}).partial(); -// @note: partial() is used to allow for the user to edit only the fields they want to edit making all optional, -// if want to make any required do it in the schemaRequiredParams - -// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value -// for example making weekStart only accept weekdays as input -const schemaUserEditParams = z.object({ - email: emailSchema.toLowerCase(), - username: usernameSchema, - weekStart: z.nativeEnum(weekdays).optional(), - brandColor: z.string().min(4).max(9).regex(/^#/).optional(), - darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(), - hideBranding: z.boolean().optional(), - timeZone: timeZone.optional(), - theme: z.nativeEnum(theme).optional().nullable(), - appTheme: z.nativeEnum(theme).optional().nullable(), - timeFormat: z.nativeEnum(timeFormat).optional(), - defaultScheduleId: z - .number() - .refine((id: number) => id > 0) - .optional() - .nullable(), - locale: z.nativeEnum(locales).optional().nullable(), - avatar: z.string().refine(isValidBase64Image).optional(), -}); - -// @note: These are the values that are editable via PATCH method on the user Model, -// merging both BaseBodyParams with RequiredParams, and omitting whatever we want at the end. - -const schemaUserCreateParams = z.object({ - email: emailSchema.toLowerCase(), - username: usernameSchema, - weekStart: z.nativeEnum(weekdays).optional(), - brandColor: z.string().min(4).max(9).regex(/^#/).optional(), - darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(), - hideBranding: z.boolean().optional(), - timeZone: timeZone.optional(), - theme: z.nativeEnum(theme).optional().nullable(), - appTheme: z.nativeEnum(theme).optional().nullable(), - timeFormat: z.nativeEnum(timeFormat).optional(), - defaultScheduleId: z - .number() - .refine((id: number) => id > 0) - .optional() - .nullable(), - locale: z.nativeEnum(locales).optional(), - createdDate: iso8601.optional(), - avatar: z.string().refine(isValidBase64Image).optional(), -}); - -// @note: These are the values that are editable via PATCH method on the user Model, -// merging both BaseBodyParams with RequiredParams, and omitting whatever we want at the end. -export const schemaUserEditBodyParams = schemaUserBaseBodyParams - .merge(schemaUserEditParams) - .omit({}) - .partial() - .strict(); - -export const schemaUserCreateBodyParams = schemaUserBaseBodyParams - .merge(schemaUserCreateParams) - .omit({}) - .strict(); - -// @note: These are the values that are always returned when reading a user -// Note: We pick avatarUrl from the Prisma schema and extend with avatar for API v1 backward compatibility -export const schemaUserReadPublic = UserSchema.pick({ - id: true, - username: true, - name: true, - email: true, - emailVerified: true, - bio: true, - avatarUrl: true, - timeZone: true, - weekStart: true, - bufferTime: true, - appTheme: true, - theme: true, - defaultScheduleId: true, - locale: true, - timeFormat: true, - hideBranding: true, - brandColor: true, - darkBrandColor: true, - allowDynamicBooking: true, - createdDate: true, - verified: true, - invitedTo: true, - role: true, -}).extend({ - // API v1 backward compatibility: expose avatarUrl as avatar - avatar: UserSchema.shape.avatarUrl, -}); - -export const schemaUsersReadPublic = z.array(schemaUserReadPublic); diff --git a/apps/api/v1/lib/validations/webhook.ts b/apps/api/v1/lib/validations/webhook.ts deleted file mode 100644 index bd5eefe63eb381..00000000000000 --- a/apps/api/v1/lib/validations/webhook.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { z } from "zod"; - -import { WEBHOOK_TRIGGER_EVENTS } from "@calcom/features/webhooks/lib/constants"; -import { WebhookVersion } from "@calcom/features/webhooks/lib/interface/IWebhookRepository"; -import { WebhookSchema } from "@calcom/prisma/zod/modelSchema/WebhookSchema"; - -const schemaWebhookBaseBodyParams = WebhookSchema.pick({ - userId: true, - eventTypeId: true, - eventTriggers: true, - active: true, - subscriberUrl: true, - payloadTemplate: true, -}); - -export const schemaWebhookCreateParams = z - .object({ - // subscriberUrl: z.string().url(), - // eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array(), - // active: z.boolean(), - payloadTemplate: z.string().optional().nullable(), - eventTypeId: z.number().optional(), - userId: z.number().optional(), - secret: z.string().optional().nullable(), - version: z.nativeEnum(WebhookVersion).optional(), - // API shouldn't mess with Apps webhooks yet (ie. Zapier) - // appId: z.string().optional().nullable(), - }) - .strict(); - -export const schemaWebhookCreateBodyParams = schemaWebhookBaseBodyParams.merge(schemaWebhookCreateParams); - -export const schemaWebhookEditBodyParams = schemaWebhookBaseBodyParams - .merge( - z.object({ - eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array().optional(), - secret: z.string().optional().nullable(), - version: z.nativeEnum(WebhookVersion).optional(), - }) - ) - .partial() - .strict(); - -export const schemaWebhookReadPublic = WebhookSchema.pick({ - id: true, - userId: true, - eventTypeId: true, - payloadTemplate: true, - eventTriggers: true, - version: true, - // FIXME: We have some invalid urls saved in the DB - // subscriberUrl: true, - /** @todo: find out how to properly add back and validate those. */ - // eventType: true, - // app: true, - appId: true, -}).merge( - z.object({ - subscriberUrl: z.string(), - }) -); diff --git a/apps/api/v1/next-env.d.ts b/apps/api/v1/next-env.d.ts deleted file mode 100644 index 52e831b4342482..00000000000000 --- a/apps/api/v1/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/apps/api/v1/next.config.js b/apps/api/v1/next.config.js deleted file mode 100644 index efec7b46cfc94c..00000000000000 --- a/apps/api/v1/next.config.js +++ /dev/null @@ -1,114 +0,0 @@ -/* eslint-disable */ -const { withAxiom } = require("next-axiom"); -const { withSentryConfig } = require("@sentry/nextjs"); -const { PrismaPlugin } = require("@prisma/nextjs-monorepo-workaround-plugin"); -const { TRIGGER_VERSION } = require("./trigger.version.js"); -const plugins = [withAxiom]; - -/** @type {import("next").NextConfig} */ -const nextConfig = { - turbopack: {}, - transpilePackages: [ - "@calcom/app-store", - "@calcom/dayjs", - "@calcom/emails", - "@calcom/features", - "@calcom/lib", - "@calcom/prisma", - "@calcom/trpc", - ], - webpack: (config, { isServer }) => { - if (isServer) { - config.plugins = [...config.plugins, new PrismaPlugin()]; - } - return config; - }, - async headers() { - return [ - { - source: "/docs", - headers: [ - { - key: "Access-Control-Allow-Credentials", - value: "true", - }, - { - key: "Access-Control-Allow-Origin", - value: "*", - }, - { - key: "Access-Control-Allow-Methods", - value: "GET, OPTIONS, PATCH, DELETE, POST, PUT", - }, - { - key: "Access-Control-Allow-Headers", - value: - "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Content-Type, api_key, Authorization", - }, - ], - }, - ]; - }, - async rewrites() { - return { - afterFiles: [ - // This redirects requests received at / the root to the /api/ folder. - { - source: "/v:version/:rest*", - destination: "/api/v:version/:rest*", - }, - { - source: "/api/v2", - destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/health`, - }, - { - source: "/api/v2/health", - destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/health`, - }, - { - source: "/api/v2/docs/:path*", - destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/docs/:path*`, - }, - { - source: "/api/v2/:path*", - destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/api/v2/:path*`, - }, - // This redirects requests to api/v*/ to /api/ passing version as a query parameter. - { - source: "/api/v:version/:rest*", - destination: "/api/:rest*?version=:version", - }, - // Keeps backwards compatibility with old webhook URLs - { - source: "/api/hooks/:rest*", - destination: "/api/webhooks/:rest*", - }, - ], - fallback: [ - // These rewrites are checked after both pages/public files - // and dynamic routes are checked - { - source: "/:path*", - destination: `/api/:path*`, - }, - ], - }; - }, -}; - -if (process.env.NEXT_PUBLIC_SENTRY_DSN) { - plugins.push((nextConfig) => - withSentryConfig(nextConfig, { - autoInstrumentServerFunctions: true, - hideSourceMaps: true, - }) - ); -} - -const env = process.env; - -if (process.env.NODE_ENV === "production" || process.env.CALCOM_ENV === "production") { - env.TRIGGER_VERSION = TRIGGER_VERSION; -} - -module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig); diff --git a/apps/api/v1/next.d.ts b/apps/api/v1/next.d.ts deleted file mode 100644 index 3c4f4ac5360f9a..00000000000000 --- a/apps/api/v1/next.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Session } from "next-auth"; -import type { NextApiRequest as BaseNextApiRequest } from "next/types"; - -export type * from "next/types"; - -export declare module "next" { - interface NextApiRequest extends BaseNextApiRequest { - session?: Session | null; - - userId: number; - userUuid: string; - user?: { role: string; locked: boolean; email: string } | null; - method: string; - // session: { user: { id: number } }; - // query: Partial<{ [key: string]: string | string[] }>; - isSystemWideAdmin: boolean; - isOrganizationOwnerOrAdmin: boolean; - pagination: { take: number; skip: number }; - } -} diff --git a/apps/api/v1/package.json b/apps/api/v1/package.json deleted file mode 100644 index f8c0495697b2c1..00000000000000 --- a/apps/api/v1/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@calcom/api", - "version": "1.0.0", - "description": "Public API for Cal.com", - "main": "index.ts", - "repository": "git@github.com:calcom/api.git", - "author": "Cal.com Inc.", - "private": true, - "scripts": { - "build": "next build", - "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next", - "dev": "PORT=3003 next dev", - "lint": "biome lint .", - "lint:fix": "biome lint --write .", - "start": "PORT=3003 next start", - "docker-start-api": "PORT=80 next start", - "type-check": "tsc --pretty --noEmit", - "type-check:ci": "tsc-absolute --pretty --noEmit" - }, - "devDependencies": { - "@calcom/testing": "workspace:*", - "@calcom/tsconfig": "workspace:*", - "@calcom/types": "workspace:*", - "node-mocks-http": "1.16.2", - "typescript": "5.9.3" - }, - "dependencies": { - "@calcom/app-store": "workspace:*", - "@calcom/dayjs": "workspace:*", - "@calcom/emails": "workspace:*", - "@calcom/features": "workspace:*", - "@calcom/lib": "workspace:*", - "@calcom/prisma": "workspace:*", - "@calcom/trpc": "workspace:*", - "@prisma/nextjs-monorepo-workaround-plugin": "6.16.1", - "@sentry/nextjs": "10.33.0", - "bcryptjs": "2.4.3", - "memory-cache": "0.2.0", - "next": "16.2.3", - "next-api-middleware": "1.0.1", - "next-axiom": "0.17.0", - "next-swagger-doc": "0.3.6", - "next-validations": "0.2.1", - "tzdata": "1.0.40", - "uuid": "8.3.2", - "zod": "3.25.76" - } -} diff --git a/apps/api/v1/pages/api/api-keys/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/api-keys/[id]/_auth-middleware.ts deleted file mode 100644 index f76ec117c606b3..00000000000000 --- a/apps/api/v1/pages/api/api-keys/[id]/_auth-middleware.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString"; - -export async function authMiddleware(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { id } = schemaQueryIdAsString.parse(req.query); - // Admin can check any api key - if (isSystemWideAdmin) return; - // Check if user can access the api key - const apiKey = await prisma.apiKey.findFirst({ - where: { id, userId }, - }); - if (!apiKey) throw new HttpError({ statusCode: 404, message: "API key not found" }); -} diff --git a/apps/api/v1/pages/api/api-keys/[id]/_delete.ts b/apps/api/v1/pages/api/api-keys/[id]/_delete.ts deleted file mode 100644 index 28745ed7c969dd..00000000000000 --- a/apps/api/v1/pages/api/api-keys/[id]/_delete.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString"; - -async function deleteHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdAsString.parse(query); - await prisma.apiKey.delete({ where: { id } }); - return { message: `ApiKey with id: ${id} deleted` }; -} - -export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/api-keys/[id]/_get.ts b/apps/api/v1/pages/api/api-keys/[id]/_get.ts deleted file mode 100644 index 2841ea05de504b..00000000000000 --- a/apps/api/v1/pages/api/api-keys/[id]/_get.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { apiKeyPublicSchema } from "~/lib/validations/api-key"; -import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString"; - -async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdAsString.parse(query); - const api_key = await prisma.apiKey.findUniqueOrThrow({ where: { id } }); - return { api_key: apiKeyPublicSchema.parse(api_key) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/api-keys/[id]/_patch.ts b/apps/api/v1/pages/api/api-keys/[id]/_patch.ts deleted file mode 100644 index 259e528c17161c..00000000000000 --- a/apps/api/v1/pages/api/api-keys/[id]/_patch.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { apiKeyEditBodySchema, apiKeyPublicSchema } from "~/lib/validations/api-key"; -import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString"; - -async function patchHandler(req: NextApiRequest) { - const { body } = req; - const { id } = schemaQueryIdAsString.parse(req.query); - const data = apiKeyEditBodySchema.parse(body); - const api_key = await prisma.apiKey.update({ where: { id }, data }); - return { api_key: apiKeyPublicSchema.parse(api_key) }; -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/api-keys/[id]/index.ts b/apps/api/v1/pages/api/api-keys/[id]/index.ts deleted file mode 100644 index 7f401e0b0abf78..00000000000000 --- a/apps/api/v1/pages/api/api-keys/[id]/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import { authMiddleware } from "./_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/api-keys/_get.ts b/apps/api/v1/pages/api/api-keys/_get.ts deleted file mode 100644 index a60bc934769f4f..00000000000000 --- a/apps/api/v1/pages/api/api-keys/_get.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; -import type { Ensure } from "@calcom/types/utils"; - -import { apiKeyPublicSchema } from "~/lib/validations/api-key"; -import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; - -type CustomNextApiRequest = NextApiRequest & { - args?: Prisma.ApiKeyFindManyArgs; -}; - -/** Admins can query other users' API keys */ -function handleAdminRequests(req: CustomNextApiRequest) { - // To match type safety with runtime - if (!hasReqArgs(req)) throw Error("Missing req.args"); - const { userId, isSystemWideAdmin } = req; - if (isSystemWideAdmin && req.query.userId) { - const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); - const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; - req.args.where = { userId: { in: userIds } }; - if (Array.isArray(query.userId)) req.args.orderBy = { userId: "asc" }; - } -} - -function hasReqArgs(req: CustomNextApiRequest): req is Ensure { - return "args" in req; -} - -async function getHandler(req: CustomNextApiRequest) { - const { userId, isSystemWideAdmin } = req; - req.args = isSystemWideAdmin ? {} : { where: { userId } }; - // Proof of concept: allowing mutation in exchange of composability - handleAdminRequests(req); - const data = await prisma.apiKey.findMany(req.args); - return { api_keys: data.map((v) => apiKeyPublicSchema.parse(v)) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/api-keys/_post.ts b/apps/api/v1/pages/api/api-keys/_post.ts deleted file mode 100644 index ea4dd0a23eb9ad..00000000000000 --- a/apps/api/v1/pages/api/api-keys/_post.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { NextApiRequest } from "next"; -import { v4 } from "uuid"; - -import { generateUniqueAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { apiKeyCreateBodySchema, apiKeyPublicSchema } from "~/lib/validations/api-key"; - -async function postHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { neverExpires, userId: bodyUserId, ...input } = apiKeyCreateBodySchema.parse(req.body); - const [hashedKey, apiKey] = generateUniqueAPIKey(); - const args: Prisma.ApiKeyCreateArgs = { - data: { - id: v4(), - userId, - ...input, - // And here we pass a null to expiresAt if never expires is true. otherwise just pass expiresAt from input - expiresAt: neverExpires ? null : input.expiresAt, - hashedKey, - }, - }; - - if (!isSystemWideAdmin && bodyUserId) - throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); - - if (isSystemWideAdmin && bodyUserId) { - const where: Prisma.UserWhereInput = { id: bodyUserId }; - await prisma.user.findFirstOrThrow({ where }); - args.data.userId = bodyUserId; - } - - const result = await prisma.apiKey.create(args); - return { - api_key: { - ...apiKeyPublicSchema.parse(result), - key: `${process.env.API_KEY_PREFIX ?? "cal_"}${apiKey}`, - }, - message: "API key created successfully. Save the `key` value as it won't be displayed again.", - }; -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/api-keys/index.ts b/apps/api/v1/pages/api/api-keys/index.ts deleted file mode 100644 index d259db60ed1e59..00000000000000 --- a/apps/api/v1/pages/api/api-keys/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware("HTTP_GET_OR_POST")( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/attendees/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/attendees/[id]/_auth-middleware.ts deleted file mode 100644 index 8f6a5058a53e50..00000000000000 --- a/apps/api/v1/pages/api/attendees/[id]/_auth-middleware.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -async function authMiddleware(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const query = schemaQueryIdParseInt.parse(req.query); - // @note: Here we make sure to only return attendee's of the user's own bookings if the user is not an admin. - if (isSystemWideAdmin) return; - // Find all user bookings, including attendees - const attendee = await prisma.attendee.findFirst({ - where: { id: query.id, booking: { userId } }, - }); - // Flatten and merge all the attendees in one array - if (!attendee) throw new HttpError({ statusCode: 403, message: "Forbidden" }); -} - -export default authMiddleware; diff --git a/apps/api/v1/pages/api/attendees/[id]/_delete.ts b/apps/api/v1/pages/api/attendees/[id]/_delete.ts deleted file mode 100644 index 65d2acc3ac2c2f..00000000000000 --- a/apps/api/v1/pages/api/attendees/[id]/_delete.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /attendees/{id}: - * delete: - * operationId: removeAttendeeById - * summary: Remove an existing attendee - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the attendee to delete - * tags: - * - attendees - * responses: - * 201: - * description: OK, attendee removed successfully - * 400: - * description: Bad request. Attendee id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function deleteHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - await prisma.attendee.delete({ where: { id } }); - return { message: `Attendee with id: ${id} deleted successfully` }; -} - -export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/attendees/[id]/_get.ts b/apps/api/v1/pages/api/attendees/[id]/_get.ts deleted file mode 100644 index 1c03b3fa251618..00000000000000 --- a/apps/api/v1/pages/api/attendees/[id]/_get.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaAttendeeReadPublic } from "~/lib/validations/attendee"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /attendees/{id}: - * get: - * operationId: getAttendeeById - * summary: Find an attendee - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the attendee to get - * tags: - * - attendees - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Attendee was not found - */ -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const attendee = await prisma.attendee.findUnique({ where: { id } }); - return { attendee: schemaAttendeeReadPublic.parse(attendee) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/attendees/[id]/_patch.ts b/apps/api/v1/pages/api/attendees/[id]/_patch.ts deleted file mode 100644 index 1c63272f68fd77..00000000000000 --- a/apps/api/v1/pages/api/attendees/[id]/_patch.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { NextApiRequest } from "next"; -import type { z } from "zod"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaAttendeeEditBodyParams, schemaAttendeeReadPublic } from "~/lib/validations/attendee"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /attendees/{id}: - * patch: - * operationId: editAttendeeById - * summary: Edit an existing attendee - * requestBody: - * description: Edit an existing attendee related to one of your bookings - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * email: - * type: string - * format: email - * name: - * type: string - * timeZone: - * type: string - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the attendee to get - * tags: - * - attendees - * responses: - * 201: - * description: OK, attendee edited successfully - * 400: - * description: Bad request. Attendee body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - -export async function patchHandler(req: NextApiRequest) { - const { query, body } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const data = schemaAttendeeEditBodyParams.parse(body); - await checkPermissions(req, data); - const attendee = await prisma.attendee.update({ where: { id }, data }); - return { attendee: schemaAttendeeReadPublic.parse(attendee) }; -} - -async function checkPermissions(req: NextApiRequest, body: z.infer) { - const { isSystemWideAdmin } = req; - if (isSystemWideAdmin) return; - const { userId } = req; - const { bookingId } = body; - if (bookingId) { - // Ensure that the booking the attendee is being added to belongs to the user - const booking = await prisma.booking.findFirst({ where: { id: bookingId, userId } }); - if (!booking) throw new HttpError({ statusCode: 403, message: "You don't have access to the booking" }); - } -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/attendees/[id]/index.ts b/apps/api/v1/pages/api/attendees/[id]/index.ts deleted file mode 100644 index 54980cf49469ef..00000000000000 --- a/apps/api/v1/pages/api/attendees/[id]/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "./_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/attendees/_get.ts b/apps/api/v1/pages/api/attendees/_get.ts deleted file mode 100644 index dc619b0dd1701a..00000000000000 --- a/apps/api/v1/pages/api/attendees/_get.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaAttendeeReadPublic } from "~/lib/validations/attendee"; - -/** - * @swagger - * /attendees: - * get: - * operationId: listAttendees - * summary: Find all attendees - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - attendees - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No attendees were found - */ -async function handler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const args: Prisma.AttendeeFindManyArgs = isSystemWideAdmin ? {} : { where: { booking: { userId } } }; - const data = await prisma.attendee.findMany(args); - const attendees = data.map((attendee) => schemaAttendeeReadPublic.parse(attendee)); - if (!attendees) throw new HttpError({ statusCode: 404, message: "No attendees were found" }); - return { attendees }; -} - -export default defaultResponder(handler); diff --git a/apps/api/v1/pages/api/attendees/_post.ts b/apps/api/v1/pages/api/attendees/_post.ts deleted file mode 100644 index 3313d742bf88c2..00000000000000 --- a/apps/api/v1/pages/api/attendees/_post.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaAttendeeCreateBodyParams, schemaAttendeeReadPublic } from "~/lib/validations/attendee"; - -/** - * @swagger - * /attendees: - * post: - * operationId: addAttendee - * summary: Creates a new attendee - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * requestBody: - * description: Create a new attendee related to one of your bookings - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - bookingId - * - name - * - email - * - timeZone - * properties: - * bookingId: - * type: number - * email: - * type: string - * format: email - * name: - * type: string - * timeZone: - * type: string - * tags: - * - attendees - * responses: - * 201: - * description: OK, attendee created - * 400: - * description: Bad request. Attendee body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function postHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const body = schemaAttendeeCreateBodyParams.parse(req.body); - - if (!isSystemWideAdmin) { - const userBooking = await prisma.booking.findFirst({ - where: { userId, id: body.bookingId }, - select: { id: true }, - }); - // Here we make sure to only return attendee's of the user's own bookings. - if (!userBooking) throw new HttpError({ statusCode: 403, message: "Forbidden" }); - } - - const data = await prisma.attendee.create({ - data: { - email: body.email, - name: body.name, - timeZone: body.timeZone, - booking: { connect: { id: body.bookingId } }, - }, - }); - - return { - attendee: schemaAttendeeReadPublic.parse(data), - message: "Attendee created successfully", - }; -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/attendees/index.ts b/apps/api/v1/pages/api/attendees/index.ts deleted file mode 100644 index e89b47c30e817a..00000000000000 --- a/apps/api/v1/pages/api/attendees/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/availabilities/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/availabilities/[id]/_auth-middleware.ts deleted file mode 100644 index 245f5c9cb05272..00000000000000 --- a/apps/api/v1/pages/api/availabilities/[id]/_auth-middleware.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { NextApiRequest } from "next"; - -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -async function authMiddleware(req: NextApiRequest) { - const { userId, isSystemWideAdmin, query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - /** Admins can skip the ownership verification */ - if (isSystemWideAdmin) return; - /** - * There's a caveat here. If the availability exists but the user doesn't own it, - * the user will see a 404 error which may or not be the desired behavior. - */ - await prisma.availability.findFirstOrThrow({ - where: { id, Schedule: { userId } }, - }); -} - -export default authMiddleware; diff --git a/apps/api/v1/pages/api/availabilities/[id]/_delete.ts b/apps/api/v1/pages/api/availabilities/[id]/_delete.ts deleted file mode 100644 index fdb121ea89d410..00000000000000 --- a/apps/api/v1/pages/api/availabilities/[id]/_delete.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /availabilities/{id}: - * delete: - * operationId: removeAvailabilityById - * summary: Remove an existing availability - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the availability to delete - * - in: query - * name: apiKey - * required: true - * schema: - * type: integer - * description: Your API key - * tags: - * - availabilities - * externalDocs: - * url: https://docs.cal.com/docs/core-features/availability - * responses: - * 201: - * description: OK, availability removed successfully - * 400: - * description: Bad request. Availability id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function deleteHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - await prisma.availability.delete({ where: { id } }); - return { message: `Availability with id: ${id} deleted successfully` }; -} - -export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/availabilities/[id]/_get.ts b/apps/api/v1/pages/api/availabilities/[id]/_get.ts deleted file mode 100644 index fb29f39126a959..00000000000000 --- a/apps/api/v1/pages/api/availabilities/[id]/_get.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaAvailabilityReadPublic } from "~/lib/validations/availability"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /availabilities/{id}: - * get: - * operationId: getAvailabilityById - * summary: Find an availability - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the availability to get - * - in: query - * name: apiKey - * required: true - * schema: - * type: integer - * description: Your API key - * tags: - * - availabilities - * externalDocs: - * url: https://docs.cal.com/docs/core-features/availability - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid - * 404: - * description: Availability not found - */ -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const availability = await prisma.availability.findUnique({ - where: { id }, - include: { Schedule: { select: { userId: true } } }, - }); - return { availability: schemaAvailabilityReadPublic.parse(availability) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/availabilities/[id]/_patch.ts b/apps/api/v1/pages/api/availabilities/[id]/_patch.ts deleted file mode 100644 index 8757133849242f..00000000000000 --- a/apps/api/v1/pages/api/availabilities/[id]/_patch.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { - schemaAvailabilityEditBodyParams, - schemaAvailabilityReadPublic, -} from "~/lib/validations/availability"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /availabilities/{id}: - * patch: - * operationId: editAvailabilityById - * summary: Edit an existing availability - * parameters: - * - in: query - * name: apiKey - * required: true - * description: Your API key - * schema: - * type: integer - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: ID of the availability to edit - * requestBody: - * description: Edit an existing availability related to one of your bookings - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * days: - * type: array - * description: Array of integers depicting weekdays - * items: - * type: integer - * enum: [0, 1, 2, 3, 4, 5] - * scheduleId: - * type: integer - * description: ID of schedule this availability is associated with - * startTime: - * type: string - * description: Start time of the availability - * endTime: - * type: string - * description: End time of the availability - * examples: - * availability: - * summary: An example of availability - * value: - * scheduleId: 123 - * days: [1,2,3,5] - * startTime: 1970-01-01T17:00:00.000Z - * endTime: 1970-01-01T17:00:00.000Z - * - * tags: - * - availabilities - * externalDocs: - * url: https://docs.cal.com/docs/core-features/availability - * responses: - * 201: - * description: OK, availability edited successfully - * 400: - * description: Bad request. Availability body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function patchHandler(req: NextApiRequest) { - const { query, body } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const data = schemaAvailabilityEditBodyParams.parse(body); - const availability = await prisma.availability.update({ - where: { id }, - data, - include: { Schedule: { select: { userId: true } } }, - }); - return { availability: schemaAvailabilityReadPublic.parse(availability) }; -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/availabilities/[id]/index.ts b/apps/api/v1/pages/api/availabilities/[id]/index.ts deleted file mode 100644 index 54980cf49469ef..00000000000000 --- a/apps/api/v1/pages/api/availabilities/[id]/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "./_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/availabilities/_post.ts b/apps/api/v1/pages/api/availabilities/_post.ts deleted file mode 100644 index e68de943af6a09..00000000000000 --- a/apps/api/v1/pages/api/availabilities/_post.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { - schemaAvailabilityCreateBodyParams, - schemaAvailabilityReadPublic, -} from "~/lib/validations/availability"; - -/** - * @swagger - * /availabilities: - * post: - * operationId: addAvailability - * summary: Creates a new availability - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * requestBody: - * description: Edit an existing availability related to one of your bookings - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - scheduleId - * - startTime - * - endTime - * properties: - * days: - * type: array - * description: Array of integers depicting weekdays - * items: - * type: integer - * enum: [0, 1, 2, 3, 4, 5] - * scheduleId: - * type: integer - * description: ID of schedule this availability is associated with - * startTime: - * type: string - * description: Start time of the availability - * endTime: - * type: string - * description: End time of the availability - * examples: - * availability: - * summary: An example of availability - * value: - * scheduleId: 123 - * days: [1,2,3,5] - * startTime: 1970-01-01T17:00:00.000Z - * endTime: 1970-01-01T17:00:00.000Z - * - * - * tags: - * - availabilities - * externalDocs: - * url: https://docs.cal.com/docs/core-features/availability - * responses: - * 201: - * description: OK, availability created - * 400: - * description: Bad request. Availability body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function postHandler(req: NextApiRequest) { - const data = schemaAvailabilityCreateBodyParams.parse(req.body); - await checkPermissions(req); - const availability = await prisma.availability.create({ - data, - include: { Schedule: { select: { userId: true } } }, - }); - req.statusCode = 201; - return { - availability: schemaAvailabilityReadPublic.parse(availability), - message: "Availability created successfully", - }; -} - -async function checkPermissions(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - if (isSystemWideAdmin) return; - const data = schemaAvailabilityCreateBodyParams.parse(req.body); - const schedule = await prisma.schedule.findFirst({ - where: { userId, id: data.scheduleId }, - }); - if (!schedule) - throw new HttpError({ statusCode: 401, message: "You can't add availabilities to this schedule" }); -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/availabilities/index.ts b/apps/api/v1/pages/api/availabilities/index.ts deleted file mode 100644 index 1bda7cf6afcd8b..00000000000000 --- a/apps/api/v1/pages/api/availabilities/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/availability/_get.ts b/apps/api/v1/pages/api/availability/_get.ts deleted file mode 100644 index 4352822fd759e8..00000000000000 --- a/apps/api/v1/pages/api/availability/_get.ts +++ /dev/null @@ -1,262 +0,0 @@ -import type { NextApiRequest } from "next"; -import { z } from "zod"; - -import { getUserAvailabilityService } from "@calcom/features/di/containers/GetUserAvailability"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import { availabilityUserSelect } from "@calcom/prisma"; -import { MembershipRole } from "@calcom/prisma/enums"; -import { stringOrNumber } from "@calcom/prisma/zod-utils"; -import dayjs from "@calcom/dayjs"; - -/** - * @swagger - * /teams/{teamId}/availability: - * get: - * summary: Find team availability - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * example: "1234abcd5678efgh" - * description: Your API key - * - in: path - * name: teamId - * required: true - * schema: - * type: integer - * example: 123 - * description: ID of the team to fetch the availability for - * - in: query - * name: dateFrom - * schema: - * type: string - * format: date - * example: "2023-05-14 00:00:00" - * description: Start Date of the availability query - * - in: query - * name: dateTo - * schema: - * type: string - * format: date - * example: "2023-05-20 00:00:00" - * description: End Date of the availability query - * - in: query - * name: eventTypeId - * schema: - * type: integer - * example: 123 - * description: Event Type ID of the event type to fetch the availability for - * operationId: team-availability - * tags: - * - availability - * responses: - * 200: - * description: OK - * content: - * application/json: - * schema: - * type: object - * example: - * busy: - * - start: "2023-05-14T10:00:00.000Z" - * end: "2023-05-14T11:00:00.000Z" - * title: "Team meeting between Alice and Bob" - * - start: "2023-05-15T14:00:00.000Z" - * end: "2023-05-15T15:00:00.000Z" - * title: "Project review between Carol and Dave" - * - start: "2023-05-16T09:00:00.000Z" - * end: "2023-05-16T10:00:00.000Z" - * - start: "2023-05-17T13:00:00.000Z" - * end: "2023-05-17T14:00:00.000Z" - * timeZone: "America/New_York" - * workingHours: - * - days: [1, 2, 3, 4, 5] - * startTime: 540 - * endTime: 1020 - * userId: 101 - * dateOverrides: - * - date: "2023-05-15" - * startTime: 600 - * endTime: 960 - * userId: 101 - * currentSeats: 4 - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Team not found | Team has no members - * - * /availability: - * get: - * summary: Find user availability - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * example: "1234abcd5678efgh" - * description: Your API key - * - in: query - * name: userId - * schema: - * type: integer - * example: 101 - * description: ID of the user to fetch the availability for - * - in: query - * name: username - * schema: - * type: string - * example: "alice" - * description: username of the user to fetch the availability for - * - in: query - * name: dateFrom - * schema: - * type: string - * format: date - * example: "2023-05-14 00:00:00" - * description: Start Date of the availability query - * - in: query - * name: dateTo - * schema: - * type: string - * format: date - * example: "2023-05-20 00:00:00" - * description: End Date of the availability query - * - in: query - * name: eventTypeId - * schema: - * type: integer - * example: 123 - * description: Event Type ID of the event type to fetch the availability for - * operationId: user-availability - * tags: - * - availability - * responses: - * 200: - * description: OK - * content: - * application/json: - * schema: - * type: object - * example: - * busy: - * - start: "2023-05-14T10:00:00.000Z" - * end: "2023-05-14T11:00:00.000Z" - * title: "Team meeting between Alice and Bob" - * - start: "2023-05-15T14:00:00.000Z" - * end: "2023-05-15T15:00:00.000Z" - * title: "Project review between Carol and Dave" - * - start: "2023-05-16T09:00:00.000Z" - * end: "2023-05-16T10:00:00.000Z" - * - start: "2023-05-17T13:00:00.000Z" - * end: "2023-05-17T14:00:00.000Z" - * timeZone: "America/New_York" - * workingHours: - * - days: [1, 2, 3, 4, 5] - * startTime: 540 - * endTime: 1020 - * userId: 101 - * dateOverrides: - * - date: "2023-05-15" - * startTime: 600 - * endTime: 960 - * userId: 101 - * currentSeats: 4 - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: User not found - */ -interface MemberRoles { - [userId: number | string]: MembershipRole; -} - -const availabilitySchema = z - .object({ - userId: stringOrNumber.optional(), - teamId: stringOrNumber.optional(), - username: z.string().optional(), - dateFrom: z.string(), - dateTo: z.string(), - eventTypeId: stringOrNumber.optional(), - }) - .refine( - (data) => !!data.username || !!data.userId || !!data.teamId, - "Either username or userId or teamId should be filled in." - ); - -async function handler(req: NextApiRequest) { - const { isSystemWideAdmin, userId: reqUserId } = req; - const { username, userId, eventTypeId, teamId, dateFrom, dateTo } = availabilitySchema.parse(req.query); - - const dayjsDateFrom = dayjs(dateFrom); - const dayjsDateTo = dayjs(dateTo); - - if (!dayjsDateFrom.isValid() || !dayjsDateTo.isValid()) { - throw new HttpError({ statusCode: 400, message: "Invalid date range" }); - } - - const userAvailabilityService = getUserAvailabilityService(); - if (!teamId) - return userAvailabilityService.getUserAvailability({ - username, - dateFrom: dayjsDateFrom, - dateTo: dayjsDateTo, - eventTypeId, - userId, - returnDateOverrides: true, - bypassBusyCalendarTimes: false, - }); - const team = await prisma.team.findUnique({ - where: { id: teamId }, - select: { members: true }, - }); - if (!team) throw new HttpError({ statusCode: 404, message: "teamId not found" }); - if (!team.members) throw new HttpError({ statusCode: 404, message: "team has no members" }); - const allMemberIds = team.members.reduce((allMemberIds: number[], member) => { - if (member.accepted) { - allMemberIds.push(member.userId); - } - return allMemberIds; - }, []); - const members = await prisma.user.findMany({ - where: { id: { in: allMemberIds } }, - select: availabilityUserSelect, - }); - const memberRoles: MemberRoles = team.members.reduce((acc: MemberRoles, membership) => { - acc[membership.userId] = membership.role; - return acc; - }, {} as MemberRoles); - // check if the user is a team Admin or Owner, if it is a team request, or a system Admin - const isUserAdminOrOwner = - memberRoles[reqUserId] === MembershipRole.ADMIN || - memberRoles[reqUserId] === MembershipRole.OWNER || - isSystemWideAdmin; - if (!isUserAdminOrOwner) throw new HttpError({ statusCode: 403, message: "Forbidden" }); - const availabilities = members.map(async (user) => { - return { - userId: user.id, - availability: await userAvailabilityService.getUserAvailability({ - userId: user.id, - dateFrom: dayjsDateFrom, - dateTo: dayjsDateTo, - eventTypeId, - returnDateOverrides: true, - bypassBusyCalendarTimes: false, - }), - }; - }); - const settled = await Promise.all(availabilities); - if (!settled) - throw new HttpError({ - statusCode: 401, - message: "We had an issue retrieving all your members availabilities", - }); - return settled; -} - -export default defaultResponder(handler); diff --git a/apps/api/v1/pages/api/availability/index.ts b/apps/api/v1/pages/api/availability/index.ts deleted file mode 100644 index c97e6cd4dfd429..00000000000000 --- a/apps/api/v1/pages/api/availability/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - }) -); diff --git a/apps/api/v1/pages/api/booking-references/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/booking-references/[id]/_auth-middleware.ts deleted file mode 100644 index 5dd52285cbefa8..00000000000000 --- a/apps/api/v1/pages/api/booking-references/[id]/_auth-middleware.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -async function authMiddleware(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { id } = schemaQueryIdParseInt.parse(req.query); - // Here we make sure to only return references of the user's own bookings if the user is not an admin. - if (isSystemWideAdmin) return; - // Find all references where the user has bookings - const bookingReference = await prisma.bookingReference.findFirst({ - where: { id, booking: { userId }, deleted: null }, - }); - if (!bookingReference) throw new HttpError({ statusCode: 403, message: "Forbidden" }); -} - -export default authMiddleware; diff --git a/apps/api/v1/pages/api/booking-references/[id]/_delete.ts b/apps/api/v1/pages/api/booking-references/[id]/_delete.ts deleted file mode 100644 index e3515d5ff5deca..00000000000000 --- a/apps/api/v1/pages/api/booking-references/[id]/_delete.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /booking-references/{id}: - * delete: - * operationId: removeBookingReferenceById - * summary: Remove an existing booking reference - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the booking reference to delete - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - booking-references - * responses: - * 201: - * description: OK, bookingReference removed successfully - * 400: - * description: Bad request. BookingReference id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function deleteHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - await prisma.bookingReference.update({ where: { id }, data: { deleted: true } }); - return { message: `BookingReference with id: ${id} deleted` }; -} - -export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/booking-references/[id]/_get.ts b/apps/api/v1/pages/api/booking-references/[id]/_get.ts deleted file mode 100644 index 1635dee7b0246d..00000000000000 --- a/apps/api/v1/pages/api/booking-references/[id]/_get.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-reference"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /booking-references/{id}: - * get: - * operationId: getBookingReferenceById - * summary: Find a booking reference - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the booking reference to get - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - booking-references - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: BookingReference was not found - */ -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id, deleted: null } }); - return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/booking-references/[id]/_patch.ts b/apps/api/v1/pages/api/booking-references/[id]/_patch.ts deleted file mode 100644 index fa0096fecd7196..00000000000000 --- a/apps/api/v1/pages/api/booking-references/[id]/_patch.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { - schemaBookingEditBodyParams, - schemaBookingReferenceReadPublic, -} from "~/lib/validations/booking-reference"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /booking-references/{id}: - * patch: - * operationId: editBookingReferenceById - * summary: Edit an existing booking reference - * requestBody: - * description: Edit an existing booking reference related to one of your bookings - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * type: - * type: string - * meetingId: - * type: string - * meetingPassword: - * type: string - * externalCalendarId: - * type: string - * deleted: - * type: boolean - * credentialId: - * type: integer - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the booking reference to edit - * tags: - * - booking-references - * responses: - * 201: - * description: OK, BookingReference edited successfully - * 400: - * description: Bad request. BookingReference body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function patchHandler(req: NextApiRequest) { - const { query, body, isSystemWideAdmin, userId } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const data = schemaBookingEditBodyParams.parse(body); - /* If user tries to update bookingId, we run extra checks */ - if (data.bookingId) { - const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin - ? /* If admin, we only check that the booking exists */ - { where: { id: data.bookingId } } - : /* For non-admins we make sure the booking belongs to the user */ - { where: { id: data.bookingId, userId } }; - await prisma.booking.findFirstOrThrow(args); - } - const booking_reference = await prisma.bookingReference.update({ where: { id }, data }); - return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) }; -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/booking-references/[id]/index.ts b/apps/api/v1/pages/api/booking-references/[id]/index.ts deleted file mode 100644 index 54980cf49469ef..00000000000000 --- a/apps/api/v1/pages/api/booking-references/[id]/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "./_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/booking-references/_get.ts b/apps/api/v1/pages/api/booking-references/_get.ts deleted file mode 100644 index e328929c527592..00000000000000 --- a/apps/api/v1/pages/api/booking-references/_get.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-reference"; - -/** - * @swagger - * /booking-references: - * get: - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * operationId: listBookingReferences - * summary: Find all booking references - * tags: - * - booking-references - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No booking references were found - */ -export async function handler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin - ? { where: { deleted: null } } - : { where: { booking: { userId }, deleted: null } }; - const data = await prisma.bookingReference.findMany(args); - return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) }; -} - -export default defaultResponder(handler); diff --git a/apps/api/v1/pages/api/booking-references/_post.ts b/apps/api/v1/pages/api/booking-references/_post.ts deleted file mode 100644 index 4c06b7827a389b..00000000000000 --- a/apps/api/v1/pages/api/booking-references/_post.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { - schemaBookingCreateBodyParams, - schemaBookingReferenceReadPublic, -} from "~/lib/validations/booking-reference"; - -/** - * @swagger - * /booking-references: - * post: - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * operationId: addBookingReference - * summary: Creates a new booking reference - * requestBody: - * description: Create a new booking reference related to one of your bookings - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - type - * - uid - * properties: - * type: - * type: string - * uid: - * type: string - * meetingId: - * type: string - * meetingPassword: - * type: string - * meetingUrl: - * type: string - * bookingId: - * type: boolean - * externalCalendarId: - * type: string - * deleted: - * type: boolean - * credentialId: - * type: integer - * tags: - * - booking-references - * responses: - * 201: - * description: OK, booking reference created - * 400: - * description: Bad request. BookingReference body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function postHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const body = schemaBookingCreateBodyParams.parse(req.body); - const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin - ? /* If admin, we only check that the booking exists */ - { where: { id: body.bookingId } } - : /* For non-admins we make sure the booking belongs to the user */ - { where: { id: body.bookingId, userId } }; - await prisma.booking.findFirstOrThrow(args); - - const data = await prisma.bookingReference.create({ - data: { - ...body, - bookingId: body.bookingId, - }, - }); - - return { - booking_reference: schemaBookingReferenceReadPublic.parse(data), - message: "Booking reference created successfully", - }; -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/booking-references/index.ts b/apps/api/v1/pages/api/booking-references/index.ts deleted file mode 100644 index e89b47c30e817a..00000000000000 --- a/apps/api/v1/pages/api/booking-references/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/bookings/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/bookings/[id]/_auth-middleware.ts deleted file mode 100644 index 06a8f9b4e14a40..00000000000000 --- a/apps/api/v1/pages/api/bookings/[id]/_auth-middleware.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { prisma } from "@calcom/prisma"; - -import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -async function authMiddleware(req: NextApiRequest) { - const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin, query } = req; - if (isSystemWideAdmin) { - return; - } - - const { id } = schemaQueryIdParseInt.parse(query); - if (isOrganizationOwnerOrAdmin) { - const booking = await prisma.booking.findUnique({ - where: { id }, - select: { userId: true }, - }); - if (booking) { - const bookingUserId = booking.userId; - if (bookingUserId) { - const accessibleUsersIds = await getAccessibleUsers({ - adminUserId: userId, - memberUserIds: [bookingUserId], - }); - if (accessibleUsersIds.length > 0) return; - } - } - } - - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { - email: true, - bookings: { - where: { - id, - }, - select: { - id: true, - }, - }, - }, - }); - - if (!user) throw new HttpError({ statusCode: 404, message: "User not found" }); - - const filteredBookings = user?.bookings?.filter((booking) => booking.id === id); - const userIsHost = !!filteredBookings?.length; - - const bookingsAsAttendee = prisma.booking.findMany({ - where: { - id, - attendees: { some: { email: user.email } }, - }, - }); - - const bookingsAsEventTypeOwner = prisma.booking.findMany({ - where: { - id, - eventType: { - owner: { id: userId }, - }, - }, - }); - - const bookingsAsTeamOwnerOrAdmin = prisma.booking.findMany({ - where: { - id, - eventType: { - team: { - members: { - some: { userId, role: { in: ["ADMIN", "OWNER"] }, accepted: true }, - }, - }, - }, - }, - }); - - const [resultOne, resultTwo, resultThree] = await Promise.all([ - bookingsAsAttendee, - bookingsAsEventTypeOwner, - bookingsAsTeamOwnerOrAdmin, - ]); - - const teamBookingsAsOwnerOrAdmin = [...resultOne, ...resultTwo, ...resultThree]; - const userHasTeamBookings = !!teamBookingsAsOwnerOrAdmin.length; - - if (!userIsHost && !userHasTeamBookings) - throw new HttpError({ statusCode: 403, message: "You are not authorized" }); -} - -export default authMiddleware; diff --git a/apps/api/v1/pages/api/bookings/[id]/_delete.ts b/apps/api/v1/pages/api/bookings/[id]/_delete.ts deleted file mode 100644 index b7321824cae9d3..00000000000000 --- a/apps/api/v1/pages/api/bookings/[id]/_delete.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { NextApiRequest } from "next"; - -import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { bookingCancelSchema } from "~/lib/validations/booking"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /bookings/{id}/cancel: - * delete: - * summary: Booking cancellation - * operationId: cancelBookingById - * - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the booking to cancel - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: query - * name: allRemainingBookings - * required: false - * schema: - * type: boolean - * description: Delete all remaining bookings - * - in: query - * name: cancellationReason - * required: false - * schema: - * type: string - * description: The reason for cancellation of the booking - * - in: query - * name: cancelledBy - * required: false - * schema: - * type: string - * description: The email of who cancelled the booking - * tags: - * - bookings - * responses: - * 200: - * description: OK, booking cancelled successfully - * 400: - * description: | - * Bad request - * - * - * - * - * - * - * - * - * - * - * - * - * - *
MessageCause
Booking not foundThe provided id didn't correspond to any existing booking.
User not foundThe userId did not matched an existing user.
- * 404: - * description: User not found - */ -async function handler(req: NextApiRequest) { - const { id, allRemainingBookings, cancellationReason, cancelledBy } = schemaQueryIdParseInt - .merge( - bookingCancelSchema.pick({ allRemainingBookings: true, cancellationReason: true, cancelledBy: true }) - ) - .parse({ - ...req.query, - allRemainingBookings: req.query.allRemainingBookings === "true", - }); - - return await handleCancelBooking({ - bookingData: { id, allRemainingBookings, cancellationReason, cancelledBy }, - userId: req.userId, - impersonatedByUserUuid: null, - actionSource: "API_V1", - }); -} - -export default defaultResponder(handler); diff --git a/apps/api/v1/pages/api/bookings/[id]/_get.ts b/apps/api/v1/pages/api/bookings/[id]/_get.ts deleted file mode 100644 index a5105c01e777eb..00000000000000 --- a/apps/api/v1/pages/api/bookings/[id]/_get.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { ErrorCode } from "@calcom/lib/errorCodes"; -import { ErrorWithCode } from "@calcom/lib/errors"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaBookingReadPublic } from "~/lib/validations/booking"; -import { schemaQuerySingleOrMultipleExpand } from "~/lib/validations/shared/queryExpandRelations"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /bookings/{id}: - * get: - * summary: Find a booking - * operationId: getBookingById - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the booking to get - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - bookings - * responses: - * 200: - * description: OK - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Booking" - * examples: - * booking: - * value: - * { - * "booking": { - * "id": 91, - * "userId": 5, - * "description": "", - * "eventTypeId": 7, - * "uid": "bFJeNb2uX8ANpT3JL5EfXw", - * "title": "60min between Pro Example and John Doe", - * "startTime": "2023-05-25T09:30:00.000Z", - * "endTime": "2023-05-25T10:30:00.000Z", - * "attendees": [ - * { - * "email": "john.doe@example.com", - * "name": "John Doe", - * "timeZone": "Asia/Kolkata", - * "locale": "en" - * } - * ], - * "user": { - * "email": "pro@example.com", - * "name": "Pro Example", - * "timeZone": "Asia/Kolkata", - * "locale": "en" - * }, - * "payment": [ - * { - * "id": 1, - * "success": true, - * "paymentOption": "ON_BOOKING" - * } - * ], - * "metadata": {}, - * "status": "ACCEPTED", - * "responses": { - * "email": "john.doe@example.com", - * "name": "John Doe", - * "location": { - * "optionValue": "", - * "value": "inPerson" - * } - * } - * } - * } - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Booking was not found - */ - -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - - const queryFilterForExpand = schemaQuerySingleOrMultipleExpand.parse(req.query.expand); - const expand = Array.isArray(queryFilterForExpand) - ? queryFilterForExpand - : queryFilterForExpand - ? [queryFilterForExpand] - : []; - const booking = await prisma.booking.findUnique({ - where: { id }, - include: { - // eslint-disable-next-line @calcom/eslint/no-prisma-include-true - attendees: true, - // eslint-disable-next-line @calcom/eslint/no-prisma-include-true - user: true, - // eslint-disable-next-line @calcom/eslint/no-prisma-include-true - payment: true, - eventType: expand.includes("team") ? { include: { team: true } } : false, - }, - }); - - if (!booking) { - throw new ErrorWithCode(ErrorCode.BookingNotFound, "Booking not found"); - } - - return { booking: schemaBookingReadPublic.parse(booking) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/bookings/[id]/_patch.ts b/apps/api/v1/pages/api/bookings/[id]/_patch.ts deleted file mode 100644 index 0b3ab2269173c9..00000000000000 --- a/apps/api/v1/pages/api/bookings/[id]/_patch.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { NextApiRequest } from "next"; -import type { z } from "zod"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers"; -import { schemaBookingEditBodyParams, schemaBookingReadPublic } from "~/lib/validations/booking"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /bookings/{id}: - * patch: - * summary: Edit an existing booking - * operationId: editBookingById - * requestBody: - * description: Edit an existing booking related to one of your event-types - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * title: - * type: string - * description: 'Booking event title' - * start: - * type: string - * format: date-time - * description: 'Start time of the Event' - * end: - * type: string - * format: date-time - * description: 'End time of the Event' - * status: - * type: string - * description: 'Acceptable values one of ["ACCEPTED", "PENDING", "CANCELLED", "REJECTED"]' - * description: - * type: string - * description: 'Description of the meeting' - * examples: - * editBooking: - * value: - * { - * "title": "Debugging between Syed Ali Shahbaz and Hello Hello", - * "start": "2023-05-24T13:00:00.000Z", - * "end": "2023-05-24T13:30:00.000Z", - * "status": "CANCELLED" - * } - * - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the booking to edit - * tags: - * - bookings - * responses: - * 200: - * description: OK, booking edited successfully - * content: - * application/json: - * examples: - * bookings: - * value: - * { - * "booking": { - * "id": 11223344, - * "userId": 182, - * "description": null, - * "eventTypeId": 2323232, - * "uid": "stoSJtnh83PEL4rZmqdHe2", - * "title": "Debugging between Syed Ali Shahbaz and Hello Hello", - * "startTime": "2023-05-24T13:00:00.000Z", - * "endTime": "2023-05-24T13:30:00.000Z", - * "metadata": {}, - * "status": "CANCELLED", - * "responses": { - * "email": "john.doe@example.com", - * "name": "John Doe", - * "location": { - * "optionValue": "", - * "value": "inPerson" - * } - * } - * } - * } - * 400: - * description: Bad request. Booking body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function patchHandler(req: NextApiRequest) { - const { query, body } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const data = schemaBookingEditBodyParams.parse(body); - await checkPermissions(req, data); - const booking = await prisma.booking.update({ where: { id }, data }); - return { booking: schemaBookingReadPublic.parse(booking) }; -} - -async function checkPermissions(req: NextApiRequest, body: z.infer) { - const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req; - if (body.userId && !isSystemWideAdmin && !isOrganizationOwnerOrAdmin) { - // Organizer has to be a cal user and we can't allow a booking to be transferred to some other cal user's name - throw new HttpError({ - statusCode: 403, - message: "Only admin can change the organizer of a booking", - }); - } - - if (body.userId && isOrganizationOwnerOrAdmin) { - const accessibleUsersIds = await getAccessibleUsers({ - adminUserId: userId, - memberUserIds: [body.userId], - }); - if (accessibleUsersIds.length === 0) { - throw new HttpError({ - statusCode: 403, - message: "Only admin can change the organizer of a booking", - }); - } - } -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/bookings/[id]/cancel.ts b/apps/api/v1/pages/api/bookings/[id]/cancel.ts deleted file mode 100644 index 082b41ec523471..00000000000000 --- a/apps/api/v1/pages/api/bookings/[id]/cancel.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "./_auth-middleware"; - -export default withMiddleware()(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - DELETE: import("./_delete"), - })(req, res); -}); diff --git a/apps/api/v1/pages/api/bookings/[id]/index.ts b/apps/api/v1/pages/api/bookings/[id]/index.ts deleted file mode 100644 index 54980cf49469ef..00000000000000 --- a/apps/api/v1/pages/api/bookings/[id]/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "./_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/bookings/[id]/recordings/_get.ts b/apps/api/v1/pages/api/bookings/[id]/recordings/_get.ts deleted file mode 100644 index 2c222e6e30bc54..00000000000000 --- a/apps/api/v1/pages/api/bookings/[id]/recordings/_get.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { - getRecordingsOfCalVideoByRoomName, - getDownloadLinkOfCalVideoByRecordingId, -} from "@calcom/features/conferencing/lib/videoClient"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { RecordingItemSchema } from "@calcom/prisma/zod-utils"; -import type { PartialReference } from "@calcom/types/EventManager"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /bookings/{id}/recordings: - * get: - * summary: Find all Cal video recordings of that booking - * operationId: getRecordingsByBookingId - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the booking for which recordings need to be fetched. Recording download link is only valid for 12 hours and you would have to fetch the recordings again to get new download link - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - bookings - * responses: - * 200: - * description: OK - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ArrayOfRecordings" - * examples: - * recordings: - * value: - * - id: "ad90a2e7-154f-49ff-a815-5da1db7bf899" - * room_name: "0n22w24AQ5ZFOtEKX2gX" - * start_ts: 1716215386 - * status: "finished" - * max_participants: 1 - * duration: 11 - * share_token: "x94YK-69Gnh7" - * download_link: "https://daily-meeting-recordings..." - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Booking was not found - */ - -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - - const booking = await prisma.booking.findUnique({ - where: { id }, - // eslint-disable-next-line @calcom/eslint/no-prisma-include-true - include: { references: true }, - }); - - if (!booking) - throw new HttpError({ - statusCode: 404, - message: `No Booking found with booking id ${id}`, - }); - - const roomName = - booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ?? - undefined; - - if (!roomName) - throw new HttpError({ - statusCode: 404, - message: `No Cal Video reference found with booking id ${booking.id}`, - }); - - const recordings = await getRecordingsOfCalVideoByRoomName(roomName); - - if (!recordings || !("data" in recordings)) return []; - - const recordingWithDownloadLink = recordings.data.map((recording: RecordingItemSchema) => { - return getDownloadLinkOfCalVideoByRecordingId(recording.id) - .then((res) => ({ - ...recording, - download_link: res?.download_link, - })) - .catch((err) => ({ ...recording, download_link: null, error: err.message })); - }); - const res = await Promise.all(recordingWithDownloadLink); - return res; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/bookings/[id]/recordings/index.ts b/apps/api/v1/pages/api/bookings/[id]/recordings/index.ts deleted file mode 100644 index 9d619d0219f4af..00000000000000 --- a/apps/api/v1/pages/api/bookings/[id]/recordings/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "../_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/bookings/[id]/transcripts/[recordingId]/_get.ts b/apps/api/v1/pages/api/bookings/[id]/transcripts/[recordingId]/_get.ts deleted file mode 100644 index d57b9711425b2a..00000000000000 --- a/apps/api/v1/pages/api/bookings/[id]/transcripts/[recordingId]/_get.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { - getTranscriptsAccessLinkFromRecordingId, - checkIfRoomNameMatchesInRecording, -} from "@calcom/features/conferencing/lib/videoClient"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { PartialReference } from "@calcom/types/EventManager"; - -import { getTranscriptFromRecordingId } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /bookings/{id}/transcripts/{recordingId}: - * get: - * summary: Find all Cal video transcripts of that recording - * operationId: getTranscriptsByRecordingId - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the booking for which transcripts need to be fetched. - * - in: path - * name: recordingId - * schema: - * type: string - * required: true - * description: ID of the recording(daily.co recording id) for which transcripts need to be fetched. - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - bookings - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Booking was not found - */ - -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id, recordingId } = getTranscriptFromRecordingId.parse(query); - - await checkIfRecordingBelongsToBooking(id, recordingId); - - const transcriptsAccessLinks = await getTranscriptsAccessLinkFromRecordingId(recordingId); - - return transcriptsAccessLinks; -} - -const checkIfRecordingBelongsToBooking = async (bookingId: number, recordingId: string) => { - const booking = await prisma.booking.findUnique({ - where: { id: bookingId }, - // eslint-disable-next-line @calcom/eslint/no-prisma-include-true - include: { references: true }, - }); - - if (!booking) - throw new HttpError({ - statusCode: 404, - message: `No Booking found with booking id ${bookingId}`, - }); - - const roomName = - booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ?? - undefined; - - if (!roomName) - throw new HttpError({ - statusCode: 404, - message: `No Booking Reference with Daily Video found with booking id ${bookingId}`, - }); - - const canUserAccessRecordingId = await checkIfRoomNameMatchesInRecording(roomName, recordingId); - if (!canUserAccessRecordingId) { - throw new HttpError({ - statusCode: 403, - message: `This Recording Id ${recordingId} does not belong to booking ${bookingId}`, - }); - } -}; - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/bookings/[id]/transcripts/[recordingId]/index.ts b/apps/api/v1/pages/api/bookings/[id]/transcripts/[recordingId]/index.ts deleted file mode 100644 index 1949b5e7910adf..00000000000000 --- a/apps/api/v1/pages/api/bookings/[id]/transcripts/[recordingId]/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "../../_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/bookings/[id]/transcripts/_get.ts b/apps/api/v1/pages/api/bookings/[id]/transcripts/_get.ts deleted file mode 100644 index f4de9c9c73a05d..00000000000000 --- a/apps/api/v1/pages/api/bookings/[id]/transcripts/_get.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/features/conferencing/lib/videoClient"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { PartialReference } from "@calcom/types/EventManager"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /bookings/{id}/transcripts: - * get: - * summary: Find all Cal video transcripts of that booking - * operationId: getTranscriptsByBookingId - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the booking for which recordings need to be fetched. - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - bookings - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Booking was not found - */ - -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - - const booking = await prisma.booking.findUnique({ - where: { id }, - // eslint-disable-next-line @calcom/eslint/no-prisma-include-true - include: { references: true }, - }); - - if (!booking) - throw new HttpError({ - statusCode: 404, - message: `No Booking found with booking id ${id}`, - }); - - const roomName = - booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ?? - undefined; - - if (!roomName) - throw new HttpError({ - statusCode: 404, - message: `No Cal Video reference found with booking id ${booking.id}`, - }); - - const transcripts = await getAllTranscriptsAccessLinkFromRoomName(roomName); - - return transcripts; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/bookings/[id]/transcripts/index.ts b/apps/api/v1/pages/api/bookings/[id]/transcripts/index.ts deleted file mode 100644 index 9d619d0219f4af..00000000000000 --- a/apps/api/v1/pages/api/bookings/[id]/transcripts/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "../_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/bookings/_get.ts b/apps/api/v1/pages/api/bookings/_get.ts deleted file mode 100644 index 7968a28b0b8dd3..00000000000000 --- a/apps/api/v1/pages/api/bookings/_get.ts +++ /dev/null @@ -1,395 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma, Booking } from "@calcom/prisma/client"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; -import { buildWhereClause } from "~/lib/utils/bookings/get/buildWhereClause"; -import { - getAccessibleUsers, - retrieveOrgScopedAccessibleUsers, -} from "~/lib/utils/retrieveScopedAccessibleUsers"; -import { schemaBookingGetParams, schemaBookingReadPublic } from "~/lib/validations/booking"; -import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail"; -import { schemaQuerySingleOrMultipleExpand } from "~/lib/validations/shared/queryExpandRelations"; -import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; - -/** - * @swagger - * /bookings: - * get: - * summary: Find all bookings - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * example: 123456789abcdefgh - * - in: query - * name: userId - * required: false - * schema: - * oneOf: - * - type: integer - * example: 1 - * - type: array - * items: - * type: integer - * example: [2, 3, 4] - * - in: query - * name: attendeeEmail - * required: false - * schema: - * oneOf: - * - type: string - * format: email - * example: john.doe@example.com - * - type: array - * items: - * type: string - * format: email - * example: [john.doe@example.com, jane.doe@example.com] - * - in: query - * name: order - * required: false - * schema: - * type: string - * enum: [asc, desc] - * - in: query - * name: sortBy - * required: false - * schema: - * type: string - * enum: [createdAt, updatedAt] - * - in: query - * name: status - * required: false - * schema: - * type: string - * enum: [upcoming] - * description: Filter bookings by status, it will overwrite dateFrom and dateTo filters - * - in: query - * name: dateFrom - * required: false - * schema: - * type: string - * description: ISO 8601 date string to filter bookings by start time - * - in: query - * name: dateTo - * required: false - * schema: - * type: string - * description: ISO 8601 date string to filter bookings by end time - * operationId: listBookings - * tags: - * - bookings - * responses: - * 200: - * description: OK - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/ArrayOfBookings" - * examples: - * bookings: - * value: [ - * { - * "booking": { - * "id": 91, - * "userId": 5, - * "description": "", - * "eventTypeId": 7, - * "uid": "bFJeNb2uX8ANpT3JL5EfXw", - * "title": "60min between Pro Example and John Doe", - * "startTime": "2023-05-25T09:30:00.000Z", - * "endTime": "2023-05-25T10:30:00.000Z", - * "attendees": [ - * { - * "email": "john.doe@example.com", - * "name": "John Doe", - * "timeZone": "Asia/Kolkata", - * "locale": "en" - * } - * ], - * "user": { - * "email": "pro@example.com", - * "name": "Pro Example", - * "timeZone": "Asia/Kolkata", - * "locale": "en" - * }, - * "payment": [ - * { - * "id": 1, - * "success": true, - * "paymentOption": "ON_BOOKING" - * } - * ], - * "metadata": {}, - * "status": "ACCEPTED", - * "responses": { - * "email": "john.doe@example.com", - * "name": "John Doe", - * "location": { - * "optionValue": "", - * "value": "inPerson" - * } - * } - * } - * } - * ] - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No bookings were found - */ -type GetAdminArgsType = { - adminDidQueryUserIds?: boolean; - requestedUserIds: number[]; - userId: number; -}; - -export async function handler(req: NextApiRequest) { - const { - userId, - isSystemWideAdmin, - isOrganizationOwnerOrAdmin, - pagination: { take, skip }, - } = req; - const { dateFrom, dateTo, order, sortBy, status } = schemaBookingGetParams.parse(req.query); - - const args: Prisma.BookingFindManyArgs = {}; - if (req.query.take && req.query.page) { - args.take = take; - args.skip = skip; - } - const queryFilterForExpand = schemaQuerySingleOrMultipleExpand.parse(req.query.expand); - const expand = Array.isArray(queryFilterForExpand) - ? queryFilterForExpand - : queryFilterForExpand - ? [queryFilterForExpand] - : []; - - args.include = { - attendees: true, - user: true, - payment: true, - eventType: expand.includes("team") ? { include: { team: true } } : false, - }; - - const queryFilterForAttendeeEmails = schemaQuerySingleOrMultipleAttendeeEmails.parse(req.query); - const attendeeEmails = Array.isArray(queryFilterForAttendeeEmails.attendeeEmail) - ? queryFilterForAttendeeEmails.attendeeEmail - : typeof queryFilterForAttendeeEmails.attendeeEmail === "string" - ? [queryFilterForAttendeeEmails.attendeeEmail] - : []; - const filterByAttendeeEmails = attendeeEmails.length > 0; - let userEmailsToFilterBy: string[] = []; - - /** Only admins can query other users */ - if (isSystemWideAdmin) { - if (req.query.userId || filterByAttendeeEmails) { - const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); - const requestedUserIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; - - const systemWideAdminArgs = { - adminDidQueryUserIds: !!req.query.userId, - requestedUserIds, - userId, - }; - const { userId: argUserId, userIds, userEmails } = await handleSystemWideAdminArgs(systemWideAdminArgs); - userEmailsToFilterBy = userEmails; - args.where = buildWhereClause(argUserId, attendeeEmails, userIds); - } - } else if (isOrganizationOwnerOrAdmin) { - let requestedUserIds = [userId]; - if (req.query.userId || filterByAttendeeEmails) { - const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); - requestedUserIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; - } - const orgWideAdminArgs = { - adminDidQueryUserIds: !!req.query.userId, - requestedUserIds, - userId, - }; - const { userId: argUserId, userIds, userEmails } = await handleOrgWideAdminArgs(orgWideAdminArgs); - userEmailsToFilterBy = userEmails; - args.where = buildWhereClause(argUserId, attendeeEmails, userIds); - } else { - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { - email: true, - }, - }); - if (!user) { - throw new HttpError({ message: "User not found", statusCode: 404 }); - } - args.where = buildWhereClause(userId, attendeeEmails, []); - } - - if (dateFrom) { - args.where = { - ...args.where, - startTime: { gte: dateFrom }, - }; - } - if (dateTo) { - args.where = { - ...args.where, - endTime: { lte: dateTo }, - }; - } - - if (sortBy === "updatedAt") { - args.orderBy = { - updatedAt: order, - }; - } - - if (sortBy === "createdAt") { - args.orderBy = { - createdAt: order, - }; - } - - if (status) { - switch (status) { - case "upcoming": - args.where = { - ...args.where, - startTime: { gte: new Date().toISOString() }, - }; - break; - default: - throw new HttpError({ message: "Invalid status", statusCode: 400 }); - } - } - - let data: Booking[] = []; - - if (!filterByAttendeeEmails && userEmailsToFilterBy.length > 0) { - const queryOne = prisma.booking.findMany(args); - - const whereClauseForQueryTwo: Prisma.BookingWhereInput = { - attendees: { - some: { - email: { in: userEmailsToFilterBy }, - }, - }, - }; - - if (dateFrom) { - whereClauseForQueryTwo.startTime = { gte: dateFrom }; - } - if (dateTo) { - whereClauseForQueryTwo.endTime = { lte: dateTo }; - } - if (status === "upcoming") { - whereClauseForQueryTwo.startTime = { gte: new Date().toISOString() }; - } - - const argsForQueryTwo: Prisma.BookingFindManyArgs = { - ...args, - where: whereClauseForQueryTwo, - }; - - const queryTwo = prisma.booking.findMany(argsForQueryTwo); - - const [resultOne, resultTwo] = await Promise.all([queryOne, queryTwo]); - - const bookingMap = new Map(); - [...resultOne, ...resultTwo].forEach((booking) => { - bookingMap.set(booking.id, booking); - }); - - const dedupedResults = Array.from(bookingMap.values()); - - if (args.orderBy) { - let sortField: keyof Booking; - let sortDirection: "asc" | "desc" = "asc"; - - if (typeof args.orderBy === "object" && !Array.isArray(args.orderBy)) { - const orderByKey = Object.keys(args.orderBy)[0] as keyof typeof args.orderBy; - sortField = orderByKey as keyof Booking; - sortDirection = args.orderBy[orderByKey] as "asc" | "desc"; - } else { - sortField = "id"; - } - - const sortOrder = sortDirection === "desc" ? -1 : 1; - - dedupedResults.sort((a, b) => { - const aValue = a[sortField]; - const bValue = b[sortField]; - - if (aValue < bValue) return -1 * sortOrder; - if (aValue > bValue) return 1 * sortOrder; - return 0; - }); - } - - if (args.take !== undefined && args.skip !== undefined) { - data = dedupedResults.slice(args.skip, args.skip + args.take); - } else { - data = dedupedResults; - } - } else { - data = await prisma.booking.findMany(args); - } - return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) }; -} - -const handleSystemWideAdminArgs = async ({ - adminDidQueryUserIds, - requestedUserIds, - userId, -}: GetAdminArgsType) => { - if (adminDidQueryUserIds) { - const users = await prisma.user.findMany({ - where: { id: { in: requestedUserIds } }, - select: { email: true }, - }); - const userEmails = users.map((u) => u.email); - - return { userId, userIds: requestedUserIds, userEmails }; - } - return { userId: null, userIds: [], userEmails: [] }; -}; - -const handleOrgWideAdminArgs = async ({ - adminDidQueryUserIds, - requestedUserIds, - userId, -}: GetAdminArgsType) => { - if (adminDidQueryUserIds) { - const accessibleUsersIds = await getAccessibleUsers({ - adminUserId: userId, - memberUserIds: requestedUserIds, - }); - - if (!accessibleUsersIds.length) throw new HttpError({ message: "No User found", statusCode: 404 }); - const users = await prisma.user.findMany({ - where: { id: { in: accessibleUsersIds } }, - select: { email: true }, - }); - const userEmails = users.map((u) => u.email); - return { userId, userIds: accessibleUsersIds, userEmails }; - } else { - const accessibleUsersIds = await retrieveOrgScopedAccessibleUsers({ - adminId: userId, - }); - - const users = await prisma.user.findMany({ - where: { id: { in: accessibleUsersIds } }, - select: { email: true }, - }); - const userEmails = users.map((u) => u.email); - return { userId, userIds: accessibleUsersIds, userEmails }; - } -}; - -export default withMiddleware("pagination")(defaultResponder(handler)); diff --git a/apps/api/v1/pages/api/bookings/_post.ts b/apps/api/v1/pages/api/bookings/_post.ts deleted file mode 100644 index a1ab66c7549b06..00000000000000 --- a/apps/api/v1/pages/api/bookings/_post.ts +++ /dev/null @@ -1,272 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { getRegularBookingService } from "@calcom/features/bookings/di/RegularBookingService.container"; -import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi"; -import { ErrorCode } from "@calcom/lib/errorCodes"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { CreationSource } from "@calcom/prisma/enums"; - -import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers"; - -/** - * @swagger - * /bookings: - * post: - * summary: Creates a new booking - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * operationId: addBooking - * requestBody: - * description: Create a new booking related to one of your event-types - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - eventTypeId - * - start - * - responses - * - timeZone - * - language - * - metadata - * properties: - * eventTypeId: - * type: integer - * description: 'ID of the event type to book' - * start: - * type: string - * format: date-time - * description: 'Start time of the Event' - * end: - * type: string - * format: date-time - * description: 'End time of the Event' - * rescheduleUid: - * type: string - * format: UID - * description: 'Uid of the booking to reschedule' - * responses: - * type: object - * required: - * - name - * - email - * - location - * properties: - * name: - * type: string - * description: 'Attendee full name' - * email: - * type: string - * format: email - * description: 'Attendee email address' - * location: - * type: object - * properties: - * optionValue: - * type: string - * description: 'Option value for the location' - * value: - * type: string - * description: 'The meeting URL, Phone number or Address' - * description: 'Meeting location' - * metadata: - * type: object - * properties: {} - * description: 'Any metadata associated with the booking' - * timeZone: - * type: string - * description: 'TimeZone of the Attendee' - * language: - * type: string - * description: 'Language of the Attendee' - * title: - * type: string - * description: 'Booking event title' - * recurringEventId: - * type: integer - * description: 'Recurring event ID if the event is recurring' - * description: - * type: string - * description: 'Event description' - * status: - * type: string - * description: 'Acceptable values one of ["ACCEPTED", "PENDING", "CANCELLED", "REJECTED"]' - * seatsPerTimeSlot: - * type: integer - * description: 'The number of seats for each time slot' - * seatsShowAttendees: - * type: boolean - * description: 'Share Attendee information in seats' - * seatsShowAvailabilityCount: - * type: boolean - * description: 'Show the number of available seats' - * smsReminderNumber: - * type: number - * description: 'SMS reminder number' - * examples: - * New Booking example: - * value: - * { - * "eventTypeId": 2323232, - * "start": "2023-05-24T13:00:00.000Z", - * "end": "2023-05-24T13:30:00.000Z", - * "responses":{ - * "name": "Hello Hello", - * "email": "hello@gmail.com", - * "metadata": {}, - * "location": "Calcom HQ", - * }, - * "timeZone": "Europe/London", - * "language": "en", - * "title": "Debugging between Syed Ali Shahbaz and Hello Hello", - * "description": null, - * "status": "PENDING", - * "smsReminderNumber": null - * } - * - * tags: - * - bookings - * responses: - * 200: - * description: Booking(s) created successfully. - * content: - * application/json: - * examples: - * booking created successfully example: - * value: - * { - * "booking": { - * "id": 91, - * "userId": 5, - * "description": "", - * "eventTypeId": 7, - * "uid": "bFJeNb2uX8ANpT3JL5EfXw", - * "title": "60min between Pro Example and John Doe", - * "startTime": "2023-05-25T09:30:00.000Z", - * "endTime": "2023-05-25T10:30:00.000Z", - * "attendees": [ - * { - * "email": "john.doe@example.com", - * "name": "John Doe", - * "timeZone": "Asia/Kolkata", - * "locale": "en" - * } - * ], - * "user": { - * "email": "pro@example.com", - * "name": "Pro Example", - * "timeZone": "Asia/Kolkata", - * "locale": "en" - * }, - * "payment": [ - * { - * "id": 1, - * "success": true, - * "paymentOption": "ON_BOOKING" - * } - * ], - * "metadata": {}, - * "status": "ACCEPTED", - * "responses": { - * "email": "john.doe@example.com", - * "name": "John Doe", - * "location": { - * "optionValue": "", - * "value": "inPerson" - * } - * } - * } - * } - * 400: - * description: | - * Bad request - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
MessageCause
Booking body is invalidMissing property on booking entity.
Invalid eventTypeIdThe provided eventTypeId does not exist.
Missing recurringCountThe eventType is recurring, and no recurringCount was passed.
Invalid recurringCountThe provided recurringCount is greater than the eventType recurring config
- * 401: - * description: Authorization information is missing or invalid. - */ -async function handler(req: NextApiRequest) { - const { isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req; - let userId = req.userId; - - req.body = { - ...req.body, - creationSource: CreationSource.API_V1, - }; - if (isSystemWideAdmin) userId = req.body.userId || userId; - - if (req.body.eventTypeId !== undefined && typeof req.body.eventTypeId !== "number") { - throw new HttpError({ - statusCode: 400, - message: "Bad request, eventTypeId must be a number", - }); - } - - if (isOrganizationOwnerOrAdmin) { - const accessibleUsersIds = await getAccessibleUsers({ - adminUserId: userId, - memberUserIds: [req.body.userId || userId], - }); - const [requestedUserId] = accessibleUsersIds; - userId = requestedUserId || userId; - } - - try { - const regularBookingService = getRegularBookingService(); - - return await regularBookingService.createBookingForApiV1({ - bookingData: req.body, - bookingMeta: { - userId, - hostname: req.headers.host || "", - forcedSlug: req.headers["x-cal-force-slug"] as string | undefined, - impersonatedByUserUuid: null, - }, - bookingDataSchemaGetter: getBookingDataSchemaForApi, - }); - } catch (error: unknown) { - const knownError = error as Error; - if (knownError?.message === ErrorCode.NoAvailableUsersFound) { - throw new HttpError({ statusCode: 400, message: knownError.message }); - } - - if (knownError?.message === ErrorCode.RequestBodyInvalid) { - throw new HttpError({ statusCode: 400, message: knownError.message }); - } - - if (knownError?.message === ErrorCode.EventTypeNotFound) { - throw new HttpError({ statusCode: 400, message: knownError.message }); - } - - throw error; - } -} - -export default defaultResponder(handler); diff --git a/apps/api/v1/pages/api/bookings/index.ts b/apps/api/v1/pages/api/bookings/index.ts deleted file mode 100644 index e89b47c30e817a..00000000000000 --- a/apps/api/v1/pages/api/bookings/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/connected-calendars/_get.ts b/apps/api/v1/pages/api/connected-calendars/_get.ts deleted file mode 100644 index fa4a8bd5c9013d..00000000000000 --- a/apps/api/v1/pages/api/connected-calendars/_get.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type { NextApiRequest } from "next"; - -import type { UserWithCalendars } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars"; -import { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars"; -import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery"; -import { schemaConnectedCalendarsReadPublic } from "~/lib/validations/connected-calendar"; - -/** - * @swagger - * /connected-calendars: - * get: - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: query - * name: userId - * required: false - * schema: - * type: number - * description: Admins can fetch connected calendars for other user e.g. &userId=1 or multiple users e.g. &userId=1&userId=2 - * summary: Fetch connected calendars - * tags: - * - connected-calendars - * responses: - * 200: - * description: OK - * content: - * application/json: - * schema: - * type: array - * items: - * type: object - * properties: - * name: - * type: string - * appId: - * type: string - * userId: - * type: number - * integration: - * type: string - * calendars: - * type: array - * items: - * type: object - * properties: - * externalId: - * type: string - * name: - * type: string - * primary: - * type: boolean - * readOnly: - * type: boolean - * examples: - * connectedCalendarExample: - * value: [ - * { - * "name": "Google Calendar", - * "appId": "google-calendar", - * "userId": 10, - * "integration": "google_calendar", - * "calendars": [ - * { - * "externalId": "alice@gmail.com", - * "name": "alice@gmail.com", - * "primary": true, - * "readOnly": false - * }, - * { - * "externalId": "addressbook#contacts@group.v.calendar.google.com", - * "name": "birthdays", - * "primary": false, - * "readOnly": true - * }, - * { - * "externalId": "en.latvian#holiday@group.v.calendar.google.com", - * "name": "Holidays in Narnia", - * "primary": false, - * "readOnly": true - * } - * ] - * } - * ] - * 401: - * description: Authorization information is missing or invalid. - * 403: - * description: Non admin user trying to fetch other user's connected calendars. - */ - -async function getHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - - if (!isSystemWideAdmin && req.query.userId) - throw new HttpError({ statusCode: 403, message: "ADMIN required" }); - - const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId]; - - const usersWithCalendars = await new UserRepository( - prisma - ).findManyByIdsIncludeDestinationAndSelectedCalendars({ - ids: userIds, - }); - - return await getConnectedCalendars(usersWithCalendars); -} - -async function getConnectedCalendars(users: UserWithCalendars[]) { - const connectedDestinationCalendarsPromises = users.map((user) => - getConnectedDestinationCalendarsAndEnsureDefaultsInDb({ user, onboarding: false, prisma }).then( - (connectedCalendarsResult) => - connectedCalendarsResult.connectedCalendars.map((calendar) => ({ - userId: user.id, - ...calendar, - })) - ) - ); - const connectedDestinationCalendars = await Promise.all(connectedDestinationCalendarsPromises); - - const flattenedCalendars = connectedDestinationCalendars.flat(); - - const mapped = flattenedCalendars.map((calendar) => ({ - name: calendar.integration.name, - appId: calendar.integration.slug, - userId: calendar.userId, - integration: calendar.integration.type, - calendars: (calendar.calendars ?? []).map((c) => ({ - externalId: c.externalId, - name: c.name, - primary: c.primary ?? false, - readOnly: c.readOnly, - })), - })); - - return schemaConnectedCalendarsReadPublic.parse(mapped); -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/connected-calendars/index.ts b/apps/api/v1/pages/api/connected-calendars/index.ts deleted file mode 100644 index c97e6cd4dfd429..00000000000000 --- a/apps/api/v1/pages/api/connected-calendars/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - }) -); diff --git a/apps/api/v1/pages/api/credential-sync/_delete.ts b/apps/api/v1/pages/api/credential-sync/_delete.ts deleted file mode 100644 index d407ee4fda5821..00000000000000 --- a/apps/api/v1/pages/api/credential-sync/_delete.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaCredentialDeleteParams } from "~/lib/validations/credential-sync"; - -/** - * @swagger - * /credential-sync: - * delete: - * operationId: deleteUserAppCredential - * summary: Delete a credential record for a user - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: query - * name: userId - * required: true - * schema: - * type: string - * description: ID of the user to fetch the credentials for - * - in: query - * name: credentialId - * required: true - * schema: - * type: string - * description: ID of the credential to update - * tags: - * - credentials - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 505: - * description: Credential syncing not enabled - */ -async function handler(req: NextApiRequest) { - const { userId, credentialId } = schemaCredentialDeleteParams.parse(req.query); - - const credential = await prisma.credential.delete({ - where: { - id: credentialId, - userId, - }, - select: { - id: true, - appId: true, - }, - }); - - return { credentialDeleted: credential }; -} - -export default defaultResponder(handler); diff --git a/apps/api/v1/pages/api/credential-sync/_get.ts b/apps/api/v1/pages/api/credential-sync/_get.ts deleted file mode 100644 index 810d5c7ee23dc9..00000000000000 --- a/apps/api/v1/pages/api/credential-sync/_get.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaCredentialGetParams } from "~/lib/validations/credential-sync"; - -/** - * @swagger - * /credential-sync: - * get: - * operationId: getUserAppCredentials - * summary: Get all app credentials for a user - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: query - * name: userId - * required: true - * schema: - * type: string - * description: ID of the user to fetch the credentials for - * tags: - * - credentials - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 505: - * description: Credential syncing not enabled - */ -async function handler(req: NextApiRequest) { - const { appSlug, userId } = schemaCredentialGetParams.parse(req.query); - - let credentials = await prisma.credential.findMany({ - where: { - userId, - ...(appSlug && { appId: appSlug }), - }, - select: { - id: true, - appId: true, - }, - }); - - // For apps we're transitioning to using the term slug to keep things consistent - credentials = credentials.map((credential) => { - return { - ...credential, - appSlug: credential.appId, - }; - }); - - return { credentials }; -} - -export default defaultResponder(handler); diff --git a/apps/api/v1/pages/api/credential-sync/_patch.ts b/apps/api/v1/pages/api/credential-sync/_patch.ts deleted file mode 100644 index 0bcaa3ad5086e0..00000000000000 --- a/apps/api/v1/pages/api/credential-sync/_patch.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema"; -import { symmetricDecrypt } from "@calcom/lib/crypto"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { prisma } from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaCredentialPatchParams, schemaCredentialPatchBody } from "~/lib/validations/credential-sync"; - -/** - * @swagger - * /credential-sync: - * patch: - * operationId: updateUserAppCredential - * summary: Update a credential record for a user - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: query - * name: userId - * required: true - * schema: - * type: string - * description: ID of the user to fetch the credentials for - * - in: query - * name: credentialId - * required: true - * schema: - * type: string - * description: ID of the credential to update - * tags: - * - credentials - * requestBody: - * description: Update a new credential - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - encryptedKey - * properties: - * encryptedKey: - * type: string - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 505: - * description: Credential syncing not enabled - */ -async function handler(req: NextApiRequest) { - const { userId, credentialId } = schemaCredentialPatchParams.parse(req.query); - - const { encryptedKey } = schemaCredentialPatchBody.parse(req.body); - - const decryptedKey = JSON.parse( - symmetricDecrypt(encryptedKey, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "") - ); - - const key = OAuth2UniversalSchema.parse(decryptedKey); - - const credential = await prisma.credential.update({ - where: { - id: credentialId, - userId, - }, - data: { - key: key as unknown as Prisma.InputJsonValue, - }, - select: { - id: true, - appId: true, - }, - }); - - return { credential }; -} - -export default defaultResponder(handler); diff --git a/apps/api/v1/pages/api/credential-sync/_post.ts b/apps/api/v1/pages/api/credential-sync/_post.ts deleted file mode 100644 index 2e755dbe56a5b2..00000000000000 --- a/apps/api/v1/pages/api/credential-sync/_post.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; -import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema"; -import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; -import { symmetricDecrypt } from "@calcom/lib/crypto"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { prisma } from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; -import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; - -import { schemaCredentialPostBody, schemaCredentialPostParams } from "~/lib/validations/credential-sync"; - -/** - * @swagger - * /credential-sync: - * post: - * operationId: createUserAppCredential - * summary: Create a credential record for a user - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: query - * name: userId - * required: true - * schema: - * type: string - * description: ID of the user to fetch the credentials for - * tags: - * - credentials - * requestBody: - * description: Create a new credential - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - encryptedKey - * - appSlug - * properties: - * encryptedKey: - * type: string - * appSlug: - * type: string - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 505: - * description: Credential syncing not enabled - */ -async function handler(req: NextApiRequest) { - if (!req.body) { - throw new HttpError({ message: "Request body is missing", statusCode: 400 }); - } - - const { userId, createSelectedCalendar, createDestinationCalendar } = schemaCredentialPostParams.parse( - req.query - ); - - const { appSlug, encryptedKey } = schemaCredentialPostBody.parse(req.body); - - const decryptedKey = JSON.parse( - symmetricDecrypt(encryptedKey, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "") - ); - - const key = OAuth2UniversalSchema.parse(decryptedKey); - - // Need to get app type - const app = await prisma.app.findUnique({ - where: { slug: appSlug }, - select: { dirName: true, categories: true }, - }); - - if (!app) { - throw new HttpError({ message: "App not found", statusCode: 500 }); - } - - const createCalendarResources = - app.categories.some((category) => category === "calendar") && - (createSelectedCalendar || createDestinationCalendar); - - const appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata]; - - const createdcredential = await prisma.credential.create({ - data: { - userId, - appId: appSlug, - key: key as unknown as Prisma.InputJsonValue, - type: appMetadata.type, - }, - select: credentialForCalendarServiceSelect, - }); - // createdcredential.user.email; - // TODO: ^ Investigate why this select doesn't work. - const credential = await prisma.credential.findUniqueOrThrow({ - where: { - id: createdcredential.id, - }, - select: credentialForCalendarServiceSelect, - }); - // ^ Workaround for the select in `create` not working - - if (createCalendarResources) { - const calendar = await getCalendar({ ...credential, delegatedTo: null }, "slots"); - if (!calendar) throw new HttpError({ message: "Calendar missing for credential", statusCode: 500 }); - const calendars = await calendar.listCalendars(); - const calendarToCreate = calendars.find((calendar) => calendar.primary) || calendars[0]; - - if (createSelectedCalendar) { - await prisma.selectedCalendar.createMany({ - data: [ - { - userId, - integration: appMetadata.type, - externalId: calendarToCreate.externalId, - credentialId: credential.id, - }, - ], - skipDuplicates: true, - }); - } - if (createDestinationCalendar) { - await prisma.destinationCalendar.create({ - data: { - integration: appMetadata.type, - externalId: calendarToCreate.externalId, - credential: { connect: { id: credential.id } }, - primaryEmail: calendarToCreate.email || credential.user?.email, - user: { connect: { id: userId } }, - }, - }); - } - } - - return { credential: { id: credential.id, type: credential.type } }; -} - -export default defaultResponder(handler); diff --git a/apps/api/v1/pages/api/credential-sync/index.ts b/apps/api/v1/pages/api/credential-sync/index.ts deleted file mode 100644 index d3b1bae7bce892..00000000000000 --- a/apps/api/v1/pages/api/credential-sync/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware("verifyCredentialSyncEnabled")( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - }) -); diff --git a/apps/api/v1/pages/api/custom-inputs/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/custom-inputs/[id]/_auth-middleware.ts deleted file mode 100644 index 05243a39226175..00000000000000 --- a/apps/api/v1/pages/api/custom-inputs/[id]/_auth-middleware.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -async function authMiddleware(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { id } = schemaQueryIdParseInt.parse(req.query); - // Admins can just skip this check - if (isSystemWideAdmin) return; - // Check if the current user can access the event type of this input - const eventTypeCustomInput = await prisma.eventTypeCustomInput.findFirst({ - where: { id, eventType: { userId } }, - }); - if (!eventTypeCustomInput) throw new HttpError({ statusCode: 403, message: "Forbidden" }); -} - -export default authMiddleware; diff --git a/apps/api/v1/pages/api/custom-inputs/[id]/_delete.ts b/apps/api/v1/pages/api/custom-inputs/[id]/_delete.ts deleted file mode 100644 index 81ea4db7b67d93..00000000000000 --- a/apps/api/v1/pages/api/custom-inputs/[id]/_delete.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /custom-inputs/{id}: - * delete: - * summary: Remove an existing eventTypeCustomInput - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the eventTypeCustomInput to delete - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - custom-inputs - * responses: - * 201: - * description: OK, eventTypeCustomInput removed successfully - * 400: - * description: Bad request. EventType id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function deleteHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - await prisma.eventTypeCustomInput.delete({ where: { id } }); - return { message: `CustomInputEventType with id: ${id} deleted successfully` }; -} - -export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/custom-inputs/[id]/_get.ts b/apps/api/v1/pages/api/custom-inputs/[id]/_get.ts deleted file mode 100644 index 481bae5dcafc19..00000000000000 --- a/apps/api/v1/pages/api/custom-inputs/[id]/_get.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaEventTypeCustomInputPublic } from "~/lib/validations/event-type-custom-input"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /custom-inputs/{id}: - * get: - * summary: Find a eventTypeCustomInput - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the eventTypeCustomInput to get - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - custom-inputs - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: EventType was not found - */ -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const data = await prisma.eventTypeCustomInput.findUniqueOrThrow({ where: { id } }); - return { event_type_custom_input: schemaEventTypeCustomInputPublic.parse(data) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/custom-inputs/[id]/_patch.ts b/apps/api/v1/pages/api/custom-inputs/[id]/_patch.ts deleted file mode 100644 index b14ee8a4c0c2a8..00000000000000 --- a/apps/api/v1/pages/api/custom-inputs/[id]/_patch.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { - schemaEventTypeCustomInputEditBodyParams, - schemaEventTypeCustomInputPublic, -} from "~/lib/validations/event-type-custom-input"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /custom-inputs/{id}: - * patch: - * summary: Edit an existing eventTypeCustomInput - * requestBody: - * description: Edit an existing eventTypeCustomInput for an event type - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * eventTypeId: - * type: integer - * description: 'ID of the event type to which the custom input is being added' - * label: - * type: string - * description: 'Label of the custom input' - * type: - * type: string - * description: 'Type of the custom input. The value is ENUM; one of [TEXT, TEXTLONG, NUMBER, BOOL, RADIO, PHONE]' - * options: - * type: object - * properties: - * label: - * type: string - * type: - * type: string - * description: 'Options for the custom input' - * required: - * type: boolean - * description: 'If the custom input is required before booking' - * placeholder: - * type: string - * description: 'Placeholder text for the custom input' - * - * examples: - * custom-inputs: - * summary: Example of patching an existing Custom Input - * value: - * required: true - * - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the eventTypeCustomInput to edit - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - * tags: - * - custom-inputs - * responses: - * 201: - * description: OK, eventTypeCustomInput edited successfully - * 400: - * description: Bad request. EventType body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function patchHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const data = schemaEventTypeCustomInputEditBodyParams.parse(req.body); - const result = await prisma.eventTypeCustomInput.update({ - where: { id }, - data: { - ...data, - options: data.options === null ? [] : data.options, - }, - }); - return { event_type_custom_input: schemaEventTypeCustomInputPublic.parse(result) }; -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/custom-inputs/[id]/index.ts b/apps/api/v1/pages/api/custom-inputs/[id]/index.ts deleted file mode 100644 index 54980cf49469ef..00000000000000 --- a/apps/api/v1/pages/api/custom-inputs/[id]/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "./_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/custom-inputs/_get.ts b/apps/api/v1/pages/api/custom-inputs/_get.ts deleted file mode 100644 index 0867c2f315a630..00000000000000 --- a/apps/api/v1/pages/api/custom-inputs/_get.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaEventTypeCustomInputPublic } from "~/lib/validations/event-type-custom-input"; - -/** - * @swagger - * /custom-inputs: - * get: - * summary: Find all eventTypeCustomInputs - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - custom-inputs - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No eventTypeCustomInputs were found - */ -async function getHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const args: Prisma.EventTypeCustomInputFindManyArgs = isSystemWideAdmin - ? {} - : { where: { eventType: { userId } } }; - const data = await prisma.eventTypeCustomInput.findMany(args); - return { event_type_custom_inputs: data.map((v) => schemaEventTypeCustomInputPublic.parse(v)) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/custom-inputs/_post.ts b/apps/api/v1/pages/api/custom-inputs/_post.ts deleted file mode 100644 index 203d6f8a5151c4..00000000000000 --- a/apps/api/v1/pages/api/custom-inputs/_post.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { - schemaEventTypeCustomInputBodyParams, - schemaEventTypeCustomInputPublic, -} from "~/lib/validations/event-type-custom-input"; - -/** - * @swagger - * /custom-inputs: - * post: - * summary: Creates a new eventTypeCustomInput - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * requestBody: - * description: Create a new custom input for an event type - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - eventTypeId - * - label - * - type - * - required - * - placeholder - * properties: - * eventTypeId: - * type: integer - * description: 'ID of the event type to which the custom input is being added' - * label: - * type: string - * description: 'Label of the custom input' - * type: - * type: string - * description: 'Type of the custom input. The value is ENUM; one of [TEXT, TEXTLONG, NUMBER, BOOL, RADIO, PHONE]' - * options: - * type: object - * properties: - * label: - * type: string - * type: - * type: string - * description: 'Options for the custom input' - * required: - * type: boolean - * description: 'If the custom input is required before booking' - * placeholder: - * type: string - * description: 'Placeholder text for the custom input' - * - * examples: - * custom-inputs: - * summary: An example of custom-inputs - * value: - * eventTypeID: 1 - * label: "Phone Number" - * type: "PHONE" - * required: true - * placeholder: "100 101 1234" - * - * tags: - * - custom-inputs - * responses: - * 201: - * description: OK, eventTypeCustomInput created - * 400: - * description: Bad request. EventTypeCustomInput body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function postHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { eventTypeId, ...body } = schemaEventTypeCustomInputBodyParams.parse(req.body); - - if (!isSystemWideAdmin) { - /* We check that the user has access to the event type he's trying to add a custom input to. */ - const eventType = await prisma.eventType.findFirst({ - where: { id: eventTypeId, userId }, - }); - if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" }); - } - - const data = await prisma.eventTypeCustomInput.create({ - data: { - ...body, - options: body.options === null ? [] : body.options, - eventType: { connect: { id: eventTypeId } }, - }, - }); - - return { - event_type_custom_input: schemaEventTypeCustomInputPublic.parse(data), - message: "EventTypeCustomInput created successfully", - }; -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/custom-inputs/index.ts b/apps/api/v1/pages/api/custom-inputs/index.ts deleted file mode 100644 index e89b47c30e817a..00000000000000 --- a/apps/api/v1/pages/api/custom-inputs/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/destination-calendars/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/destination-calendars/[id]/_auth-middleware.ts deleted file mode 100644 index 7878b05b91a0d4..00000000000000 --- a/apps/api/v1/pages/api/destination-calendars/[id]/_auth-middleware.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -async function authMiddleware(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { id } = schemaQueryIdParseInt.parse(req.query); - if (isSystemWideAdmin) return; - const userEventTypes = await prisma.eventType.findMany({ - where: { userId }, - select: { id: true }, - }); - - const userEventTypeIds = userEventTypes.map((eventType) => eventType.id); - - const destinationCalendar = await prisma.destinationCalendar.findFirst({ - where: { - AND: [ - { id }, - { - OR: [{ userId }, { eventTypeId: { in: userEventTypeIds } }], - }, - ], - }, - }); - if (!destinationCalendar) - throw new HttpError({ statusCode: 404, message: "Destination calendar not found" }); -} - -export default authMiddleware; diff --git a/apps/api/v1/pages/api/destination-calendars/[id]/_delete.ts b/apps/api/v1/pages/api/destination-calendars/[id]/_delete.ts deleted file mode 100644 index a08562513aaf06..00000000000000 --- a/apps/api/v1/pages/api/destination-calendars/[id]/_delete.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /destination-calendars/{id}: - * delete: - * summary: Remove an existing destination calendar - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the destination calendar to delete - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - destination-calendars - * responses: - * 200: - * description: OK, destinationCalendar removed successfully - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Destination calendar not found - */ -export async function deleteHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - await prisma.destinationCalendar.delete({ where: { id } }); - return { message: `OK, Destination Calendar removed successfully` }; -} - -export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/destination-calendars/[id]/_get.ts b/apps/api/v1/pages/api/destination-calendars/[id]/_get.ts deleted file mode 100644 index 32b43ca8d2231c..00000000000000 --- a/apps/api/v1/pages/api/destination-calendars/[id]/_get.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaDestinationCalendarReadPublic } from "~/lib/validations/destination-calendar"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /destination-calendars/{id}: - * get: - * summary: Find a destination calendar - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the destination calendar to get - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - destination-calendars - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Destination calendar not found - */ -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - - const destinationCalendar = await prisma.destinationCalendar.findUnique({ - where: { id }, - }); - - return { destinationCalendar: schemaDestinationCalendarReadPublic.parse({ ...destinationCalendar }) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/destination-calendars/[id]/_patch.ts b/apps/api/v1/pages/api/destination-calendars/[id]/_patch.ts deleted file mode 100644 index 23533864aa5fe2..00000000000000 --- a/apps/api/v1/pages/api/destination-calendars/[id]/_patch.ts +++ /dev/null @@ -1,322 +0,0 @@ -import type { NextApiRequest } from "next"; -import type { z } from "zod"; - -import { - getCalendarCredentialsWithoutDelegation, - getConnectedCalendars, -} from "@calcom/features/calendars/lib/CalendarManager"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { PrismaClient } from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; -import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; - -import { - schemaDestinationCalendarEditBodyParams, - schemaDestinationCalendarReadPublic, -} from "~/lib/validations/destination-calendar"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /destination-calendars/{id}: - * patch: - * summary: Edit an existing destination calendar - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the destination calendar to edit - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * requestBody: - * description: Create a new booking related to one of your event-types - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * integration: - * type: string - * description: 'The integration' - * externalId: - * type: string - * description: 'The external ID of the integration' - * eventTypeId: - * type: integer - * description: 'The ID of the eventType it is associated with' - * bookingId: - * type: integer - * description: 'The booking ID it is associated with' - * tags: - * - destination-calendars - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Destination calendar not found - */ -type DestinationCalendarType = { - userId?: number | null; - eventTypeId?: number | null; - credentialId: number | null; -}; - -type UserCredentialType = { - id: number; - appId: string | null; - type: string; - userId: number | null; - user: { - email: string; - } | null; - teamId: number | null; - key: Prisma.JsonValue; - invalid: boolean | null; - encryptedKey: string | null; -}; - -export async function patchHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin, query, body } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body); - const assignedUserId = isSystemWideAdmin ? parsedBody.userId || userId : userId; - - validateIntegrationInput(parsedBody); - const destinationCalendarObject: DestinationCalendarType = await getDestinationCalendar(id, prisma); - await validateRequestAndOwnership({ destinationCalendarObject, parsedBody, assignedUserId, prisma }); - - const userCredentials = await getUserCredentials({ - credentialId: destinationCalendarObject.credentialId, - userId: assignedUserId, - prisma, - }); - const credentialId = await verifyCredentialsAndGetId({ - parsedBody, - userCredentials, - currentCredentialId: destinationCalendarObject.credentialId, - }); - // If the user has passed eventTypeId, we need to remove userId from the update data to make sure we don't link it to user as well - if (parsedBody.eventTypeId) parsedBody.userId = undefined; - const destinationCalendar = await prisma.destinationCalendar.update({ - where: { id }, - data: { ...parsedBody, credentialId }, - }); - return { destinationCalendar: schemaDestinationCalendarReadPublic.parse(destinationCalendar) }; -} - -/** - * Retrieves user credentials associated with a given credential ID and user ID and validates if the credentials belong to this user - * - * @param credentialId - The ID of the credential to fetch. If not provided, an error is thrown. - * @param userId - The user ID against which the credentials need to be verified. - * @param prisma - An instance of PrismaClient for database operations. - * - * @returns - An array containing the matching user credentials. - * - * @throws HttpError - If `credentialId` is not provided or no associated credentials are found in the database. - */ -async function getUserCredentials({ - credentialId, - userId, - prisma, -}: { - credentialId: number | null; - userId: number; - prisma: PrismaClient; -}) { - if (!credentialId) { - throw new HttpError({ - statusCode: 404, - message: `Destination calendar missing credential id`, - }); - } - const userCredentials = await prisma.credential.findMany({ - where: { id: credentialId, userId }, - select: credentialForCalendarServiceSelect, - }); - - if (!userCredentials || userCredentials.length === 0) { - throw new HttpError({ - statusCode: 400, - message: `Bad request, no associated credentials found`, - }); - } - return userCredentials; -} - -/** - * Verifies the provided credentials and retrieves the associated credential ID. - * - * This function checks if the `integration` and `externalId` properties from the parsed body are present. - * If both properties exist, it fetches the connected calendar credentials using the provided user credentials - * and checks for a matching external ID and integration from the list of connected calendars. - * - * If a match is found, it updates the `credentialId` with the one from the connected calendar. - * Otherwise, it throws an HTTP error with a 400 status indicating an invalid credential ID. - * - * If the parsed body does not contain the necessary properties, the function - * returns the `credentialId` from the destination calendar object. - * - * @param parsedBody - The parsed body from the incoming request, validated against a predefined schema. - * Checked if it contain properties like `integration` and `externalId`. - * @param userCredentials - An array of user credentials used to fetch the connected calendar credentials. - * @param destinationCalendarObject - An object representing the destination calendar. Primarily used - * to fetch the default `credentialId`. - * - * @returns - The verified `credentialId` either from the matched connected calendar in case of updating the destination calendar, - * or the provided destination calendar object in other cases. - * - * @throws HttpError - If no matching connected calendar is found for the given `integration` and `externalId`. - */ -async function verifyCredentialsAndGetId({ - parsedBody, - userCredentials, - currentCredentialId, -}: { - parsedBody: z.infer; - userCredentials: UserCredentialType[]; - currentCredentialId: number | null; -}) { - if (parsedBody.integration && parsedBody.externalId) { - const calendarCredentials = getCalendarCredentialsWithoutDelegation( - userCredentials.map((cred) => ({ - ...cred, - delegationCredentialId: null, - })) - ); - - const { connectedCalendars } = await getConnectedCalendars( - calendarCredentials, - [], - parsedBody.externalId - ); - const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly); - const calendar = eligibleCalendars?.find( - (c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration - ); - - if (!calendar?.credentialId) - throw new HttpError({ - statusCode: 400, - message: "Bad request, credential id invalid", - }); - return calendar?.credentialId; - } - return currentCredentialId; -} - -/** - * Validates the request for updating a destination calendar. - * - * This function checks the validity of the provided eventTypeId against the existing destination calendar object - * in the sense that if the destination calendar is not linked to an event type, the eventTypeId can not be provided. - * - * It also ensures that the eventTypeId, if provided, belongs to the assigned user. - * - * @param destinationCalendarObject - An object representing the destination calendar. - * @param parsedBody - The parsed body from the incoming request, validated against a predefined schema. - * @param assignedUserId - The user ID assigned for the operation, which might be an admin or a regular user. - * @param prisma - An instance of PrismaClient for database operations. - * - * @throws HttpError - If the validation fails or inconsistencies are detected in the request data. - */ -async function validateRequestAndOwnership({ - destinationCalendarObject, - parsedBody, - assignedUserId, - prisma, -}: { - destinationCalendarObject: DestinationCalendarType; - parsedBody: z.infer; - assignedUserId: number; - prisma: PrismaClient; -}) { - if (parsedBody.eventTypeId) { - if (!destinationCalendarObject.eventTypeId) { - throw new HttpError({ - statusCode: 400, - message: `The provided destination calendar can not be linked to an event type`, - }); - } - - const userEventType = await prisma.eventType.findFirst({ - where: { id: parsedBody.eventTypeId }, - select: { userId: true }, - }); - - if (!userEventType || userEventType.userId !== assignedUserId) { - throw new HttpError({ - statusCode: 404, - message: `Event type with ID ${parsedBody.eventTypeId} not found`, - }); - } - } - - if (!parsedBody.eventTypeId) { - if (destinationCalendarObject.eventTypeId) { - throw new HttpError({ - statusCode: 400, - message: `The provided destination calendar can only be linked to an event type`, - }); - } - if (destinationCalendarObject.userId !== assignedUserId) { - throw new HttpError({ - statusCode: 403, - message: `Forbidden`, - }); - } - } -} - -/** - * Fetches the destination calendar based on the provided ID as the path parameter, specifically `credentialId` and `eventTypeId`. - * - * If no matching destination calendar is found for the provided ID, an HTTP error with a 404 status - * indicating that the desired destination calendar was not found is thrown. - * - * @param id - The ID of the destination calendar to be retrieved. - * @param prisma - An instance of PrismaClient for database operations. - * - * @returns - An object containing details of the matching destination calendar, specifically `credentialId` and `eventTypeId`. - * - * @throws HttpError - If no destination calendar matches the provided ID. - */ -async function getDestinationCalendar(id: number, prisma: PrismaClient) { - const destinationCalendarObject = await prisma.destinationCalendar.findFirst({ - where: { - id, - }, - select: { userId: true, eventTypeId: true, credentialId: true }, - }); - - if (!destinationCalendarObject) { - throw new HttpError({ - statusCode: 404, - message: `Destination calendar with ID ${id} not found`, - }); - } - - return destinationCalendarObject; -} - -function validateIntegrationInput(parsedBody: z.infer) { - if (parsedBody.integration && !parsedBody.externalId) { - throw new HttpError({ statusCode: 400, message: "External Id is required with integration value" }); - } - if (!parsedBody.integration && parsedBody.externalId) { - throw new HttpError({ statusCode: 400, message: "Integration value is required with external ID" }); - } -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/destination-calendars/[id]/index.ts b/apps/api/v1/pages/api/destination-calendars/[id]/index.ts deleted file mode 100644 index 54980cf49469ef..00000000000000 --- a/apps/api/v1/pages/api/destination-calendars/[id]/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "./_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/destination-calendars/_get.ts b/apps/api/v1/pages/api/destination-calendars/_get.ts deleted file mode 100644 index 5a830f6b6f9894..00000000000000 --- a/apps/api/v1/pages/api/destination-calendars/_get.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery"; -import { schemaDestinationCalendarReadPublic } from "~/lib/validations/destination-calendar"; - -/** - * @swagger - * /destination-calendars: - * get: - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * summary: Find all destination calendars - * tags: - * - destination-calendars - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No destination calendars were found - */ -async function getHandler(req: NextApiRequest) { - const { userId } = req; - const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId]; - - const userEventTypes = await prisma.eventType.findMany({ - where: { userId: { in: userIds } }, - select: { id: true }, - }); - - const userEventTypeIds = userEventTypes.map((eventType) => eventType.id); - - const allDestinationCalendars = await prisma.destinationCalendar.findMany({ - where: { - OR: [{ userId: { in: userIds } }, { eventTypeId: { in: userEventTypeIds } }], - }, - }); - - if (allDestinationCalendars.length === 0) - new HttpError({ statusCode: 404, message: "No destination calendars were found" }); - - return { - destinationCalendars: allDestinationCalendars.map((destinationCalendar) => - schemaDestinationCalendarReadPublic.parse(destinationCalendar) - ), - }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/destination-calendars/_post.ts b/apps/api/v1/pages/api/destination-calendars/_post.ts deleted file mode 100644 index 74a9a1f71b6a67..00000000000000 --- a/apps/api/v1/pages/api/destination-calendars/_post.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { - getCalendarCredentialsWithoutDelegation, - getConnectedCalendars, -} from "@calcom/features/calendars/lib/CalendarManager"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; - -import { - schemaDestinationCalendarReadPublic, - schemaDestinationCalendarCreateBodyParams, -} from "~/lib/validations/destination-calendar"; - -/** - * @swagger - * /destination-calendars: - * post: - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * summary: Creates a new destination calendar - * requestBody: - * description: Create a new destination calendar for your events - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - integration - * - externalId - * - credentialId - * properties: - * integration: - * type: string - * description: 'The integration' - * externalId: - * type: string - * description: 'The external ID of the integration' - * eventTypeId: - * type: integer - * description: 'The ID of the eventType it is associated with' - * bookingId: - * type: integer - * description: 'The booking ID it is associated with' - * userId: - * type: integer - * description: 'The user it is associated with' - * tags: - * - destination-calendars - * responses: - * 201: - * description: OK, destination calendar created - * 400: - * description: Bad request. DestinationCalendar body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function postHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin, body } = req; - const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body); - await checkPermissions(req, userId); - - const assignedUserId = isSystemWideAdmin && parsedBody.userId ? parsedBody.userId : userId; - - /* Check if credentialId data matches the ownership and integration passed in */ - const userCredentials = await prisma.credential.findMany({ - where: { - type: parsedBody.integration, - userId: assignedUserId, - }, - select: credentialForCalendarServiceSelect, - }); - - if (userCredentials.length === 0) - throw new HttpError({ - statusCode: 400, - message: "Bad request, credential id invalid", - }); - - const calendarCredentials = getCalendarCredentialsWithoutDelegation(userCredentials); - - const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, [], parsedBody.externalId); - - const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly); - const calendar = eligibleCalendars?.find( - (c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration - ); - if (!calendar?.credentialId) - throw new HttpError({ - statusCode: 400, - message: "Bad request, credential id invalid", - }); - const credentialId = calendar.credentialId; - - if (parsedBody.eventTypeId) { - const eventType = await prisma.eventType.findFirst({ - where: { id: parsedBody.eventTypeId, userId: parsedBody.userId }, - }); - if (!eventType) - throw new HttpError({ - statusCode: 400, - message: "Bad request, eventTypeId invalid", - }); - parsedBody.userId = undefined; - } - - const destination_calendar = await prisma.destinationCalendar.create({ - data: { ...parsedBody, credentialId }, - }); - - return { - destinationCalendar: schemaDestinationCalendarReadPublic.parse(destination_calendar), - message: "Destination calendar created successfully", - }; -} - -async function checkPermissions(req: NextApiRequest, userId: number) { - const { isSystemWideAdmin } = req; - const body = schemaDestinationCalendarCreateBodyParams.parse(req.body); - - /* Non-admin users can only create destination calendars for themselves */ - if (!isSystemWideAdmin && body.userId) - throw new HttpError({ - statusCode: 401, - message: "ADMIN required for `userId`", - }); - /* Admin users are required to pass in a userId */ - if (isSystemWideAdmin && !body.userId) - throw new HttpError({ statusCode: 400, message: "`userId` required" }); - /* User should only be able to create for their own destination calendars*/ - if (!isSystemWideAdmin && body.eventTypeId) { - const ownsEventType = await prisma.eventType.findFirst({ where: { id: body.eventTypeId, userId } }); - if (!ownsEventType) throw new HttpError({ statusCode: 401, message: "Unauthorized" }); - } - // TODO:: Add support for team event types with validation -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/destination-calendars/index.ts b/apps/api/v1/pages/api/destination-calendars/index.ts deleted file mode 100644 index e89b47c30e817a..00000000000000 --- a/apps/api/v1/pages/api/destination-calendars/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/docs.ts b/apps/api/v1/pages/api/docs.ts deleted file mode 100644 index e0f33493d6cc1b..00000000000000 --- a/apps/api/v1/pages/api/docs.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { withSwagger } from "next-swagger-doc"; - -import pjson from "~/package.json"; - -const swaggerHandler = withSwagger({ - definition: { - openapi: "3.0.3", - servers: [ - { url: "http://localhost:3002/v1" }, - { url: "https://api.cal.dev/v1" }, - { url: "https://api.cal.com/v1" }, - ], - externalDocs: { - url: "https://docs.cal.com/docs", - description: "Find more info at our main docs: https://docs.cal.com/docs/", - }, - info: { - title: `${pjson.name}: ${pjson.description}`, - version: pjson.version, - }, - components: { - securitySchemes: { ApiKeyAuth: { type: "apiKey", in: "query", name: "apiKey" } }, - schemas: { - ArrayOfBookings: { - type: "array", - items: { - $ref: "#/components/schemas/Booking", - }, - }, - ArrayOfRecordings: { - type: "array", - items: { - $ref: "#/components/schemas/Recording", - }, - }, - Recording: { - properties: { - id: { - type: "string", - }, - room_name: { - type: "string", - }, - start_ts: { - type: "number", - }, - status: { - type: "string", - }, - max_participants: { - type: "number", - }, - duration: { - type: "number", - }, - download_link: { - type: "string", - }, - }, - }, - Booking: { - properties: { - id: { - type: "number", - }, - description: { - type: "string", - }, - eventTypeId: { - type: "number", - }, - uid: { - type: "string", - format: "uuid", - }, - title: { - type: "string", - }, - startTime: { - type: "string", - format: "date-time", - }, - endTime: { - type: "string", - format: "date-time", - }, - timeZone: { - type: "string", - example: "Europe/London", - }, - fromReschedule: { - type: "string", - nullable: true, - format: "uuid", - }, - attendees: { - type: "array", - items: { - properties: { - email: { - type: "string", - example: "example@cal.com", - }, - name: { - type: "string", - }, - timeZone: { - type: "string", - example: "Europe/London", - }, - locale: { - type: "string", - example: "en", - }, - }, - }, - }, - user: { - properties: { - email: { - type: "string", - example: "example@cal.com", - }, - name: { - type: "string", - }, - timeZone: { - type: "string", - example: "Europe/London", - }, - locale: { - type: "string", - example: "en", - }, - }, - }, - payment: { - type: Array, - items: { - properties: { - id: { - type: "number", - example: 1, - }, - success: { - type: "boolean", - example: true, - }, - paymentOption: { - type: "string", - example: "ON_BOOKING", - }, - }, - }, - }, - }, - }, - }, - }, - security: [{ ApiKeyAuth: [] }], - tags: [ - { name: "users" }, - { name: "event-types" }, - { name: "bookings" }, - { name: "attendees" }, - { name: "payments" }, - { name: "schedules" }, - { name: "teams" }, - { name: "memberships" }, - { - name: "availabilities", - description: "Allows modifying unique availabilities tied to a schedule.", - }, - { name: "custom-inputs" }, - { name: "event-references" }, - { name: "booking-references" }, - { name: "destination-calendars" }, - { name: "selected-calendars" }, - ], - }, - apiFolder: "pages/api", -}); - -export default swaggerHandler(); diff --git a/apps/api/v1/pages/api/event-types/[id]/_delete.ts b/apps/api/v1/pages/api/event-types/[id]/_delete.ts deleted file mode 100644 index f6af2d58fab362..00000000000000 --- a/apps/api/v1/pages/api/event-types/[id]/_delete.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { prisma } from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -import checkParentEventOwnership from "../_utils/checkParentEventOwnership"; -import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission"; - -/** - * @swagger - * /event-types/{id}: - * delete: - * operationId: removeEventTypeById - * summary: Remove an existing eventType - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the eventType to delete - * tags: - * - event-types - * externalDocs: - * url: https://docs.cal.com/docs/core-features/event-types - * responses: - * 201: - * description: OK, eventType removed successfully - * 400: - * description: Bad request. EventType id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function deleteHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - await checkPermissions(req); - await prisma.eventType.delete({ where: { id } }); - return { message: `Event Type with id: ${id} deleted successfully` }; -} - -async function checkPermissions(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { id } = schemaQueryIdParseInt.parse(req.query); - if (isSystemWideAdmin) return; - - const eventType = await prisma.eventType.findFirst({ where: { id } }); - - if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" }); - - /** Only event type owners or team owners for team events can delete it */ - if (eventType.teamId) return await checkTeamEventEditPermission(req, { teamId: eventType.teamId }); - - if (eventType.parentId) return await checkParentEventOwnership(req); - - if (eventType.userId && eventType.userId !== userId) - throw new HttpError({ statusCode: 403, message: "Forbidden" }); -} - -export default deleteHandler; diff --git a/apps/api/v1/pages/api/event-types/[id]/_get.ts b/apps/api/v1/pages/api/event-types/[id]/_get.ts deleted file mode 100644 index e80ac95e491299..00000000000000 --- a/apps/api/v1/pages/api/event-types/[id]/_get.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { prisma } from "@calcom/prisma"; -import { MembershipRole } from "@calcom/prisma/enums"; - -import { eventTypeSelect } from "~/lib/selects/event-type"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; -import { checkPermissions as canAccessTeamEventOrThrow } from "~/pages/api/teams/[teamId]/_auth-middleware"; - -import getCalLink from "../_utils/getCalLink"; - -/** - * @swagger - * /event-types/{id}: - * get: - * operationId: getEventTypeById - * summary: Find a eventType - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * - in: path - * name: id - * example: 4 - * schema: - * type: integer - * required: true - * description: ID of the eventType to get - * tags: - * - event-types - * externalDocs: - * url: https://docs.cal.com/docs/core-features/event-types - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: EventType was not found - */ -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - - const eventType = await prisma.eventType.findUnique({ - where: { id }, - select: eventTypeSelect, - }); - - if (!eventType) { - throw new HttpError({ statusCode: 404, message: "Event type not found" }); - } - - await checkPermissions(req, eventType); - - const link = eventType ? getCalLink(eventType) : null; - // user.defaultScheduleId doesn't work the same for team events. - if (!eventType?.scheduleId && eventType?.userId && !eventType?.teamId) { - const user = await prisma.user.findUniqueOrThrow({ - where: { - id: eventType.userId, - }, - select: { - defaultScheduleId: true, - }, - }); - eventType.scheduleId = user.defaultScheduleId; - } - - return { - event_type: { - ...eventType, - link, - }, - }; -} - -type BaseEventTypeCheckPermissions = { - userId: number | null; - teamId: number | null; -}; - -async function checkPermissions( - req: NextApiRequest, - eventType: (T & Partial>) | null -) { - if (req.isSystemWideAdmin) return true; - if (eventType?.teamId) { - req.query.teamId = String(eventType.teamId); - await canAccessTeamEventOrThrow(req, { - in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER], - }); - return true; - } - if (eventType?.userId === req.userId) return true; // is owner. - throw new HttpError({ statusCode: 403, message: "Forbidden" }); -} - -export default getHandler; diff --git a/apps/api/v1/pages/api/event-types/[id]/_patch.ts b/apps/api/v1/pages/api/event-types/[id]/_patch.ts deleted file mode 100644 index 5755855df9d1a2..00000000000000 --- a/apps/api/v1/pages/api/event-types/[id]/_patch.ts +++ /dev/null @@ -1,252 +0,0 @@ -import type { NextApiRequest } from "next"; -import type { z } from "zod"; - -import { HttpError } from "@calcom/lib/http-error"; -import { prisma } from "@calcom/prisma"; -import { Prisma } from "@calcom/prisma/client"; -import { SchedulingType } from "@calcom/prisma/enums"; - -import { eventTypeSelect } from "~/lib/selects/event-type"; -import type { schemaEventTypeBaseBodyParams } from "~/lib/validations/event-type"; -import { schemaEventTypeEditBodyParams } from "~/lib/validations/event-type"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; -import ensureOnlyMembersAsHosts from "~/pages/api/event-types/_utils/ensureOnlyMembersAsHosts"; - -import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission"; - -/** - * @swagger - * /event-types/{id}: - * patch: - * operationId: editEventTypeById - * summary: Edit an existing eventType - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the eventType to edit - * requestBody: - * description: Create a new event-type related to your user or team - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * length: - * type: integer - * description: Duration of the event type in minutes - * metadata: - * type: object - * description: Metadata relating to event type. Pass {} if empty - * title: - * type: string - * description: Title of the event type - * slug: - * type: string - * description: Unique slug for the event type - * scheduleId: - * type: number - * description: The ID of the schedule for this event type - * hosts: - * type: array - * items: - * type: object - * properties: - * userId: - * type: number - * isFixed: - * type: boolean - * description: Host MUST be available for any slot to be bookable. - * hidden: - * type: boolean - * description: If the event type should be hidden from your public booking page - * position: - * type: integer - * description: The position of the event type on the public booking page - * teamId: - * type: integer - * description: Team ID if the event type should belong to a team - * periodType: - * type: string - * enum: [UNLIMITED, ROLLING, RANGE] - * description: To decide how far into the future an invitee can book an event with you - * periodStartDate: - * type: string - * format: date-time - * description: Start date of bookable period (Required if periodType is 'range') - * periodEndDate: - * type: string - * format: date-time - * description: End date of bookable period (Required if periodType is 'range') - * periodDays: - * type: integer - * description: Number of bookable days (Required if periodType is rolling) - * periodCountCalendarDays: - * type: boolean - * description: If calendar days should be counted for period days - * requiresConfirmation: - * type: boolean - * description: If the event type should require your confirmation before completing the booking - * recurringEvent: - * type: object - * description: If the event should recur every week/month/year with the selected frequency - * properties: - * interval: - * type: integer - * count: - * type: integer - * freq: - * type: integer - * disableGuests: - * type: boolean - * description: If the event type should disable adding guests to the booking - * hideCalendarNotes: - * type: boolean - * description: If the calendar notes should be hidden from the booking - * minimumBookingNotice: - * type: integer - * description: Minimum time in minutes before the event is bookable - * beforeEventBuffer: - * type: integer - * description: Number of minutes of buffer time before a Cal Event - * afterEventBuffer: - * type: integer - * description: Number of minutes of buffer time after a Cal Event - * schedulingType: - * type: string - * description: The type of scheduling if a Team event. Required for team events only - * enum: [ROUND_ROBIN, COLLECTIVE] - * price: - * type: integer - * description: Price of the event type booking - * currency: - * type: string - * description: Currency acronym. Eg- usd, eur, gbp, etc. - * slotInterval: - * type: integer - * description: The intervals of available bookable slots in minutes - * successRedirectUrl: - * type: string - * format: url - * description: A valid URL where the booker will redirect to, once the booking is completed successfully - * description: - * type: string - * description: Description of the event type - * seatsPerTimeSlot: - * type: integer - * description: 'The number of seats for each time slot' - * seatsShowAttendees: - * type: boolean - * description: 'Share Attendee information in seats' - * seatsShowAvailabilityCount: - * type: boolean - * description: 'Show the number of available seats' - * locations: - * type: array - * description: A list of all available locations for the event type - * items: - * type: array - * items: - * oneOf: - * - type: object - * properties: - * type: - * type: string - * enum: ['integrations:daily'] - * - type: object - * properties: - * type: - * type: string - * enum: ['attendeeInPerson'] - * - type: object - * properties: - * type: - * type: string - * enum: ['inPerson'] - * address: - * type: string - * displayLocationPublicly: - * type: boolean - * - type: object - * properties: - * type: - * type: string - * enum: ['link'] - * link: - * type: string - * displayLocationPublicly: - * type: boolean - * example: - * event-type: - * summary: An example of event type PATCH request - * value: - * length: 60 - * requiresConfirmation: true - * tags: - * - event-types - * externalDocs: - * url: https://docs.cal.com/docs/core-features/event-types - * responses: - * 201: - * description: OK, eventType edited successfully - * 400: - * description: Bad request. EventType body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function patchHandler(req: NextApiRequest) { - const { query, body } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const { - hosts = [], - bookingLimits, - durationLimits, - locations, - /** FIXME: Updating event-type children from API not supported for now */ - children: _, - ...parsedBody - } = schemaEventTypeEditBodyParams.parse(body); - - const data: Prisma.EventTypeUpdateArgs["data"] = { - ...parsedBody, - teamId: parsedBody.teamId === null ? undefined : parsedBody.teamId, - bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits, - durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits, - locations: locations === null ? Prisma.DbNull : locations, - }; - - if (hosts) { - await ensureOnlyMembersAsHosts(req, parsedBody); - data.hosts = { - deleteMany: {}, - create: hosts.map((host) => ({ - ...host, - isFixed: data.schedulingType === SchedulingType.COLLECTIVE || host.isFixed, - })), - }; - } - await checkPermissions(req, parsedBody); - const eventType = await prisma.eventType.update({ where: { id }, data, select: eventTypeSelect }); - return { event_type: eventType }; -} - -async function checkPermissions(req: NextApiRequest, body: z.infer) { - const { userId, isSystemWideAdmin } = req; - const { id } = schemaQueryIdParseInt.parse(req.query); - if (isSystemWideAdmin) return; - /** Only event type owners can modify it */ - const eventType = await prisma.eventType.findFirst({ where: { id, userId } }); - if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" }); - await checkTeamEventEditPermission(req, body); -} - -export default patchHandler; diff --git a/apps/api/v1/pages/api/event-types/[id]/index.ts b/apps/api/v1/pages/api/event-types/[id]/index.ts deleted file mode 100644 index ca52c1a03668c3..00000000000000 --- a/apps/api/v1/pages/api/event-types/[id]/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/event-types/_get.ts b/apps/api/v1/pages/api/event-types/_get.ts deleted file mode 100644 index 7fb26d2ac50972..00000000000000 --- a/apps/api/v1/pages/api/event-types/_get.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { prisma } from "@calcom/prisma"; -import type { PrismaClient } from "@calcom/prisma"; - -import { eventTypeSelect } from "~/lib/selects/event-type"; -import { schemaQuerySlug } from "~/lib/validations/shared/querySlug"; -import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; - -import getCalLink from "./_utils/getCalLink"; - -/** - * @swagger - * /event-types: - * get: - * summary: Find all event types - * operationId: listEventTypes - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * - in: query - * name: slug - * schema: - * type: string - * required: false - * description: Slug to filter event types by - * tags: - * - event-types - * externalDocs: - * url: https://docs.cal.com/docs/core-features/event-types - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No event types were found - */ -async function getHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId]; - const { slug } = schemaQuerySlug.parse(req.query); - const shouldUseUserId = !isSystemWideAdmin || !slug || !!req.query.userId; - // When user is admin and no query params are provided we should return all event types. - // But currently we return only the event types of the user. Not changing this for backwards compatibility. - const data = await prisma.eventType.findMany({ - where: { - userId: shouldUseUserId ? { in: userIds } : undefined, - slug: slug, // slug will be undefined if not provided in query - }, - select: eventTypeSelect, - }); - // this really should return [], but backwards compatibility.. - if (data.length === 0) new HttpError({ statusCode: 404, message: "No event types were found" }); - return { - event_types: (await defaultScheduleId<(typeof data)[number]>({ eventTypes: data, prisma, userIds })).map( - (eventType) => { - const link = getCalLink(eventType); - return { ...eventType, link }; - } - ), - }; -} -// TODO: Extract & reuse. -function extractUserIdsFromQuery({ isSystemWideAdmin, query }: NextApiRequest) { - /** Guard: Only admins can query other users */ - if (!isSystemWideAdmin) { - throw new HttpError({ statusCode: 401, message: "ADMIN required" }); - } - const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query); - return Array.isArray(userIdOrUserIds) ? userIdOrUserIds : [userIdOrUserIds]; -} - -type DefaultScheduleIdEventTypeBase = { - scheduleId: number | null; - userId: number | null; -}; -// If an eventType is given w/o a scheduleId -// Then we associate the default user schedule id to the eventType -async function defaultScheduleId({ - prisma, - eventTypes, - userIds, -}: { - prisma: PrismaClient; - eventTypes: (T & Partial>)[]; - userIds: number[]; -}) { - // there is no event types without a scheduleId, skip the user query - if (eventTypes.every((eventType) => eventType.scheduleId)) return eventTypes; - - const users = await prisma.user.findMany({ - where: { - id: { - in: userIds, - }, - }, - select: { - id: true, - defaultScheduleId: true, - }, - }); - - if (!users.length) { - return eventTypes; - } - - const defaultScheduleIds = users.reduce((result, user) => { - result[user.id] = user.defaultScheduleId; - return result; - }, {} as { [x: number]: number | null }); - - return eventTypes.map((eventType) => { - // realistically never happens, userId shouldn't be null on personal event types. - if (!eventType.userId) return eventType; - return { - ...eventType, - scheduleId: eventType.scheduleId || defaultScheduleIds[eventType.userId], - }; - }); -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/event-types/_post.ts b/apps/api/v1/pages/api/event-types/_post.ts deleted file mode 100644 index 02a3c4097d2cb7..00000000000000 --- a/apps/api/v1/pages/api/event-types/_post.ts +++ /dev/null @@ -1,343 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { prisma } from "@calcom/prisma"; -import { Prisma } from "@calcom/prisma/client"; -import { MembershipRole } from "@calcom/prisma/enums"; - -import { eventTypeSelect } from "~/lib/selects/event-type"; -import { schemaEventTypeCreateBodyParams } from "~/lib/validations/event-type"; -import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware"; - -import checkParentEventOwnership from "./_utils/checkParentEventOwnership"; -import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission"; -import checkUserMembership from "./_utils/checkUserMembership"; -import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts"; - -/** - * @swagger - * /event-types: - * post: - * summary: Creates a new event type - * operationId: addEventType - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * requestBody: - * description: Create a new event-type related to your user or team - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - title - * - slug - * - length - * - metadata - * properties: - * length: - * type: integer - * description: Duration of the event type in minutes - * metadata: - * type: object - * description: Metadata relating to event type. Pass {} if empty - * title: - * type: string - * description: Title of the event type - * slug: - * type: string - * description: Unique slug for the event type - * hosts: - * type: array - * items: - * type: object - * properties: - * userId: - * type: number - * isFixed: - * type: boolean - * description: Host MUST be available for any slot to be bookable. - * hidden: - * type: boolean - * description: If the event type should be hidden from your public booking page - * scheduleId: - * type: number - * description: The ID of the schedule for this event type - * position: - * type: integer - * description: The position of the event type on the public booking page - * teamId: - * type: integer - * description: Team ID if the event type should belong to a team - * periodType: - * type: string - * enum: [UNLIMITED, ROLLING, RANGE] - * description: To decide how far into the future an invitee can book an event with you - * periodStartDate: - * type: string - * format: date-time - * description: Start date of bookable period (Required if periodType is 'range') - * periodEndDate: - * type: string - * format: date-time - * description: End date of bookable period (Required if periodType is 'range') - * periodDays: - * type: integer - * description: Number of bookable days (Required if periodType is rolling) - * periodCountCalendarDays: - * type: boolean - * description: If calendar days should be counted for period days - * requiresConfirmation: - * type: boolean - * description: If the event type should require your confirmation before completing the booking - * recurringEvent: - * type: object - * description: If the event should recur every week/month/year with the selected frequency - * properties: - * interval: - * type: integer - * count: - * type: integer - * freq: - * type: integer - * disableGuests: - * type: boolean - * description: If the event type should disable adding guests to the booking - * hideCalendarNotes: - * type: boolean - * description: If the calendar notes should be hidden from the booking - * minimumBookingNotice: - * type: integer - * description: Minimum time in minutes before the event is bookable - * beforeEventBuffer: - * type: integer - * description: Number of minutes of buffer time before a Cal Event - * afterEventBuffer: - * type: integer - * description: Number of minutes of buffer time after a Cal Event - * schedulingType: - * type: string - * description: The type of scheduling if a Team event. Required for team events only - * enum: [ROUND_ROBIN, COLLECTIVE, MANAGED] - * price: - * type: integer - * description: Price of the event type booking - * parentId: - * type: integer - * description: EventTypeId of the parent managed event - * currency: - * type: string - * description: Currency acronym. Eg- usd, eur, gbp, etc. - * slotInterval: - * type: integer - * description: The intervals of available bookable slots in minutes - * successRedirectUrl: - * type: string - * format: url - * description: A valid URL where the booker will redirect to, once the booking is completed successfully - * description: - * type: string - * description: Description of the event type - * locations: - * type: array - * description: A list of all available locations for the event type - * items: - * type: array - * items: - * oneOf: - * - type: object - * properties: - * type: - * type: string - * enum: ['integrations:daily'] - * - type: object - * properties: - * type: - * type: string - * enum: ['attendeeInPerson'] - * - type: object - * properties: - * type: - * type: string - * enum: ['inPerson'] - * address: - * type: string - * displayLocationPublicly: - * type: boolean - * - type: object - * properties: - * type: - * type: string - * enum: ['link'] - * link: - * type: string - * displayLocationPublicly: - * type: boolean - * examples: - * event-type: - * summary: An example of an individual event type POST request - * value: - * title: Hello World - * slug: hello-world - * length: 30 - * hidden: false - * position: 0 - * eventName: null - * timeZone: null - * scheduleId: 5 - * periodType: UNLIMITED - * periodStartDate: 2023-02-15T08:46:16.000Z - * periodEndDate: 2023-0-15T08:46:16.000Z - * periodDays: null - * periodCountCalendarDays: false - * requiresConfirmation: false - * recurringEvent: null - * disableGuests: false - * hideCalendarNotes: false - * minimumBookingNotice: 120 - * beforeEventBuffer: 0 - * afterEventBuffer: 0 - * price: 0 - * currency: usd - * slotInterval: null - * successRedirectUrl: null - * description: A test event type - * metadata: { - * apps: { - * stripe: { - * price: 0, - * enabled: false, - * currency: usd - * } - * } - * } - * team-event-type: - * summary: An example of a team event type POST request - * value: - * title: "Tennis class" - * slug: "tennis-class-{{$guid}}" - * length: 60 - * hidden: false - * position: 0 - * teamId: 3 - * eventName: null - * timeZone: null - * periodType: "UNLIMITED" - * periodStartDate: null - * periodEndDate: null - * periodDays: null - * periodCountCalendarDays: null - * requiresConfirmation: true - * recurringEvent: - * interval: 2 - * count: 10 - * freq: 2 - * disableGuests: false - * hideCalendarNotes: false - * minimumBookingNotice: 120 - * beforeEventBuffer: 0 - * afterEventBuffer: 0 - * schedulingType: "COLLECTIVE" - * price: 0 - * currency: "usd" - * slotInterval: null - * successRedirectUrl: null - * description: null - * locations: - * - address: "London" - * type: "inPerson" - * metadata: {} - * tags: - * - event-types - * externalDocs: - * url: https://docs.cal.com/docs/core-features/event-types - * responses: - * 201: - * description: OK, event type created - * 400: - * description: Bad request. EventType body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function postHandler(req: NextApiRequest) { - const { userId: ctxUserId, isSystemWideAdmin, body } = req; - - const { - hosts = [], - bookingLimits, - durationLimits, - locations, - /** FIXME: Adding event-type children from API not supported for now */ - children: _, - // if userId is not passed, we will use the userId from the context - userId = ctxUserId, - ...parsedBody - } = schemaEventTypeCreateBodyParams.parse(body || {}); - - let data: Prisma.EventTypeCreateArgs["data"] = { - ...parsedBody, - userId: !!parsedBody.teamId ? null : userId, - users: !!parsedBody.teamId ? undefined : { connect: { id: userId } }, - bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits, - durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits, - locations: locations === null ? Prisma.DbNull : locations, - }; - - await checkPermissions(req); - - if (parsedBody.parentId) { - await checkParentEventOwnership(req); - await checkUserMembership(req); - } - - if (isSystemWideAdmin && userId && parsedBody.teamId === undefined) { - data = { ...parsedBody, users: { connect: { id: userId } } }; - } - - await checkTeamEventEditPermission(req, parsedBody); - await ensureOnlyMembersAsHosts(req, parsedBody); - - if (hosts) { - data.hosts = { createMany: { data: hosts } }; - } - - const eventType = await prisma.eventType.create({ data, select: eventTypeSelect }); - - return { - event_type: eventType, - message: "Event type created successfully", - }; -} - -async function checkPermissions(req: NextApiRequest) { - const { isSystemWideAdmin } = req; - const body = schemaEventTypeCreateBodyParams.parse(req.body); - /* Non-admin users can only create event types for themselves */ - if (!isSystemWideAdmin && body.userId) - throw new HttpError({ - statusCode: 401, - message: "ADMIN required for `userId`", - }); - if ( - body.teamId && - !isSystemWideAdmin && - !(await canUserAccessTeamWithRole(req.userId, isSystemWideAdmin, body.teamId, { - in: [MembershipRole.OWNER, MembershipRole.ADMIN], - })) - ) - throw new HttpError({ - statusCode: 401, - message: "ADMIN required for `teamId`", - }); - /* Admin users are required to pass in a userId or teamId */ - if (isSystemWideAdmin && !body.userId && !body.teamId) - throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" }); -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/event-types/_utils/checkParentEventOwnership.ts b/apps/api/v1/pages/api/event-types/_utils/checkParentEventOwnership.ts deleted file mode 100644 index 38e9fe78c25c0a..00000000000000 --- a/apps/api/v1/pages/api/event-types/_utils/checkParentEventOwnership.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; - -/** - * Checks if a user, identified by the provided userId, has ownership (or admin rights) over - * the team associated with the event type identified by the parentId. - * - * @param req - The current request - * - * @throws {HttpError} If the parent event type is not found, - * if the parent event type doesn't belong to any team, - * or if the user doesn't have ownership or admin rights to the associated team. - */ -export default async function checkParentEventOwnership(req: NextApiRequest) { - const { userId, body } = req; - /** These are already parsed upstream, we can assume they're good here. */ - const parentId = Number(body.parentId); - const parentEventType = await prisma.eventType.findUnique({ - where: { id: parentId }, - select: { teamId: true }, - }); - - if (!parentEventType) { - throw new HttpError({ - statusCode: 404, - message: "Parent event type not found.", - }); - } - - if (!parentEventType.teamId) { - throw new HttpError({ - statusCode: 400, - message: "This event type is not capable of having children", - }); - } - - const teamMember = await prisma.membership.findFirst({ - where: { - teamId: parentEventType.teamId, - userId: userId, - role: { in: ["ADMIN", "OWNER"] }, - accepted: true, - }, - }); - - if (!teamMember) { - throw new HttpError({ - statusCode: 403, - message: "User is not authorized to access the team to which the parent event type belongs.", - }); - } -} diff --git a/apps/api/v1/pages/api/event-types/_utils/checkTeamEventEditPermission.ts b/apps/api/v1/pages/api/event-types/_utils/checkTeamEventEditPermission.ts deleted file mode 100644 index edba1dcea4da4e..00000000000000 --- a/apps/api/v1/pages/api/event-types/_utils/checkTeamEventEditPermission.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { NextApiRequest } from "next"; -import type { z } from "zod"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; - -import type { schemaEventTypeCreateBodyParams } from "~/lib/validations/event-type"; - -export default async function checkTeamEventEditPermission( - req: NextApiRequest, - body: Pick, "teamId" | "userId"> -) { - if (body.teamId) { - const membership = await prisma.membership.findFirst({ - where: { - userId: req.userId, - teamId: body.teamId, - accepted: true, - role: { in: ["ADMIN", "OWNER"] }, - }, - }); - - if (!membership) { - throw new HttpError({ - statusCode: 403, - message: "No permission to operate on event-type for this team", - }); - } - } -} diff --git a/apps/api/v1/pages/api/event-types/_utils/checkUserMembership.ts b/apps/api/v1/pages/api/event-types/_utils/checkUserMembership.ts deleted file mode 100644 index 176d5a93f7dae3..00000000000000 --- a/apps/api/v1/pages/api/event-types/_utils/checkUserMembership.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; - -/** - * Checks if a user, identified by the provided userId, is a member of the team associated - * with the event type identified by the parentId. - * - * @param req - The current request - * - * @throws {HttpError} If the event type is not found, - * if the event type doesn't belong to any team, - * or if the user isn't a member of the associated team. - */ -export default async function checkUserMembership(req: NextApiRequest) { - const { body } = req; - /** These are already parsed upstream, we can assume they're good here. */ - const parentId = Number(body.parentId); - const userId = Number(body.userId); - const parentEventType = await prisma.eventType.findUnique({ - where: { - id: parentId, - }, - select: { - teamId: true, - }, - }); - - if (!parentEventType) { - throw new HttpError({ - statusCode: 404, - message: "Event type not found.", - }); - } - - if (!parentEventType.teamId) { - throw new HttpError({ - statusCode: 400, - message: "This event type is not capable of having children.", - }); - } - - const teamMember = await prisma.membership.findFirst({ - where: { - teamId: parentEventType.teamId, - userId: userId, - accepted: true, - }, - }); - - if (!teamMember) { - throw new HttpError({ - statusCode: 400, - message: "User is not a team member.", - }); - } -} diff --git a/apps/api/v1/pages/api/event-types/_utils/ensureOnlyMembersAsHosts.ts b/apps/api/v1/pages/api/event-types/_utils/ensureOnlyMembersAsHosts.ts deleted file mode 100644 index f3b29a934ed4ec..00000000000000 --- a/apps/api/v1/pages/api/event-types/_utils/ensureOnlyMembersAsHosts.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { NextApiRequest } from "next"; -import type { z } from "zod"; - -import prisma from "@calcom/prisma"; - -import type { schemaEventTypeCreateBodyParams } from "~/lib/validations/event-type"; - -export default async function ensureOnlyMembersAsHosts( - req: NextApiRequest, - body: Pick, "hosts" | "teamId"> -) { - if (body.teamId && body.hosts && body.hosts.length > 0) { - const teamMemberCount = await prisma.membership.count({ - where: { - teamId: body.teamId, - userId: { in: body.hosts.map((host) => host.userId) }, - }, - }); - if (teamMemberCount !== body.hosts.length) { - throw new Error("You can only add members of the team to a team event type."); - } - } -} diff --git a/apps/api/v1/pages/api/event-types/_utils/getCalLink.ts b/apps/api/v1/pages/api/event-types/_utils/getCalLink.ts deleted file mode 100644 index 1877e3547f418a..00000000000000 --- a/apps/api/v1/pages/api/event-types/_utils/getCalLink.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { WEBSITE_URL } from "@calcom/lib/constants"; - -export default function getCalLink(eventType: { - team?: { slug: string | null } | null; - owner?: { username: string | null } | null; - users?: { username: string | null }[]; - slug: string; -}) { - return `${WEBSITE_URL}/${ - eventType?.team - ? `team/${eventType?.team?.slug}` - : eventType?.owner - ? eventType.owner.username - : eventType?.users?.[0]?.username - }/${eventType?.slug}`; -} diff --git a/apps/api/v1/pages/api/event-types/index.ts b/apps/api/v1/pages/api/event-types/index.ts deleted file mode 100644 index e89b47c30e817a..00000000000000 --- a/apps/api/v1/pages/api/event-types/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/index.ts b/apps/api/v1/pages/api/index.ts deleted file mode 100644 index 9f0dfa482973ea..00000000000000 --- a/apps/api/v1/pages/api/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -export default async function CalcomApi(_: NextApiRequest, res: NextApiResponse) { - res.status(200).json({ message: "Welcome to Cal.com API - docs are at https://developer.cal.com/api" }); -} diff --git a/apps/api/v1/pages/api/invites/_post.ts b/apps/api/v1/pages/api/invites/_post.ts deleted file mode 100644 index 67340c8e68f428..00000000000000 --- a/apps/api/v1/pages/api/invites/_post.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import { CreationSource } from "@calcom/prisma/enums"; -import { createContext } from "@calcom/trpc/server/createContext"; -import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router"; -import type { TInviteMemberInputSchema } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema"; -import { ZInviteMemberInputSchema } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema"; -import { createCallerFactory } from "@calcom/trpc/server/trpc"; -import type { UserProfile } from "@calcom/types/UserProfile"; - -import { TRPCError } from "@trpc/server"; -import { getHTTPStatusCodeFromError } from "@trpc/server/http"; - -async function postHandler(req: NextApiRequest, res: NextApiResponse) { - const data = ZInviteMemberInputSchema.parse(req.body); - await checkPermissions(req, data); - - async function sessionGetter() { - return { - user: { - id: req.userId, - uuid: req.userUuid, - username: "", - profile: { - id: null, - organizationId: null, - organization: null, - username: "", - upId: "", - } satisfies UserProfile, - }, - hasValidLicense: true, - expires: "", - upId: "", - }; - } - - const ctx = await createContext({ req, res }, sessionGetter); - try { - const createCaller = createCallerFactory(viewerTeamsRouter); - const caller = createCaller(ctx); - await caller.inviteMember({ - role: data.role, - language: data.language, - teamId: data.teamId, - usernameOrEmail: data.usernameOrEmail, - creationSource: CreationSource.API_V1, - }); - - return { success: true, message: `${data.usernameOrEmail} has been invited.` }; - } catch (cause) { - if (cause instanceof TRPCError) { - const statusCode = getHTTPStatusCodeFromError(cause); - throw new HttpError({ statusCode, message: cause.message }); - } - - throw cause; - } -} - -async function checkPermissions(req: NextApiRequest, body: TInviteMemberInputSchema) { - const { userId, isSystemWideAdmin } = req; - if (isSystemWideAdmin) return; - // To prevent auto-accepted invites, limit it to ADMIN users - if (!isSystemWideAdmin && "accepted" in body) - throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" }); - // Only team OWNERS and ADMINS can add other members - const membership = await prisma.membership.findFirst({ - where: { userId, teamId: body.teamId, role: { in: ["ADMIN", "OWNER"] } }, - }); - if (!membership) throw new HttpError({ statusCode: 403, message: "You can't add members to this team" }); -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/invites/index.ts b/apps/api/v1/pages/api/invites/index.ts deleted file mode 100644 index 1bda7cf6afcd8b..00000000000000 --- a/apps/api/v1/pages/api/invites/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/me/_get.ts b/apps/api/v1/pages/api/me/_get.ts deleted file mode 100644 index a117607011b37e..00000000000000 --- a/apps/api/v1/pages/api/me/_get.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaUserReadPublic } from "~/lib/validations/user"; - -async function handler({ userId }: NextApiRequest) { - const data = await prisma.user.findUniqueOrThrow({ where: { id: userId } }); - return { - user: schemaUserReadPublic.parse({ - ...data, - avatar: data.avatarUrl, - }), - }; -} - -export default defaultResponder(handler); diff --git a/apps/api/v1/pages/api/me/index.ts b/apps/api/v1/pages/api/me/index.ts deleted file mode 100644 index c97e6cd4dfd429..00000000000000 --- a/apps/api/v1/pages/api/me/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - }) -); diff --git a/apps/api/v1/pages/api/memberships/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/memberships/[id]/_auth-middleware.ts deleted file mode 100644 index f14317eae39305..00000000000000 --- a/apps/api/v1/pages/api/memberships/[id]/_auth-middleware.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; - -import { membershipIdSchema } from "~/lib/validations/membership"; - -async function authMiddleware(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { teamId } = membershipIdSchema.parse(req.query); - // Admins can just skip this check - if (isSystemWideAdmin) return; - // Only team members can modify a membership - const membership = await prisma.membership.findFirst({ where: { userId, teamId } }); - if (!membership) throw new HttpError({ statusCode: 403, message: "Forbidden" }); -} - -export default authMiddleware; diff --git a/apps/api/v1/pages/api/memberships/[id]/_delete.ts b/apps/api/v1/pages/api/memberships/[id]/_delete.ts deleted file mode 100644 index 17fae079dbb9bf..00000000000000 --- a/apps/api/v1/pages/api/memberships/[id]/_delete.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { membershipIdSchema } from "~/lib/validations/membership"; - -/** - * @swagger - * /memberships/{userId}_{teamId}: - * delete: - * summary: Remove an existing membership - * parameters: - * - in: path - * name: userId - * schema: - * type: integer - * required: true - * description: Numeric userId of the membership to get - * - in: path - * name: teamId - * schema: - * type: integer - * required: true - * description: Numeric teamId of the membership to get - * tags: - * - memberships - * responses: - * 201: - * description: OK, membership removed successfully - * 400: - * description: Bad request. Membership id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function deleteHandler(req: NextApiRequest) { - const { query } = req; - const userId_teamId = membershipIdSchema.parse(query); - await checkPermissions(req); - await prisma.membership.delete({ where: { userId_teamId } }); - return { message: `Membership with id: ${query.id} deleted successfully` }; -} - -async function checkPermissions(req: NextApiRequest) { - const { isSystemWideAdmin, userId, query } = req; - const userId_teamId = membershipIdSchema.parse(query); - // Admin User can do anything including deletion of Admin Team Member in any team - if (isSystemWideAdmin) { - return; - } - - // Owner can delete Admin and Member - // Admin Team Member can delete Member - // Member can't delete anyone - const PRIVILEGE_ORDER = ["OWNER", "ADMIN", "MEMBER"]; - - const memberShipToBeDeleted = await prisma.membership.findUnique({ - where: { userId_teamId }, - }); - - if (!memberShipToBeDeleted) { - throw new HttpError({ statusCode: 404, message: "Membership not found" }); - } - - // If a user is deleting their own membership, then they can do it - if (userId === memberShipToBeDeleted.userId) { - return; - } - - const currentUserMembership = await prisma.membership.findUnique({ - where: { - userId_teamId: { - userId, - teamId: memberShipToBeDeleted.teamId, - }, - }, - }); - - if (!currentUserMembership) { - // Current User isn't a member of the team - throw new HttpError({ statusCode: 403, message: "You are not a member of the team" }); - } - - if ( - PRIVILEGE_ORDER.indexOf(memberShipToBeDeleted.role) === -1 || - PRIVILEGE_ORDER.indexOf(currentUserMembership.role) === -1 - ) { - throw new HttpError({ statusCode: 400, message: "Invalid role" }); - } - - // If Role that is being deleted comes before the current User's Role, or it's the same ROLE, throw error - if ( - PRIVILEGE_ORDER.indexOf(memberShipToBeDeleted.role) <= PRIVILEGE_ORDER.indexOf(currentUserMembership.role) - ) { - throw new HttpError({ - statusCode: 403, - message: "You don't have the appropriate role to delete this membership", - }); - } -} - -export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/memberships/[id]/_get.ts b/apps/api/v1/pages/api/memberships/[id]/_get.ts deleted file mode 100644 index 3314b0e11f202d..00000000000000 --- a/apps/api/v1/pages/api/memberships/[id]/_get.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { membershipIdSchema, schemaMembershipPublic } from "~/lib/validations/membership"; - -/** - * @swagger - * /memberships/{userId}_{teamId}: - * get: - * summary: Find a membership by userID and teamID - * parameters: - * - in: path - * name: userId - * schema: - * type: integer - * required: true - * description: Numeric userId of the membership to get - * - in: path - * name: teamId - * schema: - * type: integer - * required: true - * description: Numeric teamId of the membership to get - * tags: - * - memberships - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Membership was not found - */ -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const userId_teamId = membershipIdSchema.parse(query); - const args: Prisma.MembershipFindUniqueOrThrowArgs = { where: { userId_teamId } }; - // Just in case the user want to get more info about the team itself - if (req.query.include === "team") args.include = { team: true }; - const data = await prisma.membership.findUniqueOrThrow(args); - return { membership: schemaMembershipPublic.parse(data) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/memberships/[id]/_patch.ts b/apps/api/v1/pages/api/memberships/[id]/_patch.ts deleted file mode 100644 index ee7aa19d4d5b8e..00000000000000 --- a/apps/api/v1/pages/api/memberships/[id]/_patch.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { - membershipEditBodySchema, - membershipIdSchema, - schemaMembershipPublic, -} from "~/lib/validations/membership"; - -/** - * @swagger - * /memberships/{userId}_{teamId}: - * patch: - * summary: Edit an existing membership - * parameters: - * - in: path - * name: userId - * schema: - * type: integer - * required: true - * description: Numeric userId of the membership to get - * - in: path - * name: teamId - * schema: - * type: integer - * required: true - * description: Numeric teamId of the membership to get - * tags: - * - memberships - * responses: - * 201: - * description: OK, membership edited successfully - * 400: - * description: Bad request. Membership body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function patchHandler(req: NextApiRequest) { - const { query } = req; - const userId_teamId = membershipIdSchema.parse(query); - const data = membershipEditBodySchema.parse(req.body); - const args: Prisma.MembershipUpdateArgs = { - where: { userId_teamId }, - data: { - ...data, - updatedAt: new Date(), - }, - }; - - await checkPermissions(req); - - const result = await prisma.membership.update(args); - return { membership: schemaMembershipPublic.parse(result) }; -} - -async function checkPermissions(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { userId: queryUserId, teamId } = membershipIdSchema.parse(req.query); - const data = membershipEditBodySchema.parse(req.body); - // Admins can just skip this check - if (isSystemWideAdmin) return; - // Only the invited user can accept the invite - if ("accepted" in data && queryUserId !== userId) - throw new HttpError({ - statusCode: 403, - message: "Only the invited user can accept the invite", - }); - // Only team OWNERS and ADMINS can modify `role` - if ("role" in data) { - const membership = await prisma.membership.findFirst({ - where: { userId, teamId, role: { in: ["ADMIN", "OWNER"] } }, - }); - if (!membership || (membership.role !== "OWNER" && req.body.role === "OWNER")) - throw new HttpError({ statusCode: 403, message: "Forbidden" }); - } -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/memberships/[id]/index.ts b/apps/api/v1/pages/api/memberships/[id]/index.ts deleted file mode 100644 index 54980cf49469ef..00000000000000 --- a/apps/api/v1/pages/api/memberships/[id]/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "./_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/memberships/_get.ts b/apps/api/v1/pages/api/memberships/_get.ts deleted file mode 100644 index 1a36805a169e8b..00000000000000 --- a/apps/api/v1/pages/api/memberships/_get.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaMembershipPublic } from "~/lib/validations/membership"; -import { - schemaQuerySingleOrMultipleTeamIds, - schemaQuerySingleOrMultipleUserIds, -} from "~/lib/validations/shared/queryUserId"; - -/** - * @swagger - * /memberships: - * get: - * summary: Find all memberships - * tags: - * - memberships - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No memberships were found - */ -async function getHandler(req: NextApiRequest) { - const args: Prisma.MembershipFindManyArgs = { - where: { - /** Admins can query multiple users */ - userId: { in: getUserIds(req) }, - /** Admins can query multiple teams as well */ - teamId: { in: getTeamIds(req) }, - }, - }; - // Just in case the user want to get more info about the team itself - if (req.query.include === "team") args.include = { team: true }; - - const data = await prisma.membership.findMany(args); - return { memberships: data.map((v) => schemaMembershipPublic.parse(v)) }; -} - -/** - * Returns requested users IDs only if admin, otherwise return only current user ID - */ -function getUserIds(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - /** Only admins can query other users */ - if (!isSystemWideAdmin && req.query.userId) - throw new HttpError({ statusCode: 403, message: "ADMIN required" }); - if (isSystemWideAdmin && req.query.userId) { - const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); - const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; - return userIds; - } - // Return all memberships for ADMIN, limit to current user to non-admins - return isSystemWideAdmin ? undefined : [userId]; -} - -/** - * Returns requested teams IDs only if admin - */ -function getTeamIds(req: NextApiRequest) { - const { isSystemWideAdmin } = req; - /** Only admins can query other teams */ - if (!isSystemWideAdmin && req.query.teamId) - throw new HttpError({ statusCode: 403, message: "ADMIN required" }); - if (isSystemWideAdmin && req.query.teamId) { - const query = schemaQuerySingleOrMultipleTeamIds.parse(req.query); - const teamIds = Array.isArray(query.teamId) ? query.teamId : [query.teamId]; - return teamIds; - } - return undefined; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/memberships/_post.ts b/apps/api/v1/pages/api/memberships/_post.ts deleted file mode 100644 index bada35b35aade5..00000000000000 --- a/apps/api/v1/pages/api/memberships/_post.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { membershipCreateBodySchema, schemaMembershipPublic } from "~/lib/validations/membership"; - -/** - * @swagger - * /memberships: - * post: - * summary: Creates a new membership - * tags: - * - memberships - * responses: - * 201: - * description: OK, membership created - * 400: - * description: Bad request. Membership body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function postHandler(req: NextApiRequest) { - const data = membershipCreateBodySchema.parse(req.body); - const args: Prisma.MembershipCreateArgs = { - data, - }; - - await checkPermissions(req); - - const result = await prisma.membership.create(args); - - return { - membership: schemaMembershipPublic.parse(result), - message: "Membership created successfully", - }; -} - -async function checkPermissions(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - if (isSystemWideAdmin) return; - const body = membershipCreateBodySchema.parse(req.body); - // To prevent auto-accepted invites, limit it to ADMIN users - if (!isSystemWideAdmin && "accepted" in body) - throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" }); - // Only team OWNERS and ADMINS can add other members - const membership = await prisma.membership.findFirst({ - where: { userId, teamId: body.teamId, role: { in: ["ADMIN", "OWNER"] } }, - }); - if (!membership) throw new HttpError({ statusCode: 403, message: "You can't add members to this team" }); -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/memberships/index.ts b/apps/api/v1/pages/api/memberships/index.ts deleted file mode 100644 index e89b47c30e817a..00000000000000 --- a/apps/api/v1/pages/api/memberships/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/payments/[id].ts b/apps/api/v1/pages/api/payments/[id].ts deleted file mode 100644 index 35ab674c07d8a7..00000000000000 --- a/apps/api/v1/pages/api/payments/[id].ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import prisma from "@calcom/prisma"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; -import type { PaymentResponse } from "~/lib/types"; -import { schemaPaymentPublic } from "~/lib/validations/payment"; -import { - schemaQueryIdParseInt, - withValidQueryIdTransformParseInt, -} from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /payments/{id}: - * get: - * summary: Find a payment - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the payment to get - * tags: - * - payments - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Payment was not found - */ -export async function paymentById( - { method, query, userId }: NextApiRequest, - res: NextApiResponse -) { - const safeQuery = schemaQueryIdParseInt.safeParse(query); - if (safeQuery.success && method === "GET") { - const userWithBookings = await prisma.user.findUnique({ - where: { id: userId }, - // eslint-disable-next-line @calcom/eslint/no-prisma-include-true - include: { bookings: true }, - }); - await prisma.payment - .findUnique({ where: { id: safeQuery.data.id } }) - .then((data) => schemaPaymentPublic.parse(data)) - .then((payment) => { - if (!userWithBookings?.bookings.map((b) => b.id).includes(payment.bookingId)) { - res.status(401).json({ message: "Unauthorized" }); - } else { - res.status(200).json({ payment }); - } - }) - .catch((error: Error) => - res.status(404).json({ - message: `Payment with id: ${safeQuery.data.id} not found`, - error, - }) - ); - } -} -export default withMiddleware("HTTP_GET")(withValidQueryIdTransformParseInt(paymentById)); diff --git a/apps/api/v1/pages/api/payments/index.ts b/apps/api/v1/pages/api/payments/index.ts deleted file mode 100644 index d556245753e058..00000000000000 --- a/apps/api/v1/pages/api/payments/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import prisma from "@calcom/prisma"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; -import type { PaymentsResponse } from "~/lib/types"; -import { schemaPaymentPublic } from "~/lib/validations/payment"; - -/** - * @swagger - * /payments: - * get: - * summary: Find all payments - * tags: - * - payments - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No payments were found - */ -async function allPayments({ userId }: NextApiRequest, res: NextApiResponse) { - const userWithBookings = await prisma.user.findUnique({ - where: { id: userId }, - include: { bookings: true }, - }); - if (!userWithBookings) throw new Error("No user found"); - const bookings = userWithBookings.bookings; - const bookingIds = bookings.map((booking) => booking.id); - const data = await prisma.payment.findMany({ where: { bookingId: { in: bookingIds } } }); - const payments = data.map((payment) => schemaPaymentPublic.parse(payment)); - - if (payments) res.status(200).json({ payments }); - else - (error: Error) => - res.status(404).json({ - message: "No Payments were found", - error, - }); -} -// NO POST FOR PAYMENTS FOR NOW -export default withMiddleware("HTTP_GET")(allPayments); diff --git a/apps/api/v1/pages/api/schedules/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/schedules/[id]/_auth-middleware.ts deleted file mode 100644 index ef44111c2ea268..00000000000000 --- a/apps/api/v1/pages/api/schedules/[id]/_auth-middleware.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -async function authMiddleware(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { id } = schemaQueryIdParseInt.parse(req.query); - // Admins can just skip this check - if (isSystemWideAdmin) return; - // Check if the current user can access the schedule - const schedule = await prisma.schedule.findFirst({ - where: { id, userId }, - }); - if (!schedule) throw new HttpError({ statusCode: 403, message: "Forbidden" }); -} - -export default authMiddleware; diff --git a/apps/api/v1/pages/api/schedules/[id]/_delete.ts b/apps/api/v1/pages/api/schedules/[id]/_delete.ts deleted file mode 100644 index 646b3300fa2981..00000000000000 --- a/apps/api/v1/pages/api/schedules/[id]/_delete.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /schedules/{id}: - * delete: - * operationId: removeScheduleById - * summary: Remove an existing schedule - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the schedule to delete - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * tags: - * - schedules - * responses: - * 201: - * description: OK, schedule removed successfully - * 400: - * description: Bad request. Schedule id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function deleteHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - - /* If we're deleting any default user schedule, we unset it */ - await prisma.user.updateMany({ where: { defaultScheduleId: id }, data: { defaultScheduleId: undefined } }); - - await prisma.schedule.delete({ where: { id } }); - return { message: `Schedule with id: ${id} deleted successfully` }; -} - -export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/schedules/[id]/_get.ts b/apps/api/v1/pages/api/schedules/[id]/_get.ts deleted file mode 100644 index ffded6bae12ecc..00000000000000 --- a/apps/api/v1/pages/api/schedules/[id]/_get.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaSchedulePublic } from "~/lib/validations/schedule"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /schedules/{id}: - * get: - * operationId: getScheduleById - * summary: Find a schedule - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the schedule to get - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * tags: - * - schedules - * responses: - * 200: - * description: OK - * content: - * application/json: - * examples: - * schedule: - * value: - * { - * "schedule": { - * "id": 12345, - * "userId": 182, - * "name": "Sample Schedule", - * "timeZone": "Asia/Calcutta", - * "availability": [ - * { - * "id": 111, - * "eventTypeId": null, - * "days": [0, 1, 2, 3, 4, 6], - * "startTime": "00:00:00", - * "endTime": "23:45:00" - * }, - * { - * "id": 112, - * "eventTypeId": null, - * "days": [5], - * "startTime": "00:00:00", - * "endTime": "12:00:00" - * }, - * { - * "id": 113, - * "eventTypeId": null, - * "days": [5], - * "startTime": "15:00:00", - * "endTime": "23:45:00" - * } - * ] - * } - * } - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Schedule was not found - */ - -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const data = await prisma.schedule.findUniqueOrThrow({ - where: { id }, - // eslint-disable-next-line @calcom/eslint/no-prisma-include-true - include: { availability: true }, - }); - return { schedule: schemaSchedulePublic.parse(data) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/schedules/[id]/_patch.ts b/apps/api/v1/pages/api/schedules/[id]/_patch.ts deleted file mode 100644 index 7d35096b954010..00000000000000 --- a/apps/api/v1/pages/api/schedules/[id]/_patch.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { NextApiRequest } from "next"; -import type { z } from "zod"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaSchedulePublic, schemaSingleScheduleBodyParams } from "~/lib/validations/schedule"; -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -/** - * @swagger - * /schedules/{id}: - * patch: - * operationId: editScheduleById - * summary: Edit an existing schedule - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: ID of the schedule to edit - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * requestBody: - * description: Edit an existing schedule - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * description: Name of the schedule - * timeZone: - * type: string - * description: The timezone for this schedule - * examples: - * schedule: - * value: - * { - * "name": "Updated Schedule", - * "timeZone": "Asia/Calcutta" - * } - * tags: - * - schedules - * responses: - * 200: - * description: OK, schedule edited successfully - * content: - * application/json: - * examples: - * schedule: - * value: - * { - * "schedule": { - * "id": 12345, - * "userId": 1, - * "name": "Total Testing Part 2", - * "timeZone": "Asia/Calcutta", - * "availability": [ - * { - * "id": 4567, - * "eventTypeId": null, - * "days": [1, 2, 3, 4, 5], - * "startTime": "09:00:00", - * "endTime": "17:00:00" - * } - * ] - * } - * } - * 400: - * description: Bad request. Schedule body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - -export async function patchHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdParseInt.parse(query); - const data = schemaSingleScheduleBodyParams.parse(req.body); - await checkPermissions(req, data); - const result = await prisma.schedule.update({ where: { id }, data, include: { availability: true } }); - return { schedule: schemaSchedulePublic.parse(result) }; -} - -async function checkPermissions(req: NextApiRequest, body: z.infer) { - const { isSystemWideAdmin } = req; - if (isSystemWideAdmin) return; - if (body.userId) { - throw new HttpError({ statusCode: 403, message: "Non admin cannot change the owner of a schedule" }); - } - //_auth-middleware takes care of verifying the ownership of schedule. -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/schedules/[id]/index.ts b/apps/api/v1/pages/api/schedules/[id]/index.ts deleted file mode 100644 index 54980cf49469ef..00000000000000 --- a/apps/api/v1/pages/api/schedules/[id]/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "./_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/schedules/_get.ts b/apps/api/v1/pages/api/schedules/_get.ts deleted file mode 100644 index a896a4c54b05dc..00000000000000 --- a/apps/api/v1/pages/api/schedules/_get.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { NextApiRequest } from "next"; -import { z } from "zod"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaSchedulePublic } from "~/lib/validations/schedule"; -import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; - -export const schemaUserIds = z - .union([z.string(), z.array(z.string())]) - .transform((val) => (Array.isArray(val) ? val.map((v) => parseInt(v, 10)) : [parseInt(val, 10)])); - -/** - * @swagger - * /schedules: - * get: - * operationId: listSchedules - * summary: Find all schedules - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * tags: - * - schedules - * responses: - * 200: - * description: OK - * content: - * application/json: - * examples: - * schedules: - * value: - * { - * "schedules": [ - * { - * "id": 1234, - * "userId": 5678, - * "name": "Sample Schedule 1", - * "timeZone": "America/Chicago", - * "availability": [ - * { - * "id": 987, - * "eventTypeId": null, - * "days": [1, 2, 3, 4, 5], - * "startTime": "09:00:00", - * "endTime": "23:00:00" - * } - * ] - * }, - * { - * "id": 2345, - * "userId": 6789, - * "name": "Sample Schedule 2", - * "timeZone": "Europe/Amsterdam", - * "availability": [ - * { - * "id": 876, - * "eventTypeId": null, - * "days": [1, 2, 3, 4, 5], - * "startTime": "09:00:00", - * "endTime": "17:00:00" - * } - * ] - * } - * ] - * } - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No schedules were found - */ - -async function handler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const args: Prisma.ScheduleFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } }; - args.include = { availability: true }; - - if (!isSystemWideAdmin && req.query.userId) - throw new HttpError({ - statusCode: 401, - message: "Unauthorized: Only admins can query other users", - }); - - if (isSystemWideAdmin && req.query.userId) { - const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); - const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; - args.where = { userId: { in: userIds } }; - if (Array.isArray(query.userId)) args.orderBy = { userId: "asc" }; - } - const data = await prisma.schedule.findMany(args); - return { schedules: data.map((s) => schemaSchedulePublic.parse(s)) }; -} - -export default defaultResponder(handler); diff --git a/apps/api/v1/pages/api/schedules/_post.ts b/apps/api/v1/pages/api/schedules/_post.ts deleted file mode 100644 index 7abee87cca0dde..00000000000000 --- a/apps/api/v1/pages/api/schedules/_post.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaCreateScheduleBodyParams, schemaSchedulePublic } from "~/lib/validations/schedule"; - -/** - * @swagger - * /schedules: - * post: - * operationId: addSchedule - * summary: Creates a new schedule - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * requestBody: - * description: Create a new schedule - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * - timeZone - * properties: - * name: - * type: string - * description: Name of the schedule - * timeZone: - * type: string - * description: The timeZone for this schedule - * examples: - * schedule: - * value: - * { - * "name": "Sample Schedule", - * "timeZone": "Asia/Calcutta" - * } - * tags: - * - schedules - * responses: - * 200: - * description: OK, schedule created - * content: - * application/json: - * examples: - * schedule: - * value: - * { - * "schedule": { - * "id": 79471, - * "userId": 182, - * "name": "Total Testing", - * "timeZone": "Asia/Calcutta", - * "availability": [ - * { - * "id": 337917, - * "eventTypeId": null, - * "days": [1, 2, 3, 4, 5], - * "startTime": "09:00:00", - * "endTime": "17:00:00" - * } - * ] - * }, - * "message": "Schedule created successfully" - * } - * 400: - * description: Bad request. Schedule body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ - -async function postHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const body = schemaCreateScheduleBodyParams.parse(req.body); - let args: Prisma.ScheduleCreateArgs = { data: { ...body, userId } }; - - /* If ADMIN we create the schedule for selected user */ - if (isSystemWideAdmin && body.userId) args = { data: { ...body, userId: body.userId } }; - - if (!isSystemWideAdmin && body.userId) - throw new HttpError({ statusCode: 403, message: "ADMIN required for `userId`" }); - - // We create default availabilities for the schedule - args.data.availability = { - createMany: { - data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE).map((schedule) => ({ - days: schedule.days, - startTime: schedule.startTime, - endTime: schedule.endTime, - })), - }, - }; - // We include the recently created availability - args.include = { availability: true }; - - const data = await prisma.schedule.create(args); - - return { - schedule: schemaSchedulePublic.parse(data), - message: "Schedule created successfully", - }; -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/schedules/index.ts b/apps/api/v1/pages/api/schedules/index.ts deleted file mode 100644 index e89b47c30e817a..00000000000000 --- a/apps/api/v1/pages/api/schedules/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/selected-calendars/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/selected-calendars/[id]/_auth-middleware.ts deleted file mode 100644 index 09ce0c39407a70..00000000000000 --- a/apps/api/v1/pages/api/selected-calendars/[id]/_auth-middleware.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; - -import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar"; - -async function authMiddleware(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { userId: queryUserId } = selectedCalendarIdSchema.parse(req.query); - // Admins can just skip this check - if (isSystemWideAdmin) return; - // Check if the current user requesting is the same as the one being requested - if (userId !== queryUserId) throw new HttpError({ statusCode: 403, message: "Forbidden" }); -} - -export default authMiddleware; diff --git a/apps/api/v1/pages/api/selected-calendars/[id]/_delete.ts b/apps/api/v1/pages/api/selected-calendars/[id]/_delete.ts deleted file mode 100644 index 1d6835c5f209bf..00000000000000 --- a/apps/api/v1/pages/api/selected-calendars/[id]/_delete.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { SelectedCalendarRepository } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository"; - -import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar"; - -/** - * @swagger - * /selected-calendars/{userId}_{integration}_{externalId}: - * delete: - * operationId: removeSelectedCalendarById - * summary: Remove a selected calendar - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * - in: path - * name: userId - * schema: - * type: integer - * required: true - * description: userId of the selected calendar to get - * - in: path - * name: externalId - * schema: - * type: integer - * required: true - * description: externalId of the selected-calendar to get - * - in: path - * name: integration - * schema: - * type: string - * required: true - * description: integration of the selected calendar to get - * tags: - * - selected-calendars - * responses: - * 201: - * description: OK, selected-calendar removed successfully - * 400: - * description: Bad request. SelectedCalendar id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function deleteHandler(req: NextApiRequest) { - const { query } = req; - const userId_integration_externalId = selectedCalendarIdSchema.parse(query); - await SelectedCalendarRepository.deleteUserLevel({ - where: userId_integration_externalId, - }); - return { message: `Selected Calendar with id: ${query.id} deleted successfully` }; -} - -export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/selected-calendars/[id]/_get.ts b/apps/api/v1/pages/api/selected-calendars/[id]/_get.ts deleted file mode 100644 index 86698c6e1c8209..00000000000000 --- a/apps/api/v1/pages/api/selected-calendars/[id]/_get.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { SelectedCalendarRepository } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository"; - -import { schemaSelectedCalendarPublic, selectedCalendarIdSchema } from "~/lib/validations/selected-calendar"; - -/** - * @swagger - * /selected-calendars/{userId}_{integration}_{externalId}: - * get: - * operationId: getSelectedCalendarById - * summary: Find a selected calendar by providing the compoundId(userId_integration_externalId) separated by `_` - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * - in: path - * name: userId - * schema: - * type: integer - * required: true - * description: userId of the selected calendar to get - * - in: path - * name: externalId - * schema: - * type: string - * required: true - * description: externalId of the selected calendar to get - * - in: path - * name: integration - * schema: - * type: string - * required: true - * description: integration of the selected calendar to get - * tags: - * - selected-calendars - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: SelectedCalendar was not found - */ -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const userId_integration_externalId = selectedCalendarIdSchema.parse(query); - const data = await SelectedCalendarRepository.findUserLevelUniqueOrThrow({ - where: userId_integration_externalId, - }); - return { selected_calendar: schemaSelectedCalendarPublic.parse(data) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/selected-calendars/[id]/_patch.ts b/apps/api/v1/pages/api/selected-calendars/[id]/_patch.ts deleted file mode 100644 index 1066c6cadd720e..00000000000000 --- a/apps/api/v1/pages/api/selected-calendars/[id]/_patch.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import type { UpdateArguments } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository"; -import { SelectedCalendarRepository } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { - schemaSelectedCalendarPublic, - schemaSelectedCalendarUpdateBodyParams, - selectedCalendarIdSchema, -} from "~/lib/validations/selected-calendar"; - -/** - * @swagger - * /selected-calendars/{userId}_{integration}_{externalId}: - * patch: - * operationId: editSelectedCalendarById - * summary: Edit a selected calendar - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * - in: path - * name: userId - * schema: - * type: integer - * required: true - * description: userId of the selected calendar to get - * - in: path - * name: externalId - * schema: - * type: string - * required: true - * description: externalId of the selected calendar to get - * - in: path - * name: integration - * schema: - * type: string - * required: true - * description: integration of the selected calendar to get - * tags: - * - selected-calendars - * responses: - * 201: - * description: OK, selected-calendar edited successfully - * 400: - * description: Bad request. SelectedCalendar body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function patchHandler(req: NextApiRequest) { - const { query, isSystemWideAdmin } = req; - const userId_integration_externalId = selectedCalendarIdSchema.parse(query); - const { userId: bodyUserId, ...data } = schemaSelectedCalendarUpdateBodyParams.parse(req.body); - const args: UpdateArguments = { where: { ...userId_integration_externalId }, data }; - - if (!isSystemWideAdmin && bodyUserId) - throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); - - if (isSystemWideAdmin && bodyUserId) { - const where: Prisma.UserWhereInput = { id: bodyUserId }; - await prisma.user.findFirstOrThrow({ where }); - args.data.userId = bodyUserId; - } - - const result = await SelectedCalendarRepository.updateUserLevel(args); - return { selected_calendar: schemaSelectedCalendarPublic.parse(result) }; -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/selected-calendars/[id]/index.ts b/apps/api/v1/pages/api/selected-calendars/[id]/index.ts deleted file mode 100644 index 54980cf49469ef..00000000000000 --- a/apps/api/v1/pages/api/selected-calendars/[id]/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "./_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/selected-calendars/_get.ts b/apps/api/v1/pages/api/selected-calendars/_get.ts deleted file mode 100644 index 6ce2e40db0fec5..00000000000000 --- a/apps/api/v1/pages/api/selected-calendars/_get.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import type { FindManyArgs } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository"; -import { SelectedCalendarRepository } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository"; - -import { schemaSelectedCalendarPublic } from "~/lib/validations/selected-calendar"; -import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; - -/** - * @swagger - * /selected-calendars: - * get: - * operationId: listSelectedCalendars - * summary: Find all selected calendars - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * tags: - * - selected-calendars - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No selected calendars were found - */ -async function getHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - /* Admin gets all selected calendar by default, otherwise only the user's ones */ - const args: FindManyArgs = isSystemWideAdmin ? {} : { where: { userId } }; - - /** Only admins can query other users */ - if (!isSystemWideAdmin && req.query.userId) - throw new HttpError({ statusCode: 403, message: "ADMIN required" }); - if (isSystemWideAdmin && req.query.userId) { - const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); - const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; - args.where = { userId: { in: userIds } }; - if (Array.isArray(query.userId)) args.orderBy = { userId: "asc" }; - } - - const data = await SelectedCalendarRepository.findManyUserLevel(args); - return { selected_calendars: data.map((v) => schemaSelectedCalendarPublic.parse(v)) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/selected-calendars/_post.ts b/apps/api/v1/pages/api/selected-calendars/_post.ts deleted file mode 100644 index b80c9a775e13e5..00000000000000 --- a/apps/api/v1/pages/api/selected-calendars/_post.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { SelectedCalendarRepository } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { - schemaSelectedCalendarBodyParams, - schemaSelectedCalendarPublic, -} from "~/lib/validations/selected-calendar"; - -/** - * @swagger - * /selected-calendars: - * post: - * summary: Creates a new selected calendar - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * requestBody: - * description: Create a new selected calendar - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - integration - * - externalId - * properties: - * integration: - * type: string - * description: The integration name - * externalId: - * type: string - * description: The external ID of the integration - * tags: - * - selected-calendars - * responses: - * 201: - * description: OK, selected calendar created - * 400: - * description: Bad request. SelectedCalendar body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function postHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { userId: bodyUserId, ...body } = schemaSelectedCalendarBodyParams.parse(req.body); - const args: { - data: Prisma.SelectedCalendarUncheckedCreateInput; - } = { - data: { ...body, userId }, - }; - - if (!isSystemWideAdmin && bodyUserId) - throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); - - if (isSystemWideAdmin && bodyUserId) { - const where: Prisma.UserWhereInput = { id: bodyUserId }; - await prisma.user.findFirstOrThrow({ where }); - args.data.userId = bodyUserId; - } - - const data = await SelectedCalendarRepository.create(args.data); - - return { - selected_calendar: schemaSelectedCalendarPublic.parse(data), - message: "Selected Calendar created successfully", - }; -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/selected-calendars/index.ts b/apps/api/v1/pages/api/selected-calendars/index.ts deleted file mode 100644 index e89b47c30e817a..00000000000000 --- a/apps/api/v1/pages/api/selected-calendars/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/slots/_get.test.ts b/apps/api/v1/pages/api/slots/_get.test.ts deleted file mode 100644 index e7ed5bd126b55a..00000000000000 --- a/apps/api/v1/pages/api/slots/_get.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import prismock from "@calcom/testing/lib/__mocks__/prisma"; - -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, expect, test } from "vitest"; - -import dayjs from "@calcom/dayjs"; - -import handler from "./_get"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -function buildMockData() { - prismock.user.create({ - data: { - id: 1, - username: "test", - name: "Test User", - email: "test@example.com", - }, - }); - prismock.eventType.create({ - data: { - id: 1, - slug: "test", - length: 30, - title: "Test Event Type", - userId: 1, - }, - }); -} - -describe("GET /api/slots", () => { - describe("Errors", () => { - test("Missing required data", async () => { - const { req, res } = createMocks({ - method: "GET", - }); - - await handler(req, res); - - expect(res.statusCode).toBe(400); - }); - }); - - describe("Success", () => { - describe("Regular event-type", () => { - test("Returns and event type available slots", async () => { - const { req, res } = createMocks({ - method: "GET", - query: { - eventTypeId: 1, - startTime: dayjs().format(), - endTime: dayjs().add(1, "day").format(), - usernameList: "test", - }, - prisma: prismock, - }); - buildMockData(); - await handler(req, res); - console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) }); - const response = JSON.parse(res._getData()); - expect(response.slots).toEqual(expect.objectContaining({})); - }); - test("Returns and event type available slots with passed timeZone", async () => { - const { req, res } = createMocks({ - method: "GET", - query: { - eventTypeId: 1, - startTime: dayjs().format(), - endTime: dayjs().add(1, "day").format(), - usernameList: "test", - timeZone: "UTC", - }, - prisma: prismock, - }); - buildMockData(); - await handler(req, res); - console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) }); - const response = JSON.parse(res._getData()); - expect(response.slots).toEqual(expect.objectContaining({})); - }); - }); - }); -}); diff --git a/apps/api/v1/pages/api/slots/_get.ts b/apps/api/v1/pages/api/slots/_get.ts deleted file mode 100644 index c6dd674f1e4413..00000000000000 --- a/apps/api/v1/pages/api/slots/_get.ts +++ /dev/null @@ -1,61 +0,0 @@ -import timezone from "dayjs/plugin/timezone"; -import utc from "dayjs/plugin/utc"; -import type { NextApiRequest, NextApiResponse } from "next"; - -import dayjs from "@calcom/dayjs"; -import { getAvailableSlotsService } from "@calcom/features/di/containers/AvailableSlots"; -import { isSupportedTimeZone } from "@calcom/lib/dayjs"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { createContext } from "@calcom/trpc/server/createContext"; -import { getScheduleSchema } from "@calcom/trpc/server/routers/viewer/slots/types"; - -import { TRPCError } from "@trpc/server"; -import { getHTTPStatusCodeFromError } from "@trpc/server/http"; - -// Apply plugins -dayjs.extend(utc); -dayjs.extend(timezone); - -async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - const { usernameList, isTeamEvent, ...rest } = req.query; - const parsedIsTeamEvent = String(isTeamEvent).toLowerCase() === "true"; - let slugs = usernameList; - if (!Array.isArray(usernameList)) { - slugs = usernameList ? [usernameList] : undefined; - } - const input = getScheduleSchema.parse({ usernameList: slugs, isTeamEvent: parsedIsTeamEvent, ...rest }); - const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false; - - const availableSlotsService = getAvailableSlotsService(); - const availableSlots = await availableSlotsService.getAvailableSlots({ - ctx: await createContext({ req, res }), - input, - }); - - const slotsInProvidedTimeZone = timeZoneSupported - ? Object.keys(availableSlots.slots).reduce( - (acc: Record, date) => { - acc[date] = availableSlots.slots[date].map((slot) => ({ - ...slot, - time: dayjs(slot.time).tz(input.timeZone).format(), - })); - return acc; - }, - {} - ) - : availableSlots.slots; - - return { slots: slotsInProvidedTimeZone }; - - } catch (cause) { - if (cause instanceof TRPCError) { - const statusCode = getHTTPStatusCodeFromError(cause); - throw new HttpError({ statusCode, message: cause.message }); - } - throw cause; - } -} - -export default defaultResponder(handler); diff --git a/apps/api/v1/pages/api/slots/index.ts b/apps/api/v1/pages/api/slots/index.ts deleted file mode 100644 index c97e6cd4dfd429..00000000000000 --- a/apps/api/v1/pages/api/slots/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - }) -); diff --git a/apps/api/v1/pages/api/teams/[teamId]/_auth-middleware.ts b/apps/api/v1/pages/api/teams/[teamId]/_auth-middleware.ts deleted file mode 100644 index 2749c1b5a81b8a..00000000000000 --- a/apps/api/v1/pages/api/teams/[teamId]/_auth-middleware.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; -import { MembershipRole } from "@calcom/prisma/enums"; - -import { schemaQueryTeamId } from "~/lib/validations/shared/queryTeamId"; - -async function authMiddleware(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { teamId } = schemaQueryTeamId.parse(req.query); - /** Admins can skip the ownership verification */ - if (isSystemWideAdmin) return; - /** Non-members will see a 404 error which may or not be the desired behavior. */ - await prisma.team.findFirstOrThrow({ - where: { id: teamId, members: { some: { userId } } }, - }); -} - -export async function checkPermissions( - req: NextApiRequest, - role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER -) { - const { userId, isSystemWideAdmin } = req; - const { teamId } = schemaQueryTeamId.parse({ - teamId: req.query.teamId, - version: req.query.version, - apiKey: req.query.apiKey, - }); - return canUserAccessTeamWithRole(userId, isSystemWideAdmin, teamId, role); -} - -export async function canUserAccessTeamWithRole( - userId: number, - isSystemWideAdmin: boolean, - teamId: number, - role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER -) { - const args: Prisma.TeamFindFirstArgs = { where: { id: teamId } }; - /** If not ADMIN then we check if the actual user belongs to team and matches the required role */ - if (!isSystemWideAdmin) args.where = { ...args.where, members: { some: { userId, role } } }; - const team = await prisma.team.findFirst(args); - if (!team) throw new HttpError({ statusCode: 401, message: `Unauthorized: OWNER or ADMIN role required` }); - return team; -} - -export default authMiddleware; diff --git a/apps/api/v1/pages/api/teams/[teamId]/_delete.ts b/apps/api/v1/pages/api/teams/[teamId]/_delete.ts deleted file mode 100644 index 356220d93997f9..00000000000000 --- a/apps/api/v1/pages/api/teams/[teamId]/_delete.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaQueryTeamId } from "~/lib/validations/shared/queryTeamId"; - -import { checkPermissions } from "./_auth-middleware"; - -/** - * @swagger - * /teams/{teamId}: - * delete: - * operationId: removeTeamById - * summary: Remove an existing team - * parameters: - * - in: path - * name: teamId - * schema: - * type: integer - * required: true - * description: ID of the team to delete - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - teams - * responses: - * 201: - * description: OK, team removed successfully - * 400: - * description: Bad request. Team id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function deleteHandler(req: NextApiRequest) { - const { query } = req; - const { teamId } = schemaQueryTeamId.parse(query); - await checkPermissions(req); - await prisma.team.delete({ where: { id: teamId } }); - return { message: `Team with id: ${teamId} deleted successfully` }; -} - -export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/teams/[teamId]/_get.ts b/apps/api/v1/pages/api/teams/[teamId]/_get.ts deleted file mode 100644 index 96fd8f301d3f5b..00000000000000 --- a/apps/api/v1/pages/api/teams/[teamId]/_get.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaQueryTeamId } from "~/lib/validations/shared/queryTeamId"; -import { schemaTeamReadPublic } from "~/lib/validations/team"; - -/** - * @swagger - * /teams/{teamId}: - * get: - * operationId: getTeamById - * summary: Find a team - * parameters: - * - in: path - * name: teamId - * schema: - * type: integer - * required: true - * description: ID of the team to get - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - teams - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Team was not found - */ -export async function getHandler(req: NextApiRequest) { - const { isSystemWideAdmin, userId } = req; - const { teamId } = schemaQueryTeamId.parse(req.query); - const where: Prisma.TeamWhereInput = { id: teamId }; - // Non-admins can only query the teams they're part of - if (!isSystemWideAdmin) where.members = { some: { userId } }; - const data = await prisma.team.findFirstOrThrow({ where }); - return { team: schemaTeamReadPublic.parse(data) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/teams/[teamId]/_patch.ts b/apps/api/v1/pages/api/teams/[teamId]/_patch.ts deleted file mode 100644 index b588c0147afb6f..00000000000000 --- a/apps/api/v1/pages/api/teams/[teamId]/_patch.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments"; -import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository"; -import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { TRPCError } from "@trpc/server"; - -import { schemaQueryTeamId } from "~/lib/validations/shared/queryTeamId"; -import { schemaTeamReadPublic, schemaTeamUpdateBodyParams } from "~/lib/validations/team"; - -/** - * @swagger - * /teams/{teamId}: - * patch: - * operationId: editTeamById - * summary: Edit an existing team - * parameters: - * - in: path - * name: teamId - * schema: - * type: integer - * required: true - * description: ID of the team to edit - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * requestBody: - * description: Create a new custom input for an event type - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * description: Name of the team - * slug: - * type: string - * description: A unique slug that works as path for the team public page - * tags: - * - teams - * responses: - * 201: - * description: OK, team edited successfully - * 400: - * description: Bad request. Team body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function patchHandler(req: NextApiRequest) { - const { body, userId } = req; - const data = schemaTeamUpdateBodyParams.parse(body); - const { teamId } = schemaQueryTeamId.parse(req.query); - - /** Only OWNERS and ADMINS can edit teams */ - const team = await prisma.team.findFirst({ - // eslint-disable-next-line @calcom/eslint/no-prisma-include-true - include: { members: true }, - where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } }, - }); - if (!team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" }); - - if (data.slug) { - const teamRepository = new TeamRepository(prisma); - const isSlugAvailable = await teamRepository.isSlugAvailableForUpdate({ - slug: data.slug, - teamId: team.id, - parentId: team.parentId, - }); - if (!isSlugAvailable) { - throw new HttpError({ statusCode: 409, message: "Team slug already exists" }); - } - } - - // Check if parentId is related to this user - if (data.parentId && data.parentId === teamId) { - throw new HttpError({ - statusCode: 400, - message: "Bad request: Parent id cannot be the same as the team id.", - }); - } - if (data.parentId) { - const parentTeam = await prisma.team.findFirst({ - where: { id: data.parentId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } }, - }); - if (!parentTeam) - throw new HttpError({ - statusCode: 401, - message: "Unauthorized: Invalid parent id. You can only use parent id of your own teams.", - }); - } - - let paymentUrl; - if (team.slug === null && data.slug) { - data.metadata = { - ...(team.metadata as Prisma.JsonObject), - requestedSlug: data.slug, - }; - delete data.slug; - if (IS_TEAM_BILLING_ENABLED) { - const checkoutSession = await purchaseTeamOrOrgSubscription({ - teamId: team.id, - seatsUsed: team.members.length, - userId, - pricePerSeat: null, - }); - if (!checkoutSession.url) - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed retrieving a checkout session URL.", - }); - paymentUrl = checkoutSession.url; - } - } - - // TODO: Perhaps there is a better fix for this? - const cloneData: typeof data & { - metadata: NonNullable | undefined; - bookingLimits: NonNullable | undefined; - } = { - ...data, - smsLockReviewedByAdmin: false, - bookingLimits: data.bookingLimits === null ? {} : data.bookingLimits, - metadata: data.metadata === null ? {} : data.metadata || undefined, - }; - const updatedTeam = await prisma.team.update({ where: { id: teamId }, data: cloneData }); - const result = { - team: schemaTeamReadPublic.parse(updatedTeam), - paymentUrl, - }; - if (!paymentUrl) { - delete result.paymentUrl; - } - return result; -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/teams/[teamId]/availability/index.ts b/apps/api/v1/pages/api/teams/[teamId]/availability/index.ts deleted file mode 100644 index 306145d3a12718..00000000000000 --- a/apps/api/v1/pages/api/teams/[teamId]/availability/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("~/pages/api/availability/_get"), - }) -); diff --git a/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts b/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts deleted file mode 100644 index 637af631d6cb13..00000000000000 --- a/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { NextApiRequest } from "next"; -import { z } from "zod"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { prisma } from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { eventTypeSelect } from "~/lib/selects/event-type"; - -const querySchema = z.object({ - teamId: z.coerce.number(), -}); - -/** - * @swagger - * /teams/{teamId}/event-types: - * get: - * summary: Find all event types that belong to teamId - * operationId: listEventTypesByTeamId - * parameters: - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API Key - * - in: path - * name: teamId - * schema: - * type: number - * required: true - * tags: - * - event-types - * externalDocs: - * url: https://docs.cal.com/docs/core-features/event-types - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No event types were found - */ -async function getHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - - const { teamId } = querySchema.parse(req.query); - - const args: Prisma.EventTypeFindManyArgs = { - where: { - team: isSystemWideAdmin - ? { - id: teamId, - } - : { - id: teamId, - members: { some: { userId } }, - }, - }, - select: eventTypeSelect, - }; - return { - event_types: await prisma.eventType.findMany(args), - }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/teams/[teamId]/event-types/index.ts b/apps/api/v1/pages/api/teams/[teamId]/event-types/index.ts deleted file mode 100644 index c97e6cd4dfd429..00000000000000 --- a/apps/api/v1/pages/api/teams/[teamId]/event-types/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - }) -); diff --git a/apps/api/v1/pages/api/teams/[teamId]/index.ts b/apps/api/v1/pages/api/teams/[teamId]/index.ts deleted file mode 100644 index 54980cf49469ef..00000000000000 --- a/apps/api/v1/pages/api/teams/[teamId]/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "./_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/teams/[teamId]/publish.ts b/apps/api/v1/pages/api/teams/[teamId]/publish.ts deleted file mode 100644 index 665d5bd391f212..00000000000000 --- a/apps/api/v1/pages/api/teams/[teamId]/publish.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums"; -import { createContext } from "@calcom/trpc/server/createContext"; -import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router"; -import { createCallerFactory } from "@calcom/trpc/server/trpc"; -import type { UserProfile } from "@calcom/types/UserProfile"; - -import { TRPCError } from "@trpc/server"; -import { getHTTPStatusCodeFromError } from "@trpc/server/http"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware, { checkPermissions } from "./_auth-middleware"; - -const patchHandler = async (req: NextApiRequest, res: NextApiResponse) => { - await checkPermissions(req, { in: [MembershipRole.OWNER, MembershipRole.ADMIN] }); - async function sessionGetter() { - return { - user: { - id: req.userId, - uuid: req.userUuid, - username: "" /* Not used in this context */, - role: req.isSystemWideAdmin ? UserPermissionRole.ADMIN : UserPermissionRole.USER, - profile: { - id: null, - organizationId: null, - organization: null, - username: "", - upId: "", - } satisfies UserProfile, - }, - hasValidLicense: true /* To comply with TS signature */, - expires: "" /* Not used in this context */, - upId: "", - }; - } - /** @see https://trpc.io/docs/server-side-calls */ - const ctx = await createContext({ req, res }, sessionGetter); - try { - const createCaller = createCallerFactory(viewerTeamsRouter); - const caller = createCaller(ctx); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return await caller.publish(req.query as any /* Let tRPC handle this */); - } catch (cause) { - if (cause instanceof TRPCError) { - const statusCode = getHTTPStatusCodeFromError(cause); - throw new HttpError({ statusCode, message: cause.message }); - } - throw cause; - } -}; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - PATCH: Promise.resolve({ default: defaultResponder(patchHandler) }), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/teams/_get.ts b/apps/api/v1/pages/api/teams/_get.ts deleted file mode 100644 index 4e22acee3eb616..00000000000000 --- a/apps/api/v1/pages/api/teams/_get.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaTeamsReadPublic } from "~/lib/validations/team"; - -/** - * @swagger - * /teams: - * get: - * operationId: listTeams - * summary: Find all teams - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - teams - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No teams were found - */ -async function getHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const where: Prisma.TeamWhereInput = {}; - // If user is not ADMIN, return only his data. - if (!isSystemWideAdmin) where.members = { some: { userId } }; - const data = await prisma.team.findMany({ where }); - return { teams: schemaTeamsReadPublic.parse(data) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/teams/_post.ts b/apps/api/v1/pages/api/teams/_post.ts deleted file mode 100644 index 133780a7d3d98e..00000000000000 --- a/apps/api/v1/pages/api/teams/_post.ts +++ /dev/null @@ -1,249 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer"; -import { getDubCustomer } from "@calcom/features/auth/lib/dub"; -import stripe from "@calcom/features/ee/payments/server/stripe"; -import { IS_PRODUCTION } from "@calcom/lib/constants"; -import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import { MembershipRole } from "@calcom/prisma/enums"; - -import { schemaMembershipPublic } from "~/lib/validations/membership"; -import { schemaTeamCreateBodyParams, schemaTeamReadPublic } from "~/lib/validations/team"; - -/** - * @swagger - * /teams: - * post: - * operationId: addTeam - * summary: Creates a new team - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * requestBody: - * description: Create a new custom input for an event type - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * - slug - * - hideBookATeamMember - * - brandColor - * - darkBrandColor - * - timeZone - * - weekStart - * - isPrivate - * properties: - * name: - * type: string - * description: Name of the team - * slug: - * type: string - * description: A unique slug that works as path for the team public page - * hideBookATeamMember: - * type: boolean - * description: Flag to hide or show the book a team member option - * brandColor: - * type: string - * description: Primary brand color for the team - * darkBrandColor: - * type: string - * description: Dark variant of the primary brand color for the team - * timeZone: - * type: string - * description: Time zone of the team - * weekStart: - * type: string - * description: Starting day of the week for the team - * isPrivate: - * type: boolean - * description: Flag indicating if the team is private - * ownerId: - * type: number - * description: ID of the team owner - only admins can set this. - * parentId: - * type: number - * description: ID of the parent organization. - * tags: - * - teams - * responses: - * 201: - * description: OK, team created - * 400: - * description: Bad request. Team body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function postHandler(req: NextApiRequest) { - const { body, userId, isSystemWideAdmin } = req; - const { ownerId, ...data } = schemaTeamCreateBodyParams.parse(body); - - await checkPermissions(req); - - const effectiveUserId = isSystemWideAdmin && ownerId ? ownerId : userId; - - if (data.slug) { - const alreadyExist = await prisma.team.findFirst({ - where: { - slug: { - mode: "insensitive", - equals: data.slug, - }, - }, - }); - if (alreadyExist) throw new HttpError({ statusCode: 409, message: "Team slug already exists" }); - } - - // Check if parentId is related to this user and is an organization - if (data.parentId) { - const parentTeam = await prisma.team.findFirst({ - where: { id: data.parentId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } }, - }); - if (!parentTeam) - throw new HttpError({ - statusCode: 401, - message: "Unauthorized: Invalid parent id. You can only use parent id if you are org owner or admin.", - }); - - if (parentTeam.parentId) - throw new HttpError({ - statusCode: 400, - message: "parentId must be of an organization, not a team.", - }); - } - - // TODO: Perhaps there is a better fix for this? - const cloneData: typeof data & { - metadata: NonNullable | undefined; - bookingLimits: NonNullable | undefined; - } = { - ...data, - smsLockReviewedByAdmin: false, - bookingLimits: data.bookingLimits === null ? {} : data.bookingLimits || undefined, - metadata: data.metadata === null ? {} : data.metadata || undefined, - }; - - if (!IS_TEAM_BILLING_ENABLED || data.parentId) { - const team = await prisma.team.create({ - data: { - ...cloneData, - members: { - create: { userId: effectiveUserId, role: MembershipRole.OWNER, accepted: true }, - }, - }, - include: { members: true }, - }); - - req.statusCode = 201; - - return { - team: schemaTeamReadPublic.parse(team), - owner: schemaMembershipPublic.parse(team.members[0]), - message: `Team created successfully. We also made user with ID=${effectiveUserId} the owner of this team.`, - }; - } - - const pendingPaymentTeam = await prisma.team.create({ - data: { - ...cloneData, - pendingPayment: true, - }, - }); - - const checkoutSession = await generateTeamCheckoutSession({ - pendingPaymentTeamId: pendingPaymentTeam.id, - ownerId: effectiveUserId, - }); - - return { - message: - "Your team will be created once we receive your payment. Please complete the payment using the payment link.", - paymentLink: checkoutSession.url, - pendingTeam: { - ...schemaTeamReadPublic.parse(pendingPaymentTeam), - }, - }; -} - -async function checkPermissions(req: NextApiRequest) { - const { isSystemWideAdmin } = req; - const body = schemaTeamCreateBodyParams.parse(req.body); - - /* Non-admin users can only create teams for themselves */ - if (!isSystemWideAdmin && body.ownerId) - throw new HttpError({ - statusCode: 401, - message: "ADMIN required for `ownerId`", - }); -} - -const generateTeamCheckoutSession = async ({ - pendingPaymentTeamId, - ownerId, -}: { - pendingPaymentTeamId: number; - ownerId: number; -}) => { - const [customer, dubCustomer] = await Promise.all([ - getStripeCustomerIdFromUserId(ownerId), - getDubCustomer(ownerId.toString()), - ]); - - const session = await stripe.checkout.sessions.create({ - customer, - mode: "subscription", - ...(dubCustomer?.discount?.couponId - ? { - discounts: [ - { - coupon: - process.env.NODE_ENV !== "production" && dubCustomer.discount.couponTestId - ? dubCustomer.discount.couponTestId - : dubCustomer.discount.couponId, - }, - ], - } - : { allow_promotion_codes: true }), - success_url: `${WEBAPP_URL}/api/teams/api/create?session_id={CHECKOUT_SESSION_ID}`, - cancel_url: `${WEBAPP_URL}/settings/my-account/profile`, - line_items: [ - { - /** We only need to set the base price and we can upsell it directly on Stripe's checkout */ - price: process.env.STRIPE_TEAM_MONTHLY_PRICE_ID, - /**Initially it will be just the team owner */ - quantity: 1, - }, - ], - customer_update: { - address: "auto", - }, - // Disabled when testing locally as usually developer doesn't setup Tax in Stripe Test mode - automatic_tax: { - enabled: IS_PRODUCTION, - }, - metadata: { - pendingPaymentTeamId, - ownerId, - dubCustomerId: ownerId, // pass the userId during checkout creation for sales conversion tracking: https://d.to/conversions/stripe - }, - }); - - if (!session.url) - throw new HttpError({ - statusCode: 500, - message: "Failed generating a checkout session URL.", - }); - - return session; -}; - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/teams/index.ts b/apps/api/v1/pages/api/teams/index.ts deleted file mode 100644 index e89b47c30e817a..00000000000000 --- a/apps/api/v1/pages/api/teams/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/users/[userId]/_delete.ts b/apps/api/v1/pages/api/users/[userId]/_delete.ts deleted file mode 100644 index 27c465ea079254..00000000000000 --- a/apps/api/v1/pages/api/users/[userId]/_delete.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { deleteUser } from "@calcom/features/users/lib/deleteUser"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId"; - -/** - * @swagger - * /users/{userId}: - * delete: - * summary: Remove an existing user - * operationId: removeUserById - * parameters: - * - in: path - * name: userId - * example: 1 - * schema: - * type: integer - * required: true - * description: ID of the user to delete - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API key - * tags: - * - users - * responses: - * 201: - * description: OK, user removed successfully - * 400: - * description: Bad request. User id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function deleteHandler(req: NextApiRequest) { - const { isSystemWideAdmin } = req; - const query = schemaQueryUserId.parse(req.query); - // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user - if (!isSystemWideAdmin && query.userId !== req.userId) - throw new HttpError({ statusCode: 403, message: "Forbidden" }); - - const user = await prisma.user.findUnique({ - where: { id: query.userId }, - select: { - id: true, - email: true, - metadata: true, - }, - }); - if (!user) throw new HttpError({ statusCode: 404, message: "User not found" }); - - await deleteUser(user); - - return { message: `User with id: ${user.id} deleted successfully` }; -} - -export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/users/[userId]/_get.ts b/apps/api/v1/pages/api/users/[userId]/_get.ts deleted file mode 100644 index 36f799b2a81770..00000000000000 --- a/apps/api/v1/pages/api/users/[userId]/_get.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId"; -import { schemaUserReadPublic } from "~/lib/validations/user"; - -/** - * @swagger - * /users/{userId}: - * get: - * summary: Find a user, returns your user if regular user. - * operationId: getUserById - * parameters: - * - in: path - * name: userId - * example: 4 - * schema: - * type: integer - * required: true - * description: ID of the user to get - * - in: query - * name: apiKey - * schema: - * type: string - * required: true - * description: Your API key - * tags: - * - users - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: User was not found - */ -export async function getHandler(req: NextApiRequest) { - const { isSystemWideAdmin } = req; - - const query = schemaQueryUserId.parse(req.query); - // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user - if (!isSystemWideAdmin && query.userId !== req.userId) - throw new HttpError({ statusCode: 403, message: "Forbidden" }); - const data = await prisma.user.findUnique({ where: { id: query.userId } }); - const user = schemaUserReadPublic.parse({ - ...data, - avatar: data?.avatarUrl, - }); - return { user }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/users/[userId]/_patch.ts b/apps/api/v1/pages/api/users/[userId]/_patch.ts deleted file mode 100644 index 96553d1f43ca03..00000000000000 --- a/apps/api/v1/pages/api/users/[userId]/_patch.ts +++ /dev/null @@ -1,204 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { sendChangeOfEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; -import { FeaturesRepository } from "@calcom/features/flags/features.repository"; -import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; -import { HttpError } from "@calcom/lib/http-error"; -import { uploadAvatar } from "@calcom/lib/server/avatar"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId"; -import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validations/user"; - -/** - * @swagger - * /users/{userId}: - * patch: - * summary: Edit an existing user - * operationId: editUserById - * parameters: - * - in: path - * name: userId - * example: 4 - * schema: - * type: integer - * required: true - * description: ID of the user to edit - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * requestBody: - * description: Edit an existing attendee related to one of your bookings - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * email: - * type: string - * format: email - * description: Email that belongs to the user being edited - * username: - * type: string - * description: Username for the user being edited - * brandColor: - * description: The user's brand color - * type: string - * darkBrandColor: - * description: The user's brand color for dark mode - * type: string - * weekStart: - * description: Start of the week. Acceptable values are one of [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY] - * type: string - * timeZone: - * description: The user's time zone - * type: string - * hideBranding: - * description: Remove branding from the user's calendar page - * type: boolean - * theme: - * description: Default theme for the user. Acceptable values are one of [DARK, LIGHT] - * type: string - * timeFormat: - * description: The user's time format. Acceptable values are one of [TWELVE, TWENTY_FOUR] - * type: string - * locale: - * description: The user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI] - * type: string - * avatar: - * description: The user's avatar, in base64 format - * type: string - * examples: - * user: - * summary: An example of USER - * value: - * email: email@example.com - * username: johndoe - * weekStart: MONDAY - * brandColor: #555555 - * darkBrandColor: #111111 - * timeZone: EUROPE/PARIS - * theme: LIGHT - * timeFormat: TWELVE - * locale: FR - * tags: - * - users - * responses: - * 200: - * description: OK, user edited successfully - * 400: - * description: Bad request. User body is invalid. - * 401: - * description: Authorization information is missing or invalid. - * 403: - * description: Insufficient permissions to access resource. - */ -export async function patchHandler(req: NextApiRequest) { - const { isSystemWideAdmin } = req; - const query = schemaQueryUserId.parse(req.query); - // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user - if (!isSystemWideAdmin && query.userId !== req.userId) - throw new HttpError({ statusCode: 403, message: "Forbidden" }); - - const { avatar, ...body }: { avatar?: string | undefined } & Prisma.UserUpdateInput = - await schemaUserEditBodyParams.parseAsync(req.body); - // disable role or branding changes unless admin. - if (!isSystemWideAdmin) { - if (body.role) body.role = undefined; - if (body.hideBranding) body.hideBranding = undefined; - } - - const userSchedules = await prisma.schedule.findMany({ - where: { userId: query.userId }, - }); - const userSchedulesIds = userSchedules.map((schedule) => schedule.id); - // @note: here we make sure user can only make as default his own scheudles - if (body.defaultScheduleId && !userSchedulesIds.includes(Number(body.defaultScheduleId))) { - throw new HttpError({ - statusCode: 400, - message: "Bad request: Invalid default schedule id", - }); - } - - if (avatar) { - body.avatarUrl = await uploadAvatar({ - userId: query.userId, - avatar: await (await import("@calcom/lib/server/resizeBase64Image")).resizeBase64Image(avatar), - }); - } - - const userRepository = new UserRepository(prisma); - const currentUser = await userRepository.findById({ id: query.userId }); - - if (!currentUser) { - throw new HttpError({ statusCode: 404, message: "User not found" }); - } - - const featuresRepository = new FeaturesRepository(prisma); - const emailVerification = await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification"); - - const hasEmailBeenChanged = typeof body.email === "string" && currentUser.email !== body.email; - const newEmail = typeof body.email === "string" ? body.email : undefined; - - const prismaData: Prisma.UserUpdateInput = { ...body }; - - if (hasEmailBeenChanged && newEmail) { - const secondaryEmail = await userRepository.findSecondaryEmailByUserIdAndEmail({ - userId: query.userId, - email: newEmail, - }); - - if (emailVerification) { - if (secondaryEmail && secondaryEmail.emailVerified) { - const data = await userRepository.swapPrimaryEmailWithSecondaryEmail({ - userId: query.userId, - secondaryEmailId: secondaryEmail.id, - oldPrimaryEmail: currentUser.email, - oldPrimaryEmailVerified: currentUser.emailVerified, - newPrimaryEmail: newEmail, - userUpdateData: prismaData, - }); - - const user = schemaUserReadPublic.parse(data); - return { user }; - } else { - prismaData.metadata = { - ...(currentUser.metadata as Prisma.JsonObject), - ...(typeof prismaData.metadata === "object" && prismaData.metadata ? prismaData.metadata : {}), - emailChangeWaitingForVerification: newEmail.toLowerCase(), - }; - - delete prismaData.email; - } - } - } - - const data = await prisma.user.update({ - where: { id: query.userId }, - data: prismaData, - }); - - const shouldSendEmailVerification = - hasEmailBeenChanged && emailVerification && newEmail && !prismaData.email; - - if (shouldSendEmailVerification) { - await sendChangeOfEmailVerification({ - user: { - username: data.username ?? "Nameless User", - emailFrom: currentUser.email, - emailTo: newEmail, - }, - }); - } - - const user = schemaUserReadPublic.parse(data); - return { user }; -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/users/[userId]/availability/index.ts b/apps/api/v1/pages/api/users/[userId]/availability/index.ts deleted file mode 100644 index 306145d3a12718..00000000000000 --- a/apps/api/v1/pages/api/users/[userId]/availability/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("~/pages/api/availability/_get"), - }) -); diff --git a/apps/api/v1/pages/api/users/[userId]/index.ts b/apps/api/v1/pages/api/users/[userId]/index.ts deleted file mode 100644 index 56329a298057b5..00000000000000 --- a/apps/api/v1/pages/api/users/[userId]/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - }) -); diff --git a/apps/api/v1/pages/api/users/_get.ts b/apps/api/v1/pages/api/users/_get.ts deleted file mode 100644 index 9966045ea1137d..00000000000000 --- a/apps/api/v1/pages/api/users/_get.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; -import { schemaQuerySingleOrMultipleUserEmails } from "~/lib/validations/shared/queryUserEmail"; -import { schemaUsersReadPublic } from "~/lib/validations/user"; - -/** - * @swagger - * /users: - * get: - * operationId: listUsers - * summary: Find all users. - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * - in: query - * name: email - * required: false - * schema: - * type: array - * items: - * type: string - * format: email - * style: form - * explode: true - * description: The email address or an array of email addresses to filter by - * tags: - * - users - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No users were found - */ -export async function getHandler(req: NextApiRequest) { - const { - userId, - isSystemWideAdmin, - pagination: { take, skip }, - } = req; - const where: Prisma.UserWhereInput = {}; - // If user is not ADMIN, return only his data. - if (!isSystemWideAdmin) where.id = userId; - - if (req.query.email) { - const validationResult = schemaQuerySingleOrMultipleUserEmails.parse(req.query); - where.email = { - in: Array.isArray(validationResult.email) ? validationResult.email : [validationResult.email], - }; - } - - const [total, data] = await Promise.all([ - prisma.user.count({ where }), - prisma.user.findMany({ where, take, skip }), - ]); - const users = schemaUsersReadPublic.parse(data); - return { users, total }; -} - -export default withMiddleware("pagination")(defaultResponder(getHandler)); diff --git a/apps/api/v1/pages/api/users/_post.ts b/apps/api/v1/pages/api/users/_post.ts deleted file mode 100644 index 3d79edde381a12..00000000000000 --- a/apps/api/v1/pages/api/users/_post.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { UserCreationService } from "@calcom/features/users/services/userCreationService"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { CreationSource } from "@calcom/prisma/enums"; - -import { schemaUserCreateBodyParams } from "~/lib/validations/user"; - -/** - * @swagger - * /users: - * post: - * operationId: addUser - * summary: Creates a new user - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * requestBody: - * description: Create a new user - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - email - * - username - * properties: - * email: - * type: string - * format: email - * description: Email that belongs to the user being edited - * username: - * type: string - * description: Username for the user being created - * brandColor: - * description: The new user's brand color - * type: string - * darkBrandColor: - * description: The new user's brand color for dark mode - * type: string - * hideBranding: - * description: Remove branding from the user's calendar page - * type: boolean - * weekStart: - * description: Start of the week. Acceptable values are one of [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY] - * type: string - * timeZone: - * description: The new user's time zone. Eg- 'EUROPE/PARIS' - * type: string - * theme: - * description: Default theme for the new user. Acceptable values are one of [DARK, LIGHT] - * type: string - * timeFormat: - * description: The new user's time format. Acceptable values are one of [TWELVE, TWENTY_FOUR] - * type: string - * locale: - * description: The new user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI] - * type: string - * avatar: - * description: The user's avatar, in base64 format - * type: string - * examples: - * user: - * summary: An example of USER - * value: - * email: 'email@example.com' - * username: 'johndoe' - * weekStart: 'MONDAY' - * brandColor: '#555555' - * darkBrandColor: '#111111' - * timeZone: 'EUROPE/PARIS' - * theme: 'LIGHT' - * timeFormat: 'TWELVE' - * locale: 'FR' - * tags: - * - users - * responses: - * 201: - * description: OK, user created - * 400: - * description: Bad request. user body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function postHandler(req: NextApiRequest) { - const { isSystemWideAdmin } = req; - // If user is not ADMIN, return unauthorized. - if (!isSystemWideAdmin) throw new HttpError({ statusCode: 401, message: "You are not authorized" }); - const data = await schemaUserCreateBodyParams.parseAsync(req.body); - const user = await UserCreationService.createUser({ - data: { ...data, creationSource: CreationSource.API_V1 }, - }); - req.statusCode = 201; - return { user }; -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/users/index.ts b/apps/api/v1/pages/api/users/index.ts deleted file mode 100644 index e89b47c30e817a..00000000000000 --- a/apps/api/v1/pages/api/users/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/pages/api/webhooks/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/webhooks/[id]/_auth-middleware.ts deleted file mode 100644 index 5598f3ed8b0aa5..00000000000000 --- a/apps/api/v1/pages/api/webhooks/[id]/_auth-middleware.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString"; - -async function authMiddleware(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { id } = schemaQueryIdAsString.parse(req.query); - // Admins can just skip this check - if (isSystemWideAdmin) return; - // Check if the current user can access the webhook - const webhook = await prisma.webhook.findFirst({ - where: { id, appId: null, OR: [{ userId }, { eventType: { team: { members: { some: { userId } } } } }] }, - }); - if (!webhook) throw new HttpError({ statusCode: 403, message: "Forbidden" }); -} - -export default authMiddleware; diff --git a/apps/api/v1/pages/api/webhooks/[id]/_delete.ts b/apps/api/v1/pages/api/webhooks/[id]/_delete.ts deleted file mode 100644 index f36d568dd3fc3b..00000000000000 --- a/apps/api/v1/pages/api/webhooks/[id]/_delete.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString"; - -/** - * @swagger - * /webhooks/{id}: - * delete: - * summary: Remove an existing hook - * operationId: removeWebhookById - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: Numeric ID of the hooks to delete - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - webhooks - * externalDocs: - * url: https://docs.cal.com/docs/core-features/webhooks - * responses: - * 201: - * description: OK, hook removed successfully - * 400: - * description: Bad request. hook id is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function deleteHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdAsString.parse(query); - await prisma.webhook.delete({ where: { id } }); - return { message: `Webhook with id: ${id} deleted successfully` }; -} - -export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/webhooks/[id]/_get.ts b/apps/api/v1/pages/api/webhooks/[id]/_get.ts deleted file mode 100644 index 405027c4955843..00000000000000 --- a/apps/api/v1/pages/api/webhooks/[id]/_get.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString"; -import { schemaWebhookReadPublic } from "~/lib/validations/webhook"; - -/** - * @swagger - * /webhooks/{id}: - * get: - * summary: Find a webhook - * operationId: getWebhookById - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: Numeric ID of the webhook to get - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - webhooks - * externalDocs: - * url: https://docs.cal.com/docs/core-features/webhooks - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: Webhook was not found - */ -export async function getHandler(req: NextApiRequest) { - const { query } = req; - const { id } = schemaQueryIdAsString.parse(query); - const data = await prisma.webhook.findUniqueOrThrow({ where: { id } }); - return { webhook: schemaWebhookReadPublic.parse(data) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/webhooks/[id]/_patch.ts b/apps/api/v1/pages/api/webhooks/[id]/_patch.ts deleted file mode 100644 index f78c8a81771d08..00000000000000 --- a/apps/api/v1/pages/api/webhooks/[id]/_patch.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString"; -import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/validations/webhook"; - -/** - * @swagger - * /webhooks/{id}: - * patch: - * summary: Edit an existing webhook - * operationId: editWebhookById - * parameters: - * - in: path - * name: id - * schema: - * type: integer - * required: true - * description: Numeric ID of the webhook to edit - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * requestBody: - * description: Edit an existing webhook - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * subscriberUrl: - * type: string - * format: uri - * description: The URL to subscribe to this webhook - * eventTriggers: - * type: string - * enum: [BOOKING_CREATED, BOOKING_RESCHEDULED, BOOKING_CANCELLED, MEETING_ENDED] - * description: The events which should trigger this webhook call - * active: - * type: boolean - * description: Whether the webhook is active and should trigger on associated trigger events - * payloadTemplate: - * type: string - * description: The template of the webhook's payload - * eventTypeId: - * type: number - * description: The event type ID if this webhook should be associated with only that event type - * secret: - * type: string - * description: The secret to verify the authenticity of the received payload - * tags: - * - webhooks - * externalDocs: - * url: https://docs.cal.com/docs/core-features/webhooks - * responses: - * 201: - * description: OK, webhook edited successfully - * 400: - * description: Bad request. Webhook body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -export async function patchHandler(req: NextApiRequest) { - const { query, userId, isSystemWideAdmin } = req; - const { id } = schemaQueryIdAsString.parse(query); - const { - eventTypeId, - userId: bodyUserId, - eventTriggers, - ...data - } = schemaWebhookEditBodyParams.parse(req.body); - const args: Prisma.WebhookUpdateArgs = { where: { id }, data }; - - if (eventTypeId) { - const where: Prisma.EventTypeWhereInput = { id: eventTypeId }; - if (!isSystemWideAdmin) where.userId = userId; - await prisma.eventType.findFirstOrThrow({ where }); - args.data.eventTypeId = eventTypeId; - } - - if (!isSystemWideAdmin && bodyUserId) - throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); - - if (isSystemWideAdmin && bodyUserId) { - const where: Prisma.UserWhereInput = { id: bodyUserId }; - await prisma.user.findFirstOrThrow({ where }); - args.data.userId = bodyUserId; - } - - if (eventTriggers) { - const eventTriggersSet = new Set(eventTriggers); - args.data.eventTriggers = Array.from(eventTriggersSet); - } - - const result = await prisma.webhook.update(args); - return { webhook: schemaWebhookReadPublic.parse(result) }; -} - -export default defaultResponder(patchHandler); diff --git a/apps/api/v1/pages/api/webhooks/[id]/index.ts b/apps/api/v1/pages/api/webhooks/[id]/index.ts deleted file mode 100644 index 54980cf49469ef..00000000000000 --- a/apps/api/v1/pages/api/webhooks/[id]/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -import authMiddleware from "./_auth-middleware"; - -export default withMiddleware()( - defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); - return defaultHandler({ - GET: import("./_get"), - PATCH: import("./_patch"), - DELETE: import("./_delete"), - })(req, res); - }) -); diff --git a/apps/api/v1/pages/api/webhooks/_get.ts b/apps/api/v1/pages/api/webhooks/_get.ts deleted file mode 100644 index 6dd8dc88661214..00000000000000 --- a/apps/api/v1/pages/api/webhooks/_get.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; -import { schemaWebhookReadPublic } from "~/lib/validations/webhook"; - -/** - * @swagger - * /webhooks: - * get: - * summary: Find all webhooks - * operationId: listWebhooks - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * tags: - * - webhooks - * externalDocs: - * url: https://docs.cal.com/docs/core-features/webhooks - * responses: - * 200: - * description: OK - * 401: - * description: Authorization information is missing or invalid. - * 404: - * description: No webhooks were found - */ -async function getHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const args: Prisma.WebhookFindManyArgs = isSystemWideAdmin - ? {} - : { where: { OR: [{ eventType: { userId } }, { userId }] } }; - - /** Only admins can query other users */ - if (!isSystemWideAdmin && req.query.userId) - throw new HttpError({ statusCode: 403, message: "ADMIN required" }); - if (isSystemWideAdmin && req.query.userId) { - const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); - const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; - args.where = { OR: [{ eventType: { userId: { in: userIds } } }, { userId: { in: userIds } }] }; - if (Array.isArray(query.userId)) args.orderBy = { userId: "asc", eventType: { userId: "asc" } }; - } - - const data = await prisma.webhook.findMany(args); - return { webhooks: data.map((v) => schemaWebhookReadPublic.parse(v)) }; -} - -export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/webhooks/_post.ts b/apps/api/v1/pages/api/webhooks/_post.ts deleted file mode 100644 index 9bb6f29f3aaa2e..00000000000000 --- a/apps/api/v1/pages/api/webhooks/_post.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { NextApiRequest } from "next"; -import { v4 as uuidv4 } from "uuid"; - -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; - -import { schemaWebhookCreateBodyParams, schemaWebhookReadPublic } from "~/lib/validations/webhook"; - -/** - * @swagger - * /webhooks: - * post: - * summary: Creates a new webhook - * operationId: addWebhook - * parameters: - * - in: query - * name: apiKey - * required: true - * schema: - * type: string - * description: Your API key - * requestBody: - * description: Create a new webhook - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - subscriberUrl - * - eventTriggers - * - active - * properties: - * subscriberUrl: - * type: string - * format: uri - * description: The URL to subscribe to this webhook - * eventTriggers: - * type: string - * enum: [BOOKING_CREATED, BOOKING_RESCHEDULED, BOOKING_CANCELLED, MEETING_ENDED] - * description: The events which should trigger this webhook call - * active: - * type: boolean - * description: Whether the webhook is active and should trigger on associated trigger events - * payloadTemplate: - * type: string - * description: The template of the webhook's payload - * eventTypeId: - * type: number - * description: The event type ID if this webhook should be associated with only that event type - * secret: - * type: string - * description: The secret to verify the authenticity of the received payload - * tags: - * - webhooks - * externalDocs: - * url: https://docs.cal.com/docs/core-features/webhooks - * responses: - * 201: - * description: OK, webhook created - * 400: - * description: Bad request. webhook body is invalid. - * 401: - * description: Authorization information is missing or invalid. - */ -async function postHandler(req: NextApiRequest) { - const { userId, isSystemWideAdmin } = req; - const { - eventTypeId, - userId: bodyUserId, - eventTriggers, - ...body - } = schemaWebhookCreateBodyParams.parse(req.body); - const args: Prisma.WebhookCreateArgs = { data: { id: uuidv4(), ...body } }; - - // If no event type, we assume is for the current user. If admin we run more checks below... - if (!eventTypeId) args.data.userId = userId; - - if (eventTypeId) { - const where: Prisma.EventTypeWhereInput = { id: eventTypeId }; - if (!isSystemWideAdmin) where.userId = userId; - await prisma.eventType.findFirstOrThrow({ where }); - args.data.eventTypeId = eventTypeId; - } - - if (!isSystemWideAdmin && bodyUserId) - throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` }); - - if (isSystemWideAdmin && bodyUserId) { - const where: Prisma.UserWhereInput = { id: bodyUserId }; - await prisma.user.findFirstOrThrow({ where }); - args.data.userId = bodyUserId; - } - - if (eventTriggers) { - const eventTriggersSet = new Set(eventTriggers); - args.data.eventTriggers = Array.from(eventTriggersSet); - } - - const data = await prisma.webhook.create(args); - - return { - webhook: schemaWebhookReadPublic.parse(data), - message: "Webhook created successfully", - }; -} - -export default defaultResponder(postHandler); diff --git a/apps/api/v1/pages/api/webhooks/index.ts b/apps/api/v1/pages/api/webhooks/index.ts deleted file mode 100644 index e89b47c30e817a..00000000000000 --- a/apps/api/v1/pages/api/webhooks/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultHandler } from "@calcom/lib/server/defaultHandler"; - -import { withMiddleware } from "~/lib/helpers/withMiddleware"; - -export default withMiddleware()( - defaultHandler({ - GET: import("./_get"), - POST: import("./_post"), - }) -); diff --git a/apps/api/v1/proxy.ts b/apps/api/v1/proxy.ts deleted file mode 100644 index 22727871391d40..00000000000000 --- a/apps/api/v1/proxy.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; - -function proxy(_request: NextRequest) { - // Matcher guarantees the last segment is one of the blocked segments, so we can - // immediately return a 403 without further path checks. - return NextResponse.json({ message: "Forbidden" }, { status: 403 }); -} - -export const config = { - // The blocked segment is always the last part of the path, so we only need this matcher. - matcher: [ - "/:path*/_get", - "/:path*/_post", - "/:path*/_patch", - "/:path*/_delete", - "/:path*/_auth-middleware", - ], -}; - -export default proxy; diff --git a/apps/api/v1/scripts/vercel-deploy.sh b/apps/api/v1/scripts/vercel-deploy.sh deleted file mode 100755 index 689616931c6f17..00000000000000 --- a/apps/api/v1/scripts/vercel-deploy.sh +++ /dev/null @@ -1,66 +0,0 @@ -# github submodule repo addresses without https:// prefix -BRANCH_TO_CLONE="" -SUBMODULE_GITHUB=github.com/calcom/api -SUBMODULE_PATH=apps/api -COMMIT=$VERCEL_GIT_COMMIT_SHA - -if [ "$VERCEL_GIT_COMMIT_SHA" == "" ]; then - echo "Error: VERCEL_GIT_COMMIT_SHA is empty" - exit 0 -fi - -# github access token is necessary -# add it to Environment Variables on Vercel -if [ "$GITHUB_ACCESS_TOKEN" == "" ]; then - echo "Error: GITHUB_ACCESS_TOKEN is empty" - exit 0 -fi - -# We add an exception to test on staging -if [ "$VERCEL_GIT_COMMIT_REF" == "production" ]; then - BRANCH_TO_CLONE="-b $VERCEL_GIT_COMMIT_REF" -fi -if [ "$VERCEL_GIT_COMMIT_REF" == "staging" ]; then - BRANCH_TO_CLONE="-b $VERCEL_GIT_COMMIT_REF" -fi - -# stop execution on error - don't let it build if something goes wrong -set -e - -git config --global init.defaultBranch main -git config --global advice.detachedHead false - -# set up an empty temporary work directory -rm -rf ..?* .[!.]* * || true - -# checkout the current commit -git clone $BRANCH_TO_CLONE https://$GITHUB_ACCESS_TOKEN@github.com/calcom/cal.com.git . - -echo "Cloned" - -# Ensure the submodule directory exists -mkdir -p $SUBMODULE_PATH - -# set up an empty temporary work directory -rm -rf tmp || true # remove the tmp folder if exists -mkdir tmp # create the tmp folder -cd tmp # go into the tmp folder - -# checkout the current submodule commit -git init # initialise empty repo -git remote add $SUBMODULE_PATH https://$GITHUB_ACCESS_TOKEN@$SUBMODULE_GITHUB # add origin of the submodule -git fetch --depth=1 $SUBMODULE_PATH $COMMIT # fetch only the required version -git checkout $COMMIT # checkout on the right commit - -# move the submodule from tmp to the submodule path -cd .. # go folder up -rm -rf tmp/.git # remove .git -mv tmp/* $SUBMODULE_PATH/ # move the submodule to the submodule path - -# clean up -rm -rf tmp # remove the tmp folder - -git diff --quiet HEAD^ HEAD ':!/apps/docs/*' ':!/apps/website/*' ':!/apps/web/*' ':!/apps/swagger/*' ':!/apps/console/*' - -echo "✅ - Build can proceed" -exit 1 diff --git a/apps/api/v1/test/README.md b/apps/api/v1/test/README.md deleted file mode 100644 index 938d54f0bea589..00000000000000 --- a/apps/api/v1/test/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Unit and Integration Tests - -Make sure you have copied .env.test.example to .env.test - -You can run all jest tests as - -`yarn test` - -You can run tests matching specific description by following command -`yarn test -t _post` - -Tip: Use `--watchAll` flag to run tests on every change diff --git a/apps/api/v1/test/docker-compose.yml b/apps/api/v1/test/docker-compose.yml deleted file mode 100644 index 249ac4df98fcd9..00000000000000 --- a/apps/api/v1/test/docker-compose.yml +++ /dev/null @@ -1,12 +0,0 @@ -# The containers that compose the project -services: - db: - image: postgres:18 - restart: always - container_name: integration-tests-prisma - ports: - - "5433:5432" - environment: - POSTGRES_USER: prisma - POSTGRES_PASSWORD: prisma - POSTGRES_DB: tests diff --git a/apps/api/v1/test/jest-resolver.js b/apps/api/v1/test/jest-resolver.js deleted file mode 100644 index d5fb532a29d5a4..00000000000000 --- a/apps/api/v1/test/jest-resolver.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = (path, options) => { - // Call the defaultResolver, so we leverage its cache, error handling, etc. - return options.defaultResolver(path, { - ...options, - // Use packageFilter to process parsed `package.json` before the resolution (see https://www.npmjs.com/package/resolve#resolveid-opts-cb) - packageFilter: (pkg) => { - // See https://github.com/microsoft/accessibility-insights-web/blob/40416a4ae6b91baf43102f58e069eff787de4de2/src/tests/common/resolver.js - if (pkg.name === "uuid" || pkg.name === "nanoid") { - delete pkg["exports"]; - delete pkg["module"]; - } - return pkg; - }, - }); -}; diff --git a/apps/api/v1/test/jest-setup.js b/apps/api/v1/test/jest-setup.js deleted file mode 100644 index 2b69df981c3fbb..00000000000000 --- a/apps/api/v1/test/jest-setup.js +++ /dev/null @@ -1,6 +0,0 @@ -// This is a workaround for https://github.com/jsdom/jsdom/issues/2524#issuecomment-902027138 - -// See https://github.com/microsoft/accessibility-insights-web/blob/40416a4ae6b91baf43102f58e069eff787de4de2/src/tests/unit/jest-setup.ts -const { TextEncoder, TextDecoder } = require("node:util"); -global.TextEncoder = TextEncoder; -global.TextDecoder = TextDecoder; diff --git a/apps/api/v1/test/lib/attendees/_post.test.ts b/apps/api/v1/test/lib/attendees/_post.test.ts deleted file mode 100644 index 2467c8063848b8..00000000000000 --- a/apps/api/v1/test/lib/attendees/_post.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; - -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, expect, test } from "vitest"; - -import handler from "../../../pages/api/attendees/_post"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -describe("POST /api/attendees", () => { - describe("Errors", () => { - test("Returns 403 if user is not admin and has no booking", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - bookingId: 1, - email: "test@example.com", - name: "Test User", - timeZone: "UTC", - }, - }); - - prismaMock.booking.findFirst.mockResolvedValue(null); - - req.userId = 123; - // req.isAdmin = false; - await handler(req, res); - - expect(res.statusCode).toBe(403); - expect(JSON.parse(res._getData()).message).toBe("Forbidden"); - }); - - test("Returns 200 if user is admin", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - bookingId: 1, - email: "test@example.com", - name: "Test User", - timeZone: "UTC", - }, - }); - - const attendeeData = { - id: 1, - email: "test@example.com", - name: "Test User", - timeZone: "UTC", - bookingId: 1, - }; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - prismaMock.attendee.create.mockResolvedValue(attendeeData); - req.isSystemWideAdmin = true; - req.userId = 123; - await handler(req, res); - - expect(res.statusCode).toBe(200); - expect(JSON.parse(res._getData()).attendee).toEqual(attendeeData); - expect(JSON.parse(res._getData()).message).toBe("Attendee created successfully"); - }); - - test("Returns 200 if user is not admin but has a booking", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - bookingId: 1, - email: "test@example.com", - name: "Test User", - timeZone: "UTC", - }, - }); - - const userBooking = { id: 1 }; - - prismaMock.booking.findFirst.mockResolvedValue(userBooking as any); - - const attendeeData = { - id: 1, - email: "test@example.com", - name: "Test User", - timeZone: "UTC", - bookingId: 1, - }; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - prismaMock.attendee.create.mockResolvedValue(attendeeData); - - req.userId = 123; - await handler(req, res); - - expect(res.statusCode).toBe(200); - expect(JSON.parse(res._getData()).attendee).toEqual(attendeeData); - expect(JSON.parse(res._getData()).message).toBe("Attendee created successfully"); - }); - - test("Returns 400 if request body is invalid", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - // Missing required fields - }, - }); - - req.userId = 123; - await handler(req, res); - - expect(res.statusCode).toBe(400); - }); - }); -}); diff --git a/apps/api/v1/test/lib/booking-references.integration-test.ts b/apps/api/v1/test/lib/booking-references.integration-test.ts deleted file mode 100644 index 845b16ad6b12d7..00000000000000 --- a/apps/api/v1/test/lib/booking-references.integration-test.ts +++ /dev/null @@ -1,486 +0,0 @@ -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, it, expect, beforeAll, afterAll } from "vitest"; - -import prisma from "@calcom/prisma"; -import type { Booking, Credential, EventType, User } from "@calcom/prisma/client"; -import { BookingStatus } from "@calcom/prisma/enums"; - -import { patchHandler } from "../../pages/api/booking-references/[id]/_patch"; -import { handler } from "../../pages/api/booking-references/_get"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -describe("GET /api/booking-references - Soft-delete filtering", () => { - let testUser: User; - let testCredential: Credential; - let testEventType: EventType; - let testBooking: Booking; - const createdBookingReferenceIds: number[] = []; - - beforeAll(async () => { - testUser = await prisma.user.create({ - data: { - email: "api-bookingreference-test@example.com", - username: "api-bookingreference-test", - }, - }); - - testCredential = await prisma.credential.create({ - data: { - type: "google_calendar", - key: {}, - userId: testUser.id, - }, - }); - - testEventType = await prisma.eventType.create({ - data: { - title: "Test Event Type", - slug: "api-test-event-type", - length: 30, - userId: testUser.id, - }, - }); - - testBooking = await prisma.booking.create({ - data: { - uid: "api-test-booking-uid", - title: "API Test Booking", - startTime: new Date(Date.now() + 86400000), - endTime: new Date(Date.now() + 90000000), - userId: testUser.id, - eventTypeId: testEventType.id, - status: BookingStatus.ACCEPTED, - }, - }); - }); - - afterAll(async () => { - if (createdBookingReferenceIds.length > 0) { - await prisma.bookingReference.deleteMany({ - where: { - id: { - in: createdBookingReferenceIds, - }, - }, - }); - } - - await prisma.booking.delete({ - where: { - id: testBooking.id, - }, - }); - - await prisma.eventType.delete({ - where: { - id: testEventType.id, - }, - }); - - await prisma.credential.delete({ - where: { - id: testCredential.id, - }, - }); - - await prisma.user.delete({ - where: { - id: testUser.id, - }, - }); - }); - - it("should only return active booking references, excluding soft-deleted ones", async () => { - const timestamp = Date.now(); - const activeRef = await prisma.bookingReference.create({ - data: { - type: "google_calendar", - uid: `api-active-ref-${timestamp}`, - meetingId: `api-active-meeting-${timestamp}`, - credentialId: testCredential.id, - bookingId: testBooking.id, - }, - }); - createdBookingReferenceIds.push(activeRef.id); - - const deletedRef = await prisma.bookingReference.create({ - data: { - type: "google_calendar", - uid: `api-deleted-ref-${timestamp}`, - meetingId: `api-deleted-meeting-${timestamp}`, - credentialId: testCredential.id, - bookingId: testBooking.id, - deleted: true, - }, - }); - createdBookingReferenceIds.push(deletedRef.id); - - const { req } = createMocks({ - method: "GET", - }); - - req.userId = testUser.id; - - const responseData = await handler(req); - - expect(responseData.booking_references).toBeDefined(); - expect(Array.isArray(responseData.booking_references)).toBe(true); - - const returnedActiveRef = responseData.booking_references.find( - (ref: { id: number }) => ref.id === activeRef.id - ); - const returnedDeletedRef = responseData.booking_references.find( - (ref: { id: number }) => ref.id === deletedRef.id - ); - - expect(returnedActiveRef).toBeDefined(); - expect(returnedDeletedRef).toBeUndefined(); - }); - - it("should filter booking references by user when not system admin", async () => { - const timestamp = Date.now(); - const otherUser = await prisma.user.create({ - data: { - email: `other-user-bookingreference-${timestamp}@example.com`, - username: `other-user-bookingreference-${timestamp}`, - }, - }); - - const otherBooking = await prisma.booking.create({ - data: { - uid: `other-user-booking-uid-${timestamp}`, - title: "Other User Booking", - startTime: new Date(Date.now() + 86400000), - endTime: new Date(Date.now() + 90000000), - userId: otherUser.id, - eventTypeId: testEventType.id, - status: BookingStatus.ACCEPTED, - }, - }); - - const otherUserRef = await prisma.bookingReference.create({ - data: { - type: "zoom_video", - uid: `other-user-ref-${timestamp}`, - meetingId: `other-user-meeting-${timestamp}`, - credentialId: testCredential.id, - bookingId: otherBooking.id, - }, - }); - createdBookingReferenceIds.push(otherUserRef.id); - - const { req } = createMocks({ - method: "GET", - }); - - req.userId = testUser.id; - - const responseData = await handler(req); - - const userRefs = responseData.booking_references.filter( - (ref: { id: number }) => ref.id === otherUserRef.id - ); - - expect(userRefs).toHaveLength(0); - - await prisma.booking.delete({ where: { id: otherBooking.id } }); - await prisma.user.delete({ where: { id: otherUser.id } }); - }); - - it("should return only active booking references for user, not soft-deleted ones for system admin", async () => { - const timestamp = Date.now(); - const activeRef1 = await prisma.bookingReference.create({ - data: { - type: "daily_video", - uid: `admin-active-ref-1-${timestamp}`, - meetingId: `admin-active-meeting-1-${timestamp}`, - credentialId: testCredential.id, - bookingId: testBooking.id, - }, - }); - createdBookingReferenceIds.push(activeRef1.id); - - const activeRef2 = await prisma.bookingReference.create({ - data: { - type: "daily_video", - uid: `admin-active-ref-2-${timestamp}`, - meetingId: `admin-active-meeting-2-${timestamp}`, - credentialId: testCredential.id, - bookingId: testBooking.id, - }, - }); - createdBookingReferenceIds.push(activeRef2.id); - - const deletedRef = await prisma.bookingReference.create({ - data: { - type: "daily_video", - uid: `admin-deleted-ref-${timestamp}`, - meetingId: `admin-deleted-meeting-${timestamp}`, - credentialId: testCredential.id, - bookingId: testBooking.id, - deleted: true, - }, - }); - createdBookingReferenceIds.push(deletedRef.id); - - const { req } = createMocks({ - method: "GET", - }); - - req.userId = testUser.id; - - const responseData = await handler(req); - - const activeRefs = responseData.booking_references.filter( - (ref: { id: number }) => ref.id === activeRef1.id || ref.id === activeRef2.id - ); - const returnedDeletedRef = responseData.booking_references.find( - (ref: { id: number }) => ref.id === deletedRef.id - ); - - expect(activeRefs.length).toBe(2); - expect(returnedDeletedRef).toBeUndefined(); - }); - - it("should verify soft-deleted references are never returned through the API", async () => { - const timestamp = Date.now(); - const activeRef = await prisma.bookingReference.create({ - data: { - type: "office365_calendar", - uid: `never-return-active-${timestamp}`, - meetingId: `never-return-active-meeting-${timestamp}`, - credentialId: testCredential.id, - bookingId: testBooking.id, - }, - }); - createdBookingReferenceIds.push(activeRef.id); - - await prisma.bookingReference.update({ - where: { id: activeRef.id }, - data: { deleted: true }, - }); - - const { req } = createMocks({ - method: "GET", - }); - - req.userId = testUser.id; - - const responseData = await handler(req); - - const softDeletedRef = responseData.booking_references.find( - (ref: { id: number }) => ref.id === activeRef.id - ); - - expect(softDeletedRef).toBeUndefined(); - - const refInDb = await prisma.bookingReference.findUnique({ - where: { id: activeRef.id }, - }); - expect(refInDb).toBeDefined(); - expect(refInDb?.deleted).toBe(true); - }); -}); - -describe("PATCH /api/booking-references/[id] - Existing functionality", () => { - let testUser: User; - let testCredential: Credential; - let testEventType: EventType; - let testBooking: Booking; - const createdBookingReferenceIds: number[] = []; - - beforeAll(async () => { - testUser = await prisma.user.create({ - data: { - email: "api-bookingreference-patch-test@example.com", - username: "api-bookingreference-patch-test", - }, - }); - - testCredential = await prisma.credential.create({ - data: { - type: "google_calendar", - key: {}, - userId: testUser.id, - }, - }); - - testEventType = await prisma.eventType.create({ - data: { - title: "Test Event Type", - slug: "api-patch-test-event-type", - length: 30, - userId: testUser.id, - }, - }); - - testBooking = await prisma.booking.create({ - data: { - uid: "api-patch-test-booking-uid", - title: "API Patch Test Booking", - startTime: new Date(Date.now() + 86400000), - endTime: new Date(Date.now() + 90000000), - userId: testUser.id, - eventTypeId: testEventType.id, - status: BookingStatus.ACCEPTED, - }, - }); - }); - - afterAll(async () => { - if (createdBookingReferenceIds.length > 0) { - await prisma.bookingReference.deleteMany({ - where: { - id: { - in: createdBookingReferenceIds, - }, - }, - }); - } - - await prisma.booking.delete({ - where: { - id: testBooking.id, - }, - }); - - await prisma.eventType.delete({ - where: { - id: testEventType.id, - }, - }); - - await prisma.credential.delete({ - where: { - id: testCredential.id, - }, - }); - - await prisma.user.delete({ - where: { - id: testUser.id, - }, - }); - }); - - it("should successfully update an active booking reference", async () => { - const timestamp = Date.now(); - const activeRef = await prisma.bookingReference.create({ - data: { - type: "google_calendar", - uid: `patch-active-ref-${timestamp}`, - meetingId: `patch-active-meeting-${timestamp}`, - credentialId: testCredential.id, - bookingId: testBooking.id, - }, - }); - createdBookingReferenceIds.push(activeRef.id); - - const { req } = createMocks({ - method: "PATCH", - query: { id: activeRef.id.toString() }, - body: { - meetingPassword: "new-password-123", - }, - }); - - req.userId = testUser.id; - req.isSystemWideAdmin = false; - - const responseData = await patchHandler(req); - - expect(responseData.booking_reference).toBeDefined(); - expect(responseData.booking_reference.id).toBe(activeRef.id); - expect(responseData.booking_reference.meetingPassword).toBe("new-password-123"); - - const updatedRef = await prisma.bookingReference.findUnique({ - where: { id: activeRef.id }, - }); - expect(updatedRef?.meetingPassword).toBe("new-password-123"); - }); - - it("should allow updating a soft-deleted booking reference (existing functionality)", async () => { - const timestamp = Date.now(); - const deletedRef = await prisma.bookingReference.create({ - data: { - type: "google_calendar", - uid: `patch-deleted-ref-${timestamp}`, - meetingId: `patch-deleted-meeting-${timestamp}`, - credentialId: testCredential.id, - bookingId: testBooking.id, - deleted: true, - meetingPassword: "original-password", - }, - }); - createdBookingReferenceIds.push(deletedRef.id); - - const { req } = createMocks({ - method: "PATCH", - query: { id: deletedRef.id.toString() }, - body: { - meetingPassword: "updated-password", - }, - }); - - req.userId = testUser.id; - req.isSystemWideAdmin = false; - - const responseData = await patchHandler(req); - - expect(responseData.booking_reference).toBeDefined(); - expect(responseData.booking_reference.id).toBe(deletedRef.id); - expect(responseData.booking_reference.meetingPassword).toBe("updated-password"); - - const updatedRef = await prisma.bookingReference.findUnique({ - where: { id: deletedRef.id }, - }); - expect(updatedRef?.meetingPassword).toBe("updated-password"); - expect(updatedRef?.deleted).toBe(true); - }); - - it("should verify that booking references can be updated regardless of deleted status", async () => { - const timestamp = Date.now(); - const activeRef = await prisma.bookingReference.create({ - data: { - type: "zoom_video", - uid: `patch-verify-active-${timestamp}`, - meetingId: `patch-verify-active-meeting-${timestamp}`, - credentialId: testCredential.id, - bookingId: testBooking.id, - meetingPassword: "original-password", - }, - }); - createdBookingReferenceIds.push(activeRef.id); - - await prisma.bookingReference.update({ - where: { id: activeRef.id }, - data: { deleted: true }, - }); - - const { req } = createMocks({ - method: "PATCH", - query: { id: activeRef.id.toString() }, - body: { - meetingPassword: "updated-password", - }, - }); - - req.userId = testUser.id; - req.isSystemWideAdmin = false; - - const responseData = await patchHandler(req); - - expect(responseData.booking_reference).toBeDefined(); - expect(responseData.booking_reference.meetingPassword).toBe("updated-password"); - - const refInDb = await prisma.bookingReference.findUnique({ - where: { id: activeRef.id }, - }); - expect(refInDb?.meetingPassword).toBe("updated-password"); - expect(refInDb?.deleted).toBe(true); - }); -}); diff --git a/apps/api/v1/test/lib/bookings/[id]/_delete.test.ts b/apps/api/v1/test/lib/bookings/[id]/_delete.test.ts deleted file mode 100644 index 5e66eb52c436fe..00000000000000 --- a/apps/api/v1/test/lib/bookings/[id]/_delete.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; - -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, expect, test, vi, afterEach, beforeEach } from "vitest"; - -import { buildBooking, buildEventType } from "@calcom/lib/test/builder"; - -import handler from "../../../../pages/api/bookings/[id]/_delete"; - -vi.mock("@calcom/features/bookings/lib/handleCancelBooking", () => ({ - default: vi.fn().mockResolvedValue({ success: true }), - handleCancelBooking: vi.fn().mockResolvedValue({ success: true }), -})); - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -const userId = 1; -const bookingId = 123; - -beforeEach(() => { - prismaMock.user.findUnique.mockResolvedValue({ - id: userId, - email: "test@example.com", - name: "Test User", - } as any); -}); - -afterEach(() => { - vi.resetAllMocks(); -}); - -describe("DELETE /api/bookings/[id]", () => { - describe("Success", () => { - test("should cancel booking successfully", async () => { - const mockBooking = buildBooking({ - id: bookingId, - userId: userId, - eventTypeId: buildEventType().id, - }); - - prismaMock.booking.findUnique.mockResolvedValue(mockBooking); - - const { req, res } = createMocks({ - method: "DELETE", - query: { - id: bookingId.toString(), - }, - body: { - reason: "User requested cancellation", - }, - }); - - req.userId = userId; - - await handler(req, res); - - expect(res.statusCode).toBe(200); - const responseData = JSON.parse(res._getData()); - expect(responseData.success).toBe(true); - }); - - test("should allow system-wide admin to cancel any booking", async () => { - const adminUserId = 999; - const mockBooking = buildBooking({ - id: bookingId, - userId: userId, - eventTypeId: buildEventType().id, - }); - - prismaMock.booking.findUnique.mockResolvedValue(mockBooking); - - const { req, res } = createMocks({ - method: "DELETE", - query: { - id: bookingId.toString(), - }, - body: { - reason: "Admin cancellation", - }, - }); - - req.userId = adminUserId; - req.isSystemWideAdmin = true; - - await handler(req, res); - - expect(res.statusCode).toBe(200); - }); - }); - - describe("Errors", () => { - test("should return 404 when booking not found", async () => { - prismaMock.booking.findUnique.mockResolvedValue(null); - - const { req, res } = createMocks({ - method: "DELETE", - query: { - id: "999", - }, - body: { - reason: "Test cancellation", - }, - }); - - req.userId = userId; - - await handler(req, res); - - expect(res.statusCode).toBe(200); - }); - - test("should return 400 for invalid booking ID", async () => { - const { req, res } = createMocks({ - method: "DELETE", - query: { - id: "invalid", - }, - body: { - reason: "Test cancellation", - }, - }); - - req.userId = userId; - - await handler(req, res); - - expect(res.statusCode).toBe(400); - }); - - test("should return 403 when user doesn't have permission to cancel booking", async () => { - const otherUserId = 999; - const mockBooking = buildBooking({ - id: bookingId, - userId: otherUserId, - eventTypeId: buildEventType().id, - }); - - prismaMock.booking.findUnique.mockResolvedValue(mockBooking); - - const { req, res } = createMocks({ - method: "DELETE", - query: { - id: bookingId.toString(), - }, - body: { - reason: "Unauthorized cancellation", - }, - }); - - req.userId = userId; - - await handler(req, res); - - expect(res.statusCode).toBe(200); - }); - - test("should return 400 when required cancellation reason is missing", async () => { - const mockBooking = buildBooking({ - id: bookingId, - userId: userId, - eventTypeId: buildEventType().id, - }); - - prismaMock.booking.findUnique.mockResolvedValue(mockBooking); - - const { req, res } = createMocks({ - method: "DELETE", - query: { - id: bookingId.toString(), - }, - body: {}, - }); - - req.userId = userId; - - await handler(req, res); - - expect(res.statusCode).toBe(200); - }); - }); -}); diff --git a/apps/api/v1/test/lib/bookings/[id]/_patch.integration-test.ts b/apps/api/v1/test/lib/bookings/[id]/_patch.integration-test.ts deleted file mode 100644 index b32bb45afe813c..00000000000000 --- a/apps/api/v1/test/lib/bookings/[id]/_patch.integration-test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, it, expect, beforeAll, afterAll } from "vitest"; - -import prisma from "@calcom/prisma"; - -import handler from "../../../../pages/api/bookings/[id]/_patch"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -describe("PATCH /api/bookings", () => { - let member1Booking: Awaited>; - let member0Booking: Awaited>; - const createdBookingIds: number[] = []; - let testAdminUserId: number | null = null; - - beforeAll(async () => { - const member1 = await prisma.user.findFirstOrThrow({ - where: { email: "member1-acme@example.com" }, - }); - - const member0 = await prisma.user.findFirstOrThrow({ - where: { email: "member0-acme@example.com" }, - }); - - // Create bookings for testing - member1Booking = await prisma.booking.create({ - data: { - uid: `test-member1-booking-${Date.now()}`, - title: "Member 1 Test Booking", - startTime: new Date(Date.now() + 86400000), // Tomorrow - endTime: new Date(Date.now() + 90000000), // Tomorrow + 1 hour - userId: member1.id, - status: "ACCEPTED", - }, - }); - createdBookingIds.push(member1Booking.id); - - member0Booking = await prisma.booking.create({ - data: { - uid: `test-member0-booking-${Date.now()}`, - title: "Member 0 Test Booking", - startTime: new Date(Date.now() + 172800000), // Day after tomorrow - endTime: new Date(Date.now() + 176400000), // Day after tomorrow + 1 hour - userId: member0.id, - status: "ACCEPTED", - }, - }); - createdBookingIds.push(member0Booking.id); - }); - - afterAll(async () => { - if (createdBookingIds.length > 0) { - await prisma.booking.deleteMany({ - where: { id: { in: createdBookingIds } }, - }); - } - - // Clean up test admin user if created - if (testAdminUserId) { - await prisma.user.delete({ - where: { id: testAdminUserId }, - }); - } - }); - it("Returns 403 when user has no permission to the booking", async () => { - // Member2 tries to access Member0's booking - should fail - const member2 = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } }); - - const { req, res } = createMocks({ - method: "PATCH", - body: { - title: member0Booking.title, - startTime: member0Booking.startTime.toISOString(), - endTime: member0Booking.endTime.toISOString(), - userId: member2.id, - }, - query: { - id: member0Booking.id, - }, - }); - - req.userId = member2.id; - - await handler(req, res); - expect(res.statusCode).toBe(403); - }); - - it("Allows PATCH when user is system-wide admin", async () => { - // Check if admin user already exists before upserting - const existingAdmin = await prisma.user.findUnique({ where: { email: "test-admin@example.com" } }); - - // Create a system-wide admin user for this test - const adminUser = await prisma.user.upsert({ - where: { email: "test-admin@example.com" }, - update: { role: "ADMIN" }, - create: { - email: "test-admin@example.com", - username: "test-admin", - name: "Test Admin", - role: "ADMIN", - }, - }); - - // Only track for cleanup if we created it (not if it already existed) - if (!existingAdmin) { - testAdminUserId = adminUser.id; - } - - const { req, res } = createMocks({ - method: "PATCH", - body: { - title: member0Booking.title, - startTime: member0Booking.startTime.toISOString(), - endTime: member0Booking.endTime.toISOString(), - userId: member0Booking.userId, - }, - query: { - id: member0Booking.id, - }, - }); - - req.userId = adminUser.id; - req.isSystemWideAdmin = true; - - await handler(req, res); - expect(res.statusCode).toBe(200); - }); - - it("Allows PATCH when user is org-wide admin", async () => { - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); - - const { req, res } = createMocks({ - method: "PATCH", - body: { - title: member1Booking.title, - startTime: member1Booking.startTime.toISOString(), - endTime: member1Booking.endTime.toISOString(), - userId: member1Booking.userId, - }, - query: { - id: member1Booking.id, - }, - }); - - req.userId = adminUser.id; - req.isOrganizationOwnerOrAdmin = true; - - await handler(req, res); - expect(res.statusCode).toBe(200); - }); -}); diff --git a/apps/api/v1/test/lib/bookings/[id]/_patch.test.ts b/apps/api/v1/test/lib/bookings/[id]/_patch.test.ts deleted file mode 100644 index 11798591395029..00000000000000 --- a/apps/api/v1/test/lib/bookings/[id]/_patch.test.ts +++ /dev/null @@ -1,263 +0,0 @@ -import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; - -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, expect, test, vi, afterEach, beforeEach } from "vitest"; - -import { buildBooking, buildEventType } from "@calcom/lib/test/builder"; - -import handler from "../../../../pages/api/bookings/[id]/_patch"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -const userId = 1; -const bookingId = 123; - -beforeEach(() => { - prismaMock.user.findUnique.mockResolvedValue({ - id: userId, - email: "test@example.com", - name: "Test User", - } as any); -}); - -afterEach(() => { - vi.resetAllMocks(); -}); - -describe("PATCH /api/bookings/[id]", () => { - describe("Success", () => { - test("should update booking successfully", async () => { - const mockBooking = buildBooking({ - id: bookingId, - userId: userId, - title: "Original Title", - eventTypeId: buildEventType().id, - }); - - const updatedBooking = { - ...mockBooking, - title: "Updated Title", - }; - - prismaMock.booking.findUnique.mockResolvedValue(mockBooking); - prismaMock.booking.update.mockResolvedValue(updatedBooking); - - const { req, res } = createMocks({ - method: "PATCH", - query: { - id: bookingId.toString(), - }, - body: { - title: "Updated Title", - }, - }); - - req.userId = userId; - - await handler(req, res); - - expect(res.statusCode).toBe(200); - const responseData = JSON.parse(res._getData()); - expect(responseData.booking.title).toBe("Updated Title"); - }); - - test("should allow system-wide admin to update any booking", async () => { - const adminUserId = 999; - const mockBooking = buildBooking({ - id: bookingId, - userId: userId, - title: "Original Title", - eventTypeId: buildEventType().id, - }); - - const updatedBooking = { - ...mockBooking, - title: "Admin Updated Title", - }; - - prismaMock.booking.findUnique.mockResolvedValue(mockBooking); - prismaMock.booking.update.mockResolvedValue(updatedBooking); - - const { req, res } = createMocks({ - method: "PATCH", - query: { - id: bookingId.toString(), - }, - body: { - title: "Admin Updated Title", - }, - }); - - req.userId = adminUserId; - req.isSystemWideAdmin = true; - - await handler(req, res); - - expect(res.statusCode).toBe(200); - }); - - test("should update booking times successfully", async () => { - const mockBooking = buildBooking({ - id: bookingId, - userId: userId, - startTime: new Date("2024-01-01T10:00:00Z"), - endTime: new Date("2024-01-01T11:00:00Z"), - eventTypeId: buildEventType().id, - }); - - const newStartTime = new Date("2024-01-01T14:00:00Z"); - const newEndTime = new Date("2024-01-01T15:00:00Z"); - - const updatedBooking = { - ...mockBooking, - startTime: newStartTime, - endTime: newEndTime, - }; - - prismaMock.booking.findUnique.mockResolvedValue(mockBooking); - prismaMock.booking.update.mockResolvedValue(updatedBooking); - - const { req, res } = createMocks({ - method: "PATCH", - query: { - id: bookingId.toString(), - }, - body: { - startTime: newStartTime.toISOString(), - endTime: newEndTime.toISOString(), - }, - }); - - req.userId = userId; - - await handler(req, res); - - expect(res.statusCode).toBe(200); - const responseData = JSON.parse(res._getData()); - expect(new Date(responseData.booking.startTime)).toEqual(newStartTime); - expect(new Date(responseData.booking.endTime)).toEqual(newEndTime); - }); - }); - - describe("Errors", () => { - test("should return 404 when booking not found", async () => { - prismaMock.booking.findUnique.mockResolvedValue(null); - - const { req, res } = createMocks({ - method: "PATCH", - query: { - id: "999", - }, - body: { - title: "Updated Title", - }, - }); - - req.userId = userId; - - await handler(req, res); - - expect(res.statusCode).toBe(400); - }); - - test("should return 400 for invalid booking ID", async () => { - const { req, res } = createMocks({ - method: "PATCH", - query: { - id: "invalid", - }, - body: { - title: "Updated Title", - }, - }); - - req.userId = userId; - - await handler(req, res); - - expect(res.statusCode).toBe(400); - }); - - test("should return 403 when user doesn't have permission to update booking", async () => { - const otherUserId = 999; - const mockBooking = buildBooking({ - id: bookingId, - userId: otherUserId, - eventTypeId: buildEventType().id, - }); - - prismaMock.booking.findUnique.mockResolvedValue(mockBooking); - - const { req, res } = createMocks({ - method: "PATCH", - query: { - id: bookingId.toString(), - }, - body: { - title: "Unauthorized Update", - }, - }); - - req.userId = userId; - - await handler(req, res); - - expect(res.statusCode).toBe(400); - }); - - test("should return 401 when non-admin tries to change userId", async () => { - const mockBooking = buildBooking({ - id: bookingId, - userId: userId, - eventTypeId: buildEventType().id, - }); - - prismaMock.booking.findUnique.mockResolvedValue(mockBooking); - - const { req, res } = createMocks({ - method: "PATCH", - query: { - id: bookingId.toString(), - }, - body: { - userId: 999, - }, - }); - - req.userId = userId; - - await handler(req, res); - - expect(res.statusCode).toBe(403); - }); - - test("should return 400 for invalid date format", async () => { - const mockBooking = buildBooking({ - id: bookingId, - userId: userId, - eventTypeId: buildEventType().id, - }); - - prismaMock.booking.findUnique.mockResolvedValue(mockBooking); - - const { req, res } = createMocks({ - method: "PATCH", - query: { - id: bookingId.toString(), - }, - body: { - startTime: "invalid-date", - }, - }); - - req.userId = userId; - - await handler(req, res); - - expect(res.statusCode).toBe(400); - }); - }); -}); diff --git a/apps/api/v1/test/lib/bookings/[id]/recordings/_get.test.ts b/apps/api/v1/test/lib/bookings/[id]/recordings/_get.test.ts deleted file mode 100644 index 6a9caae21f4022..00000000000000 --- a/apps/api/v1/test/lib/bookings/[id]/recordings/_get.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; -import { - getDownloadLinkOfCalVideoByRecordingId, - getRecordingsOfCalVideoByRoomName, -} from "@calcom/features/conferencing/lib/videoClient"; -import { buildBooking } from "@calcom/lib/test/builder"; -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers"; -import authMiddleware from "../../../../../pages/api/bookings/[id]/_auth-middleware"; -import handler from "../../../../../pages/api/bookings/[id]/recordings/_get"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -const adminUserId = 1; -const memberUserId = 10; - -vi.mock("@calcom/features/conferencing/lib/videoClient", () => { - return { - getRecordingsOfCalVideoByRoomName: vi.fn(), - getDownloadLinkOfCalVideoByRecordingId: vi.fn(), - }; -}); - -vi.mock("~/lib/utils/retrieveScopedAccessibleUsers", () => { - return { - getAccessibleUsers: vi.fn(), - }; -}); -afterEach(() => { - vi.resetAllMocks(); -}); - -const mockGetRecordingsAndDownloadLink = () => { - const download_link = "https://URL"; - const recordingItem = { - id: "TEST_ID", - room_name: "0n22w24AQ5ZFOtEKX2gX", - start_ts: 1716215386, - status: "finished", - max_participants: 1, - duration: 11, - share_token: "TEST_TOKEN", - }; - - vi.mocked(getRecordingsOfCalVideoByRoomName).mockResolvedValue({ data: [recordingItem], total_count: 1 }); - - vi.mocked(getDownloadLinkOfCalVideoByRecordingId).mockResolvedValue({ - download_link, - }); - - return [{ ...recordingItem, download_link }]; -}; - -describe("GET /api/bookings/[id]/recordings", () => { - test("Returns recordings if user is system-wide admin", async () => { - const userId = 2; - - const bookingId = 1111; - - prismaMock.booking.findUnique.mockResolvedValue( - buildBooking({ - id: bookingId, - userId, - references: [ - { - id: 1, - type: "daily_video", - uid: "17OHkCH53pBa03FhxMbw", - meetingId: "17OHkCH53pBa03FhxMbw", - meetingPassword: "password", - meetingUrl: "https://URL", - }, - ], - }) - ); - - const mockedRecordings = mockGetRecordingsAndDownloadLink(); - const { req, res } = createMocks({ - method: "GET", - body: {}, - query: { - id: bookingId, - }, - }); - - req.isSystemWideAdmin = true; - req.userId = adminUserId; - - await authMiddleware(req); - await handler(req, res); - - expect(res.statusCode).toBe(200); - expect(JSON.parse(res._getData())).toEqual(mockedRecordings); - }); - - test("Allows GET recordings when user is org-wide admin", async () => { - const bookingId = 3333; - - prismaMock.booking.findUnique.mockResolvedValue( - buildBooking({ - id: bookingId, - userId: memberUserId, - references: [ - { id: 1, type: "daily_video", uid: "17OHkCH53pBa03FhxMbw", meetingId: "17OHkCH53pBa03FhxMbw" }, - ], - }) - ); - - const { req, res } = createMocks({ - method: "GET", - body: {}, - query: { - id: bookingId, - }, - }); - - req.userId = adminUserId; - req.isOrganizationOwnerOrAdmin = true; - const mockedRecordings = mockGetRecordingsAndDownloadLink(); - vi.mocked(getAccessibleUsers).mockResolvedValue([memberUserId]); - - await authMiddleware(req); - await handler(req, res); - - expect(res.statusCode).toBe(200); - }); -}); diff --git a/apps/api/v1/test/lib/bookings/[id]/transcripts/[recordingId]/_get.test.ts b/apps/api/v1/test/lib/bookings/[id]/transcripts/[recordingId]/_get.test.ts deleted file mode 100644 index 8b5da8904aefda..00000000000000 --- a/apps/api/v1/test/lib/bookings/[id]/transcripts/[recordingId]/_get.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; -import { - checkIfRoomNameMatchesInRecording, - getTranscriptsAccessLinkFromRecordingId, -} from "@calcom/features/conferencing/lib/videoClient"; -import { buildBooking } from "@calcom/lib/test/builder"; -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers"; -import authMiddleware from "../../../../../../pages/api/bookings/[id]/_auth-middleware"; -import handler from "../../../../../../pages/api/bookings/[id]/transcripts/[recordingId]/_get"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -vi.mock("@calcom/features/conferencing/lib/videoClient", () => { - return { - getTranscriptsAccessLinkFromRecordingId: vi.fn(), - checkIfRoomNameMatchesInRecording: vi.fn(), - }; -}); - -vi.mock("~/lib/utils/retrieveScopedAccessibleUsers", () => { - return { - getAccessibleUsers: vi.fn(), - }; -}); -afterEach(() => { - vi.resetAllMocks(); -}); - -const mockGetTranscripts = () => { - const downloadLinks = [{ format: "json", link: "https://URL1" }]; - - vi.mocked(getTranscriptsAccessLinkFromRecordingId).mockResolvedValue(downloadLinks); - vi.mocked(checkIfRoomNameMatchesInRecording).mockResolvedValue(true); - - return downloadLinks; -}; - -const recordingId = "abc-xyz"; - -describe("GET /api/bookings/[id]/transcripts/[recordingId]", () => { - test("Returns transcripts if user is system-wide admin", async () => { - const adminUserId = 1; - const userId = 2; - - const bookingId = 1111; - - prismaMock.booking.findUnique.mockResolvedValue( - buildBooking({ - id: bookingId, - userId, - references: [ - { - id: 1, - type: "daily_video", - uid: "17OHkCH53pBa03FhxMbw", - meetingId: "17OHkCH53pBa03FhxMbw", - meetingPassword: "password", - meetingUrl: "https://URL", - }, - ], - }) - ); - - const mockedTranscripts = mockGetTranscripts(); - const { req, res } = createMocks({ - method: "GET", - body: {}, - query: { - id: bookingId, - recordingId, - }, - }); - - req.isSystemWideAdmin = true; - req.userId = adminUserId; - - await authMiddleware(req); - await handler(req, res); - - expect(res.statusCode).toBe(200); - expect(JSON.parse(res._getData())).toEqual(mockedTranscripts); - }); - - test("Allows GET transcripts when user is org-wide admin", async () => { - const adminUserId = 1; - const memberUserId = 10; - const bookingId = 3333; - - prismaMock.booking.findUnique.mockResolvedValue( - buildBooking({ - id: bookingId, - userId: memberUserId, - references: [ - { id: 1, type: "daily_video", uid: "17OHkCH53pBa03FhxMbw", meetingId: "17OHkCH53pBa03FhxMbw" }, - ], - }) - ); - - const { req, res } = createMocks({ - method: "GET", - body: {}, - query: { - id: bookingId, - recordingId, - }, - }); - - req.userId = adminUserId; - req.isOrganizationOwnerOrAdmin = true; - mockGetTranscripts(); - - vi.mocked(getAccessibleUsers).mockResolvedValue([memberUserId]); - - await authMiddleware(req); - await handler(req, res); - - expect(res.statusCode).toBe(200); - }); -}); diff --git a/apps/api/v1/test/lib/bookings/[id]/transcripts/_get.test.ts b/apps/api/v1/test/lib/bookings/[id]/transcripts/_get.test.ts deleted file mode 100644 index 69b780f9c98d3a..00000000000000 --- a/apps/api/v1/test/lib/bookings/[id]/transcripts/_get.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; -import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/features/conferencing/lib/videoClient"; -import { buildBooking } from "@calcom/lib/test/builder"; -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers"; -import authMiddleware from "../../../../../pages/api/bookings/[id]/_auth-middleware"; -import handler from "../../../../../pages/api/bookings/[id]/transcripts/_get"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -vi.mock("@calcom/features/conferencing/lib/videoClient", () => { - return { - getAllTranscriptsAccessLinkFromRoomName: vi.fn(), - }; -}); - -vi.mock("~/lib/utils/retrieveScopedAccessibleUsers", () => { - return { - getAccessibleUsers: vi.fn(), - }; -}); -afterEach(() => { - vi.resetAllMocks(); -}); - -const mockGetTranscripts = () => { - const downloadLinks = ["https://URL1", "https://URL2"]; - - vi.mocked(getAllTranscriptsAccessLinkFromRoomName).mockResolvedValue(downloadLinks); - - return downloadLinks; -}; - -describe("GET /api/bookings/[id]/transcripts", () => { - test("Returns transcripts if user is system-wide admin", async () => { - const adminUserId = 1; - const userId = 2; - - const bookingId = 1111; - - prismaMock.booking.findUnique.mockResolvedValue( - buildBooking({ - id: bookingId, - userId, - references: [ - { - id: 1, - type: "daily_video", - uid: "17OHkCH53pBa03FhxMbw", - meetingId: "17OHkCH53pBa03FhxMbw", - meetingPassword: "password", - meetingUrl: "https://URL", - }, - ], - }) - ); - - const mockedTranscripts = mockGetTranscripts(); - const { req, res } = createMocks({ - method: "GET", - body: {}, - query: { - id: bookingId, - }, - }); - - req.isSystemWideAdmin = true; - req.userId = adminUserId; - - await authMiddleware(req); - await handler(req, res); - - expect(res.statusCode).toBe(200); - expect(JSON.parse(res._getData())).toEqual(mockedTranscripts); - }); - - test("Allows GET transcripts when user is org-wide admin", async () => { - const adminUserId = 1; - const memberUserId = 10; - const bookingId = 3333; - - prismaMock.booking.findUnique.mockResolvedValue( - buildBooking({ - id: bookingId, - userId: memberUserId, - references: [ - { id: 1, type: "daily_video", uid: "17OHkCH53pBa03FhxMbw", meetingId: "17OHkCH53pBa03FhxMbw" }, - ], - }) - ); - - const { req, res } = createMocks({ - method: "GET", - body: {}, - query: { - id: bookingId, - }, - }); - - req.userId = adminUserId; - req.isOrganizationOwnerOrAdmin = true; - mockGetTranscripts(); - - vi.mocked(getAccessibleUsers).mockResolvedValue([memberUserId]); - - await authMiddleware(req); - await handler(req, res); - - expect(res.statusCode).toBe(200); - }); -}); diff --git a/apps/api/v1/test/lib/bookings/_auth-middleware.integration-test.ts b/apps/api/v1/test/lib/bookings/_auth-middleware.integration-test.ts deleted file mode 100644 index 425dc04af2f529..00000000000000 --- a/apps/api/v1/test/lib/bookings/_auth-middleware.integration-test.ts +++ /dev/null @@ -1,304 +0,0 @@ -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, it, expect, test, beforeAll, afterAll } from "vitest"; - -import { prisma } from "@calcom/prisma"; -import type { User, Team, Prisma } from "@calcom/prisma/client"; -import { MembershipRole } from "@calcom/prisma/enums"; - -import authMiddleware from "../../../pages/api/bookings/[id]/_auth-middleware"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -const createEventTypeEventSelect = { - bookings: true, -} satisfies Prisma.EventTypeSelect; - -describe("Booking ownership and access in Middleware", () => { - let adminUserRef: User; - let ownerUserRef: User; - let orgOwnerUserRef: User; - let memberUserRef: User; - let orgRef: Team; - - let createEventResult1: Prisma.EventTypeGetPayload<{ select: typeof createEventTypeEventSelect }>; - let createEventResult2: Prisma.EventTypeGetPayload<{ select: typeof createEventTypeEventSelect }>; - - // mock user data - beforeAll(async () => { - //Create Users - const createAdminUser = prisma.user.create({ - data: { - username: `admin-${Date.now()}`, - name: "Admin User", - email: `admin+${Date.now()}@example.com`, - }, - }); - const createOwnerUser = prisma.user.create({ - data: { - username: `owner-${Date.now()}`, - name: "Owner User", - email: `owner+${Date.now()}@example.com`, - }, - }); - const createOrgOwnerUser = prisma.user.create({ - data: { - username: `org-owner-${Date.now()}`, - name: "Org Owner", - email: `org-owner+${Date.now()}@example.com`, - }, - }); - const createMemberUser = prisma.user.create({ - data: { - username: `member-${Date.now()}`, - name: "Member User", - email: `member+${Date.now()}@example.com`, - }, - }); - - [adminUserRef, ownerUserRef, orgOwnerUserRef, memberUserRef] = await Promise.all([ - createAdminUser, - createOwnerUser, - createOrgOwnerUser, - createMemberUser, - ]); - - //create Org & Team - orgRef = await prisma.team.create({ - data: { - name: "Org", - slug: `org-${Date.now()}`, - isOrganization: true, - children: { - create: { - name: "Team 1", - slug: `team1-${Date.now()}`, - members: { - createMany: { - data: [ - { - userId: adminUserRef.id, - role: MembershipRole.ADMIN, - accepted: true, - }, - { - userId: ownerUserRef.id, - role: MembershipRole.OWNER, - accepted: true, - }, - { - userId: memberUserRef.id, - role: MembershipRole.MEMBER, - accepted: true, - }, - ], - }, - }, - }, - }, - members: { - createMany: { - data: [ - { - userId: ownerUserRef.id, - role: MembershipRole.OWNER, - accepted: true, - }, - { - userId: memberUserRef.id, - role: MembershipRole.MEMBER, - accepted: true, - }, - { - userId: adminUserRef.id, - role: MembershipRole.MEMBER, - accepted: true, - }, - ], - }, - }, - }, - }); - - //create eventTypes - const createEventTypeEvent1 = prisma.eventType.create({ - data: { - title: "Event 1", - slug: `event-1-${Date.now()}`, - userId: ownerUserRef.id, - length: 60, - bookings: { - create: { - uid: `booking-2-${Date.now()}`, - title: "Booking 2", - userId: memberUserRef.id, - startTime: "2024-08-30T06:45:00.000Z", - endTime: "2024-08-30T07:45:00.000Z", - attendees: { - create: { - name: "Member User", - email: memberUserRef.email, - timeZone: "UTC", - }, - }, - }, - }, - }, - select: createEventTypeEventSelect, - }); - const createEventTypeEvent2 = prisma.eventType.create({ - data: { - title: "Event 2", - slug: `event-2-${Date.now()}`, - length: 60, - teamId: orgRef.id, - bookings: { - create: { - uid: `booking-1-${Date.now()}`, - title: "Booking 1", - userId: adminUserRef.id, - startTime: "2024-08-30T06:45:00.000Z", - endTime: "2024-08-30T07:45:00.000Z", - attendees: { - create: { - name: "Admin User", - email: adminUserRef.email, - timeZone: "UTC", - }, - }, - }, - }, - }, - select: createEventTypeEventSelect, - }); - - [createEventResult1, createEventResult2] = await Promise.all([ - createEventTypeEvent1, - createEventTypeEvent2, - ]); - }); - - afterAll(async () => { - console.log("Cleaning up org", orgRef.id); - await prisma.team.delete({ - where: { - id: orgRef.id, - }, - }); - console.log("Cleaning up users", [ - adminUserRef.id, - ownerUserRef.id, - orgOwnerUserRef.id, - memberUserRef.id, - ]); - await prisma.user.deleteMany({ - where: { - id: { - in: [adminUserRef.id, ownerUserRef.id, orgOwnerUserRef.id, memberUserRef.id], - }, - }, - }); - }); - - test("should not throw error for bookings where user is an attendee", async () => { - console.log(createEventResult1.bookings[0].id); - const { req } = createMocks({ - method: "GET", - query: { - id: createEventResult1.bookings[0].id, - }, - prisma, - }); - req.userId = memberUserRef.id; - await expect(authMiddleware(req)).resolves.not.toThrow(); - }); - - test("should throw error for bookings where user is not an attendee", async () => { - const { req } = createMocks({ - method: "GET", - query: { - id: 1, - }, - prisma, - }); - req.userId = memberUserRef.id; - - await expect(authMiddleware(req)).rejects.toThrow(); - }); - - test("should not throw error for booking where user is the event type owner", async () => { - const { req } = createMocks({ - method: "GET", - query: { - id: createEventResult2.bookings[0].id, - }, - prisma, - }); - req.userId = ownerUserRef.id; - await expect(authMiddleware(req)).resolves.not.toThrow(); - }); - - test("should not throw error for booking where user is team owner or admin", async () => { - const { req: req1 } = createMocks({ - method: "GET", - query: { - id: createEventResult2.bookings[0].id, - }, - prisma, - }); - const { req: req2 } = createMocks({ - method: "GET", - query: { - id: createEventResult2.bookings[0].id, - }, - prisma, - }); - req1.userId = adminUserRef.id; - req2.userId = ownerUserRef.id; - - await expect(authMiddleware(req1)).resolves.not.toThrow(); - await expect(authMiddleware(req2)).resolves.not.toThrow(); - }); - test("should throw error for booking where user is not team owner or admin", async () => { - const { req } = createMocks({ - method: "GET", - query: { - id: createEventResult2.bookings[0].id, - }, - prisma, - }); - - req.userId = memberUserRef.id; - - await expect(authMiddleware(req)).rejects.toThrow(); - }); - test("should not throw error when user is system-wide admin", async () => { - const { req } = createMocks({ - method: "GET", - query: { - id: 2, - }, - prisma, - }); - req.userId = adminUserRef.id; - req.isSystemWideAdmin = true; - - await authMiddleware(req); - }); - - it("should throw error when user is org-wide admin", async () => { - const { req } = createMocks({ - method: "GET", - query: { - id: 1, - }, - prisma, - }); - req.userId = orgOwnerUserRef.id; - req.isOrganizationOwnerOrAdmin = true; - - await expect(authMiddleware(req)).rejects.toThrow(); - }); -}); diff --git a/apps/api/v1/test/lib/bookings/_get.integration-test.ts b/apps/api/v1/test/lib/bookings/_get.integration-test.ts deleted file mode 100644 index 79b1906626574c..00000000000000 --- a/apps/api/v1/test/lib/bookings/_get.integration-test.ts +++ /dev/null @@ -1,422 +0,0 @@ -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, expect, it, beforeAll, afterAll } from "vitest"; -import { ZodError } from "zod"; - -import { prisma } from "@calcom/prisma"; - -import { handler } from "../../../pages/api/bookings/_get"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -const DefaultPagination = { - take: 10, - skip: 0, -}; - -describe("GET /api/bookings", () => { - let proUser: Awaited>; - let proUserBooking: Awaited>; - let memberUser: Awaited>; - let memberUserBooking: Awaited>; - - beforeAll(async () => { - proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } }); - proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } }); - - memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } }); - - // Find an event type for memberUser or use a simple booking - const memberEventType = await prisma.eventType.findFirst({ - where: { - OR: [ - { userId: memberUser.id }, - { team: { members: { some: { userId: memberUser.id } } } } - ] - } - }); - - memberUserBooking = await prisma.booking.create({ - data: { - uid: `test-member-booking-${Date.now()}`, - title: "Member Test Booking", - startTime: new Date(Date.now() + 86400000), // Tomorrow - endTime: new Date(Date.now() + 90000000), // Tomorrow + 1 hour - userId: memberUser.id, - eventTypeId: memberEventType?.id, - status: "ACCEPTED", - }, - }); - }); - - afterAll(async () => { - // Clean up the test booking created in beforeAll - if (memberUserBooking?.id) { - await prisma.booking.delete({ - where: { id: memberUserBooking.id }, - }); - } - }); - - it("Does not return bookings of other users when user has no permission", async () => { - const { req } = createMocks({ - method: "GET", - query: { - userId: proUser.id, // Try to access proUser's bookings - }, - pagination: DefaultPagination, - }); - - req.userId = memberUser.id; // But request is from memberUser - - const responseData = await handler(req); - const groupedUsers = new Set(responseData.bookings.map((b) => b.userId)); - - // Should only return memberUser's own bookings, not proUser's - expect(responseData.bookings.find((b) => b.userId === memberUser.id)).toBeDefined(); - expect(responseData.bookings.find((b) => b.id === memberUserBooking.id)).toBeDefined(); - expect(groupedUsers.size).toBe(1); - const firstEntry = groupedUsers.entries().next().value; - expect(firstEntry?.[0]).toBe(memberUser.id); - }); - - it("Returns bookings for regular user", async () => { - const { req } = createMocks({ - method: "GET", - pagination: DefaultPagination, - }); - - req.userId = proUser.id; - - const responseData = await handler(req); - expect(responseData.bookings.find((b) => b.id === proUserBooking.id)).toBeDefined(); - expect(responseData.bookings.find((b) => b.userId !== proUser.id)).toBeUndefined(); - }); - - it("Returns bookings for specified user when accessed by system-wide admin", async () => { - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); - const { req } = createMocks({ - method: "GET", - pagination: DefaultPagination, - query: { - userId: proUser.id, - }, - }); - - req.isSystemWideAdmin = true; - req.userId = adminUser.id; - - const responseData = await handler(req); - expect(responseData.bookings.find((b) => b.id === proUserBooking.id)).toBeDefined(); - expect(responseData.bookings.find((b) => b.userId !== proUser.id)).toBeUndefined(); - }); - - it("Returns bookings for all users when accessed by system-wide admin", async () => { - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); - const { req } = createMocks({ - method: "GET", - pagination: { - take: 100, - skip: 0, - }, - }); - - req.isSystemWideAdmin = true; - req.userId = adminUser.id; - - const responseData = await handler(req); - const groupedUsers = new Set(responseData.bookings.map((b) => b.userId)); - expect(responseData.bookings.find((b) => b.id === proUserBooking.id)).toBeDefined(); - expect(groupedUsers.size).toBeGreaterThan(2); - }); - - it("Returns bookings for org users when accessed by org admin", async () => { - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); - const { req } = createMocks({ - method: "GET", - pagination: DefaultPagination, - }); - - req.userId = adminUser.id; - req.isOrganizationOwnerOrAdmin = true; - - const responseData = await handler(req); - const groupedUsers = new Set(responseData.bookings.map((b) => b.userId)); - expect(responseData.bookings.find((b) => b.id === proUserBooking.id)).toBeUndefined(); - expect(groupedUsers.size).toBeGreaterThanOrEqual(2); - }); - - describe("Upcoming bookings feature", () => { - it("Returns only upcoming bookings when status=upcoming for regular user", async () => { - const { req } = createMocks({ - method: "GET", - query: { - status: "upcoming", - }, - pagination: DefaultPagination, - }); - - req.userId = proUser.id; - - const responseData = await handler(req); - responseData.bookings.forEach((booking) => { - expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(new Date().getTime()); - }); - }); - - it("Returns all bookings when status not specified for regular user", async () => { - const { req } = createMocks({ - method: "GET", - pagination: DefaultPagination, - }); - - req.userId = proUser.id; - - const responseData = await handler(req); - expect(responseData.bookings.find((b) => b.id === proUserBooking.id)).toBeDefined(); - - const { req: req2 } = createMocks({ - method: "GET", - pagination: DefaultPagination, - }); - - req2.userId = proUser.id; - - const responseData2 = await handler(req2); - expect(responseData2.bookings.find((b) => b.id === proUserBooking.id)).toBeDefined(); - }); - - it("Returns only upcoming bookings when status=upcoming for system-wide admin", async () => { - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); - const { req } = createMocks({ - method: "GET", - query: { - status: "upcoming", - }, - pagination: { - take: 100, - skip: 0, - }, - }); - - req.isSystemWideAdmin = true; - req.userId = adminUser.id; - - const responseData = await handler(req); - responseData.bookings.forEach((booking) => { - expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(new Date().getTime()); - }); - }); - - it("Returns only upcoming bookings when status=upcoming for org admin", async () => { - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); - const { req } = createMocks({ - method: "GET", - query: { - status: "upcoming", - }, - pagination: DefaultPagination, - }); - - req.userId = adminUser.id; - req.isOrganizationOwnerOrAdmin = true; - - const responseData = await handler(req); - responseData.bookings.forEach((booking) => { - expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(new Date().getTime()); - }); - }); - }); - - describe("Expand feature to add relational data in return payload", () => { - it("Returns only team data when expand=team is set", async () => { - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); - - // Find a team booking and a non-team booking from seed data - const team1 = await prisma.team.findFirst({ where: { slug: "team1" } }); - const teamEventType = await prisma.eventType.findFirst({ - where: { teamId: team1?.id }, - include: { bookings: true }, - }); - const teamBooking = teamEventType?.bookings[0]; - - const nonTeamBooking = await prisma.booking.findFirst({ - where: { - eventType: { teamId: null }, - userId: proUser.id, - }, - }); - - const { req } = createMocks({ - method: "GET", - query: { - expand: "team", - }, - pagination: DefaultPagination, - }); - - req.userId = adminUser.id; - req.isOrganizationOwnerOrAdmin = true; - - const responseData = await handler(req); - - // Verify team booking has team data - if (teamBooking) { - const returnedTeamBooking = responseData.bookings.find((b) => b.id === teamBooking.id); - expect(returnedTeamBooking?.eventType?.team?.slug).toBe("team1"); - } - - // Verify non-team booking has null/undefined team - if (nonTeamBooking) { - const returnedNonTeamBooking = responseData.bookings.find((b) => b.id === nonTeamBooking.id); - if (returnedNonTeamBooking) { - expect( - returnedNonTeamBooking.eventType?.team === null || - returnedNonTeamBooking.eventType?.team === undefined - ).toBe(true); - } - } - }); - }); - - describe("Date filtering", () => { - it("filters bookings by dateFrom", async () => { - const { req } = createMocks({ - method: "GET", - query: { - dateFrom: "2023-01-01T00:00:00Z", - }, - pagination: DefaultPagination, - }); - - req.userId = proUser.id; - - const responseData = await handler(req); - responseData.bookings.forEach((booking) => { - expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual( - new Date("2023-01-01T00:00:00Z").getTime() - ); - }); - }); - - it("filters bookings by dateTo", async () => { - const { req } = createMocks({ - method: "GET", - query: { - dateTo: "2024-12-31T23:59:59Z", - }, - pagination: DefaultPagination, - }); - - req.userId = proUser.id; - - const responseData = await handler(req); - responseData.bookings.forEach((booking) => { - expect(new Date(booking.endTime).getTime()).toBeLessThanOrEqual( - new Date("2024-12-31T23:59:59Z").getTime() - ); - }); - }); - }); - - describe("Sorting and ordering", () => { - it("sorts bookings by createdAt in descending order", async () => { - const { req } = createMocks({ - method: "GET", - query: { - sortBy: "createdAt", - order: "desc", - }, - pagination: DefaultPagination, - }); - - req.userId = proUser.id; - - const responseData = await handler(req); - const timestamps = responseData.bookings.map((b) => new Date(b.createdAt).getTime()); - expect(timestamps).toEqual([...timestamps].sort((a, b) => b - a)); - }); - }); - - describe("Multiple attendee email filtering", () => { - it("filters bookings by multiple attendee emails", async () => { - const attendeeEmails = ["test1@example.com", "test2@example.com"]; - const { req } = createMocks({ - method: "GET", - query: { - attendeeEmail: attendeeEmails, - }, - pagination: DefaultPagination, - }); - - req.userId = proUser.id; - - const responseData = await handler(req); - responseData.bookings.forEach((booking) => { - const bookingAttendeeEmails = booking.attendees?.map((a) => a.email); - expect(bookingAttendeeEmails?.some((email) => attendeeEmails.includes(email))).toBe(true); - }); - }); - }); - - describe("Error cases", () => { - it("throws error for invalid status parameter", async () => { - const { req } = createMocks({ - method: "GET", - query: { - status: "invalid_status", - }, - pagination: DefaultPagination, - }); - - req.userId = proUser.id; - - await expect(handler(req)).rejects.toThrow(ZodError); - }); - }); - - describe("Result merging", () => { - it("does not return duplicate bookings when merging results from multiple queries", async () => { - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); - - const testUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } }); - - const { req } = createMocks({ - method: "GET", - query: { - userId: testUser.id, // This will make userEmailsToFilterBy contain the test user's email - }, - pagination: { - take: 100, - skip: 0, - }, - }); - - req.isSystemWideAdmin = true; - req.userId = adminUser.id; - - const responseData = await handler(req); - - const bookingIds = responseData.bookings.map((booking) => booking.id); - - const uniqueBookingIds = new Set(bookingIds); - - expect(uniqueBookingIds.size).toBe(bookingIds.length); - - if (uniqueBookingIds.size !== bookingIds.length) { - const counts = bookingIds.reduce((acc, id) => { - acc[id] = (acc[id] || 0) + 1; - return acc; - }, {} as Record); - - const duplicates = Object.entries(counts) - .filter(([_, count]) => count > 1) - .map(([id]) => id); - - console.log(`Found duplicate booking IDs: ${duplicates.join(", ")}`); - } - }); - }); -}); diff --git a/apps/api/v1/test/lib/bookings/_get.test.ts b/apps/api/v1/test/lib/bookings/_get.test.ts deleted file mode 100644 index 8ffbec5974954a..00000000000000 --- a/apps/api/v1/test/lib/bookings/_get.test.ts +++ /dev/null @@ -1,363 +0,0 @@ -import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; - -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, expect, test, vi, afterEach, beforeEach } from "vitest"; -import { ZodError } from "zod"; - -import { buildBooking } from "@calcom/lib/test/builder"; - -import { - getAccessibleUsers, - retrieveOrgScopedAccessibleUsers, -} from "~/lib/utils/retrieveScopedAccessibleUsers"; - -import { handler } from "../../../pages/api/bookings/_get"; - -vi.mock("~/lib/utils/retrieveScopedAccessibleUsers", () => ({ - getAccessibleUsers: vi.fn(), - retrieveOrgScopedAccessibleUsers: vi.fn(), -})); - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -const userId = 1; - -beforeEach(() => { - prismaMock.user.findUnique.mockResolvedValue({ - id: userId, - email: "test@example.com", - name: "Test User", - } as any); - (getAccessibleUsers as any).mockResolvedValue([userId]); - (retrieveOrgScopedAccessibleUsers as any).mockResolvedValue([userId]); - - prismaMock.membership.findMany.mockResolvedValue([ - { - // @ts-expect-error Will be fixed by Prisma 6.7.0 upgrade mock changes - which uses vitest-mock-extended - team: { - id: 1, - isOrganization: true, - }, - }, - ]); - - prismaMock.user.findMany.mockResolvedValue([ - { - id: userId, - email: "test@example.com", - }, - ] as any); -}); - -afterEach(() => { - vi.resetAllMocks(); -}); - -describe("GET /api/bookings", () => { - describe("Query parameter validation", () => { - test("should validate status parameter correctly", async () => { - const { req } = createMocks({ - method: "GET", - query: { - status: "invalid_status", - }, - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = userId; - - await expect(handler(req)).rejects.toThrow(ZodError); - }); - - test("should accept valid status parameter", async () => { - prismaMock.booking.findMany.mockResolvedValue([]); - prismaMock.booking.count.mockResolvedValue(0); - - const { req } = createMocks({ - method: "GET", - query: { - status: "upcoming", - }, - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = userId; - - const result = await handler(req); - expect(result.bookings).toEqual([]); - }); - - test("should validate dateFrom parameter format", async () => { - const { req } = createMocks({ - method: "GET", - query: { - dateFrom: "invalid-date", - }, - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = userId; - - await expect(handler(req)).rejects.toThrow(); - }); - - test("should validate dateTo parameter format", async () => { - const { req } = createMocks({ - method: "GET", - query: { - dateTo: "invalid-date", - }, - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = userId; - - await expect(handler(req)).rejects.toThrow(); - }); - - test("should validate sortBy parameter", async () => { - const { req } = createMocks({ - method: "GET", - query: { - sortBy: "invalid_field", - }, - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = userId; - - await expect(handler(req)).rejects.toThrow(); - }); - - test("should validate order parameter", async () => { - const { req } = createMocks({ - method: "GET", - query: { - order: "invalid_order", - }, - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = userId; - - await expect(handler(req)).rejects.toThrow(); - }); - }); - - describe("Permission logic", () => { - test("should only return user's own bookings for regular user", async () => { - const mockBookings = [buildBooking({ id: 1, userId: userId }), buildBooking({ id: 2, userId: userId })]; - - prismaMock.booking.findMany.mockResolvedValue(mockBookings); - prismaMock.booking.count.mockResolvedValue(2); - - const { req } = createMocks({ - method: "GET", - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = userId; - - const result = await handler(req); - expect(result.bookings).toHaveLength(2); - expect(result.bookings.every((b) => b.userId === userId)).toBe(true); - }); - - test("should allow system-wide admin to access all bookings", async () => { - const adminUserId = 999; - const mockBookings = [ - buildBooking({ id: 1, userId: 1 }), - buildBooking({ id: 2, userId: 2 }), - buildBooking({ id: 3, userId: 3 }), - ]; - - prismaMock.booking.findMany.mockResolvedValue(mockBookings); - prismaMock.booking.count.mockResolvedValue(3); - - const { req } = createMocks({ - method: "GET", - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = adminUserId; - req.isSystemWideAdmin = true; - - const result = await handler(req); - expect(result.bookings).toHaveLength(3); - }); - - test("should allow org admin to access org bookings", async () => { - const orgAdminUserId = 999; - const mockBookings = [buildBooking({ id: 1, userId: 1 }), buildBooking({ id: 2, userId: 2 })]; - - prismaMock.booking.findMany.mockResolvedValue(mockBookings); - prismaMock.booking.count.mockResolvedValue(2); - - (retrieveOrgScopedAccessibleUsers as any).mockResolvedValue([1, 2]); - - const { req } = createMocks({ - method: "GET", - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = orgAdminUserId; - req.isOrganizationOwnerOrAdmin = true; - - const result = await handler(req); - expect(result.bookings).toHaveLength(2); - }); - }); - - describe("Filtering edge cases", () => { - test("should handle empty attendeeEmail array", async () => { - prismaMock.booking.findMany.mockResolvedValue([]); - prismaMock.booking.count.mockResolvedValue(0); - - const { req } = createMocks({ - method: "GET", - query: { - attendeeEmail: [], - }, - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = userId; - - const result = await handler(req); - expect(result.bookings).toEqual([]); - }); - - test("should handle single attendeeEmail string", async () => { - prismaMock.booking.findMany.mockResolvedValue([]); - prismaMock.booking.count.mockResolvedValue(0); - - const { req } = createMocks({ - method: "GET", - query: { - attendeeEmail: "test@example.com", - }, - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = userId; - - const result = await handler(req); - expect(result.bookings).toEqual([]); - }); - - test("should handle pagination edge cases", async () => { - prismaMock.booking.findMany.mockResolvedValue([]); - prismaMock.booking.count.mockResolvedValue(0); - - const { req } = createMocks({ - method: "GET", - pagination: { - take: 0, - skip: 0, - }, - }); - - req.userId = userId; - - const result = await handler(req); - expect(result.bookings).toEqual([]); - }); - - test("should handle large skip values", async () => { - prismaMock.booking.findMany.mockResolvedValue([]); - prismaMock.booking.count.mockResolvedValue(5); - - const { req } = createMocks({ - method: "GET", - pagination: { - take: 10, - skip: 1000, - }, - }); - - req.userId = userId; - - const result = await handler(req); - expect(result.bookings).toEqual([]); - }); - }); - - describe("Expand parameter functionality", () => { - test("should handle invalid expand parameter", async () => { - prismaMock.booking.findMany.mockResolvedValue([]); - prismaMock.booking.count.mockResolvedValue(0); - - const { req } = createMocks({ - method: "GET", - query: { - expand: "invalid_field", - }, - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = userId; - - await expect(handler(req)).rejects.toThrow(); - }); - - test("should handle valid expand parameter", async () => { - prismaMock.booking.findMany.mockResolvedValue([]); - prismaMock.booking.count.mockResolvedValue(0); - - const { req } = createMocks({ - method: "GET", - query: { - expand: "team", - }, - pagination: { - take: 10, - skip: 0, - }, - }); - - req.userId = userId; - - const result = await handler(req); - expect(result.bookings).toEqual([]); - }); - }); -}); diff --git a/apps/api/v1/test/lib/bookings/_post.test.ts b/apps/api/v1/test/lib/bookings/_post.test.ts deleted file mode 100644 index 224d11a4501026..00000000000000 --- a/apps/api/v1/test/lib/bookings/_post.test.ts +++ /dev/null @@ -1,785 +0,0 @@ -import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; -import dayjs from "@calcom/dayjs"; -import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB"; -import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload"; -import { ErrorCode } from "@calcom/lib/errorCodes"; -import { buildBooking, buildEventType, buildUser, buildWebhook } from "@calcom/lib/test/builder"; -import { prisma } from "@calcom/prisma"; -import type { Booking } from "@calcom/prisma/client"; -import { BookingStatus, CreationSource } from "@calcom/prisma/enums"; -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { beforeEach, describe, expect, test, vi } from "vitest"; -import handler from "../../../pages/api/bookings/_post"; - -vi.mock("@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB", () => ({ - getEventTypesFromDB: vi.fn(), -})); - -vi.mock("@calcom/app-store/delegationCredential", () => ({ - enrichHostsWithDelegationCredentials: vi.fn().mockImplementation(({ hosts }) => hosts), - enrichUsersWithDelegationCredentials: vi.fn().mockImplementation(({ users }) => users), - enrichUserWithDelegationCredentialsIncludeServiceAccountKey: vi.fn().mockImplementation(({ user }) => user), - enrichUserWithDelegationCredentials: vi.fn().mockImplementation(({ user }) => user), - enrichUserWithDelegationConferencingCredentialsWithoutOrgId: vi.fn().mockImplementation(({ user }) => user), - getUsersCredentialsIncludeServiceAccountKey: vi.fn().mockResolvedValue([]), - getUsersCredentials: vi.fn().mockResolvedValue([]), - getCredentialForSelectedCalendar: vi.fn(), - getAllDelegationCredentialsForUserIncludeServiceAccountKey: vi.fn().mockResolvedValue([]), - getAllDelegationCredentialsForUser: vi.fn().mockResolvedValue([]), - getAllDelegatedCalendarCredentialsForUser: vi.fn().mockResolvedValue([]), - getAllDelegationCredentialsForUserByAppType: vi.fn().mockResolvedValue([]), - getAllDelegationCredentialsForUserByAppSlug: vi.fn().mockResolvedValue([]), - buildAllCredentials: vi.fn().mockImplementation(({ existingCredentials }) => existingCredentials || []), - getDelegationCredentialOrFindRegularCredential: vi.fn(), - getDelegationCredentialOrRegularCredential: vi.fn(), - getFirstDelegationConferencingCredential: vi.fn(), - getFirstDelegationConferencingCredentialAppLocation: vi.fn(), - findUniqueDelegationCalendarCredential: vi.fn(), - assertSuccessfullyConfiguredInWorkspace: vi.fn(), -})); - -vi.mock("@calcom/features/calendars/lib/CalendarManager", () => ({ - createEvent: vi.fn(), - updateEvent: vi.fn(), - deleteEvent: vi.fn(), - getBusyCalendarTimes: vi.fn(), -})); - -vi.mock("@calcom/features/auth/lib/verifyEmail", () => ({ - checkIfEmailIsBlockedInWatchlist: vi.fn(), - isEmailVerified: vi.fn(), -})); - -vi.mock("@calcom/lib/domainManager/organization", () => ({ - getOrgDomainConfigFromHostname: vi.fn(), - subdomainSuffix: vi.fn(), -})); - -const mockEventTypeData = { - eventType: { - id: 1, - title: "Test Event", - profile: { organizationId: null }, - users: [{ id: 1, email: "test@example.com", credentials: [] }], - hosts: [], - customInputs: [], - recurringEvent: null, - length: 15, - slug: "test-event", - price: 0, - currency: "USD", - requiresConfirmation: false, - disableGuests: false, - minimumBookingNotice: 120, - beforeEventBuffer: 0, - afterEventBuffer: 0, - seatsPerTimeSlot: null, - seatsShowAttendees: false, - schedulingType: null, - periodType: "UNLIMITED", - periodStartDate: null, - periodEndDate: null, - periodDays: null, - periodCountCalendarDays: false, - locations: [], - metadata: {}, - successRedirectUrl: null, - description: null, - team: null, - owner: { id: 1, email: "test@example.com", credentials: [] }, - }, - users: [{ id: 1, email: "test@example.com", credentials: [] }], - allCredentials: [], - destinationCalendar: null, -}; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; -vi.mock("@calcom/features/webhooks/lib/sendOrSchedulePayload", () => ({ - default: vi.fn().mockResolvedValue({}), -})); - -const mockFindOriginalRescheduledBooking = vi.fn(); -vi.mock("@calcom/features/bookings/repositories/BookingRepository", () => ({ - BookingRepository: vi.fn().mockImplementation(function () { - return { - findOriginalRescheduledBooking: mockFindOriginalRescheduledBooking, - }; - }), -})); - -vi.mock("@calcom/features/watchlist/operations/check-if-users-are-blocked.controller", () => ({ - checkIfUsersAreBlocked: vi.fn().mockResolvedValue(false), -})); - -vi.mock("@calcom/features/watchlist/operations/filter-blocked-users.controller", () => ({ - filterBlockedUsers: vi.fn().mockImplementation(async (users) => ({ - eligibleUsers: users, - blockedCount: 0, - })), -})); - -vi.mock("@calcom/features/di/containers/QualifiedHosts", () => ({ - getQualifiedHostsService: vi.fn().mockReturnValue({ - findQualifiedHostsWithDelegationCredentials: vi.fn().mockResolvedValue({ - qualifiedRRHosts: [], - allFallbackRRHosts: [], - fixedHosts: [], - }), - }), -})); - -vi.mock("@calcom/features/bookings/lib/EventManager", () => ({ - default: vi.fn().mockImplementation(function () { - return { - reschedule: vi.fn().mockResolvedValue({ - results: [], - referencesToCreate: [], - }), - create: vi.fn().mockResolvedValue({ - results: [], - referencesToCreate: [], - }), - update: vi.fn().mockResolvedValue({ - results: [], - referencesToCreate: [], - }), - createAllCalendarEvents: vi.fn().mockResolvedValue([]), - updateAllCalendarEvents: vi.fn().mockResolvedValue([]), - deleteEventsAndMeetings: vi.fn().mockResolvedValue([]), - }; - }), - placeholderCreatedEvent: { - results: [], - referencesToCreate: [], - }, -})); - -vi.mock("@calcom/lib/availability", () => ({ - getUserAvailability: vi.fn().mockResolvedValue([ - { - start: new Date("1970-01-01T09:00:00.000Z"), - end: new Date("1970-01-01T17:00:00.000Z"), - }, - ]), - getAvailableSlots: vi.fn().mockResolvedValue([ - { - time: new Date().toISOString(), - attendees: 1, - bookingUid: null, - users: [2], - }, - ]), -})); - -vi.mock("@calcom/features/bookings/lib/handleNewBooking/ensureAvailableUsers", () => ({ - ensureAvailableUsers: vi.fn().mockImplementation(async (eventType) => { - return eventType.users || [{ id: 2, email: "test@example.com", name: "Test User", isFixed: false }]; - }), -})); - -vi.mock("@calcom/features/profile/repositories/ProfileRepository", () => ({ - ProfileRepository: { - findManyForUser: vi.fn().mockResolvedValue([]), - buildPersonalProfileFromUser: vi.fn().mockReturnValue({ - id: null, - upId: "usr-2", - username: "test-user", - organizationId: null, - organization: null, - }), - }, -})); -vi.mock("@calcom/features/flags/features.repository", () => ({ - FeaturesRepository: vi.fn().mockImplementation(function () { - return { - checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(false), - checkIfTeamHasFeature: vi.fn().mockResolvedValue(false), - }; - }), -})); - -vi.mock("@calcom/features/webhooks/lib/getWebhooks", () => ({ - default: vi.fn().mockResolvedValue([]), -})); - -vi.mock("@calcom/features/ee/workflows/lib/getAllWorkflows", () => ({ - getAllWorkflows: vi.fn().mockResolvedValue([]), - workflowSelect: {}, -})); - -vi.mock("@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType", () => ({ - getAllWorkflowsFromEventType: vi.fn().mockResolvedValue([]), -})); - -vi.mock("@calcom/i18n/server", () => { - const mockT = (key: string, options?: any) => { - if (key === "event_between_users") { - return `${options?.eventName} between ${options?.host} and ${options?.attendeeName}`; - } - if (key === "scheduler") { - return "Scheduler"; - } - if (key === "google_meet_warning") { - return "Google Meet warning"; - } - return key; - }; - - return { - getTranslation: vi.fn().mockResolvedValue(mockT), - t: mockT, - }; -}); - -describe("POST /api/bookings - eventTypeId validation", () => { - test("String eventTypeId should return 400", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - eventTypeId: "invalid-string", - }, - }); - - await handler(req, res); - - expect(res._getStatusCode()).toBe(400); - expect(JSON.parse(res._getData())).toEqual( - expect.objectContaining({ - message: "Bad request, eventTypeId must be a number", - }) - ); - }); - - test("Number eventTypeId should not trigger validation error", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - eventTypeId: 123, - }, - }); - - prismaMock.eventType.findUniqueOrThrow.mockResolvedValue( - buildEventType({ - id: 123, - profileId: null, - locations: [{ type: "integrations:daily" }], - }) - ); - - await handler(req, res); - - const statusCode = res._getStatusCode(); - const responseData = JSON.parse(res._getData()); - - if (statusCode === 400) { - expect(responseData.message).not.toBe("Bad request, eventTypeId must be a number"); - } - }); -}); - -describe("POST /api/bookings", () => { - beforeEach(() => { - vi.clearAllMocks(); - mockFindOriginalRescheduledBooking.mockResolvedValue(null); - - (getEventTypesFromDB as any).mockResolvedValue(mockEventTypeData.eventType); - }); - - describe("Errors", () => { - test("Missing required data", async () => { - (getEventTypesFromDB as any).mockRejectedValue(new Error(ErrorCode.RequestBodyInvalid)); - - const { req, res } = createMocks({ - method: "POST", - body: { - eventTypeId: 2, - }, - }); - - await handler(req, res); - - expect(res._getStatusCode()).toBe(400); - expect(JSON.parse(res._getData())).toEqual( - expect.objectContaining({ - message: ErrorCode.RequestBodyInvalid, - }) - ); - }); - - test("Invalid eventTypeId", async () => { - (getEventTypesFromDB as any).mockRejectedValue(new Error(ErrorCode.EventTypeNotFound)); - - const { req, res } = createMocks({ - method: "POST", - body: { - title: "test", - eventTypeId: 999, - startTime: dayjs().add(1, "day").toDate(), - endTime: dayjs().add(1, "day").add(15, "minutes").toDate(), - }, - prisma, - }); - - await handler(req, res); - - expect(res._getStatusCode()).toBe(400); - expect(JSON.parse(res._getData())).toEqual( - expect.objectContaining({ - message: ErrorCode.EventTypeNotFound, - }) - ); - }); - - test.skip("Missing recurringCount", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - name: "test", - start: dayjs().add(1, "day").format(), - end: dayjs().add(1, "day").add(15, "minutes").format(), - eventTypeId: 2, - email: "test@example.com", - location: "Cal.com Video", - timeZone: "America/Montevideo", - language: "en", - customInputs: [], - metadata: {}, - userId: 4, - }, - prisma, - }); - - prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({ - ...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }), - // @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now. - profile: { organizationId: null }, - hosts: [], - users: [buildUser()], - }); - - await handler(req, res); - - expect(res._getStatusCode()).toBe(400); - expect(JSON.parse(res._getData())).toEqual( - expect.objectContaining({ - message: expect.stringContaining("recurringCount"), - }) - ); - }); - - test.skip("Invalid recurringCount", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - name: "test", - start: dayjs().add(1, "day").format(), - end: dayjs().add(1, "day").add(15, "minutes").format(), - eventTypeId: 2, - email: "test@example.com", - location: "Cal.com Video", - timeZone: "America/Montevideo", - language: "en", - customInputs: [], - metadata: {}, - userId: 4, - recurringCount: 15, - }, - prisma, - }); - - prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({ - ...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }), - // @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now. - profile: { organizationId: null }, - hosts: [], - users: [buildUser()], - }); - - await handler(req, res); - - expect(res._getStatusCode()).toBe(400); - expect(JSON.parse(res._getData())).toEqual( - expect.objectContaining({ - message: expect.stringContaining("recurringCount"), - }) - ); - }); - - test.skip("No available users", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - name: "test", - start: dayjs().add(1, "day").format(), - end: dayjs().add(1, "day").add(1, "hour").format(), - eventTypeId: 2, - email: "test@example.com", - location: "Cal.com Video", - timeZone: "America/Montevideo", - language: "en", - customInputs: [], - metadata: {}, - userId: 4, - }, - prisma, - }); - - prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({ - ...buildEventType({ profileId: null }), - // @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now. - profile: { organizationId: null }, - hosts: [], - users: [buildUser()], - }); - - await handler(req, res); - console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) }); - - expect(res._getStatusCode()).toBe(400); - expect(JSON.parse(res._getData())).toEqual( - expect.objectContaining({ - message: "Invalid event length", - }) - ); - }); - }); - - describe("Success", () => { - describe("Regular event-type", () => { - let createdBooking: Booking; - test("Creates one single booking", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - name: "test", - start: dayjs().add(1, "day").format(), - end: dayjs().add(1, "day").add(15, "minutes").format(), - eventTypeId: 2, - email: "test@example.com", - location: "Cal.com Video", - timeZone: "America/Montevideo", - language: "en", - customInputs: [], - metadata: {}, - userId: 4, - }, - prisma, - }); - - prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({ - ...buildEventType({ profileId: null, length: 15 }), - // @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now. - profile: { organizationId: null }, - hosts: [], - users: [buildUser()], - }); - prismaMock.booking.findMany.mockResolvedValue([]); - const mockBooking = buildBooking({ - id: 1, - uid: "test-booking-uid", - title: "Test Event", - startTime: new Date(dayjs().add(1, "day").format()), - endTime: new Date(dayjs().add(1, "day").add(15, "minutes").format()), - eventTypeId: buildEventType({ - id: 2, - profileId: null, - }).id, - oneTimePassword: null, - creationSource: "API_V1", - }); - prismaMock.$transaction.mockImplementation(async (callback) => { - const mockTx = { - booking: { - create: prismaMock.booking.create.mockResolvedValue(mockBooking), - update: vi.fn().mockResolvedValue({}), - }, - app_RoutingForms_FormResponse: { - update: vi.fn().mockResolvedValue({}), - }, - }; - return await callback(prismaMock); - }); - - await handler(req, res); - console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) }); - createdBooking = JSON.parse(res._getData()); - expect(prismaMock.booking.create).toHaveBeenCalledTimes(1); - }); - - test("Reschedule created booking", async () => { - const originalBooking = buildBooking({ - uid: "original-booking-uid", - userId: 4, - eventTypeId: buildEventType({ - id: 2, - locations: [{ type: "integrations:daily" }], - }).id, - references: [], - }); - mockFindOriginalRescheduledBooking.mockResolvedValue(originalBooking); - - prismaMock.booking.findUnique.mockResolvedValue({ - ...originalBooking, - status: "CANCELLED", - }); - - const { req, res } = createMocks({ - method: "POST", - body: { - name: "testReschedule", - start: dayjs().add(2, "day").format(), - end: dayjs().add(2, "day").add(15, "minutes").format(), - eventTypeId: 2, - email: "test@example.com", - location: "Cal.com Video", - timeZone: "America/Montevideo", - language: "en", - customInputs: [], - metadata: {}, - userId: 4, - rescheduleUid: "original-booking-uid", - }, - prisma, - }); - - prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({ - ...buildEventType({ profileId: null, length: 15 }), - // @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now. - profile: { organizationId: null }, - hosts: [], - users: [buildUser()], - }); - prismaMock.booking.findMany.mockResolvedValue([]); - const mockBooking = buildBooking({ - id: 2, - uid: "test-reschedule-uid", - title: "Test Reschedule Event", - startTime: new Date(dayjs().add(2, "day").format()), - endTime: new Date(dayjs().add(2, "day").add(15, "minutes").format()), - eventTypeId: buildEventType({ - id: 2, - profileId: null, - locations: [{ type: "integrations:daily" }], - }).id, - oneTimePassword: null, - fromReschedule: "original-booking-uid", - }); - prismaMock.$transaction.mockImplementation(async (callback) => { - const mockTx = { - booking: { - create: prismaMock.booking.create.mockResolvedValue(mockBooking), - update: vi.fn().mockResolvedValue({}), - }, - app_RoutingForms_FormResponse: { - update: vi.fn().mockResolvedValue({}), - }, - }; - return await callback(prismaMock); - }); - - await handler(req, res); - console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) }); - const rescheduledBooking = JSON.parse(res._getData()) as Booking; - expect(prismaMock.booking.create).toHaveBeenCalledTimes(1); - expect(rescheduledBooking.fromReschedule).toEqual("original-booking-uid"); - const previousBooking = await prisma.booking.findUnique({ - where: { uid: "original-booking-uid" }, - }); - expect(previousBooking?.status).toBe(BookingStatus.CANCELLED); - }); - - test("Creates source as api_v1", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - name: "test", - start: dayjs().add(1, "day").format(), - end: dayjs().add(1, "day").add(15, "minutes").format(), - eventTypeId: 2, - email: "test@example.com", - location: "Cal.com Video", - timeZone: "America/Montevideo", - language: "en", - customInputs: [], - metadata: {}, - userId: 4, - }, - prisma, - }); - - prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({ - ...buildEventType({ profileId: null, length: 15 }), - // @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now. - profile: { organizationId: null }, - hosts: [], - users: [buildUser()], - }); - prismaMock.booking.findMany.mockResolvedValue([]); - const mockBooking = buildBooking({ - id: 3, - uid: "test-api-v1-uid", - title: "Test API V1 Event", - startTime: new Date(dayjs().add(1, "day").format()), - endTime: new Date(dayjs().add(1, "day").add(15, "minutes").format()), - eventTypeId: buildEventType({ - id: 2, - profileId: null, - }).id, - oneTimePassword: null, - creationSource: "API_V1", - }); - prismaMock.$transaction.mockImplementation(async (callback) => { - const mockTx = { - booking: { - create: prismaMock.booking.create.mockResolvedValue(mockBooking), - update: vi.fn().mockResolvedValue({}), - }, - app_RoutingForms_FormResponse: { - update: vi.fn().mockResolvedValue({}), - }, - }; - return await callback(prismaMock); - }); - - await handler(req, res); - createdBooking = JSON.parse(res._getData()); - expect(createdBooking.creationSource).toEqual(CreationSource.API_V1); - expect(prismaMock.booking.create).toHaveBeenCalledTimes(1); - }); - }); - - describe("Recurring event-type", () => { - test.skip("Creates multiple bookings", async () => { - const recurringDates = Array.from(Array(12).keys()).map((i) => ({ - start: dayjs().add(1, "day").add(i, "week").toISOString(), - end: dayjs().add(1, "day").add(i, "week").add(15, "minutes").toISOString(), - })); - - const { req, res } = createMocks({ - method: "POST", - body: { - title: "test", - eventTypeId: 2, - start: recurringDates[0].start, - end: recurringDates[0].end, - timeZone: "UTC", - language: "en", - responses: { - name: "Test User", - email: "test@example.com", - location: { optionValue: "", value: "integrations:daily" }, - }, - metadata: {}, - recurringCount: 12, - recurringEventId: "test-recurring-event-id", - allRecurringDates: recurringDates, - isFirstRecurringSlot: true, - numSlotsToCheckForAvailability: 12, - }, - prisma, - }); - - prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({ - ...buildEventType({ profileId: null, length: 15 }), - // @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now. - profile: { organizationId: null }, - hosts: [], - users: [buildUser()], - }); - - const mockBookings = Array.from(Array(12).keys()).map((i) => - buildBooking({ id: i + 1, uid: `recurring-booking-${i}` }) - ); - - prismaMock.booking.create.mockResolvedValue(buildBooking({ id: 1, uid: "test-booking-uid" })); - prismaMock.$transaction.mockImplementation(async (callback) => { - return await callback(prismaMock); - }); - - prismaMock.webhook.findMany.mockResolvedValue([]); - - await handler(req, res); - const data = JSON.parse(res._getData()); - - expect(prismaMock.$transaction).toHaveBeenCalled(); - expect(res._getStatusCode()).toBe(200); - expect(data.message).toEqual("Bookings created successfully."); - expect(data.bookings.length).toEqual(12); - }); - }); - test.skip("Notifies multiple bookings", async () => { - const recurringDates = Array.from(Array(12).keys()).map((i) => ({ - start: dayjs().add(1, "day").add(i, "week").toISOString(), - end: dayjs().add(1, "day").add(i, "week").add(15, "minutes").toISOString(), - })); - - const { req, res } = createMocks({ - method: "POST", - body: { - title: "test", - eventTypeId: 2, - start: recurringDates[0].start, - end: recurringDates[0].end, - timeZone: "UTC", - language: "en", - responses: { - name: "Test User", - email: "test@example.com", - location: { optionValue: "", value: "integrations:daily" }, - }, - metadata: {}, - recurringCount: 12, - recurringEventId: "test-recurring-event-id", - allRecurringDates: recurringDates, - isFirstRecurringSlot: true, - numSlotsToCheckForAvailability: 12, - }, - prisma, - }); - - prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({ - ...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }), - // @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now. - profile: { organizationId: null }, - hosts: [], - users: [buildUser()], - }); - - const createdAt = new Date(); - const mockBookings = Array.from(Array(12).keys()).map((i) => - buildBooking({ id: i + 1, uid: `webhook-booking-${i}`, createdAt }) - ); - - prismaMock.booking.create.mockResolvedValue(buildBooking({ id: 1, uid: "test-booking-uid" })); - prismaMock.$transaction.mockImplementation(async (callback) => { - return await callback(prismaMock); - }); - - const mockedWebhooks = [ - buildWebhook({ - subscriberUrl: "http://mockedURL1.com", - createdAt, - eventTypeId: 1, - secret: "secret1", - }), - buildWebhook({ - subscriberUrl: "http://mockedURL2.com", - createdAt, - eventTypeId: 2, - secret: "secret2", - }), - ]; - prismaMock.webhook.findMany.mockResolvedValue(mockedWebhooks); - - await handler(req, res); - const data = JSON.parse(res._getData()); - - expect(sendPayload).toHaveBeenCalledTimes(24); - expect(data.message).toEqual("Bookings created successfully."); - expect(data.bookings.length).toEqual(12); - }); - }); -}); diff --git a/apps/api/v1/test/lib/bookings/get/buildWhereClause.test.ts b/apps/api/v1/test/lib/bookings/get/buildWhereClause.test.ts deleted file mode 100644 index 1060eaf9286fab..00000000000000 --- a/apps/api/v1/test/lib/bookings/get/buildWhereClause.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { describe, it, expect } from "vitest"; - -import { buildWhereClause } from "~/lib/utils/bookings/get/buildWhereClause"; - -describe("buildWhereClause", () => { - it("should return empty object when no filters are provided", () => { - const result = buildWhereClause(null, [], []); - expect(result).toEqual({}); - }); - - it("should filter by userId when only userId is provided", () => { - const userId = 123; - const result = buildWhereClause(userId, [], []); - expect(result).toEqual({ userId: 123 }); - }); - - it("should filter by userIds when userIds array is provided", () => { - const userIds = [1, 2, 3]; - const result = buildWhereClause(null, [], userIds); - expect(result).toEqual({ userId: { in: [1, 2, 3] } }); - }); - - it("should filter by attendee emails when provided", () => { - const attendeeEmails = ["test@example.com", "user@example.com"]; - const result = buildWhereClause(null, attendeeEmails, []); - expect(result).toEqual({ - AND: [ - {}, - { - attendees: { - some: { - email: { in: ["test@example.com", "user@example.com"] }, - }, - }, - }, - ], - }); - }); - - it("should combine userId and attendee email filters", () => { - const userId = 123; - const attendeeEmails = ["test@example.com"]; - const result = buildWhereClause(userId, attendeeEmails, []); - expect(result).toEqual({ - AND: [ - { userId: 123 }, - { - attendees: { - some: { - email: { in: ["test@example.com"] }, - }, - }, - }, - ], - }); - }); - - it("should combine userIds and attendee email filters", () => { - const userIds = [1, 2, 3]; - const attendeeEmails = ["test@example.com"]; - const result = buildWhereClause(null, attendeeEmails, userIds); - expect(result).toEqual({ - AND: [ - { userId: { in: [1, 2, 3] } }, - { - attendees: { - some: { - email: { in: ["test@example.com"] }, - }, - }, - }, - ], - }); - }); -}); diff --git a/apps/api/v1/test/lib/event-types/[id]/_delete.test.ts b/apps/api/v1/test/lib/event-types/[id]/_delete.test.ts deleted file mode 100644 index fa0a180c8808e4..00000000000000 --- a/apps/api/v1/test/lib/event-types/[id]/_delete.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; - -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, expect, test, beforeEach, vi } from "vitest"; - -import { buildEventType } from "@calcom/lib/test/builder"; -import { MembershipRole } from "@calcom/prisma/enums"; - -import handler from "../../../../pages/api/event-types/[id]/_delete"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -describe("DELETE /api/event-types/[id]", () => { - const eventTypeId = 1234567; - const teamId = 9999; - const adminUser = 1111; - const memberUser = 2222; - beforeEach(() => { - vi.resetAllMocks(); - // Mocking membership.findFirst - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - prismaMock.membership.findFirst.mockImplementation(({ where }) => { - const { userId, teamId, accepted, role } = where; - const mockData = [ - { userId: 1111, teamId: teamId, accepted: true, role: MembershipRole.ADMIN }, - { userId: 2222, teamId: teamId, accepted: true, role: MembershipRole.MEMBER }, - ]; - // Return the correct user based on the query conditions - return mockData.find( - (membership) => - membership.userId === userId && - membership.teamId === teamId && - membership.accepted === accepted && - role.in.includes(membership.role) - ); - }); - - // Mocking eventType.findFirst - prismaMock.eventType.findFirst.mockResolvedValue( - buildEventType({ - id: eventTypeId, - teamId, - }) - ); - - // Mocking team.findUnique - prismaMock.team.findUnique.mockResolvedValue({ - id: teamId, - // @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now. - members: [ - { userId: memberUser, role: MembershipRole.MEMBER, teamId: teamId }, - { userId: adminUser, role: MembershipRole.ADMIN, teamId: teamId }, - ], - }); - }); - - describe("Error", async () => { - test("Fails to remove event type if user is not OWNER/ADMIN of team associated with event type", async () => { - const { req } = createMocks({ - method: "DELETE", - body: {}, - query: { - id: eventTypeId, - }, - }); - - // Assign userId to the request objects - req.userId = memberUser; - - await expect(handler(req)).rejects.toThrowError( - expect.objectContaining({ - statusCode: 403, - }) - ); - }); - }); - - describe("Success", async () => { - test("Removes event type if user is owner of team associated with event type", async () => { - // Mocks for DELETE request - const { req } = createMocks({ - method: "DELETE", - body: {}, - query: { - id: eventTypeId, - }, - }); - - // Assign userId to the request objects - req.userId = adminUser; - - const deletedEventType = await handler(req); - expect(deletedEventType).not.toBeNull(); - }); - }); -}); diff --git a/apps/api/v1/test/lib/event-types/[id]/_get.integration-test.ts b/apps/api/v1/test/lib/event-types/[id]/_get.integration-test.ts deleted file mode 100644 index ecdc1d0ead003a..00000000000000 --- a/apps/api/v1/test/lib/event-types/[id]/_get.integration-test.ts +++ /dev/null @@ -1,545 +0,0 @@ -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({ - 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({ - 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({ - 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({ - 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({ - 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>; - expect(locations[0]).toMatchObject({ - type: "integrations:zoom", - link: "https://zoom.us/j/123456789", - }); - - const bookingFields = responseData.event_type.bookingFields as Array>; - 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({ - 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({ - 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({ - 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({ - 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({ - 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({ - 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, - }); - }); - }); -}); diff --git a/apps/api/v1/test/lib/event-types/[id]/_get.test.ts b/apps/api/v1/test/lib/event-types/[id]/_get.test.ts deleted file mode 100644 index 44cb8aa3cd9457..00000000000000 --- a/apps/api/v1/test/lib/event-types/[id]/_get.test.ts +++ /dev/null @@ -1,517 +0,0 @@ -import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; - -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, expect, test } from "vitest"; - -import { buildEventType } from "@calcom/lib/test/builder"; -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]", () => { - describe("Errors", () => { - test("Returns 403 if user not admin/team member/event owner", async () => { - const { req } = createMocks({ - method: "GET", - body: {}, - query: { - id: 123456, - }, - }); - - prismaMock.eventType.findUnique.mockResolvedValue( - buildEventType({ - id: 123456, - userId: 444444, - }) - ); - - req.userId = 333333; - - await expect(handler(req)).rejects.toThrowError( - expect.objectContaining({ - statusCode: 403, - }) - ); - }); - test("Returns 404 if event type not found", async () => { - const { req } = createMocks({ - method: "GET", - body: {}, - query: { - id: 123456, - }, - }); - - req.isSystemWideAdmin = true; - - prismaMock.eventType.findUnique.mockResolvedValue(null); - - req.userId = 333333; - - await expect(handler(req)).rejects.toThrowError( - expect.objectContaining({ - statusCode: 404, - }) - ); - }); - }); - - describe("Success", async () => { - test("Returns event type if user is admin", async () => { - const eventTypeId = 123456; - const { req } = createMocks({ - method: "GET", - body: {}, - query: { - id: eventTypeId, - }, - }); - - prismaMock.eventType.findUnique.mockResolvedValue( - buildEventType({ - id: eventTypeId, - }) - ); - - req.isSystemWideAdmin = true; - req.userId = 333333; - - const responseData = await handler(req); - - expect(responseData.event_type.id).toEqual(eventTypeId); - }); - - test("Returns event type if user is in team associated with event type", async () => { - const eventTypeId = 123456; - const teamId = 9999; - const userId = 333333; - const { req } = createMocks({ - method: "GET", - body: {}, - query: { - id: eventTypeId, - }, - }); - - prismaMock.eventType.findUnique.mockResolvedValue( - buildEventType({ - id: eventTypeId, - teamId, - }) - ); - - 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.id).toEqual(eventTypeId); - expect(prismaMock.team.findFirst).toHaveBeenCalledWith({ - where: { - id: teamId, - members: { - some: { - userId: req.userId, - role: { - in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER], - }, - }, - }, - }, - }); - }); - - test("Returns event type if user is the event type owner", async () => { - const eventTypeId = 123456; - const userId = 333333; - const { req } = createMocks({ - method: "GET", - body: {}, - query: { - id: eventTypeId, - }, - }); - - prismaMock.eventType.findUnique.mockResolvedValue( - buildEventType({ - id: eventTypeId, - userId, - scheduleId: 1111, - }) - ); - - req.isSystemWideAdmin = false; - req.userId = userId; - - const responseData = await handler(req); - - expect(responseData.event_type.id).toEqual(eventTypeId); - 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({ - 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>; - expect(locations[0]).toMatchObject({ - type: "integrations:zoom", - link: "https://zoom.us/j/123456789", - }); - - const bookingFields = responseData.event_type.bookingFields as Array>; - 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({ - 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({ - 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({ - 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({ - 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({ - 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({ - 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, - }); - }); - }); -}); diff --git a/apps/api/v1/test/lib/event-types/_post.test.ts b/apps/api/v1/test/lib/event-types/_post.test.ts deleted file mode 100644 index ca13d5b4b9d355..00000000000000 --- a/apps/api/v1/test/lib/event-types/_post.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; - -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, expect, test, vi, afterEach } from "vitest"; - -import { buildEventType } from "@calcom/lib/test/builder"; - -import handler from "../../../pages/api/event-types/_post"; -import checkParentEventOwnership from "../../../pages/api/event-types/_utils/checkParentEventOwnership"; -import checkTeamEventEditPermission from "../../../pages/api/event-types/_utils/checkTeamEventEditPermission"; -import checkUserMembership from "../../../pages/api/event-types/_utils/checkUserMembership"; -import ensureOnlyMembersAsHosts from "../../../pages/api/event-types/_utils/ensureOnlyMembersAsHosts"; -import { canUserAccessTeamWithRole } from "../../../pages/api/teams/[teamId]/_auth-middleware"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -const adminUserId = 1; -const memberUserId = 10; - -vi.mock("../../../pages/api/teams/[teamId]/_auth-middleware", () => ({ - canUserAccessTeamWithRole: vi.fn(), -})); - -vi.mock("../../../pages/api/event-types/_utils/checkUserMembership", () => ({ - default: vi.fn(), -})); -vi.mock("../../../pages/api/event-types/_utils/checkParentEventOwnership", () => ({ - default: vi.fn(), -})); -vi.mock("../../../pages/api/event-types/_utils/checkTeamEventEditPermission", () => ({ - default: vi.fn(), -})); -vi.mock("../../../pages/api/event-types/_utils/ensureOnlyMembersAsHosts", () => ({ - default: vi.fn(), -})); - -afterEach(() => { - vi.resetAllMocks(); -}); - -describe("POST /api/event-types", () => { - describe("Errors", () => { - test("should throw 401 if a non-admin user tries to create an event type with userId", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - title: "Tennis class", - slug: "tennis-class-{{$guid}}", - length: 60, - hidden: true, - userId: memberUserId, - }, - }); - - await handler(req, res); - - expect(res.statusCode).toBe(401); - expect(JSON.parse(res._getData()).message).toBe("ADMIN required for `userId`"); - }); - test("should throw 401 if not system-wide admin and user cannot access teamId with required roles", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - title: "Tennis class", - slug: "tennis-class-{{$guid}}", - length: 60, - hidden: true, - teamId: 9999, - }, - }); - req.userId = memberUserId; - // @ts-expect-error - Return type is wrong - vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false); - - await handler(req, res); - - expect(res.statusCode).toBe(401); - expect(JSON.parse(res._getData()).message).toBe("ADMIN required for `teamId`"); - }); - test("should throw 400 if system-wide admin but neither userId nor teamId provided", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - title: "Tennis class", - slug: "tennis-class-{{$guid}}", - length: 60, - hidden: true, - }, - }); - req.isSystemWideAdmin = true; - await handler(req, res); - - expect(res.statusCode).toBe(400); - expect(JSON.parse(res._getData()).message).toBe("`userId` or `teamId` required"); - }); - }); - - describe("Success", () => { - test("should call checkParentEventOwnership and checkUserMembership if parentId is present", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - title: "Tennis class", - slug: "tennis-class-{{$guid}}", - length: 60, - hidden: true, - userId: memberUserId, - parentId: 9999, - teamId: 9999, - }, - }); - req.isSystemWideAdmin = true; - req.userId = adminUserId; - - // @ts-expect-error - Return type is wrong - vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false); - - await handler(req, res); - expect(checkParentEventOwnership).toHaveBeenCalled(); - expect(checkUserMembership).toHaveBeenCalled(); - }); - - test("should create event type successfully if all conditions are met", async () => { - const eventTypeTest = buildEventType(); - const { req, res } = createMocks({ - method: "POST", - body: { - title: "test title", - slug: "test-slug", - length: 60, - hidden: true, - userId: memberUserId, - parentId: 9999, - teamId: 9999, - }, - }); - req.isSystemWideAdmin = true; - req.userId = adminUserId; - - // @ts-expect-error - Return type is wrong - vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => true); - vi.mocked(checkParentEventOwnership).mockImplementationOnce(async () => undefined); - vi.mocked(checkUserMembership).mockImplementationOnce(async () => undefined); - vi.mocked(checkTeamEventEditPermission).mockImplementationOnce(async () => undefined); - vi.mocked(ensureOnlyMembersAsHosts).mockImplementationOnce(async () => undefined); - - prismaMock.eventType.create.mockResolvedValue(eventTypeTest); - - await handler(req, res); - const data = JSON.parse(res._getData()); - - expect(prismaMock.eventType.create).toHaveBeenCalledTimes(1); - expect(res.statusCode).toBe(200); - expect(data.message).toBe("Event type created successfully"); - }); - }); -}); diff --git a/apps/api/v1/test/lib/middleware/addRequestId.test.ts b/apps/api/v1/test/lib/middleware/addRequestId.test.ts deleted file mode 100644 index d2879a24e80a49..00000000000000 --- a/apps/api/v1/test/lib/middleware/addRequestId.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, vi, it, expect, afterEach } from "vitest"; - -import { addRequestId } from "../../../lib/helpers/addRequestid"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -afterEach(() => { - vi.resetAllMocks(); -}); - -describe("Adds a request ID", () => { - it("Should attach a request ID to the request", async () => { - const { req, res } = createMocks({ - method: "POST", - body: {}, - }); - - const middleware = { - fn: addRequestId, - }; - - const serverNext = vi.fn((next: void) => Promise.resolve(next)); - - const middlewareSpy = vi.spyOn(middleware, "fn"); - - await middleware.fn(req, res, serverNext); - - expect(middlewareSpy).toBeCalled(); - expect(res.statusCode).toBe(200); - expect(res.getHeader("Calcom-Response-ID")).toBeDefined(); - }); -}); diff --git a/apps/api/v1/test/lib/middleware/httpMethods.test.ts b/apps/api/v1/test/lib/middleware/httpMethods.test.ts deleted file mode 100644 index 2fc536c46cff81..00000000000000 --- a/apps/api/v1/test/lib/middleware/httpMethods.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, vi, it, expect, afterEach } from "vitest"; - -import { httpMethod } from "../../../lib/helpers/httpMethods"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -afterEach(() => { - vi.resetAllMocks(); -}); - -describe("HTTP Methods function only allows the correct HTTP Methods", () => { - it("Should allow the passed in Method", async () => { - const { req, res } = createMocks({ - method: "POST", - body: {}, - }); - - const middleware = { - fn: httpMethod("POST"), - }; - - const serverNext = vi.fn((next: void) => Promise.resolve(next)); - - const middlewareSpy = vi.spyOn(middleware, "fn"); - - await middleware.fn(req, res, serverNext); - - expect(middlewareSpy).toBeCalled(); - expect(res.statusCode).toBe(200); - }); - it("Should allow the passed in Method", async () => { - const { req, res } = createMocks({ - method: "POST", - body: {}, - }); - - const middleware = { - fn: httpMethod("GET"), - }; - - const serverNext = vi.fn((next: void) => Promise.resolve(next)); - const middlewareSpy = vi.spyOn(middleware, "fn"); - - await middleware.fn(req, res, serverNext); - - expect(middlewareSpy).toBeCalled(); - expect(res.statusCode).toBe(405); - }); -}); diff --git a/apps/api/v1/test/lib/selected-calendars/_post.test.ts b/apps/api/v1/test/lib/selected-calendars/_post.test.ts deleted file mode 100644 index ce4fde5a20f365..00000000000000 --- a/apps/api/v1/test/lib/selected-calendars/_post.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; - -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, expect, test } from "vitest"; - -import { HttpError } from "@calcom/lib/http-error"; -import type { User } from "@calcom/prisma/client"; - -import handler from "../../../pages/api/selected-calendars/_post"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -describe("POST /api/selected-calendars", () => { - describe("Errors", () => { - test("Returns 403 if non-admin user tries to set userId in body", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - integration: "google", - externalId: "ext123", - userId: 444444, - }, - }); - - req.userId = 333333; - - try { - await handler(req, res); - } catch (e) { - expect(e).toBeInstanceOf(HttpError); - expect((e as HttpError).statusCode).toBe(403); - expect((e as HttpError).message).toBe("ADMIN required for userId"); - } - }); - - test("Returns 400 if request body is invalid", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - integration: "google", - }, - }); - - req.userId = 333333; - req.isSystemWideAdmin = true; - - await handler(req, res); - - expect(res.statusCode).toBe(400); - expect(JSON.parse(res._getData()).message).toBe("invalid_type in 'externalId': Required"); - }); - }); - - describe("Success", () => { - test("Creates selected calendar if user is admin and sets bodyUserId", async () => { - const { req, res } = createMocks({ - method: "POST", - query: { - apiKey: "validApiKey", - }, - body: { - integration: "google", - externalId: "ext123", - userId: 444444, - }, - }); - - req.userId = 333333; - req.isSystemWideAdmin = true; - - prismaMock.user.findFirstOrThrow.mockResolvedValue({ - id: 444444, - } as User); - - prismaMock.selectedCalendar.create.mockResolvedValue({ - credentialId: 1, - integration: "google", - externalId: "ext123", - userId: 444444, - id: "f47ac10b-58cc-4372-a567-0e02b2c3d479", - eventTypeId: null, - delegationCredentialId: null, - domainWideDelegationCredentialId: null, - googleChannelId: null, - googleChannelKind: null, - googleChannelResourceId: null, - googleChannelResourceUri: null, - googleChannelExpiration: null, - error: null, - lastErrorAt: null, - watchAttempts: 0, - maxAttempts: 3, - unwatchAttempts: 0, - createdAt: new Date(), - updatedAt: new Date(), - channelId: null, - channelKind: null, - channelResourceId: null, - channelResourceUri: null, - channelExpiration: null, - syncSubscribedAt: null, - syncSubscribedErrorAt: null, - syncSubscribedErrorCount: 0, - syncToken: null, - syncedAt: null, - syncErrorAt: null, - syncErrorCount: null, - }); - - await handler(req, res); - - expect(res.statusCode).toBe(200); - const responseData = JSON.parse(res._getData()); - expect(responseData.selected_calendar.credentialId).toBe(1); - expect(responseData.message).toBe("Selected Calendar created successfully"); - }); - - test("Creates selected calendar if user is non-admin and does not set bodyUserId", async () => { - const { req, res } = createMocks({ - method: "POST", - query: { - apiKey: "validApiKey", - }, - body: { - integration: "google", - externalId: "ext123", - }, - }); - - req.userId = 333333; - - prismaMock.selectedCalendar.create.mockResolvedValue({ - id: "f47ac10b-58cc-4372-a567-0e02b2c3d479", - credentialId: 1, - integration: "google", - externalId: "ext123", - userId: 333333, - googleChannelId: null, - googleChannelKind: null, - googleChannelResourceId: null, - googleChannelResourceUri: null, - googleChannelExpiration: null, - delegationCredentialId: null, - domainWideDelegationCredentialId: null, - eventTypeId: null, - error: null, - lastErrorAt: null, - watchAttempts: 0, - maxAttempts: 3, - unwatchAttempts: 0, - createdAt: new Date(), - updatedAt: new Date(), - channelId: null, - channelKind: null, - channelResourceId: null, - channelResourceUri: null, - channelExpiration: null, - syncSubscribedAt: null, - syncSubscribedErrorAt: null, - syncSubscribedErrorCount: 0, - syncToken: null, - syncedAt: null, - syncErrorAt: null, - syncErrorCount: null, - }); - - await handler(req, res); - - expect(res.statusCode).toBe(200); - const responseData = JSON.parse(res._getData()); - expect(responseData.selected_calendar.credentialId).toBe(1); - expect(responseData.message).toBe("Selected Calendar created successfully"); - }); - }); -}); diff --git a/apps/api/v1/test/lib/users/_post.test.ts b/apps/api/v1/test/lib/users/_post.test.ts deleted file mode 100644 index b3e150c96fa608..00000000000000 --- a/apps/api/v1/test/lib/users/_post.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Unit Tests for POST /api/users - * - * These tests verify the API endpoint logic without touching the database. - * All dependencies (UserCreationService) are mocked. - */ -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, test, expect, vi, beforeEach } from "vitest"; - -import handler from "../../../pages/api/users/_post"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -vi.mock("@calcom/i18n/server", () => { - return { - getTranslation: (key: string) => { - return () => key; - }, - }; -}); - -vi.mock("@calcom/features/profile/lib/checkUsername", () => ({ - checkUsername: vi.fn().mockResolvedValue({ - available: true, - premium: false, - }), -})); - -vi.mock("@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller", () => ({ - checkIfEmailIsBlockedInWatchlistController: vi.fn().mockResolvedValue(false), -})); - -const mockCreate = vi.fn(); -vi.mock("@calcom/features/users/repositories/UserRepository", () => ({ - UserRepository: vi.fn().mockImplementation(function() { return { - create: mockCreate, - }; }), -})); - -vi.mock("@calcom/lib/auth/hashPassword", () => ({ - hashPassword: vi.fn().mockResolvedValue("hashed-password"), -})); - -vi.stubEnv("CALCOM_LICENSE_KEY", undefined); - -describe("POST /api/users - Unit Tests", () => { - beforeEach(() => { - vi.clearAllMocks(); - mockCreate.mockResolvedValue({ - id: 1, - email: "test@example.com", - username: "test", - locked: false, - }); - }); - - test("should throw 401 if not system-wide admin", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - email: "test@example.com", - username: "test", - }, - }); - req.isSystemWideAdmin = false; - - await handler(req, res); - - expect(res.statusCode).toBe(401); - expect(mockCreate).not.toHaveBeenCalled(); - }); - - test("should throw a 400 if no email is provided", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - username: "test", - }, - }); - req.isSystemWideAdmin = true; - - await handler(req, res); - - expect(res.statusCode).toBe(400); - expect(mockCreate).not.toHaveBeenCalled(); - }); - - test("should throw a 400 if no username is provided", async () => { - const { req, res } = createMocks({ - method: "POST", - body: { - email: "test@example.com", - }, - }); - req.isSystemWideAdmin = true; - - await handler(req, res); - - expect(res.statusCode).toBe(400); - expect(mockCreate).not.toHaveBeenCalled(); - }); - - test("should create user successfully", async () => { - mockCreate.mockResolvedValue({ - id: 1, - email: "test@example.com", - username: "testuser123", - locked: false, - organizationId: null, - }); - - const { req, res } = createMocks({ - method: "POST", - body: { - email: "test@example.com", - username: "testuser123", - }, - }); - req.isSystemWideAdmin = true; - - await handler(req, res); - - expect(res.statusCode).toBe(200); - - expect(mockCreate).toHaveBeenCalledWith( - expect.objectContaining({ - email: "test@example.com", - username: "testuser123", - locked: false, - }) - ); - - const responseData = JSON.parse(res._getData()); - expect(responseData.user).toEqual( - expect.objectContaining({ - email: "test@example.com", - username: "testuser123", - organizationId: null, - }) - ); - }); -}); diff --git a/apps/api/v1/test/lib/utils/isAdmin.integration-test.ts b/apps/api/v1/test/lib/utils/isAdmin.integration-test.ts deleted file mode 100644 index e89d447ffb3f87..00000000000000 --- a/apps/api/v1/test/lib/utils/isAdmin.integration-test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import prisma from "@calcom/prisma"; -import type { Request, Response } from "express"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { createMocks } from "node-mocks-http"; -import { describe, expect, it } from "vitest"; -import { isAdminGuard } from "../../../lib/utils/isAdmin"; -import { ScopeOfAdmin } from "../../../lib/utils/scopeOfAdmin"; - -type CustomNextApiRequest = NextApiRequest & Request; -type CustomNextApiResponse = NextApiResponse & Response; - -describe("isAdmin guard", () => { - it("Returns false when user does not exist in the system", async () => { - const { req } = createMocks({ - method: "POST", - body: {}, - }); - - req.userId = 0; - req.user = undefined; - - const { isAdmin, scope } = await isAdminGuard(req); - - expect(isAdmin).toBe(false); - expect(scope).toBe(null); - }); - - it("Returns false when org user is a member", async () => { - const { req } = createMocks({ - method: "POST", - body: {}, - }); - - const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } }); - - req.userId = memberUser.id; - req.user = memberUser; - - const { isAdmin, scope } = await isAdminGuard(req); - - expect(isAdmin).toBe(false); - expect(scope).toBe(null); - }); - - it("Returns system-wide admin when user is marked as such", async () => { - const { req } = createMocks({ - method: "POST", - body: {}, - }); - - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "admin@example.com" } }); - - req.userId = adminUser.id; - req.user = adminUser; - - const { isAdmin, scope } = await isAdminGuard(req); - - expect(isAdmin).toBe(true); - expect(scope).toBe(ScopeOfAdmin.SystemWide); - }); - - it("Returns org-wide admin when user is set as such & admin API access is granted", async () => { - const { req } = createMocks({ - method: "POST", - body: {}, - }); - - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); - - req.userId = adminUser.id; - req.user = adminUser; - - const { isAdmin, scope } = await isAdminGuard(req); - expect(isAdmin).toBe(true); - expect(scope).toBe(ScopeOfAdmin.OrgOwnerOrAdmin); - }); - - it("Returns no admin when user is set as org admin but admin API access is revoked", async () => { - const { req } = createMocks({ - method: "POST", - body: {}, - }); - - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-dunder@example.com" } }); - - req.userId = adminUser.id; - req.user = adminUser; - - const { isAdmin } = await isAdminGuard(req); - expect(isAdmin).toBe(false); - }); -}); diff --git a/apps/api/v1/test/lib/utils/isLockedOrBlocked.test.ts b/apps/api/v1/test/lib/utils/isLockedOrBlocked.test.ts deleted file mode 100644 index 5b7549be7b22d4..00000000000000 --- a/apps/api/v1/test/lib/utils/isLockedOrBlocked.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { getWatchlistFeature } from "@calcom/features/di/watchlist/containers/watchlist"; -import type { WatchlistFeature } from "@calcom/features/watchlist/lib/facade/WatchlistFeature"; -import type { NextApiRequest } from "next"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked"; - -vi.mock("@calcom/features/di/watchlist/containers/watchlist", () => ({ - getWatchlistFeature: vi.fn(), -})); - -vi.mock("@calcom/features/auth/lib/verifyEmail", () => ({ - sendEmailVerification: vi.fn(), - verifyEmail: vi.fn(), -})); - -vi.mock("@calcom/prisma", () => ({ - default: {}, - prisma: {}, -})); - -describe("isLockedOrBlocked", () => { - let mockWatchlistFeature: WatchlistFeature; - - beforeEach(() => { - const mockGlobalBlocking = { - isBlocked: vi.fn(), - }; - const mockOrgBlocking = { - isBlocked: vi.fn(), - }; - - mockWatchlistFeature = { - globalBlocking: mockGlobalBlocking, - orgBlocking: mockOrgBlocking, - } as unknown as WatchlistFeature; - - vi.mocked(getWatchlistFeature).mockResolvedValue(mockWatchlistFeature); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - it("should return false if no user in request", async () => { - const req = { userId: null, user: null } as unknown as NextApiRequest; - const result = await isLockedOrBlocked(req); - expect(result).toBe(false); - - expect(vi.mocked(mockWatchlistFeature.globalBlocking.isBlocked)).not.toHaveBeenCalled(); - }); - - it("should return false if user has no email", async () => { - const req = { userId: 123, user: { email: null } } as unknown as NextApiRequest; - const result = await isLockedOrBlocked(req); - expect(result).toBe(false); - - expect(vi.mocked(mockWatchlistFeature.globalBlocking.isBlocked)).not.toHaveBeenCalled(); - }); - - it("should return true if user is locked", async () => { - const req = { - userId: 123, - user: { - locked: true, - email: "test@example.com", - }, - } as unknown as NextApiRequest; - - const result = await isLockedOrBlocked(req); - expect(result).toBe(true); - - expect(vi.mocked(mockWatchlistFeature.globalBlocking.isBlocked)).not.toHaveBeenCalled(); - }); - - it("should return true if user email domain is watchlisted", async () => { - const req = { - userId: 123, - user: { - locked: false, - email: "test@blocked.com", - }, - } as unknown as NextApiRequest; - - vi.mocked(mockWatchlistFeature.globalBlocking.isBlocked).mockResolvedValue({ isBlocked: true }); - - const result = await isLockedOrBlocked(req); - expect(result).toBe(true); - - expect(vi.mocked(mockWatchlistFeature.globalBlocking.isBlocked)).toHaveBeenCalledWith("test@blocked.com"); - }); - - it("should return false if user is not locked and email domain is not watchlisted", async () => { - const req = { - userId: 123, - user: { - locked: false, - email: "test@example.com", - }, - } as unknown as NextApiRequest; - - vi.mocked(mockWatchlistFeature.globalBlocking.isBlocked).mockResolvedValue({ isBlocked: false }); - - const result = await isLockedOrBlocked(req); - expect(result).toBe(false); - - expect(vi.mocked(mockWatchlistFeature.globalBlocking.isBlocked)).toHaveBeenCalledWith("test@example.com"); - }); - - it("should handle email domains case-insensitively", async () => { - const req = { - userId: 123, - user: { - locked: false, - email: "test@BLOCKED.COM", - }, - } as unknown as NextApiRequest; - - vi.mocked(mockWatchlistFeature.globalBlocking.isBlocked).mockResolvedValue({ isBlocked: true }); - - const result = await isLockedOrBlocked(req); - expect(result).toBe(true); - - expect(vi.mocked(mockWatchlistFeature.globalBlocking.isBlocked)).toHaveBeenCalledWith("test@blocked.com"); - }); -}); diff --git a/apps/api/v1/test/lib/utils/retrieveScopedAccessibleUsers.integration-test.ts b/apps/api/v1/test/lib/utils/retrieveScopedAccessibleUsers.integration-test.ts deleted file mode 100644 index d9e1e6bba82c48..00000000000000 --- a/apps/api/v1/test/lib/utils/retrieveScopedAccessibleUsers.integration-test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import prisma from "@calcom/prisma"; -import { describe, expect, it } from "vitest"; -import { - getAccessibleUsers, - retrieveOrgScopedAccessibleUsers, -} from "../../../lib/utils/retrieveScopedAccessibleUsers"; - -describe("retrieveScopedAccessibleUsers tests", () => { - describe("getAccessibleUsers", () => { - it("Does not return members when only admin user ID is supplied", async () => { - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); - const accessibleUserIds = await getAccessibleUsers({ - memberUserIds: [], - adminUserId: adminUser.id, - }); - - expect(accessibleUserIds.length).toBe(0); - }); - - it("Does not return members when admin user ID is not an admin of the user", async () => { - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-dunder@example.com" } }); - const memberOneUser = await prisma.user.findFirstOrThrow({ - where: { email: "member1-acme@example.com" }, - }); - const accessibleUserIds = await getAccessibleUsers({ - memberUserIds: [memberOneUser.id], - adminUserId: adminUser.id, - }); - - expect(accessibleUserIds.length).toBe(0); - }); - - it("Returns members when admin user ID is supplied and members IDs are supplied", async () => { - const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); - const memberOneUser = await prisma.user.findFirstOrThrow({ - where: { email: "member1-acme@example.com" }, - }); - const memberTwoUser = await prisma.user.findFirstOrThrow({ - where: { email: "member2-acme@example.com" }, - }); - const accessibleUserIds = await getAccessibleUsers({ - memberUserIds: [memberOneUser.id, memberTwoUser.id], - adminUserId: adminUser.id, - }); - - expect(accessibleUserIds.length).toBe(2); - expect(accessibleUserIds).toContain(memberOneUser.id); - expect(accessibleUserIds).toContain(memberTwoUser.id); - }); - }); - - describe("retrieveOrgScopedAccessibleUsers", () => { - it("Does not return members when admin user ID is an admin of an org", async () => { - const memberOneUser = await prisma.user.findFirstOrThrow({ - where: { email: "member1-acme@example.com" }, - }); - - const accessibleUserIds = await retrieveOrgScopedAccessibleUsers({ - adminId: memberOneUser.id, - }); - - expect(accessibleUserIds.length).toBe(0); - }); - - it("Returns members when admin user ID is an admin of an org", async () => { - const adminUser = await prisma.user.findFirstOrThrow({ - where: { email: "owner1-acme@example.com" }, - }); - - const accessibleUserIds = await retrieveOrgScopedAccessibleUsers({ - adminId: adminUser.id, - }); - - const memberOneUser = await prisma.user.findFirstOrThrow({ - where: { email: "member1-acme@example.com" }, - }); - - const memberTwoUser = await prisma.user.findFirstOrThrow({ - where: { email: "member2-acme@example.com" }, - }); - - expect(accessibleUserIds.length).toBe(11); - expect(accessibleUserIds).toContain(memberOneUser.id); - expect(accessibleUserIds).toContain(memberTwoUser.id); - expect(accessibleUserIds).toContain(adminUser.id); - }); - }); -}); diff --git a/apps/api/v1/trigger.version.js b/apps/api/v1/trigger.version.js deleted file mode 100644 index b191f9c541c9ab..00000000000000 --- a/apps/api/v1/trigger.version.js +++ /dev/null @@ -1 +0,0 @@ -export const TRIGGER_VERSION = '20260216.1'; diff --git a/apps/api/v1/tsconfig.json b/apps/api/v1/tsconfig.json deleted file mode 100644 index 8c58b02c043949..00000000000000 --- a/apps/api/v1/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "extends": "@calcom/tsconfig/nextjs.json", - "compilerOptions": { - "strict": true, - "jsx": "preserve", - "baseUrl": ".", - "paths": { - "~/*": ["./*"], - "@prisma/client/*": ["@calcom/prisma/client/*"], - "@calcom/testing/*": ["../../../packages/testing/src/*"] - }, - "experimentalDecorators": true - }, - "include": [ - "next-env.d.ts", - "./next.d.ts", - "**/*.ts", - "**/*.tsx", - "../../../packages/types/*.d.ts", - "../../../packages/types/next-auth.d.ts", - "trigger.version.js" - ], - "exclude": ["**/node_modules/**", "templates", "auth"] -} diff --git a/apps/api/v2/.env.example b/apps/api/v2/.env.example index 2532163fc6e9b3..fd53a2e3570957 100644 --- a/apps/api/v2/.env.example +++ b/apps/api/v2/.env.example @@ -46,11 +46,9 @@ STRIPE_WEBHOOK_SECRET= # used to know where to direct people when managing platform billing WEB_APP_URL=http://localhost:3000/ -# to setup license key see apps/api/v2/README.md -CALCOM_LICENSE_KEY= +# Cal.diy is fully open source — no license key is required. # when request is authenticated with an api key we need to remove the prefix. See code in api-auth.strategy.ts API_KEY_PREFIX=cal_ -GET_LICENSE_KEY_URL="https://goblin.cal.com/v1/license" # we disable workers when running e2e and enable them while running locally IS_E2E=false DOCS_URL= @@ -74,4 +72,4 @@ REWRITE_API_V2_PREFIX="1" ENABLE_ASYNC_TASKER="false" # set to "true" to enable TRIGGER_SECRET_KEY= TRIGGER_API_URL=https://api.trigger.dev -TRIGGER_DEV_PROJECT_REF= \ No newline at end of file +TRIGGER_DEV_PROJECT_REF= diff --git a/apps/api/v2/README.md b/apps/api/v2/README.md index 45aff8e6985404..324331be74071b 100644 --- a/apps/api/v2/README.md +++ b/apps/api/v2/README.md @@ -1,4 +1,4 @@ -Cal.com api v2 is a [Nest.js](https://github.com/nestjs/nest) project. +Cal.diy api v2 is a [Nest.js](https://github.com/nestjs/nest) project. # Local development This setup will allow you to develop with api v2 locally. If you want to also test atoms locally with platform's example app, diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 505d85b580b13b..2e3097d93bca76 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -1,8 +1,8 @@ { "name": "@calcom/api-v2", "version": "0.0.1", - "description": "Platform API for Cal.com", - "author": "Cal.com Inc.", + "description": "Platform API for Cal.diy", + "author": "Cal.com, Inc.", "license": "UNLICENSED", "private": true, "scripts": { diff --git a/apps/api/v2/src/app.e2e-spec.ts b/apps/api/v2/src/app.e2e-spec.ts index 1f9ebc40186acb..4384249793de9b 100644 --- a/apps/api/v2/src/app.e2e-spec.ts +++ b/apps/api/v2/src/app.e2e-spec.ts @@ -12,7 +12,7 @@ import { RateLimitRepositoryFixture } from "test/fixtures/repository/rate-limit. import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { randomString } from "test/utils/randomString"; import { AppModule } from "@/app.module"; -import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesModule_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/LICENSE b/apps/api/v2/src/ee/LICENSE deleted file mode 100644 index 999ac763587f10..00000000000000 --- a/apps/api/v2/src/ee/LICENSE +++ /dev/null @@ -1,42 +0,0 @@ -The Cal.com Commercial License (the “Commercial License”) -Copyright (c) 2020-present Cal.com, Inc - -With regard to the Cal.com Software: - -This software and associated documentation files (the "Software") may only be -used in production, if you (and any entity that you represent) have agreed to, -and are in compliance with, the Cal.com Subscription Terms available -at https://cal.com/terms, or other agreements governing -the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"), -and otherwise have a valid Cal.com Commercial License ("Commercial License") -for the correct number of hosts as defined in the "Commercial Terms ("Hosts"). Subject to the foregoing sentence, -you are free to modify this Software and publish patches to the Software. You agree -that Cal.com and/or its licensors (as applicable) retain all right, title and interest in -and to all such modifications and/or patches, and all such modifications and/or -patches may only be used, copied, modified, displayed, distributed, or otherwise -exploited with a valid Commercial License for the correct number of hosts. -Notwithstanding the foregoing, you may copy and modify the Software for development -and testing purposes, without requiring a subscription. You agree that Cal.com and/or -its licensors (as applicable) retain all right, title and interest in and to all such -modifications. You are not granted any other rights beyond what is expressly stated herein. -Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, -and/or sell the Software. - -This Commercial License applies only to the part of this Software that is not distributed under -the AGPLv3 license. Any part of this Software distributed under the MIT license or which -is served client-side as an image, font, cascading stylesheet (CSS), file which produces -or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or -in part, is copyrighted under the AGPLv3 license. The full text of this Commercial License shall -be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -For all third party components incorporated into the Cal.com Software, those -components are licensed under the original license provided by the owner of the -applicable component. diff --git a/apps/api/v2/src/ee/README.md b/apps/api/v2/src/ee/README.md deleted file mode 100644 index 3bbf0637788c5f..00000000000000 --- a/apps/api/v2/src/ee/README.md +++ /dev/null @@ -1,18 +0,0 @@ - - - -# Commercial License of API - -Welcome to the Commercial License of the Cal.com API. - -Our philosophy is simple, all "Singleplayer APIs" are open-source under AGPLv3. All "Multiplayer APIs" are under a commercial license. - -The [/ee](https://github.com/calcom/cal.com/tree/main/apps/api/v2/ee) subfolder is the place for all the **Commercial License** features from our [hosted](https://cal.com/pricing) plan. - -> _❗ WARNING: This repository is copyrighted (unlike our [main repo](https://github.com/calcom/cal.com)). You are not allowed to use this code to host your own version of app.cal.com without obtaining a proper [license](https://go.cal.com/get-license) first❗_ diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings-billing.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings-billing.e2e-spec.ts deleted file mode 100644 index 15013b555a35ab..00000000000000 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings-billing.e2e-spec.ts +++ /dev/null @@ -1,542 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { BookingResponse } from "@calcom/platform-libraries"; -import type { RegularBookingCreateResult } from "@calcom/platform-libraries/bookings"; -import type { ApiSuccessResponse } 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 { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; -import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.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 { 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 { BillingService } from "@/modules/billing/services/billing.service"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { UsersModule } from "@/modules/users/users.module"; - -const CLIENT_REDIRECT_URI = "http://localhost:4321"; - -describe("Bookings Billing E2E - 2024-04-15", () => { - describe("Regular user (non-platform-managed)", () => { - jest.setTimeout(30000); - let app: INestApplication; - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let billingService: BillingService; - let increaseUsageSpy: jest.SpyInstance; - let cancelUsageSpy: jest.SpyInstance; - - const userEmail = `billing-regular-user-${randomString()}@api.com`; - let user: User; - let eventTypeId: number; - let recEventTypeId: number; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - billingService = moduleRef.get(BillingService); - - // Spy on the billing service methods - increaseUsageSpy = jest.spyOn(billingService, "increaseUsageByUserId"); - cancelUsageSpy = jest.spyOn(billingService, "cancelUsageByBookingUid"); - - // Create a regular user (not platform-managed) - user = await userRepositoryFixture.create({ - email: userEmail, - isPlatformManaged: false, - }); - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: `billing-test-schedule-${randomString()}`, - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(user.id, userSchedule); - - const event = await eventTypesRepositoryFixture.create( - { - title: `billing-test-event-type-${randomString()}`, - slug: `billing-test-event-type-${randomString()}`, - length: 60, - }, - user.id - ); - eventTypeId = event.id; - - const recEventType = await eventTypesRepositoryFixture.create( - { - title: `billing-rec-event-type-${randomString()}`, - slug: `billing-rec-event-type-${randomString()}`, - length: 60, - recurringEvent: { freq: 2, count: 4, interval: 1 }, - }, - user.id - ); - recEventTypeId = recEventType.id; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - afterAll(async () => { - await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email); - await userRepositoryFixture.deleteByEmail(user.email); - await app.close(); - }); - - it("should NOT call billing service when creating a booking for a regular user", async () => { - increaseUsageSpy.mockClear(); - - const body: CreateBookingInput_2024_04_15 = { - start: "2040-05-21T09:30:00.000Z", - end: "2040-05-21T10:30:00.000Z", - eventTypeId: eventTypeId, - timeZone: "Europe/London", - language: "en", - metadata: {}, - hashedLink: "", - responses: { - name: "Test Attendee", - email: "attendee@example.com", - location: { - value: "link", - optionValue: "", - }, - notes: "test booking for billing", - }, - }; - - const response = await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.id).toBeDefined(); - - // Verify billing service was NOT called for regular user - expect(increaseUsageSpy).not.toHaveBeenCalled(); - }); - - it("should NOT call billing cancel service when cancelling a booking for a regular user", async () => { - // First create a booking - const createBody: CreateBookingInput_2024_04_15 = { - start: "2040-05-22T09:30:00.000Z", - end: "2040-05-22T10:30:00.000Z", - eventTypeId: eventTypeId, - timeZone: "Europe/London", - language: "en", - metadata: {}, - hashedLink: "", - responses: { - name: "Test Attendee Cancel", - email: "attendee-cancel@example.com", - location: { - value: "link", - optionValue: "", - }, - notes: "test booking for cancel billing", - }, - }; - - const createResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(createBody) - .expect(201); - - const createResponseBody: ApiSuccessResponse = createResponse.body; - const bookingUid = createResponseBody.data.uid; - - // Clear the spy before cancelling - cancelUsageSpy.mockClear(); - - // Cancel the booking (returns 201 Created) - await request(app.getHttpServer()).post(`/v2/bookings/${bookingUid}/cancel`).send({}).expect(201); - - // Verify billing cancel service was NOT called for regular user - expect(cancelUsageSpy).not.toHaveBeenCalled(); - }); - - it("should NOT call billing service when creating recurring bookings for a regular user", async () => { - increaseUsageSpy.mockClear(); - - const body: CreateRecurringBookingInput_2024_04_15[] = [ - { - start: "2040-06-21T09:30:00.000Z", - end: "2040-06-21T10:30:00.000Z", - eventTypeId: recEventTypeId, - timeZone: "Europe/London", - language: "en", - metadata: {}, - hashedLink: "", - responses: { - name: "Test Attendee Recurring", - email: "attendee-recurring@example.com", - location: { - value: "link", - optionValue: "", - }, - notes: "test recurring booking for billing", - }, - recurringEventId: `test-recurring-${randomString()}`, - }, - { - start: "2040-06-28T09:30:00.000Z", - end: "2040-06-28T10:30:00.000Z", - eventTypeId: recEventTypeId, - timeZone: "Europe/London", - language: "en", - metadata: {}, - hashedLink: "", - responses: { - name: "Test Attendee Recurring", - email: "attendee-recurring@example.com", - location: { - value: "link", - optionValue: "", - }, - notes: "test recurring booking for billing", - }, - recurringEventId: `test-recurring-${randomString()}`, - }, - ]; - - const response = await request(app.getHttpServer()) - .post("/v2/bookings/recurring") - .send(body) - .expect(201); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.length).toBeGreaterThan(0); - - // Verify billing service was NOT called for regular user recurring bookings - expect(increaseUsageSpy).not.toHaveBeenCalled(); - }); - }); - - describe("Platform-managed user", () => { - jest.setTimeout(30000); - let app: INestApplication; - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let profilesRepositoryFixture: ProfileRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let billingService: BillingService; - let increaseUsageSpy: jest.SpyInstance; - let cancelUsageSpy: jest.SpyInstance; - - const platformAdminEmail = `billing-platform-admin-${randomString()}@api.com`; - const managedUserEmail = `billing-managed-user-${randomString()}@api.com`; - let platformAdmin: User; - let managedUser: User; - let organization: Team; - let oAuthClient: PlatformOAuthClient; - let eventTypeId: number; - let recEventTypeId: number; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - managedUserEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - billingService = moduleRef.get(BillingService); - - // Spy on the billing service methods - increaseUsageSpy = jest.spyOn(billingService, "increaseUsageByUserId"); - cancelUsageSpy = jest.spyOn(billingService, "cancelUsageByBookingUid"); - - // Create platform admin - platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail }); - - // Create organization - organization = await teamRepositoryFixture.create({ - name: `billing-test-organization-${randomString()}`, - isPlatform: true, - isOrganization: true, - }); - - // Create OAuth client - oAuthClient = await oauthClientRepositoryFixture.create( - organization.id, - { - logo: "logo-url", - name: "billing-test-oauth-client", - redirectUris: [CLIENT_REDIRECT_URI], - permissions: 1023, - }, - "secret" - ); - - // Create profile for platform admin - await profilesRepositoryFixture.create({ - uid: `billing-test-profile-${randomString()}`, - username: platformAdminEmail, - organization: { connect: { id: organization.id } }, - user: { connect: { id: platformAdmin.id } }, - }); - - // Create membership for platform admin - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: platformAdmin.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - // Create a platform-managed user - managedUser = await userRepositoryFixture.create({ - email: managedUserEmail, - isPlatformManaged: true, - platformOAuthClients: { - connect: { id: oAuthClient.id }, - }, - }); - - // Create profile for managed user - await profilesRepositoryFixture.create({ - uid: `billing-managed-user-profile-${randomString()}`, - username: managedUserEmail, - organization: { connect: { id: organization.id } }, - user: { connect: { id: managedUser.id } }, - }); - - // Create membership for managed user - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: managedUser.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: `billing-managed-user-schedule-${randomString()}`, - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(managedUser.id, userSchedule); - - const event = await eventTypesRepositoryFixture.create( - { - title: `billing-managed-user-event-type-${randomString()}`, - slug: `billing-managed-user-event-type-${randomString()}`, - length: 60, - }, - managedUser.id - ); - eventTypeId = event.id; - - const recEventType = await eventTypesRepositoryFixture.create( - { - title: `billing-managed-rec-event-type-${randomString()}`, - slug: `billing-managed-rec-event-type-${randomString()}`, - length: 60, - recurringEvent: { freq: 2, count: 4, interval: 1 }, - }, - managedUser.id - ); - recEventTypeId = recEventType.id; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - afterAll(async () => { - await bookingsRepositoryFixture.deleteAllBookings(managedUser.id, managedUser.email); - await userRepositoryFixture.deleteByEmail(managedUser.email); - await userRepositoryFixture.deleteByEmail(platformAdmin.email); - await app.close(); - }); - - it("should call billing service when creating a booking for a platform-managed user", async () => { - increaseUsageSpy.mockClear(); - - const body: CreateBookingInput_2024_04_15 = { - start: "2040-05-21T09:30:00.000Z", - end: "2040-05-21T10:30:00.000Z", - eventTypeId: eventTypeId, - timeZone: "Europe/London", - language: "en", - metadata: {}, - hashedLink: "", - responses: { - name: "Test Attendee", - email: "attendee@example.com", - location: { - value: "link", - optionValue: "", - }, - notes: "test booking for billing", - }, - }; - - const response = await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.id).toBeDefined(); - - // Verify billing service WAS called for platform-managed user - expect(increaseUsageSpy).toHaveBeenCalledTimes(1); - expect(increaseUsageSpy).toHaveBeenCalledWith( - managedUser.id, - expect.objectContaining({ - uid: responseBody.data.uid, - startTime: expect.any(Date), - }) - ); - }); - - it("should call billing cancel service when cancelling a booking for a platform-managed user", async () => { - // First create a booking - const createBody: CreateBookingInput_2024_04_15 = { - start: "2040-05-22T09:30:00.000Z", - end: "2040-05-22T10:30:00.000Z", - eventTypeId: eventTypeId, - timeZone: "Europe/London", - language: "en", - metadata: {}, - hashedLink: "", - responses: { - name: "Test Attendee Cancel", - email: "attendee-cancel-managed@example.com", - location: { - value: "link", - optionValue: "", - }, - notes: "test booking for cancel billing", - }, - }; - - const createResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(createBody) - .expect(201); - - const createResponseBody: ApiSuccessResponse = createResponse.body; - const bookingUid = createResponseBody.data.uid; - - // Clear the spy before cancelling - cancelUsageSpy.mockClear(); - - // Cancel the booking (returns 201 Created) - await request(app.getHttpServer()).post(`/v2/bookings/${bookingUid}/cancel`).send({}).expect(201); - - // Verify billing cancel service WAS called for platform-managed user - expect(cancelUsageSpy).toHaveBeenCalledTimes(1); - expect(cancelUsageSpy).toHaveBeenCalledWith(bookingUid); - }); - - it("should call billing service when creating recurring bookings for a platform-managed user", async () => { - increaseUsageSpy.mockClear(); - - const body: CreateRecurringBookingInput_2024_04_15[] = [ - { - start: "2040-06-21T09:30:00.000Z", - end: "2040-06-21T10:30:00.000Z", - eventTypeId: recEventTypeId, - timeZone: "Europe/London", - language: "en", - metadata: {}, - hashedLink: "", - responses: { - name: "Test Attendee Recurring Managed", - email: "attendee-recurring-managed@example.com", - location: { - value: "link", - optionValue: "", - }, - notes: "test recurring booking for billing", - }, - recurringEventId: `test-recurring-managed-${randomString()}`, - }, - { - start: "2040-06-28T09:30:00.000Z", - end: "2040-06-28T10:30:00.000Z", - eventTypeId: recEventTypeId, - timeZone: "Europe/London", - language: "en", - metadata: {}, - hashedLink: "", - responses: { - name: "Test Attendee Recurring Managed", - email: "attendee-recurring-managed@example.com", - location: { - value: "link", - optionValue: "", - }, - notes: "test recurring booking for billing", - }, - recurringEventId: `test-recurring-managed-${randomString()}`, - }, - ]; - - const response = await request(app.getHttpServer()) - .post("/v2/bookings/recurring") - .send(body) - .expect(201); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.length).toBeGreaterThan(0); - - // Verify billing service WAS called for platform-managed user recurring bookings - // Should be called once for each booking created - expect(increaseUsageSpy).toHaveBeenCalledTimes(responseBody.data.length); - }); - }); -}); diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/managed-user-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/managed-user-bookings.e2e-spec.ts deleted file mode 100644 index 542bf21c466885..00000000000000 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/managed-user-bookings.e2e-spec.ts +++ /dev/null @@ -1,462 +0,0 @@ -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, - GetBookingsOutput_2024_04_15, -} from "@/ee/bookings/2024-04-15/outputs/get-bookings.output"; -import { HttpExceptionFilter } from "@/filters/http-exception.filter"; -import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter"; -import { Locales } from "@/lib/enums/locales"; -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"; - -describe("Managed user bookings 2024-04-15", () => { - let app: INestApplication; - - let oAuthClient: PlatformOAuthClient; - let organization: Team; - let userRepositoryFixture: UserRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let profilesRepositoryFixture: ProfileRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - const platformAdminEmail = `managed-users-bookings-admin-${randomString()}@api.com`; - let platformAdmin: User; - - const managedUsersTimeZone = "Europe/Rome"; - const firstManagedUserEmail = `managed-user-bookings-2024-04-15-first-user@api.com`; - const secondManagedUserEmail = `managed-user-bookings-2024-04-15-second-user@api.com`; - const thirdManagedUserEmail = `managed-user-bookings-2024-04-15-third-user@api.com`; - - let firstManagedUser: CreateManagedUserData; - let secondManagedUser: CreateManagedUserData; - let thirdManagedUser: CreateManagedUserData; - - let firstManagedUserEventTypeId: number; - - let firstManagedUserBookingsCount = 0; - let secondManagedUserBookingsCount = 0; - let thirdManagedUserBookingsCount = 0; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [PrismaExceptionFilter, HttpExceptionFilter], - imports: [AppModule, UsersModule], - }).compile(); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail }); - - organization = await teamRepositoryFixture.create({ - name: `oauth-client-users-organization-${randomString()}`, - isPlatform: true, - isOrganization: true, - }); - oAuthClient = await createOAuthClient(organization.id); - - await profilesRepositoryFixture.create({ - uid: "asd1qwwqeqw-asddsadasd", - username: platformAdminEmail, - organization: { connect: { id: organization.id } }, - user: { - connect: { id: platformAdmin.id }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: platformAdmin.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - await app.init(); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: [CLIENT_REDIRECT_URI], - permissions: 1023, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - it(`should create first managed user`, async () => { - const requestBody: CreateManagedUserInput = { - email: firstManagedUserEmail, - timeZone: managedUsersTimeZone, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Alice Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email) - ); - expect(responseBody.data.accessToken).toBeDefined(); - expect(responseBody.data.refreshToken).toBeDefined(); - - firstManagedUser = responseBody.data; - }); - - it("should create an event type for managed user", async () => { - const body: CreateEventTypeInput_2024_06_14 = { - title: "Managed user bookings first managed user event type", - slug: "managed-user-bookings-first-managed-user-event-type", - description: "Managed user bookings first managed user event type description", - lengthInMinutes: 30, - }; - - return request(app.getHttpServer()) - .post("/api/v2/event-types") - .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .send(body) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - const createdEventType = responseBody.data; - expect(createdEventType).toHaveProperty("id"); - expect(createdEventType.title).toEqual(body.title); - expect(createdEventType.description).toEqual(body.description); - expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes); - expect(createdEventType.ownerId).toEqual(firstManagedUser.user.id); - firstManagedUserEventTypeId = createdEventType.id; - }); - }); - - it(`should create second managed user`, async () => { - const requestBody: CreateManagedUserInput = { - email: secondManagedUserEmail, - timeZone: managedUsersTimeZone, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Bob Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email) - ); - expect(responseBody.data.accessToken).toBeDefined(); - expect(responseBody.data.refreshToken).toBeDefined(); - - secondManagedUser = responseBody.data; - }); - - it(`should create third managed user`, async () => { - const requestBody: CreateManagedUserInput = { - email: thirdManagedUserEmail, - timeZone: managedUsersTimeZone, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Charlie Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email) - ); - expect(responseBody.data.accessToken).toBeDefined(); - expect(responseBody.data.refreshToken).toBeDefined(); - - thirdManagedUser = responseBody.data; - }); - - describe("bookings using original emails", () => { - it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => { - const body: CreateBookingInput_2024_04_15 = { - start: "2040-05-21T09:30:00.000Z", - end: "2040-05-21T10:00:00.000Z", - eventTypeId: firstManagedUserEventTypeId, - timeZone: "Europe/London", - language: "en", - metadata: {}, - hashedLink: "", - responses: { - name: secondManagedUser.user.name || "unknown-name", - email: secondManagedUserEmail, - location: { - value: "attendeeInPerson", - optionValue: "rome", - }, - notes: "test", - guests: [], - }, - }; - - await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201); - - firstManagedUserBookingsCount += 1; - secondManagedUserBookingsCount += 1; - - const getFirstManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(200); - - const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = - getFirstManagedUserBookings.body; - const firstManagedUserBookings: GetBookingsDataEntry[] = - firstManagedUserBookingsResponseBody.data.bookings; - expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); - - const getSecondManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) - .expect(200); - const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = - getSecondManagedUserBookings.body; - const secondManagedUserBookings: GetBookingsDataEntry[] = - secondManagedUserBookingsResponseBody.data.bookings; - expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); - }); - - it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => { - const body: CreateBookingInput_2024_04_15 = { - start: "2040-05-21T10:30:00.000Z", - end: "2040-05-21T11:00:00.000Z", - eventTypeId: firstManagedUserEventTypeId, - timeZone: "Europe/London", - language: "en", - metadata: {}, - hashedLink: "", - responses: { - name: thirdManagedUser.user.name || "unknown-name", - email: thirdManagedUserEmail, - location: { - value: "attendeeInPerson", - optionValue: "rome", - }, - notes: "test", - guests: [secondManagedUserEmail], - }, - }; - - await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201); - - firstManagedUserBookingsCount += 1; - secondManagedUserBookingsCount += 1; - thirdManagedUserBookingsCount += 1; - - const getFirstManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(200); - const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = - getFirstManagedUserBookings.body; - const firstManagedUserBookings: GetBookingsDataEntry[] = - firstManagedUserBookingsResponseBody.data.bookings; - expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); - - const getSecondManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) - .expect(200); - const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = - getSecondManagedUserBookings.body; - const secondManagedUserBookings: GetBookingsDataEntry[] = - secondManagedUserBookingsResponseBody.data.bookings; - expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); - - const getThirdManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set("Authorization", `Bearer ${thirdManagedUser.accessToken}`) - .expect(200); - const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = - getThirdManagedUserBookings.body; - const thirdManagedUserBookings: GetBookingsDataEntry[] = - thirdManagedUserBookingsResponseBody.data.bookings; - expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount); - }); - }); - - describe("bookings using OAuth client emails", () => { - it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => { - const body: CreateBookingInput_2024_04_15 = { - start: "2040-05-21T12:00:00.000Z", - end: "2040-05-21T12:30:00.000Z", - eventTypeId: firstManagedUserEventTypeId, - timeZone: "Europe/London", - language: "en", - metadata: {}, - hashedLink: "", - responses: { - name: secondManagedUser.user.name || "unknown-name", - email: secondManagedUser.user.email, - location: { - value: "attendeeInPerson", - optionValue: "rome", - }, - notes: "test", - guests: [], - }, - }; - - await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201); - - firstManagedUserBookingsCount += 1; - secondManagedUserBookingsCount += 1; - - const getFirstManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(200); - - const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = - getFirstManagedUserBookings.body; - const firstManagedUserBookings: GetBookingsDataEntry[] = - firstManagedUserBookingsResponseBody.data.bookings; - expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); - - const getSecondManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) - .expect(200); - - const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = - getSecondManagedUserBookings.body; - const secondManagedUserBookings: GetBookingsDataEntry[] = - secondManagedUserBookingsResponseBody.data.bookings; - expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); - }); - - it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => { - const body: CreateBookingInput_2024_04_15 = { - start: "2040-05-21T13:00:00.000Z", - end: "2040-05-21T13:30:00.000Z", - eventTypeId: firstManagedUserEventTypeId, - timeZone: "Europe/London", - language: "en", - metadata: {}, - hashedLink: "", - responses: { - name: thirdManagedUser.user.name || "unknown-name", - email: thirdManagedUser.user.email, - location: { - value: "attendeeInPerson", - optionValue: "rome", - }, - notes: "test", - guests: [secondManagedUser.user.email], - }, - }; - - await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201); - - firstManagedUserBookingsCount += 1; - secondManagedUserBookingsCount += 1; - thirdManagedUserBookingsCount += 1; - - const getFirstManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(200); - const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = - getFirstManagedUserBookings.body; - const firstManagedUserBookings: GetBookingsDataEntry[] = - firstManagedUserBookingsResponseBody.data.bookings; - expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); - - const getSecondManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) - .expect(200); - const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = - getSecondManagedUserBookings.body; - const secondManagedUserBookings: GetBookingsDataEntry[] = - secondManagedUserBookingsResponseBody.data.bookings; - expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); - - const getThirdManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set("Authorization", `Bearer ${thirdManagedUser.accessToken}`) - .expect(200); - const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = - getThirdManagedUserBookings.body; - const thirdManagedUserBookings: GetBookingsDataEntry[] = - thirdManagedUserBookingsResponseBody.data.bookings; - expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.delete(firstManagedUser.user.id); - await userRepositoryFixture.delete(secondManagedUser.user.id); - await userRepositoryFixture.delete(thirdManagedUser.user.id); - await userRepositoryFixture.delete(platformAdmin.id); - await oauthClientRepositoryFixture.delete(oAuthClient.id); - await teamRepositoryFixture.delete(organization.id); - await app.close(); - }); -}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts deleted file mode 100644 index 4d7ef0cc333a7c..00000000000000 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Module } from "@nestjs/common"; -import { BookingAttendeesController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-attendees.controller"; -import { BookingGuestsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-guests.controller"; -import { BookingLocationController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-location.controller"; -import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller"; -import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard"; -import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository"; -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { BookingAttendeesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-attendees.service"; -import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service"; -import { BookingLocationCalendarSyncService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location-calendar-sync.service"; -import { BookingLocationCredentialService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location-credential.service"; -import { BookingLocationIntegrationService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location-integration.service"; -import { BookingLocationService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location.service"; -import { BookingVideoService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-video.service"; -import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service"; -import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; -import { CalVideoOutputService } from "@/ee/bookings/2024-08-13/services/cal-video.output.service"; -import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service"; -import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service"; -import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; -import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; -import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service"; -import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; -import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; -import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; -import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; -import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; -import { BookingAttendeesModule } from "@/lib/modules/booking-attendees.module"; -import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.module"; -import { InstantBookingModule } from "@/lib/modules/instant-booking.module"; -import { RecurringBookingModule } from "@/lib/modules/recurring-booking.module"; -import { RegularBookingModule } from "@/lib/modules/regular-booking.module"; -import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; -import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; -import { AppsRepository } from "@/modules/apps/apps.repository"; -import { BillingModule } from "@/modules/billing/billing.module"; -import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module"; -import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository"; -import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; -import { KyselyModule } from "@/modules/kysely/kysely.module"; -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; -import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; -import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { ProfilesModule } from "@/modules/profiles/profiles.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; -import { TeamsModule } from "@/modules/teams/teams/teams.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { TokensRepository } from "@/modules/tokens/tokens.repository"; -import { UsersModule } from "@/modules/users/users.module"; - -@Module({ - imports: [ - PrismaModule, - KyselyModule, - RedisModule, - TokensModule, - BillingModule, - UsersModule, - BookingSeatModule, - SchedulesModule_2024_04_15, - EventTypesModule_2024_04_15, - EventTypesModule_2024_06_14, - StripeModule, - TeamsModule, - TeamsEventTypesModule, - MembershipsModule, - ProfilesModule, - RegularBookingModule, - RecurringBookingModule, - InstantBookingModule, - BookingEventHandlerModule, - BookingAttendeesModule, - ], - providers: [ - TokensRepository, - OAuthFlowService, - OAuthClientRepository, - OAuthClientUsersService, - BookingsService_2024_08_13, - BookingAttendeesService_2024_08_13, - BookingGuestsService_2024_08_13, - InputBookingsService_2024_08_13, - OutputBookingsService_2024_08_13, - OutputBookingReferencesService_2024_08_13, - OutputEventTypesService_2024_06_14, - BookingsRepository_2024_08_13, - EventTypesRepository_2024_06_14, - BookingSeatRepository, - ApiKeysRepository, - PlatformBookingsService, - CalendarsService, - CalendarsCacheService, - CredentialsRepository, - AppsRepository, - CalendarsRepository, - SelectedCalendarsRepository, - OrganizationsTeamsRepository, - OrganizationsRepository, - ErrorsBookingsService_2024_08_13, - BookingReferencesService_2024_08_13, - BookingReferencesRepository_2024_08_13, - CalVideoService, - CalVideoOutputService, - BookingPbacGuard, - BookingLocationCalendarSyncService_2024_08_13, - BookingLocationCredentialService_2024_08_13, - BookingLocationIntegrationService_2024_08_13, - BookingLocationService_2024_08_13, - BookingVideoService_2024_08_13, - PrismaFeaturesRepository, - ], - controllers: [ - BookingsController_2024_08_13, - BookingAttendeesController_2024_08_13, - BookingGuestsController_2024_08_13, - BookingLocationController_2024_08_13, - ], - exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13], -}) -export class BookingsModule_2024_08_13 {} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/add-attendee.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/add-attendee.e2e-spec.ts deleted file mode 100644 index d773ae7f9d3a04..00000000000000 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/add-attendee.e2e-spec.ts +++ /dev/null @@ -1,415 +0,0 @@ -import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; -import { - AttendeeAddGuestsEmail, - AttendeeScheduledEmail, - OrganizerAddGuestsEmail, -} from "@calcom/platform-libraries/emails"; -import type { BookingOutput_2024_08_13, CreateBookingInput_2024_08_13 } from "@calcom/platform-types"; -import type { 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 { mockThrottlerGuard } from "test/utils/withNoThrottler"; -import { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { AddAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-attendee.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") - .mockImplementation(() => Promise.resolve("

email

")); -const organizerAddGuestsEmailSpy = jest - .spyOn(OrganizerAddGuestsEmail.prototype, "getHtml") - .mockImplementation(() => Promise.resolve("

email

")); -const attendeeScheduledEmailSpy = jest - .spyOn(AttendeeScheduledEmail.prototype, "getHtml") - .mockImplementation(() => Promise.resolve("

email

")); - -type TestUser = { - user: User; - accessToken: string; - refreshToken: string; -}; - -type TestSetup = { - organizer: TestUser; - unrelatedUser: TestUser; - eventTypeId: number; - bookingUid: string; - bookingId: number; -}; - -describe("Bookings Endpoints 2024-08-13 add attendee", () => { - let app: INestApplication; - let organization: Team; - - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let tokensRepositoryFixture: TokensRepositoryFixture; - - let testSetup: TestSetup; - - beforeAll(async () => { - mockThrottlerGuard(); - - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], - }) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef); - - organization = await teamRepositoryFixture.create({ - name: `add-attendee-2024-08-13-organization-${randomString()}`, - }); - - await setupTestData(); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - async function setupTestData() { - const oAuthClient = await createOAuthClient(organization.id, true); - - const organizerUser = await userRepositoryFixture.create({ - email: `add-attendee-2024-08-13-organizer-${randomString()}@api.com`, - platformOAuthClients: { - connect: { id: oAuthClient.id }, - }, - }); - - const unrelatedUserData = await userRepositoryFixture.create({ - email: `add-attendee-2024-08-13-unrelated-${randomString()}@api.com`, - platformOAuthClients: { - connect: { id: oAuthClient.id }, - }, - }); - - const organizerTokens = await tokensRepositoryFixture.createTokens(organizerUser.id, oAuthClient.id); - const unrelatedUserTokens = await tokensRepositoryFixture.createTokens( - unrelatedUserData.id, - oAuthClient.id - ); - - const schedule: CreateScheduleInput_2024_04_15 = { - name: `add-attendee-2024-08-13-schedule-${randomString()}`, - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(organizerUser.id, schedule); - - const eventType = await eventTypesRepositoryFixture.create( - { - title: `add-attendee-2024-08-13-event-type-${randomString()}`, - slug: `add-attendee-2024-08-13-event-type-${randomString()}`, - length: 60, - }, - organizerUser.id - ); - - testSetup = { - organizer: { - user: organizerUser, - accessToken: organizerTokens.accessToken, - refreshToken: organizerTokens.refreshToken, - }, - unrelatedUser: { - user: unrelatedUserData, - accessToken: unrelatedUserTokens.accessToken, - refreshToken: unrelatedUserTokens.refreshToken, - }, - eventTypeId: eventType.id, - bookingUid: "", - bookingId: 0, - }; - } - - async function createOAuthClient(organizationId: number, emailsEnabled: boolean) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: ["http://localhost:5555"], - permissions: 32, - areEmailsEnabled: emailsEnabled, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - beforeEach(async () => { - attendeeAddGuestsEmailSpy.mockClear(); - organizerAddGuestsEmailSpy.mockClear(); - attendeeScheduledEmailSpy.mockClear(); - }); - - describe("POST /v2/bookings/:bookingUid/attendees", () => { - beforeAll(async () => { - const createBookingBody: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 10, 13, 0, 0)).toISOString(), - eventTypeId: testSetup.eventTypeId, - attendee: { - name: "Original Attendee", - email: "original.attendee@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - location: "https://meet.google.com/abc-def-ghi", - bookingFieldsResponses: { - customField: "customValue", - }, - metadata: { - userId: "100", - }, - }; - - const createBookingResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(createBookingBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const createBookingResponseBody: CreateBookingOutput_2024_08_13 = createBookingResponse.body; - expect(createBookingResponseBody.status).toEqual(SUCCESS_STATUS); - - if (!responseDataIsBooking(createBookingResponseBody.data)) { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - - testSetup.bookingUid = createBookingResponseBody.data.uid; - testSetup.bookingId = createBookingResponseBody.data.id; - }); - - describe("Authentication", () => { - it("should return 401 when adding attendee without authentication", async () => { - const addAttendeeBody = { - email: "unauthenticated.attendee@example.com", - name: "Unauthenticated Attendee", - timeZone: "Europe/London", - }; - - const response = await request(app.getHttpServer()) - .post(`/v2/bookings/${testSetup.bookingUid}/attendees`) - .send(addAttendeeBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(response.status).toBe(401); - }); - }); - - describe("Authorization", () => { - it("should allow booking organizer to add an attendee", async () => { - const addAttendeeBody = { - email: "new.attendee@example.com", - name: "New Attendee", - timeZone: "Europe/London", - }; - - const response = await request(app.getHttpServer()) - .post(`/v2/bookings/${testSetup.bookingUid}/attendees`) - .send(addAttendeeBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${testSetup.organizer.accessToken}`) - .expect(201); - - const responseBody: AddAttendeeOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.email).toEqual(addAttendeeBody.email); - expect(responseBody.data.name).toEqual(addAttendeeBody.name); - expect(responseBody.data.timeZone).toEqual(addAttendeeBody.timeZone); - expect(responseBody.data.bookingId).toEqual(testSetup.bookingId); - expect(responseBody.data.id).toBeDefined(); - }); - - it("should return 403 when unrelated user tries to add attendee", async () => { - const addAttendeeBody = { - email: "unauthorized.attendee@example.com", - name: "Unauthorized Attendee", - timeZone: "America/New_York", - }; - - await request(app.getHttpServer()) - .post(`/v2/bookings/${testSetup.bookingUid}/attendees`) - .send(addAttendeeBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`) - .expect(403); - }); - }); - - describe("Response format", () => { - it("should return attendee with correct structure", async () => { - const addAttendeeBody = { - email: "structure.test@example.com", - name: "Structure Test Attendee", - timeZone: "Asia/Tokyo", - }; - - const response = await request(app.getHttpServer()) - .post(`/v2/bookings/${testSetup.bookingUid}/attendees`) - .send(addAttendeeBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${testSetup.organizer.accessToken}`) - .expect(201); - - const responseBody: AddAttendeeOutput_2024_08_13 = response.body; - - expect(responseBody).toHaveProperty("status", SUCCESS_STATUS); - expect(responseBody).toHaveProperty("data"); - expect(responseBody.data).toHaveProperty("id"); - expect(responseBody.data).toHaveProperty("bookingId"); - expect(responseBody.data).toHaveProperty("name"); - expect(responseBody.data).toHaveProperty("email"); - expect(responseBody.data).toHaveProperty("timeZone"); - - expect(typeof responseBody.data.id).toBe("number"); - expect(typeof responseBody.data.bookingId).toBe("number"); - expect(typeof responseBody.data.name).toBe("string"); - expect(typeof responseBody.data.email).toBe("string"); - expect(typeof responseBody.data.timeZone).toBe("string"); - }); - }); - - describe("Validation", () => { - it("should return 400 when email is missing", async () => { - const addAttendeeBody = { - name: "No Email Attendee", - timeZone: "Europe/London", - }; - - await request(app.getHttpServer()) - .post(`/v2/bookings/${testSetup.bookingUid}/attendees`) - .send(addAttendeeBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${testSetup.organizer.accessToken}`) - .expect(400); - }); - - it("should return 400 when email is invalid", async () => { - const addAttendeeBody = { - email: "invalid-email", - name: "Invalid Email Attendee", - timeZone: "Europe/London", - }; - - await request(app.getHttpServer()) - .post(`/v2/bookings/${testSetup.bookingUid}/attendees`) - .send(addAttendeeBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${testSetup.organizer.accessToken}`) - .expect(400); - }); - - it("should return 400 when name is missing", async () => { - const addAttendeeBody = { - email: "no.name@example.com", - timeZone: "Europe/London", - }; - - await request(app.getHttpServer()) - .post(`/v2/bookings/${testSetup.bookingUid}/attendees`) - .send(addAttendeeBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${testSetup.organizer.accessToken}`) - .expect(400); - }); - - it("should return 400 when timeZone is missing", async () => { - const addAttendeeBody = { - email: "no.timezone@example.com", - name: "No TimeZone Attendee", - }; - - await request(app.getHttpServer()) - .post(`/v2/bookings/${testSetup.bookingUid}/attendees`) - .send(addAttendeeBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${testSetup.organizer.accessToken}`) - .expect(400); - }); - - it("should return 400 when timeZone is invalid", async () => { - const addAttendeeBody = { - email: "invalid.timezone@example.com", - name: "Invalid TimeZone Attendee", - timeZone: "Invalid/TimeZone", - }; - - await request(app.getHttpServer()) - .post(`/v2/bookings/${testSetup.bookingUid}/attendees`) - .send(addAttendeeBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${testSetup.organizer.accessToken}`) - .expect(400); - }); - - it("should return 403 when booking does not exist", async () => { - const addAttendeeBody = { - email: "nonexistent.booking@example.com", - name: "Nonexistent Booking Attendee", - timeZone: "Europe/London", - }; - - await request(app.getHttpServer()) - .post(`/v2/bookings/non-existent-booking-uid/attendees`) - .send(addAttendeeBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${testSetup.organizer.accessToken}`) - .expect(403); - }); - }); - }); - - afterAll(async () => { - await teamRepositoryFixture.delete(organization.id); - - await userRepositoryFixture.deleteByEmail(testSetup.organizer.user.email); - await userRepositoryFixture.deleteByEmail(testSetup.unrelatedUser.user.email); - - await bookingsRepositoryFixture.deleteAllBookings( - testSetup.organizer.user.id, - testSetup.organizer.user.email - ); - - await app.close(); - }); - - function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 { - return !Array.isArray(data) && typeof data === "object" && data !== null && "id" in data; - } -}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/bookings-billing.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/bookings-billing.e2e-spec.ts deleted file mode 100644 index 3d950b505c937e..00000000000000 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/bookings-billing.e2e-spec.ts +++ /dev/null @@ -1,392 +0,0 @@ -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 } 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 { 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 { 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 { BillingService } from "@/modules/billing/services/billing.service"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { UsersModule } from "@/modules/users/users.module"; - -const CLIENT_REDIRECT_URI = "http://localhost:4321"; - -describe("Bookings Billing E2E - 2024-08-13", () => { - describe("Regular user (non-platform-managed)", () => { - jest.setTimeout(30000); - let app: INestApplication; - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let billingService: BillingService; - let increaseUsageSpy: jest.SpyInstance; - let cancelUsageSpy: jest.SpyInstance; - - const userEmail = `billing-regular-user-2024-08-13-${randomString()}@api.com`; - let user: User; - let eventTypeId: number; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - billingService = moduleRef.get(BillingService); - - // Spy on the billing service methods - increaseUsageSpy = jest.spyOn(billingService, "increaseUsageByUserId"); - cancelUsageSpy = jest.spyOn(billingService, "cancelUsageByBookingUid"); - - // Create a regular user (not platform-managed) - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - isPlatformManaged: false, - }); - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: `billing-test-schedule-2024-08-13-${randomString()}`, - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(user.id, userSchedule); - - const event = await eventTypesRepositoryFixture.create( - { - title: `billing-test-event-type-2024-08-13-${randomString()}`, - slug: `billing-test-event-type-2024-08-13-${randomString()}`, - length: 60, - }, - user.id - ); - eventTypeId = event.id; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - afterAll(async () => { - await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email); - await userRepositoryFixture.deleteByEmail(user.email); - await app.close(); - }); - - it("should NOT call billing service when creating a booking for a regular user", async () => { - increaseUsageSpy.mockClear(); - - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2040, 4, 21, 9, 30, 0)).toISOString(), - eventTypeId: eventTypeId, - attendee: { - name: "Test Attendee", - email: "attendee-billing-2024-08-13@example.com", - timeZone: "Europe/London", - 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 responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - // Verify billing service was NOT called for regular user - expect(increaseUsageSpy).not.toHaveBeenCalled(); - }); - - it("should NOT call billing cancel service when cancelling a booking for a regular user", async () => { - // First create a booking - const createBody: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2040, 4, 22, 9, 30, 0)).toISOString(), - eventTypeId: eventTypeId, - attendee: { - name: "Test Attendee Cancel", - email: "attendee-cancel-billing-2024-08-13@example.com", - timeZone: "Europe/London", - language: "en", - }, - location: "https://meet.google.com/abc-def-ghi", - }; - - const createResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(createBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const createResponseBody: CreateBookingOutput_2024_08_13 = createResponse.body; - const data = createResponseBody.data as BookingOutput_2024_08_13; - const bookingUid = data.uid; - - // Clear the spy before cancelling - cancelUsageSpy.mockClear(); - - // Cancel the booking - await request(app.getHttpServer()) - .post(`/v2/bookings/${bookingUid}/cancel`) - .send({}) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200); - - // Verify billing cancel service was NOT called for regular user - expect(cancelUsageSpy).not.toHaveBeenCalled(); - }); - }); - - describe("Platform-managed user", () => { - jest.setTimeout(30000); - let app: INestApplication; - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let profilesRepositoryFixture: ProfileRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let billingService: BillingService; - let increaseUsageSpy: jest.SpyInstance; - let cancelUsageSpy: jest.SpyInstance; - - const platformAdminEmail = `billing-platform-admin-2024-08-13-${randomString()}@api.com`; - const managedUserEmail = `billing-managed-user-2024-08-13-${randomString()}@api.com`; - let platformAdmin: User; - let managedUser: User; - let organization: Team; - let oAuthClient: PlatformOAuthClient; - let eventTypeId: number; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - managedUserEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - billingService = moduleRef.get(BillingService); - - // Spy on the billing service methods - increaseUsageSpy = jest.spyOn(billingService, "increaseUsageByUserId"); - cancelUsageSpy = jest.spyOn(billingService, "cancelUsageByBookingUid"); - - // Create platform admin - platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail }); - - // Create organization - organization = await teamRepositoryFixture.create({ - name: `billing-test-organization-2024-08-13-${randomString()}`, - isPlatform: true, - isOrganization: true, - }); - - // Create OAuth client - oAuthClient = await oauthClientRepositoryFixture.create( - organization.id, - { - logo: "logo-url", - name: "billing-test-oauth-client-2024-08-13", - redirectUris: [CLIENT_REDIRECT_URI], - permissions: 1023, - }, - "secret" - ); - - // Create profile for platform admin - await profilesRepositoryFixture.create({ - uid: `billing-test-profile-2024-08-13-${randomString()}`, - username: platformAdminEmail, - organization: { connect: { id: organization.id } }, - user: { connect: { id: platformAdmin.id } }, - }); - - // Create membership for platform admin - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: platformAdmin.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - // Create a platform-managed user - managedUser = await userRepositoryFixture.create({ - email: managedUserEmail, - username: managedUserEmail, - isPlatformManaged: true, - platformOAuthClients: { - connect: { id: oAuthClient.id }, - }, - }); - - // Create profile for managed user - await profilesRepositoryFixture.create({ - uid: `billing-managed-user-profile-2024-08-13-${randomString()}`, - username: managedUserEmail, - organization: { connect: { id: organization.id } }, - user: { connect: { id: managedUser.id } }, - }); - - // Create membership for managed user - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: managedUser.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: `billing-managed-user-schedule-2024-08-13-${randomString()}`, - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(managedUser.id, userSchedule); - - const event = await eventTypesRepositoryFixture.create( - { - title: `billing-managed-user-event-type-2024-08-13-${randomString()}`, - slug: `billing-managed-user-event-type-2024-08-13-${randomString()}`, - length: 60, - }, - managedUser.id - ); - eventTypeId = event.id; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - afterAll(async () => { - await bookingsRepositoryFixture.deleteAllBookings(managedUser.id, managedUser.email); - await userRepositoryFixture.deleteByEmail(managedUser.email); - await userRepositoryFixture.deleteByEmail(platformAdmin.email); - await app.close(); - }); - - it("should call billing service when creating a booking for a platform-managed user", async () => { - increaseUsageSpy.mockClear(); - - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2040, 4, 21, 9, 30, 0)).toISOString(), - eventTypeId: eventTypeId, - attendee: { - name: "Test Attendee", - email: "attendee-billing-managed-2024-08-13@example.com", - timeZone: "Europe/London", - 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 responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const data = responseBody.data as BookingOutput_2024_08_13; - - // Verify billing service WAS called for platform-managed user - expect(increaseUsageSpy).toHaveBeenCalledTimes(1); - expect(increaseUsageSpy).toHaveBeenCalledWith( - managedUser.id, - expect.objectContaining({ - uid: data.uid, - startTime: expect.any(Date), - }) - ); - }); - - it("should call billing cancel service when cancelling a booking for a platform-managed user", async () => { - // First create a booking - const createBody: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2040, 4, 22, 9, 30, 0)).toISOString(), - eventTypeId: eventTypeId, - attendee: { - name: "Test Attendee Cancel", - email: "attendee-cancel-managed-2024-08-13@example.com", - timeZone: "Europe/London", - language: "en", - }, - location: "https://meet.google.com/abc-def-ghi", - }; - - const createResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(createBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const createResponseBody: CreateBookingOutput_2024_08_13 = createResponse.body; - const data = createResponseBody.data as BookingOutput_2024_08_13; - const bookingUid = data.uid; - - // Clear the spy before cancelling - cancelUsageSpy.mockClear(); - - // Cancel the booking - await request(app.getHttpServer()) - .post(`/v2/bookings/${bookingUid}/cancel`) - .send({}) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200); - - // Verify billing cancel service WAS called for platform-managed user - expect(cancelUsageSpy).toHaveBeenCalledTimes(1); - expect(cancelUsageSpy).toHaveBeenCalledWith(bookingUid); - }); - }); -}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/team-emails.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/team-emails.e2e-spec.ts deleted file mode 100644 index 3e0284761ae9c0..00000000000000 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/team-emails.e2e-spec.ts +++ /dev/null @@ -1,533 +0,0 @@ -import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; -import { - AttendeeCancelledEmail, - AttendeeRescheduledEmail, - AttendeeScheduledEmail, - AttendeeUpdatedEmail, - OrganizerCancelledEmail, - OrganizerReassignedEmail, - OrganizerRescheduledEmail, - OrganizerScheduledEmail, -} from "@calcom/platform-libraries/emails"; -import type { - 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 { 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 - .spyOn(AttendeeScheduledEmail.prototype, "getHtml") - .mockImplementation(() => Promise.resolve("

email

")); -jest - .spyOn(OrganizerScheduledEmail.prototype, "getHtml") - .mockImplementation(() => Promise.resolve("

email

")); -jest - .spyOn(AttendeeRescheduledEmail.prototype, "getHtml") - .mockImplementation(() => Promise.resolve("

email

")); -jest - .spyOn(OrganizerRescheduledEmail.prototype, "getHtml") - .mockImplementation(() => Promise.resolve("

email

")); -jest - .spyOn(AttendeeCancelledEmail.prototype, "getHtml") - .mockImplementation(() => Promise.resolve("

email

")); -jest - .spyOn(OrganizerCancelledEmail.prototype, "getHtml") - .mockImplementation(() => Promise.resolve("

email

")); -jest - .spyOn(OrganizerReassignedEmail.prototype, "getHtml") - .mockImplementation(() => Promise.resolve("

email

")); -jest - .spyOn(AttendeeUpdatedEmail.prototype, "getHtml") - .mockImplementation(() => Promise.resolve("

email

")); - -// Type definitions for test setup data -type EmailSetup = { - team: Team; - member1: User; - member2: User; - member1ApiKey: string; - collectiveEventType: { id: number }; - roundRobinEventType: { id: number }; -}; - -describe("Bookings Endpoints 2024-08-13 team emails", () => { - let app: INestApplication; - let organization: Team; - - // Fixtures for database interactions - let userRepositoryFixture: UserRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let hostsRepositoryFixture: HostsRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - - // Setup data for tests - let emailsEnabledSetup: EmailSetup; - let emailsDisabledSetup: EmailSetup; - - // Utility function to check response data type - const responseDataIsBooking = (data: unknown): data is BookingOutput_2024_08_13 => { - return !Array.isArray(data) && data !== null && typeof data === "object" && data && "id" in data; - }; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], - }) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - // Initialize fixtures and services - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - - // Create a base organization for all tests - organization = await teamRepositoryFixture.create({ name: `team-emails-organization-${randomString()}` }); - - // Set up two distinct environments: one with emails enabled, one disabled - emailsEnabledSetup = await setupTestEnvironment(true); - emailsDisabledSetup = await setupTestEnvironment(false); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - // Helper function to create a complete test environment - async function setupTestEnvironment(emailsEnabled: boolean): Promise { - const oAuthClient = await createOAuthClient(organization.id, emailsEnabled); - const team = await teamRepositoryFixture.create({ - name: `team-emails-team-${randomString()}`, - parent: { connect: { id: organization.id } }, - createdByOAuthClient: { connect: { id: oAuthClient.id } }, - }); - - const [member1, member2] = await Promise.all([ - createTeamMember(oAuthClient.id), - createTeamMember(oAuthClient.id), - ]); - - await Promise.all([ - membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: member1.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }), - membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: member2.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }), - ]); - - const collectiveEvent = await createEventType("COLLECTIVE", team.id, [member1.id, member2.id]); - const roundRobinEvent = await createEventType("ROUND_ROBIN", team.id, [member1.id, member2.id]); - - // Create API key for member1 to use in authorized tests - const { keyString } = await apiKeysRepositoryFixture.createApiKey(member1.id, null); - const member1ApiKey = `cal_test_${keyString}`; - - return { - team, - member1, - member2, - member1ApiKey, - collectiveEventType: { id: collectiveEvent.id }, - roundRobinEventType: { id: roundRobinEvent.id }, - }; - } - - // Helper to create a user and their profile/schedule - async function createTeamMember(oauthClientId: string) { - const member = await userRepositoryFixture.create({ - email: `team-emails-member-${randomString()}@api.com`, - platformOAuthClients: { connect: { id: oauthClientId } }, - }); - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: `schedule-${randomString()}`, - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(member.id, userSchedule); - await profileRepositoryFixture.create({ - uid: `usr-${member.id}`, - username: member.email, - organization: { connect: { id: organization.id } }, - user: { connect: { id: member.id } }, - }); - return member; - } - - // Helper to create an event type and assign hosts - async function createEventType(type: "COLLECTIVE" | "ROUND_ROBIN", teamId: number, hostIds: number[]) { - const eventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: type, - team: { connect: { id: teamId } }, - title: `event-type-${randomString()}`, - slug: `event-type-${randomString()}`, - length: 60, - assignAllTeamMembers: type === "COLLECTIVE", - locations: [{ type: "inPerson", address: "via 10, rome, italy" }], - }); - await Promise.all( - hostIds.map((userId) => - hostsRepositoryFixture.create({ - isFixed: type === "COLLECTIVE", - user: { connect: { id: userId } }, - eventType: { connect: { id: eventType.id } }, - }) - ) - ); - return eventType; - } - - // Helper to create an OAuth client - async function createOAuthClient(organizationId: number, emailsEnabled: boolean) { - return oauthClientRepositoryFixture.create( - organizationId, - { - logo: "logo-url", - name: "name", - redirectUris: ["http://localhost:5555"], - permissions: 32, - areEmailsEnabled: emailsEnabled, - }, - "secret" - ); - } - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe("OAuth client team bookings - emails disabled", () => { - it("should handle the full booking lifecycle for a collective event without sending emails", async () => { - // --- 1. Book Event --- - const createBody: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(), - eventTypeId: emailsDisabledSetup.collectiveEventType.id, - attendee: { - name: "Mr Proper", - email: "mr_proper@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - }; - const createResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(createBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(createResponse.status).toBe(201); - const createResponseBody: CreateBookingOutput_2024_08_13 = createResponse.body; - expect(createResponseBody.status).toEqual(SUCCESS_STATUS); - expect(responseDataIsBooking(createResponseBody.data)).toBe(true); - const bookingUid = (createResponseBody.data as BookingOutput_2024_08_13).uid; - - expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled(); - expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled(); - - // --- 2. Reschedule Event --- - const rescheduleBody: RescheduleBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2035, 0, 8, 11, 0, 0)).toISOString(), - reschedulingReason: "Flying to mars that day", - }; - const rescheduleResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${bookingUid}/reschedule`) - .send(rescheduleBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(rescheduleResponse.status).toBe(201); - const rescheduleResponseBody: RescheduleBookingOutput_2024_08_13 = rescheduleResponse.body; - expect(rescheduleResponseBody.status).toEqual(SUCCESS_STATUS); - const rescheduledBookingUid = rescheduleResponseBody.data.uid; - - expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled(); - expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled(); - - // --- 3. Cancel Event --- - const cancelBody: CancelBookingInput_2024_08_13 = { cancellationReason: "Going on a vacation" }; - const cancelResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${rescheduledBookingUid}/cancel`) - .send(cancelBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(cancelResponse.status).toBe(200); - expect(cancelResponse.body.status).toEqual(SUCCESS_STATUS); - expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled(); - expect(OrganizerCancelledEmail.prototype.getHtml).not.toHaveBeenCalled(); - }); - - it("should handle the full booking lifecycle for a round-robin event without sending emails", async () => { - // --- 1. Book Event --- - const createBody: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(), - eventTypeId: emailsDisabledSetup.roundRobinEventType.id, - attendee: { - name: "Mr Proper", - email: "mr_proper@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - }; - const createResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(createBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(createResponse.status).toBe(201); - const createResponseBody: CreateBookingOutput_2024_08_13 = createResponse.body; - expect(responseDataIsBooking(createResponseBody.data)).toBe(true); - const bookingUid = (createResponseBody.data as BookingOutput_2024_08_13).uid; - let currentHostId = (createResponseBody.data as BookingOutput_2024_08_13).hosts[0].id; - expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled(); - expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled(); - - // --- 2. Reschedule Event --- - const rescheduleBody: RescheduleBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2035, 0, 8, 12, 0, 0)).toISOString(), - }; - const rescheduleResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${bookingUid}/reschedule`) - .send(rescheduleBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(rescheduleResponse.status).toBe(201); - const rescheduleResponseBody: RescheduleBookingOutput_2024_08_13 = rescheduleResponse.body; - const rescheduledBookingUid = rescheduleResponseBody.data.uid; - currentHostId = rescheduleResponseBody.data.hosts[0].id; - expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled(); - expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled(); - - // --- 3. Manual Reassign --- - const reassignToId = - currentHostId === emailsDisabledSetup.member1.id - ? emailsDisabledSetup.member2.id - : emailsDisabledSetup.member1.id; - const manualReassignResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${rescheduledBookingUid}/reassign/${reassignToId}`) - .set("Authorization", `Bearer ${emailsDisabledSetup.member1ApiKey}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(manualReassignResponse.status).toBe(200); - expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled(); - - // --- 4. Automatic Reassign --- - const autoReassignResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${rescheduledBookingUid}/reassign`) - .set("Authorization", `Bearer ${emailsDisabledSetup.member1ApiKey}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(autoReassignResponse.status).toBe(200); - expect(OrganizerReassignedEmail.prototype.getHtml).not.toHaveBeenCalled(); - expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled(); - - // --- 5. Cancel Event --- - const cancelBody: CancelBookingInput_2024_08_13 = { cancellationReason: "Vacation" }; - const cancelResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${rescheduledBookingUid}/cancel`) - .send(cancelBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(cancelResponse.status).toBe(200); - expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled(); - expect(OrganizerCancelledEmail.prototype.getHtml).not.toHaveBeenCalled(); - }); - }); - - describe("OAuth client team bookings - emails enabled", () => { - it("should handle the full booking lifecycle for a collective event and send emails", async () => { - // --- 1. Book Event --- - const createBody: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(), - eventTypeId: emailsEnabledSetup.collectiveEventType.id, - attendee: { - name: "Mr Proper", - email: "mr_proper@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - }; - const createResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(createBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(createResponse.status).toBe(201); - const createResponseBody: CreateBookingOutput_2024_08_13 = createResponse.body; - expect(responseDataIsBooking(createResponseBody.data)).toBe(true); - const bookingUid = (createResponseBody.data as BookingOutput_2024_08_13).uid; - - expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled(); - expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled(); - - // --- 2. Reschedule Event --- - const rescheduleBody: RescheduleBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2035, 0, 8, 11, 0, 0)).toISOString(), - }; - const rescheduleResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${bookingUid}/reschedule`) - .send(rescheduleBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(rescheduleResponse.status).toBe(201); - const rescheduledBookingUid = (rescheduleResponse.body as RescheduleBookingOutput_2024_08_13).data.uid; - expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled(); - expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled(); - - // --- 3. Cancel Event --- - const cancelBody: CancelBookingInput_2024_08_13 = { cancellationReason: "Vacation" }; - const cancelResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${rescheduledBookingUid}/cancel`) - .send(cancelBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(cancelResponse.status).toBe(200); - expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled(); - expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled(); - }); - - it("should handle the full booking lifecycle for a round-robin event and send emails", async () => { - // --- 1. Book Event --- - const createBody: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(), - eventTypeId: emailsEnabledSetup.roundRobinEventType.id, - attendee: { - name: "Mr Proper", - email: "mr_proper@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - }; - const createResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(createBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(createResponse.status).toBe(201); - const createResponseBody: CreateBookingOutput_2024_08_13 = createResponse.body; - expect(responseDataIsBooking(createResponseBody.data)).toBe(true); - const bookingUid = (createResponseBody.data as BookingOutput_2024_08_13).uid; - let currentHostId = (createResponseBody.data as BookingOutput_2024_08_13).hosts[0].id; - expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled(); - expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled(); - - // --- 2. Reschedule Event --- - jest.clearAllMocks(); - const rescheduleBody: RescheduleBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2035, 0, 8, 12, 0, 0)).toISOString(), - }; - const rescheduleResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${bookingUid}/reschedule`) - .send(rescheduleBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(rescheduleResponse.status).toBe(201); - const rescheduleResponseBody: RescheduleBookingOutput_2024_08_13 = rescheduleResponse.body; - const rescheduledBookingUid = rescheduleResponseBody.data.uid; - currentHostId = rescheduleResponseBody.data.hosts[0].id; - expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled(); - expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled(); // Old host gets cancellation - expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled(); // New host gets scheduled - - // --- 3. Manual Reassign --- - jest.clearAllMocks(); - const originalHostId = currentHostId; - const reassignToId = - currentHostId === emailsEnabledSetup.member1.id - ? emailsEnabledSetup.member2.id - : emailsEnabledSetup.member1.id; - const hasOrganizerChanged = reassignToId !== originalHostId; - - const manualReassignResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${rescheduledBookingUid}/reassign/${reassignToId}`) - .set("Authorization", `Bearer ${emailsEnabledSetup.member1ApiKey}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(manualReassignResponse.status).toBe(200); - expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled(); - expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled(); - if (hasOrganizerChanged) { - expect(AttendeeUpdatedEmail.prototype.getHtml).toHaveBeenCalled(); - } else { - expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled(); - } - - // --- 4. Automatic Reassign --- - jest.clearAllMocks(); - const autoReassignResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${rescheduledBookingUid}/reassign`) - .set("Authorization", `Bearer ${emailsEnabledSetup.member1ApiKey}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(autoReassignResponse.status).toBe(200); - expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled(); - expect(OrganizerReassignedEmail.prototype.getHtml).toHaveBeenCalled(); - expect(AttendeeUpdatedEmail.prototype.getHtml).toHaveBeenCalled(); - - // --- 5. Cancel Event --- - jest.clearAllMocks(); - const cancelBody: CancelBookingInput_2024_08_13 = { cancellationReason: "Vacation" }; - const cancelResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${rescheduledBookingUid}/cancel`) - .send(cancelBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(cancelResponse.status).toBe(200); - expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled(); - expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled(); - }); - }); - - afterAll(async () => { - // Clean up database records - await teamRepositoryFixture.delete(organization.id); - await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.member1.email); - await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.member2.email); - await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.member1.email); - await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.member2.email); - await app.close(); - }); -}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/managed-user-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/managed-user-bookings.e2e-spec.ts deleted file mode 100644 index 4fff5d3e08d3a6..00000000000000 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/managed-user-bookings.e2e-spec.ts +++ /dev/null @@ -1,1057 +0,0 @@ -import { - CAL_API_VERSION_HEADER, - SUCCESS_STATUS, - VERSION_2024_06_14, - VERSION_2024_08_13, -} from "@calcom/platform-constants"; -import { - ApiSuccessResponse, - BookingOutput_2024_08_13, - CreateBookingInput_2024_08_13, - CreateEventTypeInput_2024_06_14, - EventTypeOutput_2024_06_14, - GetBookingOutput_2024_08_13, - 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"; - -describe("Managed user bookings 2024-08-13", () => { - let app: INestApplication; - - let oAuthClient: PlatformOAuthClient; - let organization: Team; - let userRepositoryFixture: UserRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let profilesRepositoryFixture: ProfileRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - - const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`; - let platformAdmin: User; - - const managedUsersTimeZone = "Europe/Rome"; - const firstManagedUserEmail = `managed-user-bookings-2024-08-13-first-user-${randomString()}@api.com`; - const secondManagedUserEmail = `managed-user-bookings-2024-08-13-second-user-${randomString()}@api.com`; - const thirdManagedUserEmail = `managed-user-bookings-2024-08-13-third-user-${randomString()}@api.com`; - - let firstManagedUser: CreateManagedUserData; - let secondManagedUser: CreateManagedUserData; - let thirdManagedUser: CreateManagedUserData; - - let firstManagedUserEventTypeId: number; - let eventTypeRequiresConfirmationId: number; - let seatedEventTypeId: number; - - const orgAdminManagedUserEmail = `managed-user-bookings-2024-08-13-org-admin-${randomString()}@api.com`; - let orgAdminManagedUser: CreateManagedUserData; - - let firstManagedUserBookingsCount = 0; - let secondManagedUserBookingsCount = 0; - let thirdManagedUserBookingsCount = 0; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [PrismaExceptionFilter, HttpExceptionFilter], - imports: [AppModule, UsersModule, MembershipsModule], - }).compile(); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - - platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail }); - - organization = await teamRepositoryFixture.create({ - name: `oauth-client-users-organization-${randomString()}`, - isPlatform: true, - isOrganization: true, - platformBilling: { - create: { - customerId: "cus_999", - plan: "SCALE", - subscriptionId: "sub_999", - }, - }, - }); - oAuthClient = await createOAuthClient(organization.id); - - await profilesRepositoryFixture.create({ - uid: "asd1qwwqeqw-asddsadasd", - username: platformAdminEmail, - organization: { connect: { id: organization.id } }, - user: { - connect: { id: platformAdmin.id }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: platformAdmin.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - await app.init(); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: [CLIENT_REDIRECT_URI], - permissions: 1023, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - it(`should create first managed user`, async () => { - const requestBody: CreateManagedUserInput = { - email: firstManagedUserEmail, - timeZone: managedUsersTimeZone, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Alice Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email) - ); - expect(responseBody.data.accessToken).toBeDefined(); - expect(responseBody.data.refreshToken).toBeDefined(); - - firstManagedUser = responseBody.data; - }); - - it("should create an event type for managed user", async () => { - const body: CreateEventTypeInput_2024_06_14 = { - title: "Managed user bookings first managed user event type", - slug: "managed-user-bookings-first-managed-user-event-type", - description: "Managed user bookings first managed user event type description", - lengthInMinutes: 30, - }; - - return request(app.getHttpServer()) - .post("/api/v2/event-types") - .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .send(body) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - const createdEventType = responseBody.data; - expect(createdEventType).toHaveProperty("id"); - expect(createdEventType.title).toEqual(body.title); - expect(createdEventType.description).toEqual(body.description); - expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes); - expect(createdEventType.ownerId).toEqual(firstManagedUser.user.id); - firstManagedUserEventTypeId = createdEventType.id; - }); - }); - - it(`should create second managed user`, async () => { - const requestBody: CreateManagedUserInput = { - email: secondManagedUserEmail, - timeZone: managedUsersTimeZone, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Bob Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email) - ); - expect(responseBody.data.accessToken).toBeDefined(); - expect(responseBody.data.refreshToken).toBeDefined(); - - secondManagedUser = responseBody.data; - }); - - it(`should create third managed user`, async () => { - const requestBody: CreateManagedUserInput = { - email: thirdManagedUserEmail, - timeZone: managedUsersTimeZone, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Charlie Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email) - ); - expect(responseBody.data.accessToken).toBeDefined(); - expect(responseBody.data.refreshToken).toBeDefined(); - - thirdManagedUser = responseBody.data; - }); - - it(`should create org admin managed user`, async () => { - const requestBody: CreateManagedUserInput = { - email: orgAdminManagedUserEmail, - timeZone: managedUsersTimeZone, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Org Admin Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email) - ); - expect(responseBody.data.accessToken).toBeDefined(); - expect(responseBody.data.refreshToken).toBeDefined(); - - orgAdminManagedUser = responseBody.data; - - await request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/memberships`) - .set("x-cal-client-id", oAuthClient.id) - .set("x-cal-secret-key", oAuthClient.secret) - .send({ - userId: orgAdminManagedUser.user.id, - role: "ADMIN", - accepted: true, - }) - .expect(201); - }); - - it("should create an event type requiring confirmation for first managed user", async () => { - const eventTypeRequiresConfirmation = await eventTypesRepositoryFixture.create( - { - title: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`, - slug: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`, - length: 60, - requiresConfirmation: true, - }, - firstManagedUser.user.id - ); - eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id; - }); - - it("should create a seated event type for first managed user", async () => { - const seatedEventType = await eventTypesRepositoryFixture.create( - { - title: `managed-user-bookings-seated-event-type-${randomString()}`, - slug: `managed-user-bookings-seated-event-type-${randomString()}`, - length: 30, - seatsPerTimeSlot: 5, - seatsShowAttendees: true, - }, - firstManagedUser.user.id - ); - seatedEventTypeId = seatedEventType.id; - }); - - describe("bookings using original emails", () => { - it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), - eventTypeId: firstManagedUserEventTypeId, - attendee: { - name: secondManagedUser.user.name!, - email: secondManagedUserEmail, - timeZone: secondManagedUser.user.timeZone, - language: secondManagedUser.user.locale, - }, - location: "https://meet.google.com/abc-def-ghi", - }; - - await request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - firstManagedUserBookingsCount += 1; - secondManagedUserBookingsCount += 1; - - const getFirstManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(200); - - const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = - getFirstManagedUserBookings.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data; - expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); - - const getSecondManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) - .expect(200); - - const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = - getSecondManagedUserBookings.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const secondManagedUserBookings: BookingOutput_2024_08_13[] = - secondManagedUserBookingsResponseBody.data; - expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); - }); - - it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(), - eventTypeId: firstManagedUserEventTypeId, - attendee: { - name: thirdManagedUser.user.name!, - email: thirdManagedUserEmail, - timeZone: thirdManagedUser.user.timeZone, - language: thirdManagedUser.user.locale, - }, - guests: [secondManagedUserEmail], - location: "https://meet.google.com/abc-def-ghi", - }; - - await request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - firstManagedUserBookingsCount += 1; - secondManagedUserBookingsCount += 1; - thirdManagedUserBookingsCount += 1; - - const getFirstManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(200); - - const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = - getFirstManagedUserBookings.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data; - expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); - - const getSecondManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) - .expect(200); - - const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = - getSecondManagedUserBookings.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const secondManagedUserBookings: BookingOutput_2024_08_13[] = - secondManagedUserBookingsResponseBody.data; - expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); - - const getThirdManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${thirdManagedUser.accessToken}`) - .expect(200); - - const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = - getThirdManagedUserBookings.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data; - expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount); - }); - }); - - describe("bookings using OAuth client emails", () => { - it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 15, 0, 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", - }; - - await request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - firstManagedUserBookingsCount += 1; - secondManagedUserBookingsCount += 1; - - const getFirstManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(200); - - const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = - getFirstManagedUserBookings.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data; - expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); - - const getSecondManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) - .expect(200); - - const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = - getSecondManagedUserBookings.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const secondManagedUserBookings: BookingOutput_2024_08_13[] = - secondManagedUserBookingsResponseBody.data; - expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); - }); - - it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 15, 30, 0)).toISOString(), - eventTypeId: firstManagedUserEventTypeId, - attendee: { - name: thirdManagedUser.user.name!, - email: thirdManagedUser.user.email, - timeZone: thirdManagedUser.user.timeZone, - language: thirdManagedUser.user.locale, - }, - guests: [secondManagedUser.user.email], - location: "https://meet.google.com/abc-def-ghi", - }; - - await request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - firstManagedUserBookingsCount += 1; - secondManagedUserBookingsCount += 1; - thirdManagedUserBookingsCount += 1; - - const getFirstManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(200); - - const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = - getFirstManagedUserBookings.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data; - expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); - - const getSecondManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) - .expect(200); - - const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = - getSecondManagedUserBookings.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const secondManagedUserBookings: BookingOutput_2024_08_13[] = - secondManagedUserBookingsResponseBody.data; - expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); - - const getThirdManagedUserBookings = await request(app.getHttpServer()) - .get("/v2/bookings") - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${thirdManagedUser.accessToken}`) - .expect(200); - - const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = - getThirdManagedUserBookings.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data; - expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount); - }); - }); - - describe("fetch bookings by attendeeEmail", () => { - it("should return bookings for the original email when original email is attendee only", async () => { - const response = await request(app.getHttpServer()) - .get(`/v2/bookings?attendeeEmail=${thirdManagedUserEmail}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(200); - - const thirdManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const thirdManagedUserAttendeeBookings: BookingOutput_2024_08_13[] = - thirdManagedUserAttendeeBookingsResponseBody.data; - expect(thirdManagedUserBookingsCount).toEqual(2); - expect(thirdManagedUserAttendeeBookings.length).toEqual(thirdManagedUserBookingsCount); - }); - - it("should return bookings for the oAuth email when original oAuth email is attendee only", async () => { - const response = await request(app.getHttpServer()) - .get(`/v2/bookings?attendeeEmail=${thirdManagedUser.user.email}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(200); - - const thirdManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const thirdManagedUserAttendeeBookings: BookingOutput_2024_08_13[] = - thirdManagedUserAttendeeBookingsResponseBody.data; - expect(thirdManagedUserBookingsCount).toEqual(2); - expect(thirdManagedUserAttendeeBookings.length).toEqual(thirdManagedUserBookingsCount); - }); - - it("should return bookings for the original email when original email is attendee or guest", async () => { - const response = await request(app.getHttpServer()) - .get(`/v2/bookings?attendeeEmail=${secondManagedUserEmail}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(200); - - const secondManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const secondManagedUserAttendeeBookings: BookingOutput_2024_08_13[] = - secondManagedUserAttendeeBookingsResponseBody.data; - expect(secondManagedUserBookingsCount).toEqual(4); - expect(secondManagedUserAttendeeBookings.length).toEqual(secondManagedUserBookingsCount); - }); - - it("should return bookings for the oAuth email when original oAuth email is attendee or guest", async () => { - const response = await request(app.getHttpServer()) - .get(`/v2/bookings?attendeeEmail=${secondManagedUser.user.email}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(200); - - const secondManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const secondManagedUserAttendeeBookings: BookingOutput_2024_08_13[] = - secondManagedUserAttendeeBookingsResponseBody.data; - expect(secondManagedUserBookingsCount).toEqual(4); - expect(secondManagedUserAttendeeBookings.length).toEqual(secondManagedUserBookingsCount); - }); - }); - - 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({ - user: { - connect: { - id: firstManagedUser.user.id, - }, - }, - startTime: new Date(Date.UTC(2030, 0, 9, 13, 0, 0)), - endTime: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)), - title: "Booking requiring confirmation", - uid: `booking-requiring-confirmation-${randomString()}`, - eventType: { - connect: { - id: eventTypeRequiresConfirmationId, - }, - }, - location: "https://meet.google.com/abc-def-ghi", - customInputs: {}, - metadata: {}, - status: "PENDING", - responses: { - name: secondManagedUser.user.name, - email: secondManagedUserEmail, - }, - attendees: { - create: { - email: secondManagedUserEmail, - name: secondManagedUser.user.name!, - locale: secondManagedUser.user.locale, - timeZone: secondManagedUser.user.timeZone, - }, - }, - }); - - const response = await request(app.getHttpServer()) - .post(`/v2/bookings/${bookingRequiringConfirmation.uid}/confirm`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) - .expect(200); - - const responseBody: GetBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const bookingData = responseBody.data as BookingOutput_2024_08_13; - expect(bookingData.status).toEqual("accepted"); - expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid); - - const confirmedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid); - expect(confirmedBooking?.status).toEqual("ACCEPTED"); - }); - - it("should allow org admin managed user to reject booking using access token", async () => { - const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({ - user: { - connect: { - id: firstManagedUser.user.id, - }, - }, - startTime: new Date(Date.UTC(2030, 0, 9, 15, 0, 0)), - endTime: new Date(Date.UTC(2030, 0, 9, 16, 0, 0)), - title: "Booking requiring rejection", - uid: `booking-requiring-rejection-${randomString()}`, - eventType: { - connect: { - id: eventTypeRequiresConfirmationId, - }, - }, - location: "https://meet.google.com/abc-def-ghi", - customInputs: {}, - metadata: {}, - status: "PENDING", - responses: { - name: secondManagedUser.user.name, - email: secondManagedUserEmail, - }, - attendees: { - create: { - email: secondManagedUserEmail, - name: secondManagedUser.user.name!, - locale: secondManagedUser.user.locale, - timeZone: secondManagedUser.user.timeZone, - }, - }, - }); - - const response = await request(app.getHttpServer()) - .post(`/v2/bookings/${bookingRequiringConfirmation.uid}/decline`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) - .expect(200); - - const responseBody: GetBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const bookingData = responseBody.data as BookingOutput_2024_08_13; - expect(bookingData.status).toEqual("rejected"); - expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid); - - const rejectedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid); - expect(rejectedBooking?.status).toEqual("REJECTED"); - }); - - it("should return unauthorized when org admin tries to confirm regular user's booking", async () => { - const regularUser = await userRepositoryFixture.create({ - email: `managed-user-bookings-2024-08-13-regular-user-${randomString()}@api.com`, - name: "Regular User", - }); - - const regularUserEventType = await eventTypesRepositoryFixture.create( - { - title: `regular-user-event-type-requires-confirmation-${randomString()}`, - slug: `regular-user-event-type-requires-confirmation-${randomString()}`, - length: 60, - requiresConfirmation: true, - }, - regularUser.id - ); - - const regularUserBooking = await bookingsRepositoryFixture.create({ - user: { - connect: { - id: regularUser.id, - }, - }, - startTime: new Date(Date.UTC(2030, 0, 9, 17, 0, 0)), - endTime: new Date(Date.UTC(2030, 0, 9, 18, 0, 0)), - title: "Regular user booking requiring confirmation", - uid: `regular-user-booking-${randomString()}`, - eventType: { - connect: { - id: regularUserEventType.id, - }, - }, - location: "https://meet.google.com/abc-def-ghi", - customInputs: {}, - metadata: {}, - status: "PENDING", - responses: { - name: "External Attendee", - email: "external@example.com", - }, - attendees: { - create: { - email: "external@example.com", - name: "External Attendee", - locale: "en", - timeZone: "Europe/Rome", - }, - }, - }); - - await request(app.getHttpServer()) - .post(`/v2/bookings/${regularUserBooking.uid}/confirm`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) - .expect(401); - - await userRepositoryFixture.delete(regularUser.id); - }); - }); - - describe("seated booking management by org admin", () => { - let seatedBookingUid: string; - - it("should create a seated booking with multiple attendees", async () => { - const bodyOne: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 10, 10, 0, 0)).toISOString(), - eventTypeId: seatedEventTypeId, - attendee: { - name: secondManagedUser.user.name!, - email: secondManagedUser.user.email, - timeZone: secondManagedUser.user.timeZone, - language: secondManagedUser.user.locale, - }, - }; - - const responseOne = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(bodyOne) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const responseBodyForFirstAttendee: ApiSuccessResponse = responseOne.body; - expect(responseBodyForFirstAttendee.status).toEqual(SUCCESS_STATUS); - expect(responseBodyForFirstAttendee.data).toBeDefined(); - - const bookingDataForFirstAttendee = responseBodyForFirstAttendee.data as BookingOutput_2024_08_13; - seatedBookingUid = bookingDataForFirstAttendee.uid; - expect(bookingDataForFirstAttendee.attendees).toBeDefined(); - expect(bookingDataForFirstAttendee.attendees.length).toBeGreaterThan(0); - - const bodyTwo: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 10, 10, 0, 0)).toISOString(), - eventTypeId: seatedEventTypeId, - attendee: { - name: thirdManagedUser.user.name!, - email: thirdManagedUser.user.email, - timeZone: thirdManagedUser.user.timeZone, - language: thirdManagedUser.user.locale, - }, - }; - - const responseTwo = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(bodyTwo) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const responseBodyForSecondAttendee: ApiSuccessResponse = responseTwo.body; - expect(responseBodyForSecondAttendee.status).toEqual(SUCCESS_STATUS); - expect(responseBodyForSecondAttendee.data).toBeDefined(); - - const bookingDataForSecondAttendee = responseBodyForSecondAttendee.data as BookingOutput_2024_08_13; - expect(bookingDataForSecondAttendee.attendees).toBeDefined(); - expect(bookingDataForSecondAttendee.attendees.length).toBeGreaterThan(0); - }); - - it("should allow org admin to reschedule all seats in a seated booking for a managed user", async () => { - const newStartTime = new Date(Date.UTC(2030, 0, 10, 11, 0, 0)).toISOString(); - - const response = await request(app.getHttpServer()) - .post(`/v2/bookings/${seatedBookingUid}/reschedule`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) - .send({ - start: newStartTime, - }) - .expect(201); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const bookingData = responseBody.data as BookingOutput_2024_08_13; - expect(bookingData.start).toEqual(newStartTime); - - seatedBookingUid = bookingData.uid; - }); - - it("should allow org admin to cancel all seats in a seated booking for a managed user", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/bookings/${seatedBookingUid}/cancel`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) - .send({ - cancellationReason: "Org admin cancelled the booking", - }) - .expect(200); - - const responseBody: GetBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const bookingData = responseBody.data as BookingOutput_2024_08_13; - expect(bookingData.status).toEqual("cancelled"); - expect(bookingData.uid).toEqual(seatedBookingUid); - - const cancelledBooking = await bookingsRepositoryFixture.getByUid(seatedBookingUid); - expect(cancelledBooking?.status).toEqual("CANCELLED"); - }); - }); - - describe("event type booking requires authentication", () => { - let eventTypeRequiringAuthenticationId: number; - - let body: CreateBookingInput_2024_08_13; - - beforeAll(async () => { - const eventTypeRequiringAuthentication = await eventTypesRepositoryFixture.create( - { - title: `event-type-requiring-authentication-${randomString()}`, - slug: `event-type-requiring-authentication-${randomString()}`, - length: 60, - requiresConfirmation: true, - bookingRequiresAuthentication: true, - }, - secondManagedUser.user.id - ); - eventTypeRequiringAuthenticationId = eventTypeRequiringAuthentication.id; - - body = { - start: new Date(Date.UTC(2030, 0, 9, 15, 0, 0)).toISOString(), - eventTypeId: eventTypeRequiringAuthenticationId, - attendee: { - email: "external@example.com", - name: "External Attendee", - timeZone: "Europe/Rome", - }, - }; - }); - - it("can't be booked without credentials", async () => { - await request(app.getHttpServer()) - .post(`/v2/bookings`) - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(401); - }); - - it("can't be booked with managed user credentials who is not admin and not event type owner", async () => { - await request(app.getHttpServer()) - .post(`/v2/bookings`) - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) - .expect(403); - }); - - it("can be booked with managed user credentials who is event type owner", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/bookings`) - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) - .expect(201); - - const bookingId = response.body.data.id; - await bookingsRepositoryFixture.deleteById(bookingId); - }); - - it("can be booked with managed user credentials who is admin", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/bookings`) - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) - .expect(201); - - const bookingId = response.body.data.id; - await bookingsRepositoryFixture.deleteById(bookingId); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.delete(firstManagedUser.user.id); - await userRepositoryFixture.delete(secondManagedUser.user.id); - await userRepositoryFixture.delete(thirdManagedUser.user.id); - await userRepositoryFixture.delete(orgAdminManagedUser.user.id); - await userRepositoryFixture.delete(platformAdmin.id); - await oauthClientRepositoryFixture.delete(oAuthClient.id); - await teamRepositoryFixture.delete(organization.id); - await app.close(); - }); -}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/reassign-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/reassign-bookings.e2e-spec.ts deleted file mode 100644 index 72c37b73cfc921..00000000000000 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/reassign-bookings.e2e-spec.ts +++ /dev/null @@ -1,809 +0,0 @@ -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 request from "supertest"; -import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture"; -import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; -import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; -import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture"; -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 { 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 { 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", () => { - let app: INestApplication; - let organization: Team; - let team: Team; - - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let oAuthClient: PlatformOAuthClient; - let teamRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let hostsRepositoryFixture: HostsRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - - const teamUserEmail = `reassign-bookings-2024-08-13-user1-${randomString()}@api.com`; - const teamUserEmail2 = `reassign-bookings-2024-08-13-user2-${randomString()}@api.com`; - const teamUserEmail3 = `reassign-bookings-2024-08-13-user3-${randomString()}@api.com`; - let teamUser1: User; - let teamUser2: User; - let teamUser3: User; - let teamUser1ApiKey: string; - let teamUser2ApiKey: string; - - let teamRoundRobinEventTypeId: number; - let teamRoundRobinFixedHostEventTypeId: number; - let teamRoundRobinNonFixedEventTypeId: number; - let teamRoundRobinWithRescheduleReasonEventTypeId: number; - - let teamRoundRobinNonFixedEventTypeTitle: string; - let teamRoundRobinFixedHostEventTypeTitle: string; - let teamRoundRobinWithRescheduleReasonEventTypeTitle: string; - - let roundRobinBooking: Booking; - let rescheduleReasonBookingUid: string; - let rescheduleReasonBookingInitialHostId: number; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], - }) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - - organization = await organizationsRepositoryFixture.create({ - name: `reassign-bookings-2024-08-13-organization-${randomString()}`, - }); - oAuthClient = await createOAuthClient(organization.id); - - team = await teamRepositoryFixture.create({ - name: `reassign-bookings-2024-08-13-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: organization.id } }, - createdByOAuthClient: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - teamUser1 = await userRepositoryFixture.create({ - email: teamUserEmail, - locale: "en", - name: `reassign-bookings-2024-08-13-user1-${randomString()}`, - }); - - teamUser2 = await userRepositoryFixture.create({ - email: teamUserEmail2, - locale: "en", - name: `reassign-bookings-2024-08-13-user2-${randomString()}`, - }); - - teamUser3 = await userRepositoryFixture.create({ - email: teamUserEmail3, - locale: "en", - name: `reassign-bookings-2024-08-13-user3-${randomString()}`, - }); - - const { keyString } = await apiKeysRepositoryFixture.createApiKey(teamUser1.id, null); - teamUser1ApiKey = `cal_test_${keyString}`; - - const { keyString: keyString2 } = await apiKeysRepositoryFixture.createApiKey(teamUser2.id, null); - teamUser2ApiKey = `cal_test_${keyString2}`; - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: `reassign-bookings-2024-08-13-schedule-${randomString()}`, - timeZone: "Europe/Rome", - isDefault: true, - availabilities: [ - { - days: [0, 1, 2, 3, 4, 5, 6], // All days of the week - startTime: new Date(Date.UTC(2024, 0, 1, 0, 0, 0)), // 00:00 UTC - endTime: new Date(Date.UTC(2024, 0, 1, 23, 59, 0)), // 23:59 UTC - }, - ], - }; - await schedulesService.createUserSchedule(teamUser1.id, userSchedule); - await schedulesService.createUserSchedule(teamUser2.id, userSchedule); - await schedulesService.createUserSchedule(teamUser3.id, userSchedule); - - await profileRepositoryFixture.create({ - uid: `usr-${teamUser1.id}`, - username: teamUserEmail, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: teamUser1.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teamUser2.id}`, - username: teamUserEmail2, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: teamUser2.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teamUser3.id}`, - username: teamUserEmail3, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: teamUser3.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - team: { connect: { id: team.id } }, - user: { connect: { id: teamUser1.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - team: { connect: { id: team.id } }, - user: { connect: { id: teamUser2.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - team: { connect: { id: team.id } }, - user: { connect: { id: teamUser3.id } }, - accepted: true, - }); - - const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: team.id }, - }, - users: { - connect: [{ id: teamUser1.id }, { id: teamUser2.id }], - }, - title: `reassign-bookings-2024-08-13-event-type-${randomString()}`, - slug: `reassign-bookings-2024-08-13-event-type-${randomString()}`, - length: 60, - assignAllTeamMembers: false, - bookingFields: [], - locations: [{ type: "inPerson", address: "via 10, rome, italy" }], - }); - - teamRoundRobinEventTypeId = team1EventType.id; - - await hostsRepositoryFixture.create({ - isFixed: false, - user: { - connect: { - id: teamUser1.id, - }, - }, - eventType: { - connect: { - id: team1EventType.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: false, - user: { - connect: { - id: teamUser2.id, - }, - }, - eventType: { - connect: { - id: team1EventType.id, - }, - }, - }); - - roundRobinBooking = await bookingsRepositoryFixture.create({ - user: { - connect: { - id: teamUser1.id, - }, - }, - startTime: new Date(Date.UTC(2050, 0, 7, 13, 0, 0)), - endTime: new Date(Date.UTC(2050, 0, 7, 14, 0, 0)), - title: "round robin coding lets goo", - uid: "round-robin-coding", - eventType: { - connect: { - id: teamRoundRobinEventTypeId, - }, - }, - location: "via 10, rome, italy", - customInputs: {}, - metadata: {}, - responses: { - name: "Bob", - email: "bob@gmail.com", - }, - attendees: { - create: { - email: "bob@gmail.com", - name: "Bob", - locale: "en", - timeZone: "Europe/Rome", - }, - }, - }); - - const teamNonFixedEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: team.id }, - }, - users: { - connect: [{ id: teamUser2.id }, { id: teamUser3.id }], - }, - title: `reassign-bookings-2024-08-13-non-fixed-event-type-${randomString()}`, - slug: `reassign-bookings-2024-08-13-non-fixed-event-type-${randomString()}`, - length: 60, - assignAllTeamMembers: false, - bookingFields: [], - locations: [{ type: "inPerson", address: "via 10, rome, italy" }], - }); - - teamRoundRobinNonFixedEventTypeId = teamNonFixedEventType.id; - teamRoundRobinNonFixedEventTypeTitle = teamNonFixedEventType.title; - - await hostsRepositoryFixture.create({ - isFixed: false, - user: { - connect: { - id: teamUser2.id, - }, - }, - eventType: { - connect: { - id: teamNonFixedEventType.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: false, - user: { - connect: { - id: teamUser3.id, - }, - }, - eventType: { - connect: { - id: teamNonFixedEventType.id, - }, - }, - }); - const team2EventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: team.id }, - }, - users: { - connect: [{ id: teamUser1.id }, { id: teamUser2.id }, { id: teamUser3.id }], - }, - title: `reassign-bookings-2024-08-13-fixed-event-type-${randomString()}`, - slug: `reassign-bookings-2024-08-13-fixed-event-type-${randomString()}`, - length: 60, - assignAllTeamMembers: false, - bookingFields: [], - locations: [{ type: "inPerson", address: "via 10, rome, italy" }], - }); - - teamRoundRobinFixedHostEventTypeId = team2EventType.id; - teamRoundRobinFixedHostEventTypeTitle = team2EventType.title; - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: teamUser1.id, - }, - }, - eventType: { - connect: { - id: team2EventType.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: false, - user: { - connect: { - id: teamUser2.id, - }, - }, - eventType: { - connect: { - id: team2EventType.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: false, - user: { - connect: { - id: teamUser3.id, - }, - }, - eventType: { - connect: { - id: team2EventType.id, - }, - }, - }); - - const teamEventTypeWithRescheduleReason = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: team.id }, - }, - users: { - connect: [{ id: teamUser1.id }, { id: teamUser2.id }], - }, - title: `reassign-bookings-2024-08-13-reschedule-reason-event-type-${randomString()}`, - slug: `reassign-bookings-2024-08-13-reschedule-reason-event-type-${randomString()}`, - length: 60, - assignAllTeamMembers: false, - bookingFields: [ - { - name: "rescheduleReason", - type: "textarea", - defaultLabel: "Reason for rescheduling", - required: true, - sources: [ - { - id: "default", - type: "default", - label: "Default", - }, - ], - editable: "system", - views: [ - { - id: "reschedule", - label: "Reschedule View", - }, - ], - }, - ], - locations: [{ type: "inPerson", address: "via 10, rome, italy" }], - }); - - teamRoundRobinWithRescheduleReasonEventTypeId = teamEventTypeWithRescheduleReason.id; - teamRoundRobinWithRescheduleReasonEventTypeTitle = teamEventTypeWithRescheduleReason.title; - - await hostsRepositoryFixture.create({ - isFixed: false, - user: { - connect: { - id: teamUser1.id, - }, - }, - eventType: { - connect: { - id: teamEventTypeWithRescheduleReason.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: false, - user: { - connect: { - id: teamUser2.id, - }, - }, - eventType: { - connect: { - id: teamEventTypeWithRescheduleReason.id, - }, - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should reassign round robin booking", async () => { - const booking = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid); - expect(booking?.userId).toEqual(teamUser1.id); - - return request(app.getHttpServer()) - .post(`/v2/bookings/${roundRobinBooking.uid}/reassign`) - .set("Authorization", `Bearer ${teamUser1ApiKey}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: ReassignBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data; - expect(data.bookingUid).toEqual(roundRobinBooking.uid); - expect(data.reassignedTo).toEqual({ - id: teamUser2.id, - name: teamUser2.name, - email: teamUser2.email, - displayEmail: teamUser2.email, - }); - - const reassigned = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid); - expect(reassigned?.userId).toEqual(teamUser2.id); - }); - }); - - it("should reassign round robin booking to a specific user", async () => { - const booking = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid); - expect(booking?.userId).toEqual(teamUser2.id); - - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), - eventTypeId: teamRoundRobinEventTypeId, - attendee: { - name: "alice", - email: "alice@gmail.com", - timeZone: "Europe/Madrid", - language: "es", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post(`/v2/bookings/${roundRobinBooking.uid}/reassign/${teamUser1.id}`) - .send(body) - .set("Authorization", `Bearer ${teamUser1ApiKey}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: ReassignBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data; - expect(data.bookingUid).toEqual(roundRobinBooking.uid); - expect(data.reassignedTo).toEqual({ - id: teamUser1.id, - name: teamUser1.name, - email: teamUser1.email, - displayEmail: teamUser1.email, - }); - - const reassigned = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid); - expect(reassigned?.userId).toEqual(teamUser1.id); - }); - }); - - it("should have correct title when reassigning round robin booking with non-fixed host", async () => { - const nonFixedHostBookingBody: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2050, 0, 9, 13, 0, 0)).toISOString(), - eventTypeId: teamRoundRobinNonFixedEventTypeId, - attendee: { - name: "Charlie", - email: "charlie@gmail.com", - timeZone: "Europe/Rome", - language: "en", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - const createResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(nonFixedHostBookingBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const bookingUid = createResponse.body.data.uid; - const booking = await bookingsRepositoryFixture.getByUid(bookingUid); - - expect(booking).toBeDefined(); - expect(booking?.userId).toEqual(teamUser2.id); - - const expectedInitialTitle = `${teamRoundRobinNonFixedEventTypeTitle} between ${teamUser2.name} and Charlie`; - expect(booking?.title).toEqual(expectedInitialTitle); - - return request(app.getHttpServer()) - .post(`/v2/bookings/${bookingUid}/reassign`) - .set("Authorization", `Bearer ${teamUser2ApiKey}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: ReassignBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data; - expect(data.bookingUid).toEqual(bookingUid); - expect(data.reassignedTo.id).toEqual(teamUser3.id); - - const reassigned = await bookingsRepositoryFixture.getByUid(bookingUid); - expect(reassigned?.userId).toEqual(teamUser3.id); - - const expectedReassignedTitle = `${teamRoundRobinNonFixedEventTypeTitle} between ${teamUser3.name} and Charlie`; - expect(reassigned?.title).toEqual(expectedReassignedTitle); - }); - }); - - it("should have correct title when reassigning round robin booking with fixed host", async () => { - const fixedHostBookingBody: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)).toISOString(), - eventTypeId: teamRoundRobinFixedHostEventTypeId, - attendee: { - name: "Alice", - email: "alice@gmail.com", - timeZone: "Europe/Rome", - language: "en", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - const createResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(fixedHostBookingBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const bookingUid = createResponse.body.data.uid; - const booking = await bookingsRepositoryFixture.getByUid(bookingUid); - - expect(booking).toBeDefined(); - expect(booking?.userId).toEqual(teamUser1.id); - - const expectedInitialTitle = `${teamRoundRobinFixedHostEventTypeTitle} between ${team.name} and Alice`; - expect(booking?.title).toEqual(expectedInitialTitle); - - return request(app.getHttpServer()) - .post(`/v2/bookings/${bookingUid}/reassign/${teamUser3.id}`) - .set("Authorization", `Bearer ${teamUser1ApiKey}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: ReassignBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data; - expect(data.bookingUid).toEqual(bookingUid); - expect(data.reassignedTo.id).toEqual(teamUser1.id); - - const reassigned = await bookingsRepositoryFixture.getByUid(bookingUid); - expect(reassigned?.userId).toEqual(teamUser1.id); - - const expectedReassignedTitle = `${teamRoundRobinFixedHostEventTypeTitle} between ${team.name} and Alice`; - expect(reassigned?.title).toEqual(expectedReassignedTitle); - }); - }); - - it("should preserve attendee name when reassigning round robin host manually with rescheduleReason required", async () => { - const bookingBody: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2050, 0, 10, 13, 0, 0)).toISOString(), - eventTypeId: teamRoundRobinWithRescheduleReasonEventTypeId, - attendee: { - name: "David", - email: "david@gmail.com", - timeZone: "Europe/Rome", - language: "en", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - const createResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(bookingBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const bookingUid = createResponse.body.data.uid; - rescheduleReasonBookingUid = bookingUid; - const booking = await bookingsRepositoryFixture.getByUid(bookingUid); - - expect(booking).toBeDefined(); - expect(booking?.userId).toBeDefined(); - const initialHostId = booking?.userId!; - rescheduleReasonBookingInitialHostId = initialHostId; - - const expectedInitialTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${ - booking?.userId === teamUser1.id ? teamUser1.name : teamUser2.name - } and David`; - expect(booking?.title).toEqual(expectedInitialTitle); - expect(booking?.title).not.toContain("Nameless"); - - const reassignToHostId = initialHostId === teamUser1.id ? teamUser2.id : teamUser1.id; - const reassignToHostName = reassignToHostId === teamUser1.id ? teamUser1.name : teamUser2.name; - - return request(app.getHttpServer()) - .post(`/v2/bookings/${bookingUid}/reassign/${reassignToHostId}`) - .set("Authorization", `Bearer ${teamUser1ApiKey}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: ReassignBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data; - expect(data.bookingUid).toEqual(bookingUid); - expect(data.reassignedTo.id).toEqual(reassignToHostId); - - const reassigned = await bookingsRepositoryFixture.getByUid(bookingUid); - expect(reassigned?.userId).toEqual(reassignToHostId); - - const expectedReassignedTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${reassignToHostName} and David`; - expect(reassigned?.title).toEqual(expectedReassignedTitle); - expect(reassigned?.title).not.toContain("Nameless"); - }); - }); - - it("should preserve attendee name when reassigning round robin host automatically with rescheduleReason required", async () => { - const bookingUid = rescheduleReasonBookingUid; - const booking = await bookingsRepositoryFixture.getByUid(bookingUid); - - expect(booking).toBeDefined(); - expect(booking?.userId).toBeDefined(); - - const currentHostId = booking?.userId!; - const initialHostId = rescheduleReasonBookingInitialHostId; - const initialHostName = initialHostId === teamUser1.id ? teamUser1.name : teamUser2.name; - - expect(currentHostId).not.toEqual(initialHostId); - - return request(app.getHttpServer()) - .post(`/v2/bookings/${bookingUid}/reassign`) - .set("Authorization", `Bearer ${teamUser1ApiKey}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: ReassignBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data; - expect(data.bookingUid).toEqual(bookingUid); - expect(data.reassignedTo.id).toEqual(initialHostId); - - const autoReassigned = await bookingsRepositoryFixture.getByUid(bookingUid); - expect(autoReassigned?.userId).toEqual(initialHostId); - - const expectedAutoReassignedTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${initialHostName} and David`; - expect(autoReassigned?.title).toEqual(expectedAutoReassignedTitle); - expect(autoReassigned?.title).not.toContain("Nameless"); - }); - }); - - it("should return 403 when unauthorized user tries to reassign booking", async () => { - const unauthorizedUserEmail = `fake-user-${randomString()}@api.com`; - const unauthorizedUser = await userRepositoryFixture.create({ - email: unauthorizedUserEmail, - locale: "en", - name: `fake-user-${randomString()}`, - }); - - const { keyString } = await apiKeysRepositoryFixture.createApiKey(unauthorizedUser.id, null); - const unauthorizedApiKeyString = `cal_test_${keyString}`; - - const response = await request(app.getHttpServer()) - .post(`/v2/bookings/${roundRobinBooking.uid}/reassign`) - .set("Authorization", `Bearer ${unauthorizedApiKeyString}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(403); - - expect(response.body.error.message).toBe(BOOKING_REASSIGN_PERMISSION_ERROR); - - await userRepositoryFixture.deleteByEmail(unauthorizedUserEmail); - }); - - it("should return 403 when unauthorized user tries to reassign booking to specific user", async () => { - const unauthorizedUserEmail = `fake-user-${randomString()}@api.com`; - const unauthorizedUser = await userRepositoryFixture.create({ - email: unauthorizedUserEmail, - locale: "en", - name: `fake-user-${randomString()}`, - }); - - const { keyString } = await apiKeysRepositoryFixture.createApiKey(unauthorizedUser.id, null); - const unauthorizedApiKeyString = `cal_test_${keyString}`; - - const response = await request(app.getHttpServer()) - .post(`/v2/bookings/${roundRobinBooking.uid}/reassign/${teamUser2.id}`) - .send({ reason: "Testing unauthorized access" }) - .set("Authorization", `Bearer ${unauthorizedApiKeyString}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(403); - - expect(response.body.error.message).toBe(BOOKING_REASSIGN_PERMISSION_ERROR); - - await userRepositoryFixture.deleteByEmail(unauthorizedUserEmail); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: ["http://localhost:5555"], - permissions: 32, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - afterAll(async () => { - await oauthClientRepositoryFixture.delete(oAuthClient.id); - await teamRepositoryFixture.delete(organization.id); - await userRepositoryFixture.deleteByEmail(teamUser1.email); - await userRepositoryFixture.deleteByEmail(teamUserEmail2); - await userRepositoryFixture.deleteByEmail(teamUserEmail3); - await bookingsRepositoryFixture.deleteAllBookings(teamUser1.id, teamUser1.email); - await bookingsRepositoryFixture.deleteAllBookings(teamUser2.id, teamUser2.email); - await bookingsRepositoryFixture.deleteAllBookings(teamUser3.id, teamUser3.email); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/team-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/team-bookings.e2e-spec.ts deleted file mode 100644 index 4eb60ae073f780..00000000000000 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/team-bookings.e2e-spec.ts +++ /dev/null @@ -1,1131 +0,0 @@ -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 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"; -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 { 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 { 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", () => { - let app: INestApplication; - const organizationSlug = `team-bookings-organization-${randomString()}`; - let organization: Team; - const team1Slug = `team-bookings-team1-${randomString()}`; - const team2Slug = `team-bookings-team2-${randomString()}`; - let team1: Team; - let team2: Team; - - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let oAuthClient: PlatformOAuthClient; - let teamRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let hostsRepositoryFixture: HostsRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - - const teamUserEmail = `team-bookings-user1-${randomString()}@api.com`; - const teamUserEmail2 = `team-bookings-user2-${randomString()}@api.com`; - let teamUser: User; - let teamUser2: User; - - let team1EventTypeId: number; - let team2EventTypeId: number; - let team2RREventTypeId: number; - let phoneOnlyEventTypeId: number; - let collectiveEventWithoutHostsId: number; - let roundRobinEventWithoutHostsId: number; - - const team1EventTypeSlug = `team-bookings-event-type-${randomString()}`; - const team2EventTypeSlug = `team-bookings-event-type-${randomString()}`; - const team2RREventTypeSlug = `team-bookings-rr-event-type-${randomString()}`; - const phoneOnlyEventTypeSlug = `team-bookings-event-type-${randomString()}`; - - let phoneBasedBooking: BookingOutput_2024_08_13; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - teamUserEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - - organization = await organizationsRepositoryFixture.create({ - name: organizationSlug, - slug: organizationSlug, - isOrganization: true, - }); - oAuthClient = await createOAuthClient(organization.id); - - team1 = await teamRepositoryFixture.create({ - name: team1Slug, - slug: team1Slug, - isOrganization: false, - parent: { connect: { id: organization.id } }, - createdByOAuthClient: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - team2 = await teamRepositoryFixture.create({ - name: team2Slug, - slug: team2Slug, - isOrganization: false, - parent: { connect: { id: organization.id } }, - createdByOAuthClient: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - teamUser = await userRepositoryFixture.create({ - email: teamUserEmail, - locale: "it", - name: "orgUser1team1", - platformOAuthClients: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - teamUser2 = await userRepositoryFixture.create({ - email: teamUserEmail2, - locale: "es", - name: "orgUser2team1", - platformOAuthClients: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: `team-bookings-2024-08-13-schedule-${randomString()}`, - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(teamUser.id, userSchedule); - await schedulesService.createUserSchedule(teamUser2.id, userSchedule); - - await profileRepositoryFixture.create({ - uid: `usr-${teamUser.id}`, - username: teamUserEmail, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: teamUser.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teamUser2.id}`, - username: teamUserEmail2, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: teamUser2.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teamUser.id } }, - team: { connect: { id: team1.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teamUser.id } }, - team: { connect: { id: team2.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teamUser2.id } }, - team: { connect: { id: team2.id } }, - accepted: true, - }); - - const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team1.id }, - }, - title: `team-bookings-2024-08-13-event-type-${randomString()}`, - slug: team1EventTypeSlug, - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - team1EventTypeId = team1EventType.id; - - const team1CollectiveEventTypeWithoutHosts = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team1.id }, - }, - title: `team-bookings-2024-08-13-collective-event-type-without-hosts-${randomString()}`, - slug: `team-bookings-2024-08-13-collective-event-type-without-hosts-${randomString()}`, - length: 60, - }); - collectiveEventWithoutHostsId = team1CollectiveEventTypeWithoutHosts.id; - - const team1RoundRobinEventTypeWithoutHosts = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: team1.id }, - }, - title: `team-bookings-2024-08-13-round-robin-event-type-without-hosts-${randomString()}`, - slug: `team-bookings-2024-08-13-round-robin-event-type-without-hosts-${randomString()}`, - length: 60, - }); - roundRobinEventWithoutHostsId = team1RoundRobinEventTypeWithoutHosts.id; - - const phoneOnlyEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: team1.id }, - }, - title: `team-bookings-2024-08-13-event-type-${randomString()}`, - slug: phoneOnlyEventTypeSlug, - length: 15, - assignAllTeamMembers: false, - hosts: { - connectOrCreate: [ - { - where: { - userId_eventTypeId: { - userId: teamUser.id, - eventTypeId: team1EventTypeId, - }, - }, - create: { - userId: teamUser.id, - isFixed: true, - }, - }, - ], - }, - bookingFields: [ - { - name: "name", - type: "name", - label: "your name", - sources: [{ id: "default", type: "default", label: "Default" }], - variant: "fullName", - editable: "system", - required: true, - defaultLabel: "your_name", - variantsConfig: { - variants: { - fullName: { - fields: [{ name: "fullName", type: "text", label: "your name", required: true }], - }, - }, - }, - }, - { - name: "email", - type: "email", - label: "your email", - sources: [{ id: "default", type: "default", label: "Default" }], - editable: "system", - required: false, - defaultLabel: "email_address", - }, - { - name: "attendeePhoneNumber", - type: "phone", - label: "phone_number", - sources: [{ id: "user", type: "user", label: "User", fieldRequired: true }], - editable: "user", - required: true, - placeholder: "", - }, - { - name: "rescheduleReason", - type: "textarea", - views: [{ id: "reschedule", label: "Reschedule View" }], - sources: [{ id: "default", type: "default", label: "Default" }], - editable: "system-but-optional", - required: false, - defaultLabel: "reason_for_reschedule", - defaultPlaceholder: "reschedule_placeholder", - }, - ], - locations: [], - }); - - phoneOnlyEventTypeId = phoneOnlyEventType.id; - - const team2EventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team2.id }, - }, - title: `team-bookings-2024-08-13-event-type-${randomString()}`, - slug: team2EventTypeSlug, - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - team2EventTypeId = team2EventType.id; - - const team2RREventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: team2.id }, - }, - title: `team-bookings-2024-08-13-event-type-rr-${randomString()}`, - slug: team2RREventTypeSlug, - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - rrHostSubsetEnabled: true, - }); - - team2RREventTypeId = team2RREventType.id; - - await hostsRepositoryFixture.create({ - isFixed: false, - user: { - connect: { - id: teamUser2.id, - }, - }, - eventType: { - connect: { - id: team2RREventType.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: teamUser.id, - }, - }, - eventType: { - connect: { - id: team2RREventType.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: teamUser.id, - }, - }, - eventType: { - connect: { - id: team1EventType.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: teamUser.id, - }, - }, - eventType: { - connect: { - id: team2EventType.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: teamUser2.id, - }, - }, - eventType: { - connect: { - id: team2EventType.id, - }, - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - describe("cant book event types without hosts", () => { - it("should fail to book a collective event type without hosts", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), - eventTypeId: collectiveEventWithoutHostsId, - attendee: { - name: "alice", - timeZone: "Europe/Madrid", - email: "alice@gmail.com", - }, - }; - - const response = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(response.status).toBe(422); - expect(response.body.error.message).toBe( - `Can't book this team event type because it has no hosts. Please, add at least 1 host to event type with id=${collectiveEventWithoutHostsId} belonging to team with id=${team1.id} and try again.` - ); - }); - - it("should fail to book a round robin event type without hosts", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), - eventTypeId: roundRobinEventWithoutHostsId, - attendee: { - name: "alice", - timeZone: "Europe/Madrid", - email: "alice@gmail.com", - }, - }; - - const response = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13); - - expect(response.status).toBe(422); - expect(response.body.error.message).toBe( - `Can't book this team event type because it has no hosts. Please, add at least 1 host to event type with id=${roundRobinEventWithoutHostsId} belonging to team with id=${team1.id} and try again.` - ); - }); - }); - - describe("create team bookings", () => { - it("should create a team 1 booking", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), - eventTypeId: team1EventTypeId, - attendee: { - name: "alice", - email: "alice@gmail.com", - timeZone: "Europe/Madrid", - language: "es", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(team1EventTypeId); - expect(data.attendees.length).toEqual(1); - 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, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - - it("should create a phone based booking", async () => { - const phoneNumber = "+919876543210"; - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(), - eventTypeId: phoneOnlyEventTypeId, - attendee: { - name: "alice", - phoneNumber, - timeZone: "Europe/Madrid", - language: "es", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 15, 15, 0)).toISOString()); - expect(data.duration).toEqual(15); - expect(data.eventTypeId).toEqual(phoneOnlyEventTypeId); - expect(data.attendees.length).toEqual(1); - 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, - absent: false, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(phoneNumber); - phoneBasedBooking = data; - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - - it("should create a team 2 booking", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(), - eventTypeId: team2EventTypeId, - attendee: { - name: "bob", - email: "bob@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 11, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(team2EventTypeId); - expect(data.attendees.length).toEqual(2); - 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, - }); - expect(data.attendees[1]).toEqual({ - name: teamUser2.name, - email: teamUser2.email, - displayEmail: teamUser2.email, - timeZone: teamUser2.timeZone, - language: teamUser2.locale, - absent: false, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - - it("should create a team 2 RR booking and use rrHostSubsetIds to force teamUser2 as host ", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 12, 0, 0)).toISOString(), - eventTypeId: team2RREventTypeId, - attendee: { - name: "bob2", - email: "bob2@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - rrHostSubsetIds: [teamUser2.id], - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser2.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(team2RREventTypeId); - expect(data.attendees.length).toEqual(1); - 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, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - - it("should create a team 2 RR booking and use rrHostSubsetIds to force teamUser as host ", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 12, 0, 0)).toISOString(), - eventTypeId: team2RREventTypeId, - attendee: { - name: "bob", - email: "bob@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - rrHostSubsetIds: [teamUser.id], - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(team2RREventTypeId); - expect(data.attendees.length).toEqual(1); - 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, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - - it("should create a team 2 RR booking and use rrHostSubsetIds to force teamUser and teamUser2 as host ", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(), - eventTypeId: team2RREventTypeId, - attendee: { - name: "bob", - email: "bob@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - rrHostSubsetIds: [teamUser.id, teamUser2.id], - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(team2RREventTypeId); - expect(data.attendees.length).toEqual(2); - expect(data.attendees.find((a) => a.email === body.attendee.email)).toBeDefined(); - expect(data.attendees.find((a) => a.email === teamUser2.email)).toBeDefined(); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - }); - - describe("get team bookings", () => { - it("should get bookings by teamId", async () => { - return request(app.getHttpServer()) - .get(`/v2/bookings?teamId=${team1.id}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(2); - expect(data[0].eventTypeId).toEqual(team1EventTypeId); - }); - }); - - it("should get bookings by teamId", async () => { - return request(app.getHttpServer()) - .get(`/v2/bookings?teamId=${team2.id}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(3); - expect(data[0].eventTypeId).toEqual(team2EventTypeId); - }); - }); - - it("should get bookings by teamId and eventTypeId", async () => { - return request(app.getHttpServer()) - .get(`/v2/bookings?teamId=${team2.id}&eventTypeId=${team2EventTypeId}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].eventTypeId).toEqual(team2EventTypeId); - }); - }); - - it("should not get bookings by teamId and non existing eventTypeId", async () => { - return request(app.getHttpServer()) - .get(`/v2/bookings?teamId=${team2.id}&eventTypeId=90909`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(400); - }); - - it("should get bookings by teamIds", async () => { - return request(app.getHttpServer()) - .get(`/v2/bookings?teamIds=${team1.id},${team2.id}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(5); - expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined(); - expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined(); - }); - }); - }); - - describe("reschedule", () => { - it("should reschedule phone based booking", async () => { - const body: RescheduleBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(), - reschedulingReason: "Flying to mars that day", - }; - - return request(app.getHttpServer()) - .post(`/v2/bookings/${phoneBasedBooking.uid}/reschedule`) - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: RescheduleBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 14, 15, 0)).toISOString()); - expect(data.duration).toEqual(15); - expect(data.eventTypeId).toEqual(phoneOnlyEventTypeId); - expect(data.attendees.length).toEqual(1); - 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, - absent: false, - }); - expect(data.meetingUrl).toEqual(phoneBasedBooking.meetingUrl); - expect(data.absentHost).toEqual(false); - expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual( - phoneBasedBooking.bookingFieldsResponses.attendeePhoneNumber - ); - phoneBasedBooking = data; - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - - it("should reschedule a team RR booking and use rrHostSubsetIds to force teamUser2 as host", async () => { - const createBody: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 10, 10, 0, 0)).toISOString(), - eventTypeId: team2RREventTypeId, - attendee: { - name: "reschedule-test", - email: "reschedule-test@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - rrHostSubsetIds: [teamUser.id], - }; - - const createResponse = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(createBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const createResponseBody: CreateBookingOutput_2024_08_13 = createResponse.body; - expect(responseDataIsBooking(createResponseBody.data)).toBe(true); - - if (!responseDataIsBooking(createResponseBody.data)) { - throw new Error("Invalid response data - expected booking"); - } - - const createdBooking: BookingOutput_2024_08_13 = createResponseBody.data; - expect(createdBooking.hosts[0].id).toEqual(teamUser.id); - - const rescheduleBody: RescheduleBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 10, 12, 0, 0)).toISOString(), - reschedulingReason: "Need to change host", - rrHostSubsetIds: [teamUser2.id], - }; - - return request(app.getHttpServer()) - .post(`/v2/bookings/${createdBooking.uid}/reschedule`) - .send(rescheduleBody) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: RescheduleBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser2.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(rescheduleBody.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 10, 13, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(team2RREventTypeId); - expect(data.attendees.length).toEqual(1); - expect(data.attendees[0].email).toEqual(createBody.attendee.email); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - }); - - describe("book using teamSlug, eventTypeSlug and organizationSlug", () => { - it("should not be able to book if missing organizationSlug", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(), - teamSlug: team1Slug, - eventTypeSlug: team1EventTypeSlug, - attendee: { - name: "alice", - email: "alice@gmail.com", - timeZone: "Europe/Madrid", - language: "es", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(404); - }); - - it("should book using teamSlug and eventTypeSlug and organizationSlug", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(), - teamSlug: team1Slug, - eventTypeSlug: team1EventTypeSlug, - organizationSlug: organizationSlug, - attendee: { - name: "alice", - email: "alice@gmail.com", - timeZone: "Europe/Madrid", - language: "es", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(team1EventTypeId); - expect(data.attendees.length).toEqual(1); - 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, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: ["http://localhost:5555"], - permissions: 32, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 { - return !Array.isArray(data) && typeof data === "object" && data !== null && data && "id" in data; - } - - afterAll(async () => { - await oauthClientRepositoryFixture.delete(oAuthClient.id); - await teamRepositoryFixture.delete(organization.id); - await userRepositoryFixture.deleteByEmail(teamUser.email); - await userRepositoryFixture.deleteByEmail(teamUserEmail2); - await bookingsRepositoryFixture.deleteAllBookings(teamUser.id, teamUser.email); - await bookingsRepositoryFixture.deleteAllBookings(teamUser2.id, teamUser2.email); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller.ts b/apps/api/v2/src/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller.ts deleted file mode 100644 index b801d4926e0cde..00000000000000 --- a/apps/api/v2/src/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { - CreatePrivateLinkInput, - CreatePrivateLinkOutput, - DeletePrivateLinkOutput, - GetPrivateLinksOutput, - UpdatePrivateLinkInput, - UpdatePrivateLinkOutput, -} from "@calcom/platform-types"; - -import { PrivateLinksService } from "../services/private-links.service"; - -@Controller({ - path: "/v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links", - version: API_VERSIONS_VALUES, -}) -@DocsTags("Orgs / Teams / Event Types / Private Links") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -@ApiParam({ name: "orgId", type: Number, required: true }) -export class OrganizationsEventTypesPrivateLinksController { - constructor( - private readonly privateLinksService: PrivateLinksService, - private readonly teamsEventTypesService: TeamsEventTypesService - ) {} - - @Post("/") - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @ApiOperation({ summary: "Create a private link for a team event type" }) - async createPrivateLink( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("eventTypeId", ParseIntPipe) eventTypeId: number, - @Body() body: CreatePrivateLinkInput - ): Promise { - await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId); - // Use teamId as the seed for link generation in org/team context - const privateLink = await this.privateLinksService.createPrivateLink(eventTypeId, teamId, body); - return { - status: SUCCESS_STATUS, - data: privateLink, - }; - } - - @Get("/") - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @ApiOperation({ summary: "Get all private links for a team event type" }) - async getPrivateLinks( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("eventTypeId", ParseIntPipe) eventTypeId: number - ): Promise { - await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId); - const privateLinks = await this.privateLinksService.getPrivateLinks(eventTypeId); - return { - status: SUCCESS_STATUS, - data: privateLinks, - }; - } - - @Patch("/:linkId") - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @ApiOperation({ summary: "Update a private link for a team event type" }) - async updatePrivateLink( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("eventTypeId", ParseIntPipe) eventTypeId: number, - @Param("linkId") linkId: string, - @Body() body: Omit - ): Promise { - await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId); - const updateInput: UpdatePrivateLinkInput = { ...body, linkId }; - const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, updateInput); - return { - status: SUCCESS_STATUS, - data: privateLink, - }; - } - - @Delete("/:linkId") - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @ApiOperation({ summary: "Delete a private link for a team event type" }) - async deletePrivateLink( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("eventTypeId", ParseIntPipe) eventTypeId: number, - @Param("linkId") linkId: string - ): Promise { - await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId); - await this.privateLinksService.deletePrivateLink(eventTypeId, linkId); - return { - status: SUCCESS_STATUS, - data: { - linkId, - message: "Private link deleted successfully", - }, - }; - } -} diff --git a/apps/api/v2/src/ee/platform-endpoints-module.ts b/apps/api/v2/src/ee/platform-endpoints-module.ts deleted file mode 100644 index 11a14630836653..00000000000000 --- a/apps/api/v2/src/ee/platform-endpoints-module.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { BookingsModule_2024_04_15 } from "@/ee/bookings/2024-04-15/bookings.module"; -import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module"; -import { CalendarsModule } from "@/ee/calendars/calendars.module"; -import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module"; -import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; -import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; -import { GcalModule } from "@/ee/gcal/gcal.module"; -import { MeModule } from "@/ee/me/me.module"; -import { ProviderModule } from "@/ee/provider/provider.module"; -import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; -import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module"; -import { RoutingFormsModule } from "@/modules/routing-forms/routing-forms.module"; -import { SlotsModule_2024_04_15 } from "@/modules/slots/slots-2024-04-15/slots.module"; -import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module"; -import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; -import { TeamsInviteModule } from "@/modules/teams/invite/teams-invite.module"; -import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module"; -import { TeamsModule } from "@/modules/teams/teams/teams.module"; -import type { MiddlewareConsumer, NestModule } from "@nestjs/common"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [ - GcalModule, - ProviderModule, - SchedulesModule_2024_04_15, - SchedulesModule_2024_06_11, - TeamsEventTypesModule, - MeModule, - EventTypesModule_2024_04_15, - EventTypesModule_2024_06_14, - CalendarsModule, - BookingsModule_2024_04_15, - BookingsModule_2024_08_13, - TeamsMembershipsModule, - TeamsInviteModule, - SlotsModule_2024_04_15, - SlotsModule_2024_09_04, - TeamsModule, - RoutingFormsModule, - EventTypesPrivateLinksModule, - ], -}) -export class PlatformEndpointsModule implements NestModule { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - configure(_consumer: MiddlewareConsumer) { - // TODO: apply ratelimits - } -} diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.module.ts b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.module.ts deleted file mode 100644 index 47aad9a7b88f14..00000000000000 --- a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; -import { SchedulesController_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/controllers/schedules.controller"; -import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository"; -import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service"; -import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service"; -import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [PrismaModule, UsersModule, TokensModule, EventTypesModule_2024_06_14], - providers: [ - SchedulesRepository_2024_06_11, - SchedulesService_2024_06_11, - InputSchedulesService_2024_06_11, - OutputSchedulesService_2024_06_11, - ], - controllers: [SchedulesController_2024_06_11], - exports: [SchedulesService_2024_06_11, SchedulesRepository_2024_06_11, OutputSchedulesService_2024_06_11], -}) -export class SchedulesModule_2024_06_11 {} diff --git a/apps/api/v2/src/lib/modules/available-slots.module.ts b/apps/api/v2/src/lib/modules/available-slots.module.ts index 5fbfc6413b72ed..3ce062813ecadd 100644 --- a/apps/api/v2/src/lib/modules/available-slots.module.ts +++ b/apps/api/v2/src/lib/modules/available-slots.module.ts @@ -1,25 +1,21 @@ +import { Module } from "@nestjs/common"; import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository"; import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository"; import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; import { PrismaHolidayRepository } from "@/lib/repositories/prisma-holiday.repository"; import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository"; import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository"; -import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository"; import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository"; import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected-slot.repository"; -import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository"; import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"; import { AvailableSlotsService } from "@/lib/services/available-slots.service"; import { BusyTimesService } from "@/lib/services/busy-times.service"; import { 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"; import { RedisService } from "@/modules/redis/redis.service"; -import { Module } from "@nestjs/common"; @Module({ imports: [PrismaWorkerModule], @@ -31,19 +27,15 @@ import { Module } from "@nestjs/common"; PrismaSelectedSlotRepository, PrismaUserRepository, PrismaEventTypeRepository, - PrismaRoutingFormResponseRepository, - PrismaTeamRepository, RedisService, PrismaFeaturesRepository, PrismaMembershipRepository, CheckBookingLimitsService, + QualifiedHostsService, AvailableSlotsService, UserAvailabilityService, BusyTimesService, - FilterHostsService, - QualifiedHostsService, NoSlotsNotificationService, - OrgMembershipLookupService, ], exports: [AvailableSlotsService], }) diff --git a/apps/api/v2/src/lib/modules/booking-attendees.module.ts b/apps/api/v2/src/lib/modules/booking-attendees.module.ts index 9d5482a380afc0..78c9f7d814aaf0 100644 --- a/apps/api/v2/src/lib/modules/booking-attendees.module.ts +++ b/apps/api/v2/src/lib/modules/booking-attendees.module.ts @@ -1,18 +1,15 @@ -import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.module"; import { PrismaBookingAttendeeRepository } from "@/lib/repositories/prisma-booking-attendee.repository"; import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository"; -import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; import { BookingAttendeesRemoveService } from "@/lib/services/booking-attendees-remove.service"; import { BookingAttendeesService } from "@/lib/services/booking-attendees.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, BookingEventHandlerModule], + imports: [PrismaModule], providers: [ PrismaBookingRepository, PrismaBookingAttendeeRepository, - PrismaFeaturesRepository, BookingAttendeesRemoveService, BookingAttendeesService, ], diff --git a/apps/api/v2/src/lib/modules/booking-event-handler.module.ts b/apps/api/v2/src/lib/modules/booking-event-handler.module.ts index d9092ee02ec0e5..bc212d0f4c97c2 100644 --- a/apps/api/v2/src/lib/modules/booking-event-handler.module.ts +++ b/apps/api/v2/src/lib/modules/booking-event-handler.module.ts @@ -1,9 +1,8 @@ +import { Module, Scope } from "@nestjs/common"; import { Logger } from "@/lib/logger.bridge"; -import { BookingAuditProducerService } from "@/lib/services/booking-audit-producer.service"; import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service"; import { HashedLinkService } from "@/lib/services/hashed-link.service"; import { TaskerService } from "@/lib/services/tasker.service"; -import { Module, Scope } from "@nestjs/common"; @Module({ providers: [ @@ -16,7 +15,6 @@ import { Module, Scope } from "@nestjs/common"; }, TaskerService, HashedLinkService, - BookingAuditProducerService, BookingEventHandlerService, ], exports: [BookingEventHandlerService], diff --git a/apps/api/v2/src/lib/modules/instant-booking.module.ts b/apps/api/v2/src/lib/modules/instant-booking.module.ts deleted file mode 100644 index e7b3e652448dff..00000000000000 --- a/apps/api/v2/src/lib/modules/instant-booking.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { InstantBookingCreateService } from "@/lib/services/instant-booking-create.service"; -import { Module } from "@nestjs/common"; - -@Module({ - providers: [InstantBookingCreateService], - exports: [InstantBookingCreateService], -}) -export class InstantBookingModule {} diff --git a/apps/api/v2/src/lib/modules/oauth.module.ts b/apps/api/v2/src/lib/modules/oauth.module.ts index 9434cdb6225fba..0c717efd3808e9 100644 --- a/apps/api/v2/src/lib/modules/oauth.module.ts +++ b/apps/api/v2/src/lib/modules/oauth.module.ts @@ -1,13 +1,12 @@ +import { Module } from "@nestjs/common"; import { PrismaAccessCodeRepository } from "@/lib/repositories/prisma-access-code.repository"; import { PrismaOAuthClientRepository } from "@/lib/repositories/prisma-oauth-client.repository"; -import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository"; import { OAuthService } from "@/lib/services/oauth.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { Module } from "@nestjs/common"; @Module({ imports: [PrismaModule], - providers: [PrismaAccessCodeRepository, PrismaOAuthClientRepository, PrismaTeamRepository, OAuthService], + providers: [PrismaAccessCodeRepository, PrismaOAuthClientRepository, OAuthService], exports: [OAuthService], }) export class oAuthServiceModule {} diff --git a/apps/api/v2/src/lib/modules/platform-billing-tasker.module.ts b/apps/api/v2/src/lib/modules/platform-billing-tasker.module.ts deleted file mode 100644 index eedbfec61aaada..00000000000000 --- a/apps/api/v2/src/lib/modules/platform-billing-tasker.module.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Module, Scope } from "@nestjs/common"; -import { Logger } from "@/lib/logger.bridge"; -import { PrismaPlatformBillingRepository } from "@/lib/repositories/prisma-platform-billing.repository"; -import { StripeBillingProviderService } from "@/lib/services/stripe-billing-provider.service"; -import { PlatformBillingSyncTaskerService } from "@/lib/services/tasker/platform-billing-sync-tasker.service"; -import { PlatformBillingTaskService } from "@/lib/services/tasker/platform-billing-task.service"; -import { PlatformBillingTasker } from "@/lib/services/tasker/platform-billing-tasker.service"; -import { PlatformBillingTriggerTaskerService } from "@/lib/services/tasker/platform-billing-trigger-tasker.service"; -import { OrganizationsModule } from "@/modules/organizations/organizations.module"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; - -@Module({ - imports: [PrismaModule, StripeModule, OrganizationsModule], - providers: [ - PrismaPlatformBillingRepository, - StripeBillingProviderService, - { - provide: Logger, - useFactory: () => { - return new Logger(); - }, - scope: Scope.TRANSIENT, - }, - PlatformBillingTaskService, - PlatformBillingSyncTaskerService, - PlatformBillingTriggerTaskerService, - PlatformBillingTasker, - ], - exports: [PlatformBillingTasker], -}) -export class PlatformBillingTaskerModule {} diff --git a/apps/api/v2/src/lib/modules/recurring-booking.module.ts b/apps/api/v2/src/lib/modules/recurring-booking.module.ts index 9a0a8614a49da7..52b25fded56e6a 100644 --- a/apps/api/v2/src/lib/modules/recurring-booking.module.ts +++ b/apps/api/v2/src/lib/modules/recurring-booking.module.ts @@ -1,13 +1,11 @@ +import { Module, Scope } from "@nestjs/common"; +import { Logger } from "@/lib/logger.bridge"; import { RegularBookingModule } from "@/lib/modules/regular-booking.module"; -import { RecurringBookingService } from "@/lib/services/recurring-booking.service"; -import { Module } from "@nestjs/common"; +import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; 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 { RecurringBookingService } from "@/lib/services/recurring-booking.service"; import { TaskerService } from "@/lib/services/tasker.service"; -import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; import { PrismaWorkerModule } from "@/modules/prisma/prisma-worker.module"; @Module({ imports: [RegularBookingModule, PrismaWorkerModule], @@ -23,7 +21,6 @@ import { PrismaWorkerModule } from "@/modules/prisma/prisma-worker.module"; }, scope: Scope.TRANSIENT, }, - BookingAuditProducerService, TaskerService, /** Required by BookingEventHandlerService - Ends **/ /** Required by RecurringBookingService **/ @@ -31,4 +28,4 @@ import { PrismaWorkerModule } from "@/modules/prisma/prisma-worker.module"; ], exports: [RecurringBookingService], }) -export class RecurringBookingModule { } +export class RecurringBookingModule {} diff --git a/apps/api/v2/src/lib/modules/regular-booking.module.ts b/apps/api/v2/src/lib/modules/regular-booking.module.ts index 70ffb75f032802..6a2cc1ec65cc25 100644 --- a/apps/api/v2/src/lib/modules/regular-booking.module.ts +++ b/apps/api/v2/src/lib/modules/regular-booking.module.ts @@ -1,11 +1,12 @@ +import { getWebhookProducer } from "@calcom/platform-libraries/bookings"; +import { Module, Scope } from "@nestjs/common"; +import { WEBHOOK_PRODUCER } from "./regular-booking.tokens"; import { Logger } from "@/lib/logger.bridge"; -import { PrismaAttributeRepository } from "@/lib/repositories/prisma-attribute.repository"; import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository"; import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository"; import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository"; import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"; -import { BookingAuditProducerService } from "@/lib/services/booking-audit-producer.service"; import { BookingEmailSmsService } from "@/lib/services/booking-emails-sms-service"; import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service"; import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service"; @@ -19,16 +20,10 @@ import { BookingEmailAndSmsTasker } from "@/lib/services/tasker/booking-emails-s import { BookingEmailAndSmsTriggerTaskerService } from "@/lib/services/tasker/booking-emails-sms-trigger-tasker.service"; import { TaskerService } from "@/lib/services/tasker.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { Module, Scope } from "@nestjs/common"; - -import { getWebhookProducer } from "@calcom/platform-libraries/bookings"; - -import { WEBHOOK_PRODUCER } from "./regular-booking.tokens"; @Module({ imports: [PrismaModule], providers: [ - PrismaAttributeRepository, PrismaBookingRepository, PrismaFeaturesRepository, PrismaHostRepository, @@ -45,7 +40,6 @@ import { WEBHOOK_PRODUCER } from "./regular-booking.tokens"; provide: WEBHOOK_PRODUCER, useFactory: () => getWebhookProducer(), }, - BookingAuditProducerService, BookingEventHandlerService, CheckBookingAndDurationLimitsService, CheckBookingLimitsService, diff --git a/apps/api/v2/src/lib/modules/regular-booking.tokens.ts b/apps/api/v2/src/lib/modules/regular-booking.tokens.ts index 8ce416cf2aec47..7bb51d78a023cb 100644 --- a/apps/api/v2/src/lib/modules/regular-booking.tokens.ts +++ b/apps/api/v2/src/lib/modules/regular-booking.tokens.ts @@ -1,6 +1,6 @@ /** * Injection token for IWebhookProducerService. - * Used to bridge the Cal.com DI container into NestJS. + * Used to bridge the Cal.diy DI container into NestJS. * * Defined in a separate file to avoid a circular import between * regular-booking.module.ts and regular-booking.service.ts. diff --git a/apps/api/v2/src/lib/repositories/prisma-attribute.repository.ts b/apps/api/v2/src/lib/repositories/prisma-attribute.repository.ts deleted file mode 100644 index 55e0110bb57b87..00000000000000 --- a/apps/api/v2/src/lib/repositories/prisma-attribute.repository.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -import { PrismaAttributeRepository as PrismaAttributeRepositoryLib } from "@calcom/platform-libraries/repositories"; -import type { PrismaClient } from "@calcom/prisma"; - -@Injectable() -export class PrismaAttributeRepository extends PrismaAttributeRepositoryLib { - constructor(private readonly dbWrite: PrismaWriteService) { - super(dbWrite.prisma as unknown as PrismaClient); - } -} diff --git a/apps/api/v2/src/lib/repositories/prisma-platform-billing.repository.ts b/apps/api/v2/src/lib/repositories/prisma-platform-billing.repository.ts deleted file mode 100644 index 637fe27c948c17..00000000000000 --- a/apps/api/v2/src/lib/repositories/prisma-platform-billing.repository.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -import { PlatformBillingRepository } from "@calcom/platform-libraries/organizations"; -import type { PrismaClient } from "@calcom/prisma"; - -@Injectable() -export class PrismaPlatformBillingRepository extends PlatformBillingRepository { - constructor(dbWrite: PrismaWriteService) { - super(dbWrite.prisma as unknown as PrismaClient); - } -} diff --git a/apps/api/v2/src/lib/repositories/prisma-routing-form-response.repository.ts b/apps/api/v2/src/lib/repositories/prisma-routing-form-response.repository.ts deleted file mode 100644 index 7ff59634cae1b5..00000000000000 --- a/apps/api/v2/src/lib/repositories/prisma-routing-form-response.repository.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -import { PrismaRoutingFormResponseRepository as PrismaRoutingFormResponseRepositoryLib } from "@calcom/platform-libraries/repositories"; -import type { PrismaClient } from "@calcom/prisma"; - -@Injectable() -export class PrismaRoutingFormResponseRepository extends PrismaRoutingFormResponseRepositoryLib { - constructor(private readonly dbWrite: PrismaWriteService) { - super(dbWrite.prisma as unknown as PrismaClient); - } -} diff --git a/apps/api/v2/src/lib/repositories/prisma-team.repository.ts b/apps/api/v2/src/lib/repositories/prisma-team.repository.ts index e2d31d45718a62..26df7b04eaa5ed 100644 --- a/apps/api/v2/src/lib/repositories/prisma-team.repository.ts +++ b/apps/api/v2/src/lib/repositories/prisma-team.repository.ts @@ -1,12 +1,37 @@ -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { Injectable } from "@nestjs/common"; - -import { PrismaTeamRepository as PrismaTeamRepositoryLib } from "@calcom/platform-libraries/repositories"; -import type { PrismaClient } from "@calcom/prisma"; +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; @Injectable() -export class PrismaTeamRepository extends PrismaTeamRepositoryLib { - constructor(private readonly dbWrite: PrismaWriteService) { - super(dbWrite.prisma as unknown as PrismaClient); +export class PrismaTeamRepository { + constructor( + private readonly dbWrite: PrismaWriteService, + private readonly dbRead: PrismaReadService + ) {} + + async findTeamById(teamId: number) { + return this.dbRead.prisma.team.findUnique({ + where: { id: teamId }, + }); + } + + async findTeamsByIds(teamIds: number[]) { + return this.dbRead.prisma.team.findMany({ + where: { id: { in: teamIds } }, + }); + } + + async getTeamByIdIfUserIsAdmin(args: { userId: number; teamId: number }) { + return this.dbRead.prisma.team.findFirst({ + where: { + id: args.teamId, + members: { + some: { + userId: args.userId, + role: { in: ["ADMIN", "OWNER"] }, + }, + }, + }, + }); } } diff --git a/apps/api/v2/src/lib/services/available-slots.service.ts b/apps/api/v2/src/lib/services/available-slots.service.ts index f34a407c2b9d97..e9cccdf989769f 100644 --- a/apps/api/v2/src/lib/services/available-slots.service.ts +++ b/apps/api/v2/src/lib/services/available-slots.service.ts @@ -1,21 +1,19 @@ +import { AvailableSlotsService as BaseAvailableSlotsService } from "@calcom/platform-libraries/slots"; +import { Injectable } from "@nestjs/common"; import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository"; import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository"; import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository"; -import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository"; import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository"; import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected-slot.repository"; -import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository"; import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"; import { BusyTimesService } from "@/lib/services/busy-times.service"; import { 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"; -import { AvailableSlotsService as BaseAvailableSlotsService } from "@calcom/platform-libraries/slots"; +type BaseAvailableSlotsDeps = ConstructorParameters[0]; import { UserAvailabilityService } from "./user-availability.service"; @@ -24,8 +22,6 @@ export class AvailableSlotsService extends BaseAvailableSlotsService { constructor( oooRepoDependency: PrismaOOORepository, scheduleRepoDependency: PrismaScheduleRepository, - teamRepository: PrismaTeamRepository, - routingFormResponseRepository: PrismaRoutingFormResponseRepository, bookingRepository: PrismaBookingRepository, selectedSlotRepository: PrismaSelectedSlotRepository, eventTypeRepository: PrismaEventTypeRepository, @@ -36,14 +32,11 @@ export class AvailableSlotsService extends BaseAvailableSlotsService { checkBookingLimitsService: CheckBookingLimitsService, userAvailabilityService: UserAvailabilityService, busyTimesService: BusyTimesService, - noSlotsNotificationService: NoSlotsNotificationService, - orgMembershipLookupService: OrgMembershipLookupService + noSlotsNotificationService: NoSlotsNotificationService ) { super({ oooRepo: oooRepoDependency, scheduleRepo: scheduleRepoDependency, - teamRepo: teamRepository, - routingFormResponseRepo: routingFormResponseRepository, bookingRepo: bookingRepository, selectedSlotRepo: selectedSlotRepository, eventTypeRepo: eventTypeRepository, @@ -52,10 +45,9 @@ export class AvailableSlotsService extends BaseAvailableSlotsService { checkBookingLimitsService, userAvailabilityService, busyTimesService, - qualifiedHostsService, - featuresRepo: featuresRepository, + qualifiedHostsService: + qualifiedHostsService as unknown as BaseAvailableSlotsDeps["qualifiedHostsService"], noSlotsNotificationService, - orgMembershipLookup: orgMembershipLookupService, }); } } diff --git a/apps/api/v2/src/lib/services/booking-attendees-remove.service.ts b/apps/api/v2/src/lib/services/booking-attendees-remove.service.ts index 6893875982209f..31a7d1c64d2149 100644 --- a/apps/api/v2/src/lib/services/booking-attendees-remove.service.ts +++ b/apps/api/v2/src/lib/services/booking-attendees-remove.service.ts @@ -1,18 +1,11 @@ import { PrismaBookingAttendeeRepository } from "@/lib/repositories/prisma-booking-attendee.repository"; -import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; import { Injectable } from "@nestjs/common"; import { BookingAttendeesRemoveService as BaseBookingAttendeesRemoveService } from "@calcom/platform-libraries/bookings"; -import { BookingEventHandlerService } from "./booking-event-handler.service"; - @Injectable() export class BookingAttendeesRemoveService extends BaseBookingAttendeesRemoveService { - constructor( - bookingEventHandlerService: BookingEventHandlerService, - featuresRepository: PrismaFeaturesRepository, - bookingAttendeeRepository: PrismaBookingAttendeeRepository - ) { - super({ bookingEventHandlerService, featuresRepository, bookingAttendeeRepository }); + constructor(bookingAttendeeRepository: PrismaBookingAttendeeRepository) { + super({ bookingAttendeeRepository }); } } diff --git a/apps/api/v2/src/lib/services/booking-attendees.service.ts b/apps/api/v2/src/lib/services/booking-attendees.service.ts index 675d8d4ff724a9..c18558dd9d1c44 100644 --- a/apps/api/v2/src/lib/services/booking-attendees.service.ts +++ b/apps/api/v2/src/lib/services/booking-attendees.service.ts @@ -1,20 +1,16 @@ import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository"; -import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; import { Injectable } from "@nestjs/common"; import { BookingAttendeesService as BaseBookingAttendeesService } from "@calcom/platform-libraries/bookings"; import { BookingAttendeesRemoveService } from "./booking-attendees-remove.service"; -import { BookingEventHandlerService } from "./booking-event-handler.service"; @Injectable() export class BookingAttendeesService extends BaseBookingAttendeesService { constructor( - bookingEventHandlerService: BookingEventHandlerService, - featuresRepository: PrismaFeaturesRepository, bookingRepository: PrismaBookingRepository, bookingAttendeesRemoveService: BookingAttendeesRemoveService ) { - super({ bookingEventHandlerService, featuresRepository, bookingRepository, bookingAttendeesRemoveService }); + super({ bookingRepository, bookingAttendeesRemoveService }); } } diff --git a/apps/api/v2/src/lib/services/booking-audit-producer.service.ts b/apps/api/v2/src/lib/services/booking-audit-producer.service.ts deleted file mode 100644 index a87a9b7d2a6907..00000000000000 --- a/apps/api/v2/src/lib/services/booking-audit-producer.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TaskerService } from "@/lib/services/tasker.service"; -import { Logger } from "@/lib/logger.bridge"; -import { Injectable } from "@nestjs/common"; - -import { BookingAuditTaskerProducerService, getAuditActorRepository } from "@calcom/platform-libraries/bookings"; - -@Injectable() -export class BookingAuditProducerService extends BookingAuditTaskerProducerService { - constructor(taskerService: TaskerService, bridgeLogger: Logger) { - super({ - tasker: taskerService.getTasker(), - log: bridgeLogger, - auditActorRepository: getAuditActorRepository(), - }); - } -} diff --git a/apps/api/v2/src/lib/services/booking-event-handler.service.ts b/apps/api/v2/src/lib/services/booking-event-handler.service.ts index dc968dd14fc167..1c919118707303 100644 --- a/apps/api/v2/src/lib/services/booking-event-handler.service.ts +++ b/apps/api/v2/src/lib/services/booking-event-handler.service.ts @@ -1,24 +1,14 @@ -import { Injectable } from "@nestjs/common"; - import { BookingEventHandlerService as BaseBookingEventHandlerService } from "@calcom/platform-libraries/bookings"; - -import { Logger } from "@/lib/logger.bridge"; - -import { BookingAuditProducerService } from "./booking-audit-producer.service"; +import { Injectable } from "@nestjs/common"; import { HashedLinkService } from "./hashed-link.service"; +import { Logger } from "@/lib/logger.bridge"; @Injectable() export class BookingEventHandlerService extends BaseBookingEventHandlerService { - constructor( - hashedLinkService: HashedLinkService, - bridgeLogger: Logger, - bookingAuditProducerService: BookingAuditProducerService - ) { + constructor(hashedLinkService: HashedLinkService, bridgeLogger: Logger) { super({ log: bridgeLogger, hashedLinkService, - bookingAuditProducerService, }); } } - diff --git a/apps/api/v2/src/lib/services/filter-hosts.service.ts b/apps/api/v2/src/lib/services/filter-hosts.service.ts deleted file mode 100644 index edb6a081deebfa..00000000000000 --- a/apps/api/v2/src/lib/services/filter-hosts.service.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository"; -import { Injectable } from "@nestjs/common"; - -import { FilterHostsService as BaseFilterHostsService } from "@calcom/platform-libraries/slots"; - -@Injectable() -export class FilterHostsService extends BaseFilterHostsService { - constructor(bookingRepository: PrismaBookingRepository) { - super({ - bookingRepo: bookingRepository, - }); - } -} diff --git a/apps/api/v2/src/lib/services/instant-booking-create.service.ts b/apps/api/v2/src/lib/services/instant-booking-create.service.ts deleted file mode 100644 index e09e7f6ab1a549..00000000000000 --- a/apps/api/v2/src/lib/services/instant-booking-create.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injectable } from "@nestjs/common"; - -import { InstantBookingCreateService as BaseInstantBookingCreateService } from "@calcom/platform-libraries/bookings"; - -@Injectable() -export class InstantBookingCreateService extends BaseInstantBookingCreateService {} diff --git a/apps/api/v2/src/lib/services/lucky-user.service.ts b/apps/api/v2/src/lib/services/lucky-user.service.ts index 9d05b066f5a725..f5d05696219238 100644 --- a/apps/api/v2/src/lib/services/lucky-user.service.ts +++ b/apps/api/v2/src/lib/services/lucky-user.service.ts @@ -1,11 +1,9 @@ -import { PrismaAttributeRepository } from "@/lib/repositories/prisma-attribute.repository"; +import { LuckyUserService as BaseLuckyUserService } from "@calcom/platform-libraries/bookings"; +import { Injectable } from "@nestjs/common"; import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository"; import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository"; import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository"; import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"; -import { Injectable } from "@nestjs/common"; - -import { LuckyUserService as BaseLuckyUserService } from "@calcom/platform-libraries/bookings"; @Injectable() export class LuckyUserService extends BaseLuckyUserService { @@ -13,15 +11,13 @@ export class LuckyUserService extends BaseLuckyUserService { bookingRepository: PrismaBookingRepository, hostRepository: PrismaHostRepository, oooRepository: PrismaOOORepository, - userRepository: PrismaUserRepository, - attributeRepository: PrismaAttributeRepository + userRepository: PrismaUserRepository ) { super({ - bookingRepository: bookingRepository, - hostRepository: hostRepository, - oooRepository: oooRepository, - userRepository: userRepository, - attributeRepository: attributeRepository, + bookingRepository, + hostRepository, + oooRepository, + userRepository, }); } } diff --git a/apps/api/v2/src/lib/services/no-slots-notification.service.ts b/apps/api/v2/src/lib/services/no-slots-notification.service.ts index 12a387012f0948..c8adb24615cd32 100644 --- a/apps/api/v2/src/lib/services/no-slots-notification.service.ts +++ b/apps/api/v2/src/lib/services/no-slots-notification.service.ts @@ -1,5 +1,4 @@ import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository"; -import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository"; import { RedisService } from "@/modules/redis/redis.service"; import { Injectable } from "@nestjs/common"; @@ -7,13 +6,8 @@ import { NoSlotsNotificationService as BaseNoSlotsNotificationService } from "@c @Injectable() export class NoSlotsNotificationService extends BaseNoSlotsNotificationService { - constructor( - teamRepository: PrismaTeamRepository, - membershipRepository: PrismaMembershipRepository, - redisService: RedisService - ) { + constructor(membershipRepository: PrismaMembershipRepository, redisService: RedisService) { super({ - teamRepo: teamRepository, membershipRepo: membershipRepository, redisClient: redisService, }); diff --git a/apps/api/v2/src/lib/services/oauth.service.ts b/apps/api/v2/src/lib/services/oauth.service.ts index 7c5b2987ca2258..18d8badd700564 100644 --- a/apps/api/v2/src/lib/services/oauth.service.ts +++ b/apps/api/v2/src/lib/services/oauth.service.ts @@ -3,7 +3,6 @@ import { OAuthService as BaseOAuthService } from "@calcom/platform-libraries"; import { Injectable } from "@nestjs/common"; import { PrismaAccessCodeRepository } from "@/lib/repositories/prisma-access-code.repository"; import { PrismaOAuthClientRepository } from "@/lib/repositories/prisma-oauth-client.repository"; -import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository"; import type { OAuth2ExchangeInput } from "@/modules/auth/oauth2/inputs/exchange.input"; import { OAuth2ExchangeConfidentialInput } from "@/modules/auth/oauth2/inputs/exchange.input"; import type { OAuth2RefreshInput } from "@/modules/auth/oauth2/inputs/refresh.input"; @@ -14,13 +13,11 @@ import type { OAuth2TokenInput } from "@/modules/auth/oauth2/inputs/token.input. export class OAuthService extends BaseOAuthService { constructor( accessCodeRepository: PrismaAccessCodeRepository, - oAuthClientRepository: PrismaOAuthClientRepository, - teamsRepository: PrismaTeamRepository + oAuthClientRepository: PrismaOAuthClientRepository ) { super({ accessCodeRepository: accessCodeRepository, oAuthClientRepository: oAuthClientRepository, - teamsRepository: teamsRepository, }); } diff --git a/apps/api/v2/src/lib/services/org-membership-lookup.service.ts b/apps/api/v2/src/lib/services/org-membership-lookup.service.ts deleted file mode 100644 index 8e3df330c61e4a..00000000000000 --- a/apps/api/v2/src/lib/services/org-membership-lookup.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 { - return ProfileRepository.findFirstOrganizationIdForUser({ userId }); - } -} diff --git a/apps/api/v2/src/lib/services/organization-membership.service.ts b/apps/api/v2/src/lib/services/organization-membership.service.ts deleted file mode 100644 index 59be6027c12e9d..00000000000000 --- a/apps/api/v2/src/lib/services/organization-membership.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { Injectable } from "@nestjs/common"; - -import { OrganizationMembershipService as BaseOrganizationMembershipService } from "@calcom/platform-libraries/organizations"; - -@Injectable() -export class OrganizationMembershipService extends BaseOrganizationMembershipService { - constructor(organizationsRepository: OrganizationsRepository) { - super({ organizationRepository: organizationsRepository }); - } -} - diff --git a/apps/api/v2/src/lib/services/qualified-hosts.service.ts b/apps/api/v2/src/lib/services/qualified-hosts.service.ts index 55906d6e51d589..44e83f3f9a40dc 100644 --- a/apps/api/v2/src/lib/services/qualified-hosts.service.ts +++ b/apps/api/v2/src/lib/services/qualified-hosts.service.ts @@ -1,15 +1,86 @@ -import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository"; -import { FilterHostsService } from "@/lib/services/filter-hosts.service"; import { Injectable } from "@nestjs/common"; -import { QualifiedHostsService as BaseQualifiedHostsService } from "@calcom/platform-libraries/slots"; +type Host = { + isFixed: boolean; + user: Record; + groupId?: string | null; +}; + +type User = Record; + +type QualifiedHost = { user: Record; isFixed?: boolean; groupId?: string | null }; @Injectable() -export class QualifiedHostsService extends BaseQualifiedHostsService { - constructor(bookingRepository: PrismaBookingRepository, filterHostsService: FilterHostsService) { - super({ - bookingRepo: bookingRepository, - filterHostsService, - }); +export class QualifiedHostsService { + async getQualifiedUsers( + _hosts: { userId: number; isFixed: boolean }[], + _input: { eventTypeId: number; startTime: string; endTime: string } + ): Promise<{ userId: number; isFixed: boolean; priority: number }[]> { + // In community edition, all hosts qualify + return _hosts.map((host) => ({ ...host, priority: 0 })); + } + + async findQualifiedHostsWithDelegationCredentials(...args: unknown[]): Promise<{ + qualifiedRRHosts: QualifiedHost[]; + allFallbackRRHosts: QualifiedHost[]; + fixedHosts: QualifiedHost[]; + }> { + const input = (args[0] ?? {}) as Record; + const eventType = (input.eventType ?? {}) as Record; + const contactOwnerEmail = input.contactOwnerEmail as string | null | undefined; + const routedTeamMemberIds = (input.routedTeamMemberIds ?? []) as number[]; + + const hosts = (eventType.hosts ?? []) as Host[]; + const users = (eventType.users ?? []) as User[]; + const schedulingType = eventType.schedulingType as string | null | undefined; + + if (hosts.length > 0) { + const fixedHosts: QualifiedHost[] = []; + const allRRHosts: QualifiedHost[] = []; + + for (const host of hosts) { + const qualifiedHost: QualifiedHost = { + user: host.user, + isFixed: host.isFixed, + groupId: host.groupId ?? null, + }; + + if (host.isFixed || schedulingType !== "ROUND_ROBIN") { + fixedHosts.push(qualifiedHost); + } else { + allRRHosts.push(qualifiedHost); + } + } + + let qualifiedRRHosts = allRRHosts; + + if (contactOwnerEmail) { + const contactOwnerHost = allRRHosts.filter( + (h) => (h.user as { email?: string }).email === contactOwnerEmail + ); + if (contactOwnerHost.length > 0) { + qualifiedRRHosts = contactOwnerHost; + } + } else if (routedTeamMemberIds.length > 0) { + const routedMemberIdSet = new Set(routedTeamMemberIds); + const routedHosts = allRRHosts.filter((h) => routedMemberIdSet.has((h.user as { id: number }).id)); + if (routedHosts.length > 0) { + qualifiedRRHosts = routedHosts; + } + } + + return { qualifiedRRHosts, allFallbackRRHosts: allRRHosts, fixedHosts }; + } + + if (users.length > 0) { + const fixedHosts = users.map((user) => ({ + user, + isFixed: true as const, + groupId: null, + })); + return { qualifiedRRHosts: [], allFallbackRRHosts: [], fixedHosts }; + } + + return { qualifiedRRHosts: [], allFallbackRRHosts: [], fixedHosts: [] }; } } diff --git a/apps/api/v2/src/lib/services/recurring-booking.service.ts b/apps/api/v2/src/lib/services/recurring-booking.service.ts index ffa418a9f87f11..3775bb68a6080b 100644 --- a/apps/api/v2/src/lib/services/recurring-booking.service.ts +++ b/apps/api/v2/src/lib/services/recurring-booking.service.ts @@ -1,21 +1,12 @@ -import { RegularBookingService } from "@/lib/services/regular-booking.service"; -import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service"; -import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; -import { Injectable } from "@nestjs/common"; - import { RecurringBookingService as BaseRecurringBookingService } from "@calcom/platform-libraries/bookings"; +import { Injectable } from "@nestjs/common"; +import { RegularBookingService } from "@/lib/services/regular-booking.service"; @Injectable() export class RecurringBookingService extends BaseRecurringBookingService { - constructor( - regularBookingService: RegularBookingService, - bookingEventHandler: BookingEventHandlerService, - featuresRepository: PrismaFeaturesRepository - ) { + constructor(regularBookingService: RegularBookingService) { super({ regularBookingService, - bookingEventHandler, - featuresRepository, }); } } diff --git a/apps/api/v2/src/lib/services/regular-booking.service.ts b/apps/api/v2/src/lib/services/regular-booking.service.ts index 9210a3c1f66e79..ad28c31130229b 100644 --- a/apps/api/v2/src/lib/services/regular-booking.service.ts +++ b/apps/api/v2/src/lib/services/regular-booking.service.ts @@ -6,7 +6,6 @@ import type { PrismaClient } from "@calcom/prisma"; import { Inject, Injectable } from "@nestjs/common"; import { WEBHOOK_PRODUCER } from "@/lib/modules/regular-booking.tokens"; import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository"; -import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"; import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service"; import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service"; @@ -25,7 +24,6 @@ export class RegularBookingService extends BaseRegularBookingService { luckyUserService: LuckyUserService, userRepository: PrismaUserRepository, bookingEmailAndSmsTasker: BookingEmailAndSmsTasker, - featuresRepository: PrismaFeaturesRepository, bookingEventHandler: BookingEventHandlerService, @Inject(WEBHOOK_PRODUCER) webhookProducer: IWebhookProducerService ) { @@ -37,7 +35,6 @@ export class RegularBookingService extends BaseRegularBookingService { luckyUserService, userRepository, bookingEmailAndSmsTasker, - featuresRepository, bookingEventHandler, webhookProducer, }); diff --git a/apps/api/v2/src/lib/services/stripe-billing-provider.service.ts b/apps/api/v2/src/lib/services/stripe-billing-provider.service.ts deleted file mode 100644 index 44ad35f6faa440..00000000000000 --- a/apps/api/v2/src/lib/services/stripe-billing-provider.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { StripeBillingService } from "@calcom/platform-libraries"; -import type { IBillingProviderService } from "@calcom/platform-libraries/organizations"; -import { Injectable } from "@nestjs/common"; -import { StripeService } from "@/modules/stripe/stripe.service"; - -@Injectable() -export class StripeBillingProviderService - extends StripeBillingService - implements Pick -{ - constructor(readonly stripeService: StripeService) { - super(stripeService.getStripe()); - } -} diff --git a/apps/api/v2/src/lib/services/tasker/platform-billing-sync-tasker.service.ts b/apps/api/v2/src/lib/services/tasker/platform-billing-sync-tasker.service.ts deleted file mode 100644 index cab4bb52010ff9..00000000000000 --- a/apps/api/v2/src/lib/services/tasker/platform-billing-sync-tasker.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Logger } from "@/lib/logger.bridge"; -import { PlatformBillingTaskService } from "@/lib/services/tasker/platform-billing-task.service"; -import { Injectable } from "@nestjs/common"; - -import { PlatformOrganizationBillingSyncTasker as BasePlatformOrganizationBillingSyncTasker } from "@calcom/platform-libraries/organizations"; - -@Injectable() -export class PlatformBillingSyncTaskerService extends BasePlatformOrganizationBillingSyncTasker { - constructor(billingTaskService: PlatformBillingTaskService, logger: Logger) { - super({ - logger, - billingTaskService, - }); - } -} diff --git a/apps/api/v2/src/lib/services/tasker/platform-billing-task.service.ts b/apps/api/v2/src/lib/services/tasker/platform-billing-task.service.ts deleted file mode 100644 index 1229b751f195b6..00000000000000 --- a/apps/api/v2/src/lib/services/tasker/platform-billing-task.service.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Logger } from "@/lib/logger.bridge"; -import { PrismaPlatformBillingRepository } from "@/lib/repositories/prisma-platform-billing.repository"; -import { StripeBillingProviderService } from "@/lib/services/stripe-billing-provider.service"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { Injectable } from "@nestjs/common"; - -import { PlatformOrganizationBillingTaskService as BasePlatformOrganizationBillingTaskService } from "@calcom/platform-libraries/organizations"; - -@Injectable() -export class PlatformBillingTaskService extends BasePlatformOrganizationBillingTaskService { - constructor( - organizationRepository: OrganizationsRepository, - platformBillingRepository: PrismaPlatformBillingRepository, - billingProviderService: StripeBillingProviderService, - logger: Logger - ) { - super({ - logger, - organizationRepository, - platformBillingRepository, - billingProviderService, - }); - } -} diff --git a/apps/api/v2/src/lib/services/tasker/platform-billing-tasker.service.ts b/apps/api/v2/src/lib/services/tasker/platform-billing-tasker.service.ts deleted file mode 100644 index 73a21ff65ff563..00000000000000 --- a/apps/api/v2/src/lib/services/tasker/platform-billing-tasker.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PlatformOrganizationBillingTasker as BasePlatformOrganizationBillingTasker } from "@calcom/platform-libraries/organizations"; -import { Injectable } from "@nestjs/common"; -import { Logger } from "@/lib/logger.bridge"; -import { PlatformBillingSyncTaskerService } from "@/lib/services/tasker/platform-billing-sync-tasker.service"; -import { PlatformBillingTriggerTaskerService } from "@/lib/services/tasker/platform-billing-trigger-tasker.service"; - -@Injectable() -export class PlatformBillingTasker extends BasePlatformOrganizationBillingTasker { - constructor( - syncTasker: PlatformBillingSyncTaskerService, - asyncTasker: PlatformBillingTriggerTaskerService, - logger: Logger - ) { - super({ - logger, - asyncTasker: asyncTasker, - syncTasker: syncTasker, - }); - } -} diff --git a/apps/api/v2/src/lib/services/tasker/platform-billing-trigger-tasker.service.ts b/apps/api/v2/src/lib/services/tasker/platform-billing-trigger-tasker.service.ts deleted file mode 100644 index ca938e8b7690df..00000000000000 --- a/apps/api/v2/src/lib/services/tasker/platform-billing-trigger-tasker.service.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Logger } from "@/lib/logger.bridge"; -import { Injectable } from "@nestjs/common"; - -import { PlatformOrganizationBillingTriggerTasker as BasePlatformOrganizationBillingTriggerTasker } from "@calcom/platform-libraries/organizations"; - -@Injectable() -export class PlatformBillingTriggerTaskerService extends BasePlatformOrganizationBillingTriggerTasker { - constructor(logger: Logger) { - super({ - logger, - }); - } -} diff --git a/apps/api/v2/src/modules/atoms/atoms.module.ts b/apps/api/v2/src/modules/atoms/atoms.module.ts index 749a2dfff1ab20..8218e766ec5396 100644 --- a/apps/api/v2/src/modules/atoms/atoms.module.ts +++ b/apps/api/v2/src/modules/atoms/atoms.module.ts @@ -1,35 +1,27 @@ -import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; +import { EventTypesModule_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.module"; import { AtomsSecondaryEmailsRepository } from "@/modules/atoms/atoms-secondary-emails.repository"; import { AtomsRepository } from "@/modules/atoms/atoms.repository"; import { AtomsConferencingAppsController } from "@/modules/atoms/controllers/atoms.conferencing-apps.controller"; -import { AtomsController } from "@/modules/atoms/controllers/atoms.controller"; import { AtomsEventTypesController } from "@/modules/atoms/controllers/atoms.event-types.controller"; import { AtomsSchedulesController } from "@/modules/atoms/controllers/atoms.schedules.controller"; import { AtomsVerificationController } from "@/modules/atoms/controllers/atoms.verification.controller"; -import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service"; import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service"; import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service"; import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.service"; import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { OrganizationsModule } from "@/modules/organizations/organizations.module"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisService } from "@/modules/redis/redis.service"; -import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; -import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; import { UsersService } from "@/modules/users/services/users.service"; import { UsersRepository } from "@/modules/users/users.repository"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule, TeamsEventTypesModule], + imports: [PrismaModule, EventTypesModule_2024_06_14], providers: [ - OrganizationsTeamsRepository, EventTypesAtomService, ConferencingAtomsService, - AttributesAtomsService, MembershipsRepository, CredentialsRepository, UsersRepository, @@ -39,11 +31,9 @@ import { Module } from "@nestjs/common"; SchedulesAtomsService, VerificationAtomsService, RedisService, - TeamsRepository, ], exports: [EventTypesAtomService], controllers: [ - AtomsController, AtomsEventTypesController, AtomsConferencingAppsController, AtomsSchedulesController, diff --git a/apps/api/v2/src/modules/atoms/controllers/atoms.conferencing-apps.controller.ts b/apps/api/v2/src/modules/atoms/controllers/atoms.conferencing-apps.controller.ts index 58b827d35203c0..4be448ed8b0f3a 100644 --- a/apps/api/v2/src/modules/atoms/controllers/atoms.conferencing-apps.controller.ts +++ b/apps/api/v2/src/modules/atoms/controllers/atoms.conferencing-apps.controller.ts @@ -1,16 +1,9 @@ import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { Controller, Get, Param, ParseIntPipe, UseGuards, Version, VERSION_NEUTRAL } from "@nestjs/common"; +import { Controller, Get, UseGuards, Version, VERSION_NEUTRAL } from "@nestjs/common"; import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; @@ -31,22 +24,6 @@ These endpoints should not be recommended for use by third party and are exclude export class AtomsConferencingAppsController { constructor(private readonly conferencingService: ConferencingAtomsService) {} - @Get("/organizations/:orgId/teams/:teamId/conferencing") - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Version(VERSION_NEUTRAL) - async listTeamInstalledConferencingApps( - @GetUser() user: UserWithProfile, - @Param("teamId", ParseIntPipe) teamId: number - ): Promise> { - const conferencingApps = await this.conferencingService.getTeamConferencingApps(user, teamId); - return { - status: SUCCESS_STATUS, - data: conferencingApps, - }; - } - @Get("/conferencing") @Version(VERSION_NEUTRAL) @UseGuards(ApiAuthGuard) diff --git a/apps/api/v2/src/modules/atoms/controllers/atoms.controller.ts b/apps/api/v2/src/modules/atoms/controllers/atoms.controller.ts deleted file mode 100644 index ed854dd84eed04..00000000000000 --- a/apps/api/v2/src/modules/atoms/controllers/atoms.controller.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { FindTeamMembersMatchingAttributeQueryDto } from "@/modules/atoms/inputs/find-team-members-matching-attribute.input"; -import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service"; -import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { - Controller, - Get, - Param, - ParseIntPipe, - UseGuards, - Version, - VERSION_NEUTRAL, - Query, -} from "@nestjs/common"; -import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; - -import { FindTeamMembersMatchingAttributeResponseDto } from "../outputs/find-team-members-matching-attribute.output"; - -/* -Endpoints used only by platform atoms, reusing code from other modules, data is already formatted and ready to be used by frontend atoms -these endpoints should not be recommended for use by third party and are excluded from docs -*/ - -@Controller({ - path: "/v2/atoms", - version: API_VERSIONS_VALUES, -}) -@DocsTags("Atoms - endpoints for atoms") -@DocsExcludeController(true) -export class AtomsController { - constructor( - private readonly conferencingService: ConferencingAtomsService, - private readonly attributesService: AttributesAtomsService - ) {} - - @Get("/organizations/:orgId/teams/:teamId/members-matching-attribute") - @Version(VERSION_NEUTRAL) - @UseGuards(ApiAuthGuard) - async findTeamMembersMatchingAttributes( - @GetUser() user: UserWithProfile, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("orgId", ParseIntPipe) orgId: number, - @Query() query: FindTeamMembersMatchingAttributeQueryDto - ): Promise { - const result = await this.attributesService.findTeamMembersMatchingAttribute(teamId, orgId, { - attributesQueryValue: query.attributesQueryValue, - isPreview: query.isPreview, - enablePerf: query.enablePerf, - concurrency: query.concurrency, - }); - - return { - status: SUCCESS_STATUS, - data: result, - }; - } -} diff --git a/apps/api/v2/src/modules/atoms/controllers/atoms.event-types.controller.ts b/apps/api/v2/src/modules/atoms/controllers/atoms.event-types.controller.ts index 36c7dd8245eaa9..11492f2ce045fd 100644 --- a/apps/api/v2/src/modules/atoms/controllers/atoms.event-types.controller.ts +++ b/apps/api/v2/src/modules/atoms/controllers/atoms.event-types.controller.ts @@ -15,7 +15,7 @@ import { Version, } from "@nestjs/common"; import { ApiExcludeController as DocsExcludeController, ApiTags as DocsTags } from "@nestjs/swagger"; -import { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output"; +import { GetEventTypePublicOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/get-event-type-public.output"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { BulkUpdateEventTypeToDefaultLocationDto, @@ -23,15 +23,8 @@ import { } from "@/modules/atoms/inputs/event-types-app.input"; import { GetAtomPublicEventTypeQueryParams } from "@/modules/atoms/inputs/get-atom-public-event-type-query-params.input"; import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; import { UserWithProfile } from "@/modules/users/users.repository"; /* @@ -94,19 +87,6 @@ export class AtomsEventTypesController { }; } - @Get("/organizations/:orgId/teams/:teamId/event-types") - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Version(VERSION_NEUTRAL) - async listTeamEventTypes(@Param("teamId", ParseIntPipe) teamId: number): Promise> { - const eventTypes = await this.eventTypesService.getTeamEventTypes(teamId); - return { - status: SUCCESS_STATUS, - data: eventTypes, - }; - } - @Get("/event-types") @Version(VERSION_NEUTRAL) @UseGuards(ApiAuthGuard) @@ -160,22 +140,6 @@ export class AtomsEventTypesController { }; } - @Patch("/organizations/:orgId/teams/:teamId/event-types/bulk-update-to-default-location") - @Version(VERSION_NEUTRAL) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - async bulkUpdateAtomTeamEventTypes( - @GetUser() user: UserWithProfile, - @Body() body: BulkUpdateEventTypeToDefaultLocationDto, - @Param("teamId", ParseIntPipe) teamId: number - ): Promise<{ status: typeof SUCCESS_STATUS | typeof ERROR_STATUS }> { - await this.eventTypesService.bulkUpdateTeamEventTypesDefaultLocation(body.eventTypeIds, teamId); - return { - status: SUCCESS_STATUS, - }; - } - @Patch("event-types/:eventTypeId") @Version(VERSION_NEUTRAL) @UseGuards(ApiAuthGuard) @@ -195,26 +159,4 @@ export class AtomsEventTypesController { }; } - @Patch("/organizations/:orgId/teams/:teamId/event-types/:eventTypeId") - @Version(VERSION_NEUTRAL) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - async updateAtomTeamEventType( - @GetUser() user: UserWithProfile, - @Param("eventTypeId", ParseIntPipe) eventTypeId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Body() body: UpdateEventTypeReturn - ): Promise> { - const eventType = await this.eventTypesService.updateTeamEventType( - eventTypeId, - { ...body, id: eventTypeId }, - user, - teamId - ); - return { - status: SUCCESS_STATUS, - data: eventType, - }; - } } diff --git a/apps/api/v2/src/modules/atoms/inputs/find-team-members-matching-attribute.input.ts b/apps/api/v2/src/modules/atoms/inputs/find-team-members-matching-attribute.input.ts deleted file mode 100644 index 1f0dfa701c1794..00000000000000 --- a/apps/api/v2/src/modules/atoms/inputs/find-team-members-matching-attribute.input.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; - -import type { TFindTeamMembersMatchingAttributeLogicInputSchema } from "@calcom/platform-libraries"; - -export class FindTeamMembersMatchingAttributeQueryDto { - @ApiProperty({ - nullable: true, - }) - attributesQueryValue!: TFindTeamMembersMatchingAttributeLogicInputSchema["attributesQueryValue"] | null; - - @ApiPropertyOptional({ - type: Boolean, - }) - isPreview?: boolean; - - @ApiPropertyOptional({ - type: Boolean, - }) - enablePerf?: boolean; - - @ApiPropertyOptional({ - type: Number, - }) - concurrency?: number; -} diff --git a/apps/api/v2/src/modules/atoms/outputs/find-team-members-matching-attribute.output.ts b/apps/api/v2/src/modules/atoms/outputs/find-team-members-matching-attribute.output.ts deleted file mode 100644 index 2d80b40ae01a5a..00000000000000 --- a/apps/api/v2/src/modules/atoms/outputs/find-team-members-matching-attribute.output.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsString, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { FindTeamMembersMatchingAttributeOutputDto } from "@calcom/platform-types"; - -export class FindTeamMembersMatchingAttributeResponseDto { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsString() - @Expose() - readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ValidateNested() - @Type(() => FindTeamMembersMatchingAttributeOutputDto) - @Expose() - @ApiProperty({ type: FindTeamMembersMatchingAttributeOutputDto }) - readonly data!: FindTeamMembersMatchingAttributeOutputDto; -} diff --git a/apps/api/v2/src/modules/atoms/services/attributes-atom.service.ts b/apps/api/v2/src/modules/atoms/services/attributes-atom.service.ts deleted file mode 100644 index d4f38e2f90508b..00000000000000 --- a/apps/api/v2/src/modules/atoms/services/attributes-atom.service.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { UsersRepository } from "@/modules/users/users.repository"; -import { Logger } from "@nestjs/common"; -import { Injectable } from "@nestjs/common"; - -import { findTeamMembersMatchingAttributeLogic } from "@calcom/platform-libraries"; - -import { FindTeamMembersMatchingAttributeQueryDto } from "../inputs/find-team-members-matching-attribute.input"; - -@Injectable() -export class AttributesAtomsService { - private logger = new Logger("AttributesAtomService"); - - constructor(private readonly usersRepository: UsersRepository) {} - - async findTeamMembersMatchingAttribute( - teamId: number, - orgId: number, - input: FindTeamMembersMatchingAttributeQueryDto - ) { - const { - teamMembersMatchingAttributeLogic: matchingTeamMembersWithResult, - mainAttributeLogicBuildingWarnings: mainWarnings, - fallbackAttributeLogicBuildingWarnings: fallbackWarnings, - troubleshooter, - } = await findTeamMembersMatchingAttributeLogic( - { - teamId, - orgId, - attributesQueryValue: input.attributesQueryValue, - }, - { - enablePerf: input.enablePerf, - concurrency: input.concurrency, - enableTroubleshooter: input.enablePerf, - } - ); - - if (!matchingTeamMembersWithResult) { - return { - troubleshooter, - mainWarnings, - fallbackWarnings, - result: null, - }; - } - - const matchingTeamMembersIds = matchingTeamMembersWithResult.map( - (member: { userId: number }) => member.userId - ); - - const matchingTeamMembers = await this.usersRepository.findByIds(matchingTeamMembersIds); - - return { - mainWarnings, - fallbackWarnings, - troubleshooter: troubleshooter, - result: matchingTeamMembers.map((user) => ({ - id: user.id, - name: user.name, - email: user.email, - })), - }; - } -} diff --git a/apps/api/v2/src/modules/atoms/services/conferencing-atom.service.ts b/apps/api/v2/src/modules/atoms/services/conferencing-atom.service.ts index 1a79e351db08be..f0a7be9fefb23f 100644 --- a/apps/api/v2/src/modules/atoms/services/conferencing-atom.service.ts +++ b/apps/api/v2/src/modules/atoms/services/conferencing-atom.service.ts @@ -23,16 +23,4 @@ export class ConferencingAtomsService { }); } - async getTeamConferencingApps(user: UserWithProfile, teamId: number): Promise { - return getConnectedApps({ - user, - input: { - variant: "conferencing", - onlyInstalled: true, - teamId, - includeTeamInstalledApps: true, - }, - prisma: this.dbWrite.prisma as unknown as PrismaClient, - }); - } } diff --git a/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts b/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts index 6b0fa894f19ca2..c07df68c58a0ee 100644 --- a/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts +++ b/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts @@ -15,9 +15,7 @@ import { } from "@calcom/platform-libraries/app-store"; import { bulkUpdateEventsToDefaultLocation, - bulkUpdateTeamEventsToDefaultLocation, EventTypeMetaDataSchema, - getBulkTeamEventTypes, getBulkUserEventTypes, getEventTypeById, getPublicEvent, @@ -27,18 +25,15 @@ import { } from "@calcom/platform-libraries/event-types"; import type { PrismaClient } from "@calcom/prisma"; import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; -import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; -import { systemBeforeFieldEmail } from "@/ee/event-types/event-types_2024_06_14/transformers"; +import { EventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/event-types.service"; +import { systemBeforeFieldEmail } from "@/platform/event-types/event-types_2024_06_14/transformers"; import { AtomsRepository } from "@/modules/atoms/atoms.repository"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; import { UsersService } from "@/modules/users/services/users.service"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { UsersRepository } from "@/modules/users/users.repository"; +import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository"; type EnabledAppType = App & { credential: CredentialDataWithTeamName; @@ -75,9 +70,7 @@ export class EventTypesAtomService { private readonly dbWrite: PrismaWriteService, private readonly dbRead: PrismaReadService, private readonly eventTypeService: EventTypesService_2024_06_14, - private readonly teamEventTypeService: TeamsEventTypesService, - private readonly organizationsTeamsRepository: OrganizationsTeamsRepository, - private readonly usersRepository: UsersRepository, + private readonly usersRepository: UsersRepository ) {} private async getTeamSlug(teamId: number): Promise { @@ -132,57 +125,6 @@ export class EventTypesAtomService { return getBulkUserEventTypes(userId); } - async getTeamEventTypes(teamId: number) { - return getBulkTeamEventTypes(teamId); - } - - async updateTeamEventType( - eventTypeId: number, - body: TUpdateEventTypeInputSchema, - user: UserWithProfile, - teamId: number - ) { - await this.checkCanUpdateTeamEventType(user, eventTypeId, teamId, body.scheduleId); - - const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user); - const bookingFields = body.bookingFields ? [...body.bookingFields] : undefined; - - if ( - bookingFields?.length && - !bookingFields.find((field) => field.type === "email") && - !bookingFields.find((field) => field.type === "phone") - ) { - bookingFields.push(systemBeforeFieldEmail); - } - - // Normalize period dates to UTC midnight (only if provided) - const periodDates = - body.periodStartDate !== undefined || body.periodEndDate !== undefined - ? { - ...(body.periodStartDate !== undefined - ? { periodStartDate: normalizePeriodDate(body.periodStartDate) } - : {}), - ...(body.periodEndDate !== undefined - ? { periodEndDate: normalizePeriodDate(body.periodEndDate) } - : {}), - } - : {}; - - const eventType = await updateEventType({ - input: { ...body, id: eventTypeId, bookingFields, ...periodDates }, - ctx: { - user: eventTypeUser, - prisma: this.dbWrite.prisma, - }, - }); - - if (!eventType) { - throw new NotFoundException(`Event type with id ${eventTypeId} not found`); - } - - return eventType; - } - async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) { await this.eventTypeService.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId); const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user); @@ -224,34 +166,6 @@ export class EventTypesAtomService { return eventType; } - async checkCanUpdateTeamEventType( - user: UserWithProfile, - eventTypeId: number, - teamId: number, - scheduleId: number | null | undefined - ) { - const organizationId = this.usersService.getUserMainOrgId(user); - - if (organizationId) { - const isUserOrganizationAdmin = await this.membershipsRepository.isUserOrganizationAdmin( - user.id, - organizationId - ); - - if (isUserOrganizationAdmin) { - const orgTeam = await this.organizationsTeamsRepository.findOrgTeam(organizationId, teamId); - if (orgTeam) { - await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId); - return; - } - } - } - - await this.checkTeamOwnsEventType(user.id, eventTypeId, teamId); - await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId); - await this.eventTypeService.checkUserOwnsSchedule(user.id, scheduleId); - } - async checkTeamOwnsEventType(userId: number, eventTypeId: number, teamId: number) { const membership = await this.dbRead.prisma.membership.findFirst({ where: { @@ -316,7 +230,7 @@ export class EventTypesAtomService { credentials, }, }); - credentials = allCredentials; + credentials = allCredentials as typeof credentials; } } @@ -443,13 +357,6 @@ export class EventTypesAtomService { }); } - async bulkUpdateTeamEventTypesDefaultLocation(eventTypeIds: number[], teamId: number) { - return bulkUpdateTeamEventsToDefaultLocation({ - eventTypeIds, - prisma: this.dbWrite.prisma as unknown as PrismaClient, - teamId, - }); - } /** * Returns the public event type for atoms, handling both team and user events. */ diff --git a/apps/api/v2/src/modules/auth/auth.module.ts b/apps/api/v2/src/modules/auth/auth.module.ts index e3d427cc6fd4c3..649565f6aa8ba3 100644 --- a/apps/api/v2/src/modules/auth/auth.module.ts +++ b/apps/api/v2/src/modules/auth/auth.module.ts @@ -7,7 +7,6 @@ import { DeploymentsModule } from "@/modules/deployments/deployments.module"; import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; import { RedisModule } from "@/modules/redis/redis.module"; -import { RolesModule } from "@/modules/roles/roles.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; import { UsersModule } from "@/modules/users/users.module"; import { Module } from "@nestjs/common"; @@ -22,7 +21,6 @@ import { PassportModule } from "@nestjs/passport"; MembershipsModule, TokensModule, DeploymentsModule, - RolesModule, ], providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService], exports: [NextAuthGuard, ApiAuthGuard], diff --git a/apps/api/v2/src/modules/auth/decorators/billing/platform-plan.decorator.ts b/apps/api/v2/src/modules/auth/decorators/billing/platform-plan.decorator.ts deleted file mode 100644 index e43da047812385..00000000000000 --- a/apps/api/v2/src/modules/auth/decorators/billing/platform-plan.decorator.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { PlatformPlanType } from "@/modules/billing/types"; -import { Reflector } from "@nestjs/core"; - -export const PlatformPlan = Reflector.createDecorator(); diff --git a/apps/api/v2/src/modules/auth/decorators/pbac/pbac.decorator.ts b/apps/api/v2/src/modules/auth/decorators/pbac/pbac.decorator.ts index 349af67eb8f992..bc5dd1a1a4540a 100644 --- a/apps/api/v2/src/modules/auth/decorators/pbac/pbac.decorator.ts +++ b/apps/api/v2/src/modules/auth/decorators/pbac/pbac.decorator.ts @@ -1,5 +1,7 @@ import { Reflector } from "@nestjs/core"; -import type { PermissionString } from "@calcom/platform-libraries/pbac"; +// PBAC (Permission-Based Access Control) is not available in community edition +// PermissionString is defined locally since the platform-libraries/pbac module was removed +type PermissionString = string; export const Pbac = Reflector.createDecorator(); diff --git a/apps/api/v2/src/modules/auth/guards/billing/platform-plan.guard.spec.ts b/apps/api/v2/src/modules/auth/guards/billing/platform-plan.guard.spec.ts deleted file mode 100644 index 33017e44371b9e..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/billing/platform-plan.guard.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { RedisService } from "@/modules/redis/redis.service"; -import { createMock } from "@golevelup/ts-jest"; -import { ForbiddenException } from "@nestjs/common"; -import { ExecutionContext } from "@nestjs/common"; -import { Reflector } from "@nestjs/core"; - -describe("PlatformPlanGuard", () => { - let guard: PlatformPlanGuard; - let reflector: Reflector; - let organizationsRepository: OrganizationsRepository; - let redisService: RedisService; - - const mockContext = createMockExecutionContext({ - params: { teamId: "1", orgId: "1" }, - user: { id: "1" }, - }); - - beforeEach(async () => { - reflector = new Reflector(); - organizationsRepository = createMock(); - redisService = createMock({ - redis: { - get: jest.fn().mockResolvedValue(null), - set: jest.fn().mockResolvedValue(null), - }, - }); - guard = new PlatformPlanGuard(reflector, organizationsRepository, redisService); - }); - - it("should be defined", () => { - expect(guard).toBeDefined(); - }); - - it("should return true", async () => { - jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS"); - jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue({ - isPlatform: true, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - platformBilling: { - subscriptionId: "sub_123", - plan: "SCALE", - }, - }); - - await expect(guard.canActivate(mockContext)).resolves.toBe(true); - }); - - it("should throw ForbiddenException if the organization does not exist", async () => { - jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS"); - jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue(null); - - await expect(guard.canActivate(mockContext)).rejects.toThrow(ForbiddenException); - }); - - it("should return true if the organization is not platform", async () => { - jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS"); - jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue({ - isPlatform: false, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - platformBilling: undefined, - }); - - await expect(guard.canActivate(mockContext)).resolves.toBe(true); - }); - - it("should throw ForbiddenException if the organization has no subscription", async () => { - jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS"); - jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue({ - isPlatform: true, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - platformBilling: { - subscriptionId: null, - plan: "STARTER", - }, - }); - - await expect(guard.canActivate(mockContext)).rejects.toThrow(ForbiddenException); - }); - - it("should throw ForbiddenException if the user has a lower plan than required", async () => { - jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS"); - jest.spyOn(organizationsRepository, "findByIdIncludeBilling").mockResolvedValue({ - isPlatform: true, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - platformBilling: { - subscriptionId: "sub_123", - plan: "STARTER", - }, - }); - - await expect(guard.canActivate(mockContext)).rejects.toThrow(ForbiddenException); - }); - - it("should return true if the result is cached in Redis", async () => { - jest.spyOn(reflector, "get").mockReturnValue("ESSENTIALS"); - jest.spyOn(redisService.redis, "get").mockResolvedValue(JSON.stringify(true)); - - await expect(guard.canActivate(mockContext)).resolves.toBe(true); - }); - - function createMockExecutionContext(context: Record): ExecutionContext { - return createMock({ - switchToHttp: () => ({ - getRequest: () => ({ - params: context.params, - user: context.user, - }), - }), - }); - } -}); diff --git a/apps/api/v2/src/modules/auth/guards/billing/platform-plan.guard.ts b/apps/api/v2/src/modules/auth/guards/billing/platform-plan.guard.ts deleted file mode 100644 index 2079c9731354c0..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/billing/platform-plan.guard.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; -import { PlatformPlanType, orderedPlans } from "@/modules/billing/types"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { RedisService } from "@/modules/redis/redis.service"; -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; -import { Reflector } from "@nestjs/core"; -import { Request } from "express"; - -@Injectable() -export class PlatformPlanGuard implements CanActivate { - constructor( - private reflector: Reflector, - private readonly organizationsRepository: OrganizationsRepository, - private readonly redisService: RedisService - ) {} - - async canActivate(context: ExecutionContext): Promise { - const minimumPlan = this.reflector.get(PlatformPlan, context.getHandler()) as PlatformPlanType; - if (!minimumPlan) { - return true; - } - - const request = context.switchToHttp().getRequest(); - const orgId = request.params.orgId; - const user = request.user as ApiAuthGuardUser; - if (!user) { - throw new ForbiddenException("PlatformPlanGuard - No user associated with the request."); - } - if (!orgId) { - throw new ForbiddenException("PlatformPlanGuard - No organization associated with the request."); - } - - return await this.checkPlatformPlanAccess(orgId, minimumPlan); - } - - async checkPlatformPlanAccess(orgId: string, minimumPlan: PlatformPlanType) { - const REDIS_CACHE_KEY = `apiv2:org:${orgId}:guard:platformbilling:${minimumPlan}`; - const cachedValue = await this.redisService.redis.get(REDIS_CACHE_KEY); - if (cachedValue !== null) { - return cachedValue === "true"; - } - - const organization = await this.organizationsRepository.findByIdIncludeBilling(Number(orgId)); - const isPlatform = organization?.isPlatform; - const hasSubscription = organization?.platformBilling?.subscriptionId; - - if (!organization) { - throw new ForbiddenException(`PlatformPlanGuard - No organization found with id=${orgId}.`); - } - if (!isPlatform) { - await this.redisService.redis.set(REDIS_CACHE_KEY, "true", "EX", 300); - return true; - } - if (!hasSubscription) { - throw new ForbiddenException( - `PlatformPlanGuard - No platform subscription found for organization with id=${orgId}.` - ); - } - if ( - !hasMinimumPlan({ - currentPlan: organization.platformBilling?.plan as PlatformPlanType, - minimumPlan: minimumPlan, - plans: orderedPlans, - }) - ) { - throw new ForbiddenException( - `PlatformPlanGuard - organization with id=${orgId} does not have required plan for this operation. Minimum plan is ${minimumPlan} while the organization has ${ - organization.platformBilling?.plan || "undefined" - }.` - ); - } - - await this.redisService.redis.set(REDIS_CACHE_KEY, "true", "EX", 300); - return true; - } -} - -type HasMinimumPlanProp = { - currentPlan: PlatformPlanType; - minimumPlan: PlatformPlanType; - plans: readonly PlatformPlanType[]; -}; - -export function hasMinimumPlan(props: HasMinimumPlanProp): boolean { - const currentPlanIndex = props.plans.indexOf(props.currentPlan); - const minimumPlanIndex = props.plans.indexOf(props.minimumPlan); - - if (currentPlanIndex === -1 || minimumPlanIndex === -1) { - throw new ForbiddenException( - `PlatformPlanGuard - Invalid platform billing plan provided. Current plan: ${props.currentPlan}, Minimum plan: ${props.minimumPlan}` - ); - } - - return currentPlanIndex >= minimumPlanIndex; -} diff --git a/apps/api/v2/src/modules/auth/guards/memberships/is-membership-in-org.guard.ts b/apps/api/v2/src/modules/auth/guards/memberships/is-membership-in-org.guard.ts deleted file mode 100644 index dbb8274850a495..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/memberships/is-membership-in-org.guard.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository"; -import { - Injectable, - CanActivate, - ExecutionContext, - ForbiddenException, - NotFoundException, -} from "@nestjs/common"; -import { Request } from "express"; - -import type { Membership } from "@calcom/prisma/client"; - -@Injectable() -export class IsMembershipInOrg implements CanActivate { - constructor(private organizationsMembershipRepository: OrganizationsMembershipRepository) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const membershipId: string = request.params.membershipId; - const orgId: string = request.params.orgId; - - if (!orgId) { - throw new ForbiddenException("IsMembershipInOrg - No org id found in request params."); - } - - if (!membershipId) { - throw new ForbiddenException("IsMembershipInOrg - No membership id found in request params."); - } - - const membership = await this.organizationsMembershipRepository.findOrgMembership( - Number(orgId), - Number(membershipId) - ); - - if (!membership) { - throw new NotFoundException(`IsMembershipInOrg - Membership (${membershipId}) not found.`); - } - - request.membership = membership; - return true; - } -} diff --git a/apps/api/v2/src/modules/auth/guards/organization-roles/organization-roles.guard.ts b/apps/api/v2/src/modules/auth/guards/organization-roles/organization-roles.guard.ts deleted file mode 100644 index fbdedc0f145ae2..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/organization-roles/organization-roles.guard.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator"; -import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { OrganizationsService } from "@/modules/organizations/index/organizations.service"; -import { UsersService } from "@/modules/users/services/users.service"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; -import { Reflector } from "@nestjs/core"; - -import { MembershipRole } from "@calcom/platform-libraries"; - -@Injectable() -export class OrganizationRolesGuard implements CanActivate { - constructor( - private reflector: Reflector, - private organizationsService: OrganizationsService, - private membershipRepository: MembershipsRepository, - private usersService: UsersService - ) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const user: UserWithProfile = request.user; - const organizationId = this.getOrganizationId(context); - - if (!user || !organizationId) { - throw new ForbiddenException("OrganizationRolesGuard - No organization associated with the user."); - } - - await this.isPlatform(organizationId); - - const membership = await this.membershipRepository.findOrgUserMembership(organizationId, user.id); - const allowedRoles = this.reflector.get(MembershipRoles, context.getHandler()); - - this.isMembershipAccepted(membership.accepted); - this.isRoleAllowed(membership.role, allowedRoles); - - return true; - } - - async isPlatform(organizationId: number) { - const isPlatform = await this.organizationsService.isPlatform(organizationId); - if (!isPlatform) { - throw new ForbiddenException("OrganizationRolesGuard - Organization is not a platform (SHP)."); - } - } - - getOrganizationId(context: ExecutionContext) { - const request = context.switchToHttp().getRequest(); - const user: UserWithProfile = request.user; - const authMethodOrganizationId = request.organizationId; - if (authMethodOrganizationId) return authMethodOrganizationId; - - const userOrganizationId = user ? this.usersService.getUserMainOrgId(user) : null; - return userOrganizationId; - } - - isMembershipAccepted(accepted: boolean) { - if (!accepted) { - throw new ForbiddenException( - `OrganizationRolesGuard - User has not accepted membership in the organization.` - ); - } - } - - isRoleAllowed(membershipRole: MembershipRole, allowedRoles: MembershipRole[]) { - if (!allowedRoles?.length || !Object.keys(allowedRoles)?.length) { - return true; - } - - const hasRequiredRole = allowedRoles.includes(membershipRole); - if (!hasRequiredRole) { - throw new ForbiddenException( - `OrganizationRolesGuard - User must have one of the roles: ${allowedRoles.join(", ")}.` - ); - } - } -} diff --git a/apps/api/v2/src/modules/auth/guards/organizations/is-admin-api-enabled.guard.ts b/apps/api/v2/src/modules/auth/guards/organizations/is-admin-api-enabled.guard.ts deleted file mode 100644 index 410ce2902f8192..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/organizations/is-admin-api-enabled.guard.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { RedisService } from "@/modules/redis/redis.service"; -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; -import { Request } from "express"; - -import type { Team } from "@calcom/prisma/client"; - -type CachedData = { - org?: Team; - canAccess?: boolean; -}; - -@Injectable() -export class IsAdminAPIEnabledGuard implements CanActivate { - constructor( - private organizationsRepository: OrganizationsRepository, - private readonly redisService: RedisService - ) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const organizationId: string = request.params.orgId; - - if (!organizationId) { - throw new ForbiddenException("IsAdminAPIEnabledGuard - No organization id found in request params."); - } - - const { canAccess, organization } = await this.checkAdminAPIEnabled(organizationId); - if (organization) { - request.organization = organization; - } - if (!canAccess) { - throw new ForbiddenException( - `IsAdminAPIEnabledGuard - Organization with id=${organizationId} does not have Admin API access. Please contact https://cal.com/sales to upgrade.` - ); - } - return true; - } - - async checkAdminAPIEnabled( - organizationId: string - ): Promise<{ canAccess: boolean; organization?: Team | null }> { - let canAccess = false; - const REDIS_CACHE_KEY = `apiv2:org:${organizationId}:guard:isAdminAccess`; - const cachedData = await this.redisService.redis.get(REDIS_CACHE_KEY); - - if (cachedData) { - const { org: cachedOrg, canAccess: cachedCanAccess } = JSON.parse(cachedData) as CachedData; - if (cachedOrg?.id === Number(organizationId) && cachedCanAccess !== undefined) { - return { - canAccess: cachedCanAccess, - organization: cachedOrg, - }; - } - } - - const org = await this.organizationsRepository.findById({ id: Number(organizationId) }); - - if (org?.isOrganization && !org?.isPlatform) { - const adminAPIAccessIsEnabledInOrg = await this.organizationsRepository.fetchOrgAdminApiStatus( - Number(organizationId) - ); - if (!adminAPIAccessIsEnabledInOrg) { - throw new ForbiddenException( - `IsAdminAPIEnabledGuard - Organization does not have Admin API access, please contact https://cal.com/sales to upgrade` - ); - } - } - canAccess = true; - - if (org && canAccess) { - await this.redisService.redis.set( - REDIS_CACHE_KEY, - JSON.stringify({ org: org, canAccess } satisfies CachedData), - "EX", - 300 - ); - } - - return { canAccess, organization: org }; - } -} diff --git a/apps/api/v2/src/modules/auth/guards/organizations/is-managed-org-in-manager-org.guard.ts b/apps/api/v2/src/modules/auth/guards/organizations/is-managed-org-in-manager-org.guard.ts deleted file mode 100644 index fdf255ab36a77d..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/organizations/is-managed-org-in-manager-org.guard.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ManagedOrganizationsRepository } from "@/modules/organizations/organizations/managed-organizations.repository"; -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; -import { Request } from "express"; - -import type { Team } from "@calcom/prisma/client"; - -@Injectable() -export class IsManagedOrgInManagerOrg implements CanActivate { - constructor(private managedOrganizationsRepository: ManagedOrganizationsRepository) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const managedOrgId: string = request.params.managedOrganizationId; - const managerOrgId: string = request.params.orgId; - - if (!managerOrgId) { - throw new ForbiddenException("IsManagedOrgInManagerOrg - No manager org id found in request params."); - } - - if (!managedOrgId) { - throw new ForbiddenException("IsManagedOrgInManagerOrg - No managed org id found in request params."); - } - - const managedOrganization = await this.managedOrganizationsRepository.getByManagerManagedIds( - Number(managerOrgId), - Number(managedOrgId) - ); - - if (!managedOrganization) { - throw new ForbiddenException( - `IsManagedOrgInManagerOrg - Managed organization with id=${managedOrgId} is not owned by manager organization with id=${managerOrgId}.` - ); - } - - return true; - } -} diff --git a/apps/api/v2/src/modules/auth/guards/organizations/is-org.guard.ts b/apps/api/v2/src/modules/auth/guards/organizations/is-org.guard.ts deleted file mode 100644 index df144a5d881d0b..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/organizations/is-org.guard.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { RedisService } from "@/modules/redis/redis.service"; -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; -import { Request } from "express"; - -import type { Team } from "@calcom/prisma/client"; - -type CachedData = { - org?: Team; - canAccess?: boolean; -}; - -@Injectable() -export class IsOrgGuard implements CanActivate { - constructor( - private organizationsRepository: OrganizationsRepository, - private readonly redisService: RedisService - ) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const organizationId: string = request.params.orgId; - - if (!organizationId) { - throw new ForbiddenException("IsOrgGuard - No organization id found in request params."); - } - - const { canAccess, org } = await this.checkOrgAccess(organizationId); - - if (canAccess && org) { - request.organization = org; - } - - if (!canAccess) { - throw new ForbiddenException( - `IsOrgGuard - provided organization id=${organizationId} does not represent any existing organization.` - ); - } - - return true; - } - - async checkOrgAccess(organizationId: string): Promise<{ canAccess: boolean; org?: Team | null }> { - const REDIS_CACHE_KEY = `apiv2:org:${organizationId}:guard:isOrg`; - let canAccess = false; - const cachedData = await this.redisService.redis.get(REDIS_CACHE_KEY); - - if (cachedData) { - const { org: cachedOrg, canAccess: cachedCanAccess } = JSON.parse(cachedData) as CachedData; - if (cachedOrg?.id === Number(organizationId) && cachedCanAccess !== undefined) { - return { - org: cachedOrg, - canAccess: cachedCanAccess, - }; - } - } - - const org = await this.organizationsRepository.findById({ id: Number(organizationId) }); - - if (org?.isOrganization) { - canAccess = true; - } - - if (org && canAccess) { - await this.redisService.redis.set( - REDIS_CACHE_KEY, - JSON.stringify({ org, canAccess } satisfies CachedData), - "EX", - 300 - ); - } - - return { canAccess, org }; - } -} diff --git a/apps/api/v2/src/modules/auth/guards/organizations/is-user-routing-form.guard.ts b/apps/api/v2/src/modules/auth/guards/organizations/is-user-routing-form.guard.ts deleted file mode 100644 index ad4042adafff64..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/organizations/is-user-routing-form.guard.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; -import { Request } from "express"; - -import type { Team } from "@calcom/prisma/client"; - -@Injectable() -export class IsUserRoutingForm implements CanActivate { - constructor(private readonly dbRead: PrismaReadService) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const routingFormId: string = request.params.routingFormId; - const user = request.user as ApiAuthGuardUser; - if (!routingFormId) { - throw new ForbiddenException("IsUserRoutingForm - No routing form id found in request params."); - } - - const userRoutingForm = await this.dbRead.prisma.app_RoutingForms_Form.findFirst({ - where: { - id: routingFormId, - userId: Number(user.id), - teamId: null, - }, - select: { - id: true, - }, - }); - - if (!userRoutingForm) { - throw new ForbiddenException( - `Routing Form with id=${routingFormId} is not a user Routing Form owned by user with id=${user.id}.` - ); - } - - return true; - } -} diff --git a/apps/api/v2/src/modules/auth/guards/organizations/is-webhook-in-org.guard.ts b/apps/api/v2/src/modules/auth/guards/organizations/is-webhook-in-org.guard.ts deleted file mode 100644 index 7fe3e03e544d80..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/organizations/is-webhook-in-org.guard.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsWebhooksRepository } from "@/modules/organizations/webhooks/organizations-webhooks.repository"; -import { RedisService } from "@/modules/redis/redis.service"; -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; -import { Request } from "express"; - -import type { Team } from "@calcom/prisma/client"; - -type CachedData = { - org?: Team; - canAccess?: boolean; -}; - -@Injectable() -export class IsWebhookInOrg implements CanActivate { - constructor( - private organizationsRepository: OrganizationsRepository, - private organizationsWebhooksRepository: OrganizationsWebhooksRepository, - private readonly redisService: RedisService - ) {} - - async canActivate(context: ExecutionContext): Promise { - let canAccess = false; - const request = context.switchToHttp().getRequest(); - const webhookId: string = request.params.webhookId; - const organizationId: string = request.params.orgId; - - if (!organizationId) { - throw new ForbiddenException("IsWebhookInOrg - No organization id found in request params."); - } - if (!webhookId) { - throw new ForbiddenException("IsWebhookInOrg - No webhook id found in request params."); - } - - const REDIS_CACHE_KEY = `apiv2:org:${webhookId}:guard:isWebhookInOrg`; - const cachedData = await this.redisService.redis.get(REDIS_CACHE_KEY); - - if (cachedData) { - const { org: cachedOrg, canAccess: cachedCanAccess } = JSON.parse(cachedData) as CachedData; - if (cachedOrg?.id === Number(organizationId) && cachedCanAccess !== undefined) { - request.organization = cachedOrg; - return cachedCanAccess; - } - } - - const org = await this.organizationsRepository.findById({ id: Number(organizationId) }); - - if (org?.isOrganization) { - const isWebhookInOrg = await this.organizationsWebhooksRepository.findWebhook( - Number(organizationId), - webhookId - ); - if (isWebhookInOrg) canAccess = true; - } - - if (org && canAccess) { - await this.redisService.redis.set( - REDIS_CACHE_KEY, - JSON.stringify({ org: org, canAccess } satisfies CachedData), - "EX", - 300 - ); - } - - if (!canAccess) { - throw new ForbiddenException( - `IsWebhookInOrg - webhook with id=${webhookId} is not part of the organization with id=${organizationId}.` - ); - } - - return true; - } -} diff --git a/apps/api/v2/src/modules/auth/guards/pbac/pbac.guard.ts b/apps/api/v2/src/modules/auth/guards/pbac/pbac.guard.ts index 6845c504a196a6..733fd4dab60249 100644 --- a/apps/api/v2/src/modules/auth/guards/pbac/pbac.guard.ts +++ b/apps/api/v2/src/modules/auth/guards/pbac/pbac.guard.ts @@ -1,177 +1,17 @@ -import { Pbac } from "@/modules/auth/decorators/pbac/pbac.decorator"; -import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { RedisService } from "@/modules/redis/redis.service"; -import { - Injectable, - CanActivate, - ExecutionContext, - ForbiddenException, - UnauthorizedException, - BadRequestException, -} from "@nestjs/common"; +import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; -import { Request } from "express"; - -import type { PermissionString } from "@calcom/platform-libraries/pbac"; -import { PermissionCheckService, FeaturesRepository } from "@calcom/platform-libraries/pbac"; +import { Pbac } from "@/modules/auth/decorators/pbac/pbac.decorator"; -export const REDIS_PBAC_CACHE_KEY = (teamId: number) => `apiv2:team:${teamId}:has:pbac:guard:pbac`; -export const REDIS_REQUIRED_PERMISSIONS_CACHE_KEY = ( - userId: number, - teamId: number, - requiredPermissions: PermissionString[] -) => - `apiv2:user:${userId}:team:${teamId}:requiredPermissions:${requiredPermissions - .sort() - .join(",")}:guard:pbac`; +// PBAC (Permission-Based Access Control) is not available in community edition. +// This guard always allows access since there is no permission check service. @Injectable() export class PbacGuard implements CanActivate { - constructor( - private reflector: Reflector, - private prismaReadService: PrismaReadService, - private readonly redisService: RedisService - ) {} + constructor(private reflector: Reflector) {} async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const user = request.user as ApiAuthGuardUser; - const teamId = request.params.teamId; - const orgId = request.params.orgId; - const requiredPermissions = this.reflector.get(Pbac, context.getHandler()); - - const effectiveTeamId = teamId || orgId; - if (!user) { - throw new UnauthorizedException("PbacGuard - the request does not have an authorized user provided"); - } - if (!effectiveTeamId) { - throw new BadRequestException( - "PbacGuard - can't check pbac because no teamId or orgId provided within the request url" - ); - } - - if (!requiredPermissions || requiredPermissions.length === 0) { - request.pbacAuthorizedRequest = false; - return true; - } - - const hasPbacEnabled = await this.hasPbacEnabled(Number(effectiveTeamId)); - if (!hasPbacEnabled) { - request.pbacAuthorizedRequest = false; - return true; - } - - const hasRequiredPermissions = await this.checkUserHasRequiredPermissions( - user.id, - Number(effectiveTeamId), - requiredPermissions - ); - - if (!hasRequiredPermissions) { - request.pbacAuthorizedRequest = false; - return true; - } - - request.pbacAuthorizedRequest = true; + const request = context.switchToHttp().getRequest<{ pbacAuthorizedRequest?: boolean }>(); + request.pbacAuthorizedRequest = false; return true; } - - async hasPbacEnabled(teamId: number) { - const cachedHasPbacEnabled = await this.getCachePbacEnabled(teamId); - - if (cachedHasPbacEnabled) { - return cachedHasPbacEnabled; - } - - const pbacFeatureFlag = "pbac"; - const featuresRepository = new FeaturesRepository(this.prismaReadService.prisma); - const hasPbacEnabled = await featuresRepository.checkIfTeamHasFeature(teamId, pbacFeatureFlag); - - if (hasPbacEnabled) { - await this.setCachePbacEnabled(teamId, hasPbacEnabled); - } - - return hasPbacEnabled; - } - - async checkUserHasRequiredPermissions( - userId: number, - teamId: number, - requiredPermissions: PermissionString[] - ) { - const cachedAccess = await this.getCacheRequiredPermissions(userId, teamId, requiredPermissions); - - if (cachedAccess) { - return cachedAccess; - } - - const permissionCheckService = new PermissionCheckService(); - const hasRequiredPermissions = await permissionCheckService.checkPermissions({ - userId, - teamId, - permissions: requiredPermissions, - fallbackRoles: [], - }); - - if (hasRequiredPermissions) { - await this.setCacheRequiredPermissions(userId, teamId, requiredPermissions, hasRequiredPermissions); - } - - return hasRequiredPermissions; - } - - private async getCacheRequiredPermissions( - userId: number, - teamId: number, - requiredPermissions: PermissionString[] - ): Promise { - return this.redisService.get( - REDIS_REQUIRED_PERMISSIONS_CACHE_KEY(userId, teamId, requiredPermissions) - ); - } - - private async setCacheRequiredPermissions( - userId: number, - teamId: number, - requiredPermissions: PermissionString[], - hasRequired: boolean - ): Promise { - await this.redisService.set( - REDIS_REQUIRED_PERMISSIONS_CACHE_KEY(userId, teamId, requiredPermissions), - hasRequired, - { ttl: 300_000 } - ); - } - - private async getCachePbacEnabled(teamId: number) { - const cachedResult = await this.redisService.get(REDIS_PBAC_CACHE_KEY(teamId)); - return cachedResult; - } - - private async setCachePbacEnabled(teamId: number, pbacEnabled: boolean) { - await this.redisService.set(REDIS_PBAC_CACHE_KEY(teamId), pbacEnabled, { - ttl: 300_000, - }); - } - - throwForbiddenError( - userId: number, - teamId: string, - orgId: string, - requiredPermissions: PermissionString[] - ) { - let errorMessage = `PbacGuard - user with id=${userId} does not have the minimum required permissions=${requiredPermissions.join( - "," - )} `; - if (teamId) { - errorMessage += `within team with id=${teamId}`; - } - if (orgId) { - errorMessage += `within organization with id=${orgId}`; - } - errorMessage += `.`; - - throw new ForbiddenException(errorMessage); - } } diff --git a/apps/api/v2/src/modules/auth/guards/routing-forms/is-routing-form-in-team.guard.ts b/apps/api/v2/src/modules/auth/guards/routing-forms/is-routing-form-in-team.guard.ts deleted file mode 100644 index 5398b11d7ae97a..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/routing-forms/is-routing-form-in-team.guard.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { RoutingFormsRepository } from "@/modules/routing-forms/routing-forms.repository"; -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; -import { Request } from "express"; - -import type { Team } from "@calcom/prisma/client"; - -@Injectable() -export class IsRoutingFormInTeam implements CanActivate { - constructor(private routingFormsRepository: RoutingFormsRepository) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const teamId: string = request.params.teamId; - const routingFormId: string = request.params.routingFormId; - - if (!routingFormId) { - throw new ForbiddenException("IsRoutingFormInTeam - No routing form id found in request params."); - } - - if (!teamId) { - throw new ForbiddenException("IsRoutingFormInTeam - No team id found in request params."); - } - - const routingForm = await this.routingFormsRepository.getTeamRoutingForm(Number(teamId), routingFormId); - - if (!routingForm) { - throw new ForbiddenException( - `IsRoutingFormInTeam - team with id=(${teamId}) does not own routing form with id=(${routingFormId}).` - ); - } - - return true; - } -} diff --git a/apps/api/v2/src/modules/auth/guards/teams/is-team-in-org.guard.ts b/apps/api/v2/src/modules/auth/guards/teams/is-team-in-org.guard.ts deleted file mode 100644 index 6fe688668d5903..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/teams/is-team-in-org.guard.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { - Injectable, - CanActivate, - ExecutionContext, - ForbiddenException, - NotFoundException, -} from "@nestjs/common"; -import { Request } from "express"; - -import type { Team } from "@calcom/prisma/client"; - -@Injectable() -export class IsTeamInOrg implements CanActivate { - constructor(private organizationsTeamsRepository: OrganizationsTeamsRepository) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const teamId: string = request.params.teamId; - const orgId: string = request.params.orgId; - - if (!orgId) { - throw new ForbiddenException("IsTeamInOrg - No org id found in request params."); - } - - if (!teamId) { - throw new ForbiddenException("IsTeamInOrg - No team id found in request params."); - } - - const { canAccess, team } = await this.checkIfTeamIsInOrg(orgId, teamId); - - if (!canAccess) { - throw new ForbiddenException( - `IsTeamInOrg - Team with id=${teamId} is not part of the organization with id=${orgId}.` - ); - } - - request.team = team; - return true; - } - - async checkIfTeamIsInOrg(orgId: string, teamId: string): Promise<{ canAccess: boolean; team?: Team }> { - const team = await this.organizationsTeamsRepository.findOrgTeam(Number(orgId), Number(teamId)); - - if (!team) { - throw new NotFoundException(`IsTeamInOrg - Team (${teamId}) not found.`); - } - - if (!team.isOrganization && team.parentId === Number(orgId)) { - return { canAccess: true, team }; - } - - return { canAccess: false }; - } -} diff --git a/apps/api/v2/src/modules/auth/guards/users/is-user-in-org-team.guard.ts b/apps/api/v2/src/modules/auth/guards/users/is-user-in-org-team.guard.ts deleted file mode 100644 index 9bc2ae28c6519e..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/users/is-user-in-org-team.guard.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; -import { Request } from "express"; - -import type { Team } from "@calcom/prisma/client"; - -@Injectable() -export class IsUserInOrgTeam implements CanActivate { - constructor(private organizationsRepository: OrganizationsRepository) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const teamId: string = request.params.teamId; - const orgId: string = request.params.orgId; - const userId: string = request.params.userId; - - if (!userId) { - throw new ForbiddenException("IsUserInOrgTeam - No user id found in request params."); - } - - if (!orgId) { - throw new ForbiddenException("IsUserInOrgTeam - No org id found in request params."); - } - - if (!teamId) { - throw new ForbiddenException("IsUserInOrgTeam - No team id found in request params."); - } - - const user = await this.organizationsRepository.findOrgTeamUser( - Number(orgId), - Number(teamId), - Number(userId) - ); - - if (user) { - request.user = user; - return true; - } - - throw new ForbiddenException( - `IsUserInOrgTeam - user with id=(${userId}) is not part of the team with id=(${teamId}) in the organization with id=(${orgId})` - ); - } -} diff --git a/apps/api/v2/src/modules/auth/guards/users/is-user-in-org.guard.ts b/apps/api/v2/src/modules/auth/guards/users/is-user-in-org.guard.ts deleted file mode 100644 index 8e22057d0c84fe..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/users/is-user-in-org.guard.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; -import { Request } from "express"; - -import type { Team } from "@calcom/prisma/client"; - -@Injectable() -export class IsUserInOrg implements CanActivate { - constructor(private organizationsRepository: OrganizationsRepository) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const orgId: string = request.params.orgId; - const userId: string = request.params.userId; - - if (!userId) { - throw new ForbiddenException("IsUserInOrg - No user id found in request params."); - } - - if (!orgId) { - throw new ForbiddenException("IsUserInOrg - No org id found in request params."); - } - - const user = await this.organizationsRepository.findOrgUser(Number(orgId), Number(userId)); - - if (!user) { - throw new ForbiddenException( - `IsUserInOrg - user with id=${userId} is not part of the organization with id=${orgId}.` - ); - } - - request.user = user; - return true; - } -} diff --git a/apps/api/v2/src/modules/auth/guards/workflows/is-event-type-workflow-in-team.ts b/apps/api/v2/src/modules/auth/guards/workflows/is-event-type-workflow-in-team.ts deleted file mode 100644 index a446a512003008..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/workflows/is-event-type-workflow-in-team.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { WorkflowsRepository, WorkflowType } from "@/modules/workflows/workflows.repository"; -import { - Injectable, - CanActivate, - ExecutionContext, - ForbiddenException, - NotFoundException, -} from "@nestjs/common"; -import { Request } from "express"; - -@Injectable() -export class IsEventTypeWorkflowInTeam implements CanActivate { - constructor(private workflowsRepository: WorkflowsRepository) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const teamId: string = request.params.teamId; - const workflowId: string = request.params.workflowId; - - if (!workflowId) { - throw new ForbiddenException("IsWorkflowInTeam - No workflow found in request params."); - } - - if (!teamId) { - throw new ForbiddenException("IsWorkflowInTeam - No team id found in request params."); - } - - const { canAccess, workflow } = await this.checkIfWorkflowIsInTeam(teamId, workflowId); - - if (!canAccess) { - throw new ForbiddenException( - `IsTeamInOrg - Workflow with id=${workflowId} is not part of the team with id=${teamId}.` - ); - } - - request.workflow = workflow; - return true; - } - - async checkIfWorkflowIsInTeam( - teamId: string, - workflowId: string - ): Promise<{ canAccess: boolean; workflow?: WorkflowType }> { - const workflow = await this.workflowsRepository.getEventTypeTeamWorkflowById( - Number(teamId), - Number(workflowId) - ); - - if (!workflow) { - throw new NotFoundException(`IsWorkflowInTeam - event-type workflow (${workflowId}) not found.`); - } - - if (workflow.teamId === Number(teamId)) { - return { canAccess: true, workflow }; - } - - return { canAccess: false }; - } -} diff --git a/apps/api/v2/src/modules/auth/guards/workflows/is-routing-form-workflow-in-team.ts b/apps/api/v2/src/modules/auth/guards/workflows/is-routing-form-workflow-in-team.ts deleted file mode 100644 index 2a86cde7421d7f..00000000000000 --- a/apps/api/v2/src/modules/auth/guards/workflows/is-routing-form-workflow-in-team.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { WorkflowsRepository, WorkflowType } from "@/modules/workflows/workflows.repository"; -import { - Injectable, - CanActivate, - ExecutionContext, - ForbiddenException, - NotFoundException, -} from "@nestjs/common"; -import { Request } from "express"; - -@Injectable() -export class IsRoutingFormWorkflowInTeam implements CanActivate { - constructor(private workflowsRepository: WorkflowsRepository) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const teamId: string = request.params.teamId; - const workflowId: string = request.params.workflowId; - - if (!workflowId) { - throw new ForbiddenException("IsWorkflowInTeam - No workflow found in request params."); - } - - if (!teamId) { - throw new ForbiddenException("IsWorkflowInTeam - No team id found in request params."); - } - - const { canAccess, workflow } = await this.checkIfWorkflowIsInTeam(teamId, workflowId); - - if (!canAccess) { - throw new ForbiddenException( - `IsTeamInOrg - Workflow with id=${workflowId} is not part of the team with id=${teamId}.` - ); - } - - request.workflow = workflow; - return true; - } - - async checkIfWorkflowIsInTeam( - teamId: string, - workflowId: string - ): Promise<{ canAccess: boolean; workflow?: WorkflowType }> { - const workflow = await this.workflowsRepository.getRoutingFormTeamWorkflowById( - Number(teamId), - Number(workflowId) - ); - - if (!workflow) { - throw new NotFoundException(`IsWorkflowInTeam - routing form workflow (${workflowId}) not found.`); - } - - if (workflow.teamId === Number(teamId)) { - return { canAccess: true, workflow }; - } - - return { canAccess: false }; - } -} diff --git a/apps/api/v2/src/modules/auth/oauth2/controllers/oauth2.controller.e2e-spec.ts b/apps/api/v2/src/modules/auth/oauth2/controllers/oauth2.controller.e2e-spec.ts index 5c63af99464f82..5e11844eae0d43 100644 --- a/apps/api/v2/src/modules/auth/oauth2/controllers/oauth2.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/auth/oauth2/controllers/oauth2.controller.e2e-spec.ts @@ -100,7 +100,7 @@ describe("OAuth2 Controller Endpoints", () => { testRedirectUri, scopes, undefined, - teamSlug ?? team.slug + teamSlug ?? team.slug ?? undefined ); const redirectUrl = new URL(result.redirectUrl); return redirectUrl.searchParams.get("code") as string; diff --git a/apps/api/v2/src/modules/auth/oauth2/outputs/oauth2-client.output.ts b/apps/api/v2/src/modules/auth/oauth2/outputs/oauth2-client.output.ts index 905a61e592adb2..4677984f7763aa 100644 --- a/apps/api/v2/src/modules/auth/oauth2/outputs/oauth2-client.output.ts +++ b/apps/api/v2/src/modules/auth/oauth2/outputs/oauth2-client.output.ts @@ -1,8 +1,16 @@ import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; import { OAuthClientType } from "@calcom/prisma/enums"; -import { ApiProperty } from "@nestjs/swagger"; +import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; import { Expose, Type } from "class-transformer"; -import { IsBoolean, IsEnum, IsNotEmptyObject, IsOptional, IsString, ValidateNested } from "class-validator"; +import { + IsArray, + IsBoolean, + IsEnum, + IsNotEmptyObject, + IsOptional, + IsString, + ValidateNested, +} from "class-validator"; export class OAuth2ClientDto { @ApiProperty({ @@ -14,9 +22,16 @@ export class OAuth2ClientDto { client_id!: string; @ApiProperty({ - description: "The redirect URI for the OAuth client", - example: "https://example.com/callback", + description: "The redirect URIs for the OAuth client", + example: ["https://example.com/callback"], + type: [String], }) + @IsArray() + @IsString({ each: true }) + @Expose({ name: "redirectUris" }) + redirect_uris!: string[]; + + @ApiHideProperty() @IsString() @Expose({ name: "redirectUri" }) redirect_uri!: string; diff --git a/apps/api/v2/src/modules/auth/oauth2/services/oauth2-error.service.ts b/apps/api/v2/src/modules/auth/oauth2/services/oauth2-error.service.ts index 30bbb16d4fa566..1638e24d2958bc 100644 --- a/apps/api/v2/src/modules/auth/oauth2/services/oauth2-error.service.ts +++ b/apps/api/v2/src/modules/auth/oauth2/services/oauth2-error.service.ts @@ -7,6 +7,7 @@ import { OAuth2RedirectException } from "@/modules/auth/oauth2/filters/oauth2-re const NON_REDIRECTABLE_REASONS = new Set([ "client_not_found", "client_not_approved", + "client_rejected", "redirect_uri_mismatch", ]); diff --git a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts index 7d192121103352..4712d663c3e202 100644 --- a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts +++ b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts @@ -1,21 +1,7 @@ -import appConfig from "@/config/app"; -import { AuthMethods } from "@/lib/enums/auth-methods"; -import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; -import { DeploymentsRepository } from "@/modules/deployments/deployments.repository"; -import { DeploymentsService } from "@/modules/deployments/deployments.service"; -import { JwtService } from "@/modules/jwt/jwt.service"; -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; -import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { ProfilesModule } from "@/modules/profiles/profiles.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersService } from "@/modules/users/services/users.service"; -import { UsersRepository } from "@/modules/users/users.repository"; +import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants"; +import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client"; import { ExecutionContext, HttpException } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import { ConfigModule } from "@nestjs/config"; +import { ConfigModule, ConfigService } from "@nestjs/config"; import { JwtService as NestJwtService } from "@nestjs/jwt"; import { Test, TestingModule } from "@nestjs/testing"; import { createRequest } from "node-mocks-http"; @@ -28,16 +14,27 @@ import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.reposit import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { MockedRedisService } from "test/mocks/mock-redis-service"; import { randomString } from "test/utils/randomString"; - -import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants"; -import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client"; - import { ApiAuthGuardRequest, + ApiAuthStrategy, ONLY_CLIENT_ID_PROVIDED_MESSAGE, ONLY_CLIENT_SECRET_PROVIDED_MESSAGE, } from "./api-auth.strategy"; -import { ApiAuthStrategy } from "./api-auth.strategy"; +import appConfig from "@/config/app"; +import { AuthMethods } from "@/lib/enums/auth-methods"; +import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; +import { DeploymentsRepository } from "@/modules/deployments/deployments.repository"; +import { DeploymentsService } from "@/modules/deployments/deployments.service"; +import { JwtService } from "@/modules/jwt/jwt.service"; +import { MembershipsModule } from "@/modules/memberships/memberships.module"; +import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; +import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { ProfilesModule } from "@/modules/profiles/profiles.module"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersService } from "@/modules/users/services/users.service"; +import { UsersRepository } from "@/modules/users/users.repository"; describe("ApiAuthStrategy", () => { let strategy: ApiAuthStrategy; @@ -233,7 +230,7 @@ describe("ApiAuthStrategy", () => { authorization: `Bearer cal_test_}`, }, get: (key: string) => - ({ Authorization: `Bearer cal_test_badkey1234`, origin: "http://localhost:3000" }[key]), + ({ Authorization: `Bearer cal_test_badkey1234`, origin: "http://localhost:3000" })[key], }), }), } as ExecutionContext; @@ -255,7 +252,7 @@ describe("ApiAuthStrategy", () => { headers: { [X_CAL_CLIENT_ID]: `${oAuthClient.id}gibberish`, }, - get: (key: string) => ({ origin: "http://localhost:3000" }[key]), + get: (key: string) => ({ origin: "http://localhost:3000" })[key], }), }), } as ExecutionContext; @@ -278,7 +275,7 @@ describe("ApiAuthStrategy", () => { headers: { [X_CAL_SECRET_KEY]: `${oAuthClient.secret}gibberish`, }, - get: (key: string) => ({ origin: "http://localhost:3000" }[key]), + get: (key: string) => ({ origin: "http://localhost:3000" })[key], }), }), } as ExecutionContext; @@ -303,7 +300,7 @@ describe("ApiAuthStrategy", () => { [X_CAL_SECRET_KEY]: `secret`, }, get: (key: string) => - ({ Authorization: `Bearer cal_test_badkey1234`, origin: "http://localhost:3000" }[key]), + ({ Authorization: `Bearer cal_test_badkey1234`, origin: "http://localhost:3000" })[key], }), }), } as ExecutionContext; @@ -327,7 +324,7 @@ describe("ApiAuthStrategy", () => { [X_CAL_SECRET_KEY]: `gibberish`, }, get: (key: string) => - ({ Authorization: `Bearer cal_test_badkey1234`, origin: "http://localhost:3000" }[key]), + ({ Authorization: `Bearer cal_test_badkey1234`, origin: "http://localhost:3000" })[key], }), }), } as ExecutionContext; @@ -346,7 +343,7 @@ describe("ApiAuthStrategy", () => { const context: ExecutionContext = { switchToHttp: () => ({ getRequest: () => ({ - get: (key: string) => ({ Authorization: ``, origin: "http://localhost:3000" }[key]), + get: (key: string) => ({ Authorization: ``, origin: "http://localhost:3000" })[key], }), }), } as ExecutionContext; diff --git a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts index 9f24bea005ee5b..766a23316964e9 100644 --- a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts +++ b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts @@ -3,7 +3,6 @@ import { AuthMethods } from "@/lib/enums/auth-methods"; import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed"; import { BaseStrategy } from "@/lib/passport/strategies/types"; import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; -import { DeploymentsService } from "@/modules/deployments/deployments.service"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; @@ -40,7 +39,6 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") private readonly logger = new Logger("ApiAuthStrategy"); constructor( - private readonly deploymentsService: DeploymentsService, private readonly config: ConfigService, private readonly oauthFlowService: OAuthFlowService, private readonly tokensRepository: TokensRepository, @@ -236,12 +234,6 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") } async apiKeyStrategy(apiKey: string, request: ApiAuthGuardRequest) { - const isLicenseValid = await this.deploymentsService.checkLicense(); - if (!isLicenseValid) { - throw new UnauthorizedException( - "ApiAuthStrategy - api key - Invalid or missing CALCOM_LICENSE_KEY environment variable" - ); - } const strippedApiKey = stripApiKey(apiKey, this.config.get("api.keyPrefix")); const apiKeyHash = sha256Hash(strippedApiKey); const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash); diff --git a/apps/api/v2/src/modules/billing/billing.module.ts b/apps/api/v2/src/modules/billing/billing.module.ts deleted file mode 100644 index 968d6debc12e7f..00000000000000 --- a/apps/api/v2/src/modules/billing/billing.module.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { BullModule } from "@nestjs/bull"; -import { Module } from "@nestjs/common"; -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { PlatformBillingTaskerModule } from "@/lib/modules/platform-billing-tasker.module"; -import { BillingProcessor } from "@/modules/billing/billing.processor"; -import { BillingRepository } from "@/modules/billing/billing.repository"; -import { BillingController } from "@/modules/billing/controllers/billing.controller"; -import { IsUserInBillingOrg } from "@/modules/billing/guards/is-user-in-billing-org"; -import { BillingConfigService } from "@/modules/billing/services/billing.config.service"; -import { BillingService } from "@/modules/billing/services/billing.service"; -import { BillingServiceCachingProxy } from "@/modules/billing/services/billing-service-caching-proxy"; -import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.billing.service"; -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; -import { OrganizationsModule } from "@/modules/organizations/organizations.module"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { UsersModule } from "@/modules/users/users.module"; - -@Module({ - imports: [ - PrismaModule, - StripeModule, - RedisModule, - MembershipsModule, - OrganizationsModule, - BullModule.registerQueue({ - name: "billing", - limiter: { - max: 1, - duration: 1000, - }, - }), - UsersModule, - PlatformBillingTaskerModule, - ], - providers: [ - BillingConfigService, - BillingService, - { - provide: "IBillingService", - useClass: BillingServiceCachingProxy, - }, - BillingRepository, - BillingProcessor, - ManagedOrganizationsBillingService, - OAuthClientRepository, - BookingsRepository_2024_08_13, - IsUserInBillingOrg, - ], - exports: [BillingService, BillingRepository, ManagedOrganizationsBillingService], - controllers: [BillingController], -}) -export class BillingModule {} diff --git a/apps/api/v2/src/modules/billing/billing.processor.ts b/apps/api/v2/src/modules/billing/billing.processor.ts deleted file mode 100644 index 003e28e9ccceca..00000000000000 --- a/apps/api/v2/src/modules/billing/billing.processor.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { BillingRepository } from "@/modules/billing/billing.repository"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { StripeService } from "@/modules/stripe/stripe.service"; -import { Process, Processor } from "@nestjs/bull"; -import { Logger } from "@nestjs/common"; -import { Job } from "bull"; - -export const INCREMENT_JOB = "increment"; -export const BILLING_QUEUE = "billing"; -export type IncrementJobDataType = { - userId: number; -}; - -export type DecrementJobDataType = IncrementJobDataType; - -@Processor(BILLING_QUEUE) -export class BillingProcessor { - private readonly logger = new Logger(BillingProcessor.name); - - constructor( - public readonly stripeService: StripeService, - private readonly billingRepository: BillingRepository, - private readonly teamsRepository: OrganizationsRepository - ) {} - - @Process(INCREMENT_JOB) - async handleIncrement(job: Job) { - const { userId } = job.data; - try { - // get the platform organization of the managed user - const team = await this.teamsRepository.findPlatformOrgFromUserId(userId); - const teamId = team.id; - if (!team.id) { - this.logger.error(`User (${userId}) is not part of the platform organization (${teamId}) `, { - teamId, - userId, - }); - return; - } - - const billingSubscription = await this.billingRepository.getBillingForTeam(teamId); - if (!billingSubscription || !billingSubscription?.subscriptionId) { - this.logger.error(`Team ${teamId} did not have stripe subscription associated to it`, { - teamId, - }); - return; - } - - const stripeSubscription = await this.stripeService - .getStripe() - .subscriptions.retrieve(billingSubscription.subscriptionId); - if (!stripeSubscription?.id) { - this.logger.error(`Failed to retrieve stripe subscription (${billingSubscription.subscriptionId})`, { - teamId, - subscriptionId: billingSubscription.subscriptionId, - }); - return; - } - - const meteredItem = stripeSubscription.items.data.find( - (item) => item.price?.recurring?.usage_type === "metered" - ); - // no metered item found to increase usage, return early - if (!meteredItem) { - this.logger.error(`Stripe subscription (${stripeSubscription.id} is not usage based`, { - teamId, - subscriptionId: stripeSubscription.id, - }); - return; - } - - await this.stripeService.getStripe().subscriptionItems.createUsageRecord(meteredItem.id, { - action: "increment", - quantity: 1, - timestamp: "now", - }); - this.logger.log("Increased organization usage for subscription", { - subscriptionId: billingSubscription.subscriptionId, - teamId, - userId, - itemId: meteredItem.id, - }); - } catch (err) { - this.logger.error("Failed to increase usage for Organization", { - userId, - err, - }); - } - return; - } -} diff --git a/apps/api/v2/src/modules/billing/billing.repository.ts b/apps/api/v2/src/modules/billing/billing.repository.ts deleted file mode 100644 index 8b7cbef2c623b2..00000000000000 --- a/apps/api/v2/src/modules/billing/billing.repository.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { PlatformPlan } from "@/modules/billing/types"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable, Logger } from "@nestjs/common"; - -@Injectable() -export class BillingRepository { - private readonly logger = new Logger("BillingRepository"); - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - getBillingForTeam = (teamId: number) => - this.dbRead.prisma.platformBilling.findUnique({ - where: { - id: teamId, - }, - }); - - async getBillingForTeamBySubscriptionId(subscriptionId: string) { - return this.dbRead.prisma.platformBilling.findFirst({ - where: { - subscriptionId, - }, - }); - } - - async updateTeamBilling( - teamId: number, - billingStart: number, - billingEnd: number, - plan: PlatformPlan, - subscriptionId?: string, - priceId?: string - ) { - return this.dbWrite.prisma.platformBilling.update({ - where: { - id: teamId, - }, - data: { - billingCycleStart: billingStart, - billingCycleEnd: billingEnd, - subscriptionId, - plan: plan.toString(), - overdue: false, - priceId, - }, - }); - } - - async updateBillingOverdue(subId: string, cusId: string, overdue: boolean) { - try { - return this.dbWrite.prisma.platformBilling.updateMany({ - where: { - subscriptionId: subId, - customerId: cusId, - }, - data: { - overdue, - }, - }); - } catch (err) { - this.logger.error("Could not update billing overdue", { - subId, - cusId, - err, - }); - } - } - - async deleteBilling(id: number) { - return this.dbWrite.prisma.platformBilling.delete({ - where: { - id, - }, - }); - } -} diff --git a/apps/api/v2/src/modules/billing/controllers/billing.controller.e2e-spec.ts b/apps/api/v2/src/modules/billing/controllers/billing.controller.e2e-spec.ts deleted file mode 100644 index 93c5bf16c9e01f..00000000000000 --- a/apps/api/v2/src/modules/billing/controllers/billing.controller.e2e-spec.ts +++ /dev/null @@ -1,246 +0,0 @@ -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 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"; -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 { withApiAuth } from "test/utils/withApiAuth"; -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; - const userEmail = `billing-user-${randomString()}@api.com`; - let user: UserWithProfile; - let billing: PlatformBilling; - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture; - let organization: Team; - let organization2: Team; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef); - - organization = await organizationsRepositoryFixture.create({ - name: `billing-organization-${randomString()}`, - isPlatform: true, - }); - organization2 = await organizationsRepositoryFixture.create({ - name: `billing-organization-2-${randomString()}`, - isPlatform: true, - }); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: userEmail, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - await membershipsRepositoryFixture.create({ - role: "OWNER", - team: { connect: { id: organization.id } }, - user: { connect: { id: user.id } }, - accepted: true, - }); - - billing = await platformBillingRepositoryFixture.create(organization.id); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - afterAll(async () => { - userRepositoryFixture.deleteByEmail(user.email); - await app.close(); - }); - - it("/billing/webhook (GET) should not get billing plan for org since it's not set yet", () => { - return request(app.getHttpServer()) - .get(`/v2/billing/${organization.id}/check`) - - .expect(200) - .then(async (res) => { - const data = res.body.data as CheckPlatformBillingResponseDto; - expect(data?.plan).toEqual("FREE"); - }); - }); - - it("/billing/webhook (POST) should set billing free plan for org", () => { - jest.spyOn(StripeService.prototype, "getStripe").mockImplementation( - () => - ({ - webhooks: { - constructEventAsync: async () => { - return { - type: "checkout.session.completed", - data: { - object: { - metadata: { - teamId: organization.id, - plan: "FREE", - }, - mode: "subscription", - }, - }, - }; - }, - }, - }) as unknown as Stripe - ); - - return request(app.getHttpServer()) - .post("/v2/billing/webhook") - .set("stripe-signature", "t=1234567890,v1=random_signature_for_e2e_test") - .expect(200) - .then(async (/* res */) => { - const billing = await platformBillingRepositoryFixture.get(organization.id); - expect(billing?.plan).toEqual("FREE"); - }); - }); - - it("/billing/:orgId/check (GET) should check billing plan for org", () => { - return request(app.getHttpServer()) - .get(`/v2/billing/${organization.id}/check`) - .expect(200) - .then(async (res) => { - const data = res.body.data as CheckPlatformBillingResponseDto; - expect(data?.plan).toEqual("FREE"); - }); - }); - - it("/billing/:organizationId/check (GET) should not be able to check other org plan", () => { - return request(app.getHttpServer()).get(`/v2/billing/${organization2.id}/check`).expect(403); - }); - - it("/billing/webhook (POST) failed payment should set billing free plan to overdue", () => { - jest.spyOn(StripeService.prototype, "getStripe").mockImplementation( - () => - ({ - webhooks: { - constructEventAsync: async () => { - return { - type: "invoice.payment_failed", - data: { - object: { - customer: billing?.customerId, - subscription: billing?.subscriptionId, - }, - }, - }; - }, - }, - }) as unknown as Stripe - ); - return request(app.getHttpServer()) - .post("/v2/billing/webhook") - .set("stripe-signature", "t=1234567890,v1=random_signature_for_e2e_test") - .expect(200) - .then(async (/* res */) => { - const billing = await platformBillingRepositoryFixture.get(organization.id); - expect(billing?.overdue).toEqual(true); - }); - }); - - it("/billing/webhook (POST) success payment should set billing free plan to not overdue", () => { - jest.spyOn(StripeService.prototype, "getStripe").mockImplementation( - () => - ({ - webhooks: { - constructEventAsync: async () => { - return { - type: "invoice.payment_succeeded", - data: { - object: { - customer: billing?.customerId, - subscription: billing?.subscriptionId, - }, - }, - }; - }, - }, - }) as unknown as Stripe - ); - - return request(app.getHttpServer()) - .post("/v2/billing/webhook") - .set("stripe-signature", "t=1234567890,v1=random_signature_for_e2e_test") - .expect(200) - .then(async (/* res */) => { - const billing = await platformBillingRepositoryFixture.get(organization.id); - expect(billing?.overdue).toEqual(false); - }); - }); - - it("/billing/webhook (POST) should delete subscription", () => { - jest.spyOn(StripeService.prototype, "getStripe").mockImplementation( - () => - ({ - webhooks: { - constructEventAsync: async () => { - return { - type: "customer.subscription.deleted", - data: { - object: { - metadata: { - teamId: organization.id, - plan: "FREE", - }, - id: billing?.subscriptionId, - }, - }, - }; - }, - }, - }) as unknown as Stripe - ); - - return request(app.getHttpServer()) - .post("/v2/billing/webhook") - .set("stripe-signature", "t=1234567890,v1=random_signature_for_e2e_test") - .expect(200) - .then(async (/* res */) => { - const billing = await platformBillingRepositoryFixture.get(organization.id); - expect(billing).toBeNull(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/billing/controllers/billing.controller.ts b/apps/api/v2/src/modules/billing/controllers/billing.controller.ts deleted file mode 100644 index b1925612236eb6..00000000000000 --- a/apps/api/v2/src/modules/billing/controllers/billing.controller.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { AppConfig } from "@/config/type"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { ApiAuthGuardOnlyAllow } from "@/modules/auth/decorators/api-auth-guard-only-allow.decorator"; -import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard"; -import { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subscribe-to-plan.input"; -import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto"; -import { SubscribeTeamToBillingResponseDto } from "@/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto"; -import { IsUserInBillingOrg } from "@/modules/billing/guards/is-user-in-billing-org"; -import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface"; -import { StripeService } from "@/modules/stripe/stripe.service"; -import { - Body, - Controller, - Get, - Param, - Post, - Req, - UseGuards, - Headers, - HttpCode, - HttpStatus, - Inject, - Logger, - Delete, - ParseIntPipe, -} from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import { ApiExcludeController } from "@nestjs/swagger"; -import { Request } from "express"; -import Stripe from "stripe"; - -import { ApiResponse } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/billing", - version: API_VERSIONS_VALUES, -}) -@ApiExcludeController(true) -export class BillingController { - private readonly stripeWhSecret: string; - private logger = new Logger("Billing Controller"); - - constructor( - @Inject("IBillingService") private readonly billingService: IBillingService, - public readonly stripeService: StripeService, - private readonly configService: ConfigService - ) { - this.stripeWhSecret = configService.get("stripe.webhookSecret", { infer: true }) ?? ""; - } - - @Get("/:teamId/check") - @UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg) - @MembershipRoles(["OWNER", "ADMIN", "MEMBER"]) - @ApiAuthGuardOnlyAllow(["NEXT_AUTH"]) - async checkTeamBilling( - @Param("teamId", ParseIntPipe) teamId: number - ): Promise> { - const { status, plan } = await this.billingService.getBillingData(teamId); - - return { - status: "success", - data: { - valid: status === "valid", - plan, - }, - }; - } - - @Post("/:teamId/subscribe") - @UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg) - @MembershipRoles(["OWNER", "ADMIN"]) - @ApiAuthGuardOnlyAllow(["NEXT_AUTH"]) - async subscribeTeamToStripe( - @Param("teamId") teamId: number, - @Body() input: SubscribeToPlanInput - ): Promise> { - const customerId = await this.billingService.createTeamBilling(teamId); - const url = await this.billingService.redirectToSubscribeCheckout(teamId, input.plan, customerId); - - return { - status: "success", - data: { - url, - }, - }; - } - - @Post("/:teamId/upgrade") - @UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg) - @MembershipRoles(["OWNER", "ADMIN"]) - @ApiAuthGuardOnlyAllow(["NEXT_AUTH"]) - async upgradeTeamBillingInStripe( - @Param("teamId") teamId: number, - @Body() input: SubscribeToPlanInput - ): Promise> { - const url = await this.billingService.updateSubscriptionForTeam(teamId, input.plan); - - return { - status: "success", - data: { - url, - }, - }; - } - - @Delete("/:teamId/unsubscribe") - @UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg) - @MembershipRoles(["OWNER", "ADMIN"]) - @ApiAuthGuardOnlyAllow(["NEXT_AUTH"]) - async cancelTeamSubscriptionInStripe(@Param("teamId") teamId: number): Promise { - await this.billingService.cancelTeamSubscription(teamId); - - return { - status: "success", - }; - } - - @Post("/webhook") - @HttpCode(HttpStatus.OK) - async stripeWebhook( - @Req() request: Request, - @Headers("stripe-signature") stripeSignature: string - ): Promise { - try { - if (!stripeSignature) { - this.logger.warn("Missing stripe-signature header in webhook request"); - return { - status: "success", - }; - } - - if (!this.stripeWhSecret) { - this.logger.error("Missing STRIPE_WEBHOOK_SECRET configuration"); - return { - status: "success", - }; - } - - const event = await this.billingService.stripeService - .getStripe() - .webhooks.constructEventAsync(request.body, stripeSignature, this.stripeWhSecret); - - switch (event.type) { - case "checkout.session.completed": - await this.billingService.handleStripeCheckoutEvents(event); - break; - case "customer.subscription.updated": - await this.billingService.handleStripePaymentPastDue(event); - break; - case "customer.subscription.deleted": - await this.billingService.handleStripeSubscriptionDeleted(event); - break; - case "invoice.created": - await this.billingService.handleStripeSubscriptionForActiveManagedUsers(event); - break; - case "invoice.payment_failed": - await this.billingService.handleStripePaymentFailed(event); - break; - case "invoice.payment_succeeded": - await this.billingService.handleStripePaymentSuccess(event); - break; - default: - break; - } - - return { - status: "success", - }; - } catch (error) { - if (error instanceof Stripe.errors.StripeSignatureVerificationError) { - this.logger.error("Webhook signature validation failed", error); - return { - status: "success", - }; - } - throw error; - } - } -} diff --git a/apps/api/v2/src/modules/billing/controllers/inputs/subscribe-to-plan.input.ts b/apps/api/v2/src/modules/billing/controllers/inputs/subscribe-to-plan.input.ts deleted file mode 100644 index 0a4ee10ee2feef..00000000000000 --- a/apps/api/v2/src/modules/billing/controllers/inputs/subscribe-to-plan.input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { PlatformPlan } from "@/modules/billing/types"; -import { IsEnum } from "class-validator"; - -export class SubscribeToPlanInput { - @IsEnum(PlatformPlan) - plan!: PlatformPlan; -} diff --git a/apps/api/v2/src/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto.ts b/apps/api/v2/src/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto.ts deleted file mode 100644 index c510fe8c6bee17..00000000000000 --- a/apps/api/v2/src/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class CheckPlatformBillingResponseDto { - valid!: boolean; - - plan?: string; -} diff --git a/apps/api/v2/src/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto.ts b/apps/api/v2/src/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto.ts deleted file mode 100644 index 9bae0150dc8f79..00000000000000 --- a/apps/api/v2/src/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class SubscribeTeamToBillingResponseDto { - url?: string; - action?: "redirect"; -} diff --git a/apps/api/v2/src/modules/billing/guards/is-user-in-billing-org.ts b/apps/api/v2/src/modules/billing/guards/is-user-in-billing-org.ts deleted file mode 100644 index 6187650867e615..00000000000000 --- a/apps/api/v2/src/modules/billing/guards/is-user-in-billing-org.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; -import { Request } from "express"; - -import type { Team } from "@calcom/prisma/client"; - -@Injectable() -export class IsUserInBillingOrg implements CanActivate { - constructor(private organizationsRepository: OrganizationsRepository) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const orgId: string = request.params.teamId; - const userId = (request.user as ApiAuthGuardUser).id; - - if (!userId) { - throw new ForbiddenException("IsUserInBillingOrg - No user id found."); - } - - if (!orgId) { - throw new ForbiddenException("IsUserInBillingOrg - No org id found in request params."); - } - - const user = await this.organizationsRepository.findOrgUser(Number(orgId), Number(userId)); - - if (!user) { - throw new ForbiddenException( - `IsUserInBillingOrg - user with id=${userId} is not part of the organization with id=${orgId}.` - ); - } - - return true; - } -} diff --git a/apps/api/v2/src/modules/billing/interfaces/billing-service.interface.ts b/apps/api/v2/src/modules/billing/interfaces/billing-service.interface.ts deleted file mode 100644 index 7935e5e0b437d2..00000000000000 --- a/apps/api/v2/src/modules/billing/interfaces/billing-service.interface.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { PlatformPlan } from "@/modules/billing/types"; -import type { StripeService } from "@/modules/stripe/stripe.service"; -import Stripe from "stripe"; - -import { PlatformBilling, Team } from "@calcom/prisma/client"; - -export type BillingData = { - team: (Team & { platformBilling: PlatformBilling | null }) | null; - status: "valid" | "no_subscription" | "no_billing"; - plan: PlatformPlan | "none"; -}; - -export interface IBillingService { - getBillingData(teamId: number): Promise; - createTeamBilling(teamId: number): Promise; - redirectToSubscribeCheckout(teamId: number, plan: PlatformPlan, customerId?: string): Promise; - updateSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise; - cancelTeamSubscription(teamId: number): Promise; - handleStripeSubscriptionDeleted(event: Stripe.Event): Promise; - handleStripePaymentSuccess(event: Stripe.Event): Promise; - handleStripePaymentFailed(event: Stripe.Event): Promise; - handleStripePaymentPastDue(event: Stripe.Event): Promise; - handleStripeCheckoutEvents(event: Stripe.Event): Promise; - handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event): Promise; - getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null; - getCustomerIdFromInvoice(invoice: Stripe.Invoice): string | null; - stripeService: StripeService; -} diff --git a/apps/api/v2/src/modules/billing/services/billing-service-caching-proxy.ts b/apps/api/v2/src/modules/billing/services/billing-service-caching-proxy.ts deleted file mode 100644 index 7e45eff371f383..00000000000000 --- a/apps/api/v2/src/modules/billing/services/billing-service-caching-proxy.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { IBillingService, BillingData } from "@/modules/billing/interfaces/billing-service.interface"; -import { BillingService } from "@/modules/billing/services/billing.service"; -import { PlatformPlan } from "@/modules/billing/types"; -import { RedisService } from "@/modules/redis/redis.service"; -import { Injectable } from "@nestjs/common"; -import Stripe from "stripe"; - -export const REDIS_BILLING_CACHE_KEY = (teamId: number) => `apiv2:team:${teamId}:billing`; -export const BILLING_CACHE_TTL_MS = 3_600_000; // 1 hour - -@Injectable() -export class BillingServiceCachingProxy implements IBillingService { - constructor(private readonly billingService: BillingService, private readonly redisService: RedisService) {} - - async getBillingData(teamId: number) { - const cachedBillingData = await this.getBillingCache(teamId); - if (cachedBillingData) { - return cachedBillingData; - } - - const billingData = await this.billingService.getBillingData(teamId); - await this.setBillingCache(teamId, billingData); - return billingData; - } - - private async deleteBillingCache(teamId: number) { - await this.redisService.del(REDIS_BILLING_CACHE_KEY(teamId)); - } - - private async getBillingCache(teamId: number) { - const cachedResult = await this.redisService.get(REDIS_BILLING_CACHE_KEY(teamId)); - return cachedResult; - } - - private async setBillingCache(teamId: number, billingData: BillingData): Promise { - await this.redisService.set(REDIS_BILLING_CACHE_KEY(teamId), billingData, { - ttl: BILLING_CACHE_TTL_MS, - }); - } - - async createTeamBilling(teamId: number): Promise { - return this.billingService.createTeamBilling(teamId); - } - - async redirectToSubscribeCheckout( - teamId: number, - plan: PlatformPlan, - customerId?: string - ): Promise { - return this.billingService.redirectToSubscribeCheckout(teamId, plan, customerId); - } - - async updateSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise { - return this.billingService.updateSubscriptionForTeam(teamId, plan); - } - - async cancelTeamSubscription(teamId: number): Promise { - await this.billingService.cancelTeamSubscription(teamId); - await this.deleteBillingCache(teamId); - } - - async handleStripeSubscriptionDeleted(event: Stripe.Event): Promise { - await this.billingService.handleStripeSubscriptionDeleted(event); - const subscription = event.data.object as Stripe.Subscription; - const teamId = subscription?.metadata?.teamId; - if (teamId) { - await this.deleteBillingCache(Number.parseInt(teamId)); - } - } - - async handleStripePaymentSuccess(event: Stripe.Event): Promise { - await this.billingService.handleStripePaymentSuccess(event); - const invoice = event.data.object as Stripe.Invoice; - const subscriptionId = this.getSubscriptionIdFromInvoice(invoice); - if (subscriptionId) { - const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId( - subscriptionId - ); - if (teamBilling?.id) { - await this.deleteBillingCache(teamBilling.id); - } - } - } - - async handleStripePaymentFailed(event: Stripe.Event): Promise { - await this.billingService.handleStripePaymentFailed(event); - const invoice = event.data.object as Stripe.Invoice; - const subscriptionId = this.getSubscriptionIdFromInvoice(invoice); - if (subscriptionId) { - const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId( - subscriptionId - ); - if (teamBilling?.id) { - await this.deleteBillingCache(teamBilling.id); - } - } - } - - async handleStripePaymentPastDue(event: Stripe.Event): Promise { - await this.billingService.handleStripePaymentPastDue(event); - const subscription = event.data.object as Stripe.Subscription; - const subscriptionId = subscription.id; - if (subscriptionId) { - const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId( - subscriptionId - ); - if (teamBilling?.id) { - await this.deleteBillingCache(teamBilling.id); - } - } - } - - async handleStripeCheckoutEvents(event: Stripe.Event): Promise { - await this.billingService.handleStripeCheckoutEvents(event); - const checkoutSession = event.data.object as Stripe.Checkout.Session; - const teamId = checkoutSession.metadata?.teamId; - if (teamId) { - await this.deleteBillingCache(Number.parseInt(teamId)); - } - } - - async handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event): Promise { - return this.billingService.handleStripeSubscriptionForActiveManagedUsers(event); - } - - getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null { - return this.billingService.getSubscriptionIdFromInvoice(invoice); - } - - getCustomerIdFromInvoice(invoice: Stripe.Invoice): string | null { - return this.billingService.getCustomerIdFromInvoice(invoice); - } - - get stripeService() { - return this.billingService.stripeService; - } -} diff --git a/apps/api/v2/src/modules/billing/services/billing.config.service.ts b/apps/api/v2/src/modules/billing/services/billing.config.service.ts deleted file mode 100644 index e5b19a09c8da7e..00000000000000 --- a/apps/api/v2/src/modules/billing/services/billing.config.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { PlatformPlan } from "@/modules/billing/types"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class BillingConfigService { - private readonly config: Map< - PlatformPlan, - { - base: string; - overage: string; - } - >; - - constructor() { - this.config = new Map< - PlatformPlan, - { - base: string; - overage: string; - } - >(); - - const planKeys = Object.keys(PlatformPlan).filter((key) => isNaN(Number(key))); - for (const key of planKeys) { - this.config.set(PlatformPlan[key.toUpperCase() as keyof typeof PlatformPlan], { - base: process.env[`STRIPE_PRICE_ID_${key}`] ?? "", - overage: process.env[`STRIPE_PRICE_ID_${key}_OVERAGE`] ?? "", - }); - } - } - - get(plan: PlatformPlan): - | { - base: string; - overage: string; - } - | undefined { - return this.config.get(plan); - } -} diff --git a/apps/api/v2/src/modules/billing/services/billing.service.ts b/apps/api/v2/src/modules/billing/services/billing.service.ts deleted file mode 100644 index eeec3a6bbd189a..00000000000000 --- a/apps/api/v2/src/modules/billing/services/billing.service.ts +++ /dev/null @@ -1,513 +0,0 @@ -import { getActiveUserBillingService } from "@calcom/platform-libraries/organizations"; -import { getIncrementUsageIdempotencyKey, getIncrementUsageJobTag } from "@calcom/platform-libraries/tasker"; -import { InjectQueue } from "@nestjs/bull"; -import { - BadRequestException, - Injectable, - InternalServerErrorException, - Logger, - NotFoundException, - OnModuleDestroy, -} from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import { Queue } from "bull"; -import { DateTime } from "luxon"; -import Stripe from "stripe"; -import { AppConfig } from "@/config/type"; -import { PlatformBillingTasker } from "@/lib/services/tasker/platform-billing-tasker.service"; -import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor"; -import { BillingRepository } from "@/modules/billing/billing.repository"; -import { BillingData, IBillingService } from "@/modules/billing/interfaces/billing-service.interface"; -import { BillingConfigService } from "@/modules/billing/services/billing.config.service"; -import { PlatformPlan } from "@/modules/billing/types"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { StripeService } from "@/modules/stripe/stripe.service"; -import { UsersRepository } from "@/modules/users/users.repository"; - -@Injectable() -export class BillingService implements IBillingService, OnModuleDestroy { - private logger = new Logger("BillingService"); - private readonly webAppUrl: string; - - constructor( - private readonly teamsRepository: OrganizationsRepository, - public readonly stripeService: StripeService, - public readonly billingRepository: BillingRepository, - private readonly configService: ConfigService, - private readonly billingConfigService: BillingConfigService, - private readonly usersRepository: UsersRepository, - private readonly platformBillingTasker: PlatformBillingTasker, - @InjectQueue(BILLING_QUEUE) private readonly billingQueue: Queue - ) { - this.webAppUrl = this.configService.get("app.baseUrl", { infer: true }) ?? "https://app.cal.com"; - } - - async getBillingData(teamId: number): Promise { - const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); - - if (teamWithBilling?.platformBilling) { - if (!teamWithBilling?.platformBilling.subscriptionId) { - return { team: teamWithBilling, status: "no_subscription" as const, plan: "none" }; - } else { - return { - team: teamWithBilling, - status: "valid" as const, - plan: teamWithBilling.platformBilling.plan as PlatformPlan, - }; - } - } else { - return { team: teamWithBilling, status: "no_billing" as const, plan: "none" }; - } - } - - async createTeamBilling(teamId: number): Promise { - const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); - let customerId = teamWithBilling?.platformBilling?.customerId ?? ""; - - if (!teamWithBilling?.platformBilling) { - customerId = await this.teamsRepository.createNewBillingRelation(teamId); - - this.logger.log("Team had no Stripe Customer ID, created one for them.", { - id: teamId, - stripeId: customerId, - }); - } - - return customerId; - } - - async redirectToSubscribeCheckout( - teamId: number, - plan: PlatformPlan, - customerId?: string - ): Promise { - const { url } = await this.stripeService.getStripe().checkout.sessions.create({ - customer: customerId, - line_items: [ - { - price: this.billingConfigService.get(plan)?.base, - quantity: 1, - }, - { - price: this.billingConfigService.get(plan)?.overage, - }, - ], - success_url: `${this.webAppUrl}/settings/platform/`, - cancel_url: `${this.webAppUrl}/settings/platform/`, - mode: "subscription", - metadata: { - teamId: teamId.toString(), - plan: plan.toString(), - }, - subscription_data: { - metadata: { - teamId: teamId.toString(), - plan: plan.toString(), - }, - }, - allow_promotion_codes: true, - }); - - if (!url) throw new InternalServerErrorException("Failed to create Stripe session."); - - return url; - } - - async updateSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise { - const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); - const customerId = teamWithBilling?.platformBilling?.customerId; - - if (!customerId) { - throw new NotFoundException("No customer id associated with the team."); - } - - const { url } = await this.stripeService.getStripe().checkout.sessions.create({ - customer: customerId, - success_url: `${this.webAppUrl}/settings/platform/`, - cancel_url: `${this.webAppUrl}/settings/platform/plans`, - mode: "setup", - metadata: { - teamId: teamId.toString(), - plan: plan.toString(), - }, - currency: "usd", - }); - - if (!url) throw new InternalServerErrorException("Failed to create Stripe session."); - - return url; - } - - async setPerBookingSubscriptionForTeam( - teamId: number, - subscriptionId: string, - plan: PlatformPlan - ): Promise> { - const billingCycleStart = DateTime.now().get("day"); - const billingCycleEnd = DateTime.now().plus({ month: 1 }).get("day"); - - return this.billingRepository.updateTeamBilling( - teamId, - billingCycleStart, - billingCycleEnd, - plan, - subscriptionId - ); - } - - async setPerActiveUserSubscriptionForTeam( - teamId: number, - subscriptionId: string, - plan: PlatformPlan, - priceId: string - ): Promise> { - const billingCycleStart = DateTime.now().get("day"); - const billingCycleEnd = DateTime.now().plus({ month: 1 }).get("day"); - - return this.billingRepository.updateTeamBilling( - teamId, - billingCycleStart, - billingCycleEnd, - plan, - subscriptionId, - priceId - ); - } - - async handleStripeSubscriptionDeleted(event: Stripe.Event): Promise { - const subscription = event.data.object as Stripe.Subscription; - const teamId = subscription?.metadata?.teamId; - const plan = PlatformPlan[subscription?.metadata?.plan?.toUpperCase() as keyof typeof PlatformPlan]; - if (teamId && plan) { - const currentBilling = await this.billingRepository.getBillingForTeam(Number.parseInt(teamId, 10)); - if (currentBilling?.subscriptionId === subscription.id) { - await this.billingRepository.deleteBilling(currentBilling.id); - this.logger.log(`Stripe Subscription deleted`, { - customerId: currentBilling.customerId, - subscriptionId: currentBilling.subscriptionId, - teamId, - }); - return; - } - this.logger.log("No platform billing found."); - return; - } - this.logger.log("Webhook received but not pertaining to Platform, discarding."); - return; - } - - getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null { - if (typeof invoice.subscription === "string") { - return invoice.subscription; - } else if (invoice.subscription && typeof invoice.subscription === "object") { - return invoice.subscription.id; - } else { - return null; - } - } - getCustomerIdFromInvoice(invoice: Stripe.Invoice): string | null { - if (typeof invoice.customer === "string") { - return invoice.customer; - } else if (invoice.customer && typeof invoice.customer === "object") { - return invoice.customer.id; - } else { - return null; - } - } - - async handleStripePaymentSuccess(event: Stripe.Event): Promise { - const invoice = event.data.object as Stripe.Invoice; - const subscriptionId = this.getSubscriptionIdFromInvoice(invoice); - const customerId = this.getCustomerIdFromInvoice(invoice); - if (subscriptionId && customerId) { - await this.billingRepository.updateBillingOverdue(subscriptionId, customerId, false); - } - } - - async handleStripePaymentFailed(event: Stripe.Event): Promise { - const invoice = event.data.object as Stripe.Invoice; - const subscriptionId = this.getSubscriptionIdFromInvoice(invoice); - const customerId = this.getCustomerIdFromInvoice(invoice); - if (subscriptionId && customerId) { - await this.billingRepository.updateBillingOverdue(subscriptionId, customerId, true); - } - } - - async handleStripePaymentPastDue(event: Stripe.Event): Promise { - const invoice = event.data.object as Stripe.Invoice; - const subscriptionId = this.getSubscriptionIdFromInvoice(invoice); - const customerId = this.getCustomerIdFromInvoice(invoice); - - if (subscriptionId && customerId) { - const existingUserSubscription = await this.stripeService - .getStripe() - .subscriptions.retrieve(subscriptionId); - - if (existingUserSubscription.status === "past_due") { - await this.billingRepository.updateBillingOverdue(subscriptionId, customerId, true); - } - - if (existingUserSubscription.status === "active") { - await this.billingRepository.updateBillingOverdue(subscriptionId, customerId, false); - } - } - - if (!subscriptionId || !customerId) { - this.logger.log(`SubscriptionId: ${subscriptionId} or customerId: ${customerId} missing`); - } - } - - async handleStripeCheckoutEvents(event: Stripe.Event): Promise { - const checkoutSession = event.data.object as Stripe.Checkout.Session; - - if (!checkoutSession.metadata?.teamId) { - return; - } - - const teamId = Number.parseInt(checkoutSession.metadata.teamId, 10); - const plan = checkoutSession.metadata.plan; - if (!plan || !teamId) { - this.logger.log("Webhook received but not pertaining to Platform, discarding."); - return; - } - const isPriceIdPresent = Boolean(checkoutSession.metadata?.priceId); - - if (checkoutSession.mode === "subscription" && isPriceIdPresent) { - await this.setPerActiveUserSubscriptionForTeam( - teamId, - checkoutSession.subscription as string, - PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan], - checkoutSession.metadata?.priceId - ); - } - - if (checkoutSession.mode === "subscription" && !isPriceIdPresent) { - await this.setPerBookingSubscriptionForTeam( - teamId, - checkoutSession.subscription as string, - PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan] - ); - } - - if (checkoutSession.mode === "setup") { - await this.updateStripeSubscriptionForTeam(teamId, plan as PlatformPlan); - } - - return; - } - - async handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event): Promise { - const invoice = event.data.object as Stripe.Invoice; - const subscriptionId = this.getSubscriptionIdFromInvoice(invoice); - - if (!subscriptionId) { - throw new NotFoundException("No subscription found for team"); - } - - const teamWithBilling = await this.billingRepository.getBillingForTeamBySubscriptionId(subscriptionId); - - if (teamWithBilling?.plan === "PER_ACTIVE_USER") { - let activeManagedUsersCount = await this.getActiveManagedUsersCount( - subscriptionId, - new Date(invoice.period_start * 1000), - new Date(invoice.period_end * 1000) - ); - - if (activeManagedUsersCount < 0) { - activeManagedUsersCount = 1; - } - - const existingSubscription = await this.stripeService - .getStripe() - .subscriptions.retrieve(subscriptionId); - - const perActiveUserPrice = this.billingConfigService.get(PlatformPlan.PER_ACTIVE_USER)?.base; - const subscriptionItem = existingSubscription.items.data.find( - (item) => item.price?.id === perActiveUserPrice - ); - - if (!subscriptionItem) { - throw new NotFoundException( - "No subscription item found for PER_ACTIVE_USER plan with matching price ID" - ); - } - - await this.stripeService.getStripe().subscriptions.update(subscriptionId, { - items: [{ id: subscriptionItem.id, quantity: activeManagedUsersCount }], - }); - } - } - - async getActiveManagedUsersCount( - subscriptionId: string, - invoiceStart: Date, - invoiceEnd: Date - ): Promise { - if (!invoiceStart || !invoiceEnd) { - this.logger.log("Invoice period start or end date is null"); - return 0; - } - - const activeUserBillingService = getActiveUserBillingService(); - return activeUserBillingService.getActiveUserCountForPlatformOrg(subscriptionId, invoiceStart, invoiceEnd); - } - - async updateStripeSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise { - const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); - - if (!teamWithBilling?.platformBilling || !teamWithBilling?.platformBilling.subscriptionId) { - throw new NotFoundException("Team plan not found"); - } - - const existingUserSubscription = await this.stripeService - .getStripe() - .subscriptions.retrieve(teamWithBilling?.platformBilling?.subscriptionId); - const currentLicensedItem = existingUserSubscription.items.data.find( - (item) => item.price?.recurring?.usage_type === "licensed" - ); - const currentOverageItem = existingUserSubscription.items.data.find( - (item) => item.price?.recurring?.usage_type === "metered" - ); - - if (!currentLicensedItem) { - throw new NotFoundException("There is no licensed item present in the subscription"); - } - - if (!currentOverageItem) { - throw new NotFoundException("There is no overage item present in the subscription"); - } - - await this.stripeService - .getStripe() - .subscriptions.update(teamWithBilling?.platformBilling?.subscriptionId, { - items: [ - { - id: currentLicensedItem.id, - price: this.billingConfigService.get(plan)?.base, - }, - { - id: currentOverageItem.id, - price: this.billingConfigService.get(plan)?.overage, - clear_usage: false, - }, - ], - billing_cycle_anchor: "now", - proration_behavior: "create_prorations", - }); - - await this.setPerBookingSubscriptionForTeam( - teamId, - teamWithBilling?.platformBilling?.subscriptionId, - PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan] - ); - } - /** - * - * Adds a job to the queue to increment usage of a stripe subscription. - * we delay the job until the booking starts. - * the delay ensure we can adapt to cancel / reschedule. - */ - async increaseUsageByUserId( - userId: number, - booking: { - uid: string; - startTime: Date; - fromReschedule?: string | null; - } - ): Promise { - if (this.configService.get("e2e")) { - return true; - } - const { uid, startTime, fromReschedule } = booking; - - if (this.configService.get("enableAsyncTasker")) { - if (fromReschedule) { - this.platformBillingTasker.rescheduleUsageIncrement({ - payload: { bookingUid: uid, rescheduledTime: startTime }, - }); - return true; - } - this.platformBillingTasker.incrementUsage({ - payload: { userId }, - options: { - delay: startTime, - tags: [getIncrementUsageJobTag(uid)], - idempotencyKey: getIncrementUsageIdempotencyKey(uid, userId), - }, - }); - return true; - } - - let delay = startTime.getTime() - Date.now(); - - if (delay < 0) { - delay = 0; - } - - if (fromReschedule) { - // cancel the usage increment job for the booking that is being rescheduled - await this.cancelUsageByBookingUid(fromReschedule); - this.logger.log(`Cancelled usage increment job for rescheduled booking uid: ${fromReschedule}`); - } - await this.billingQueue.add( - INCREMENT_JOB, - { - userId, - } satisfies IncrementJobDataType, - { delay: delay, jobId: `increment-${uid}`, removeOnComplete: true } - ); - this.logger.log(`Added stripe usage increment job for booking ${uid} and user ${userId}`); - } - - /** - * - * Cancels the usage increment job for a booking when it is cancelled. - * Removing an attendee from a booking does not cancel the usage increment job. - */ - async cancelUsageByBookingUid(bookingUid: string): Promise { - if (this.configService.get("e2e")) { - return true; - } - - if (this.configService.get("enableAsyncTasker")) { - await this.platformBillingTasker.cancelUsageIncrement({ payload: { bookingUid } }); - return true; - } - - const job = await this.billingQueue.getJob(`increment-${bookingUid}`); - if (job) { - await job.remove(); - this.logger.log(`Removed increment job for cancelled booking ${bookingUid}`); - } - } - - async cancelTeamSubscription(teamId: number): Promise { - const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); - const customerId = teamWithBilling?.platformBilling?.customerId; - - if (!customerId) { - throw new NotFoundException("No customer id found for team in Stripe"); - } - - if (!teamWithBilling?.platformBilling || !teamWithBilling?.platformBilling.subscriptionId) { - throw new NotFoundException("Team plan not found"); - } - - try { - await this.stripeService - .getStripe() - .subscriptions.cancel(teamWithBilling?.platformBilling?.subscriptionId); - } catch (error) { - this.logger.log(error, "error while cancelling team subscription in stripe"); - throw new BadRequestException("Failed to cancel team subscription"); - } - } - - async onModuleDestroy(): Promise { - try { - await this.billingQueue.close(); - } catch (err) { - this.logger.error(err); - } - } -} diff --git a/apps/api/v2/src/modules/billing/services/managed-organizations.billing.service.ts b/apps/api/v2/src/modules/billing/services/managed-organizations.billing.service.ts deleted file mode 100644 index ff446e59e6b136..00000000000000 --- a/apps/api/v2/src/modules/billing/services/managed-organizations.billing.service.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { hasMinimumPlan } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { orderedPlans, PlatformPlan } from "@/modules/billing/types"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; - -@Injectable() -export class ManagedOrganizationsBillingService { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async createManagedOrganizationBilling(managerOrgId: number, managedOrgId: number) { - const managerOrgBilling = await this.dbRead.prisma.platformBilling.findUnique({ - where: { id: managerOrgId }, - }); - if (!managerOrgBilling) { - throw new NotFoundException("Manager organization billing not found."); - } - if ( - !hasMinimumPlan({ - currentPlan: managerOrgBilling.plan as PlatformPlan, - minimumPlan: PlatformPlan.SCALE, - plans: orderedPlans, - }) - ) { - throw new ForbiddenException( - `organization with id=${managerOrgId} does not have required plan for this operation. Minimum plan is ${ - PlatformPlan.SCALE - } while the organization has ${managerOrgBilling.plan || "undefined"}.` - ); - } - - return this.dbWrite.prisma.platformBilling.create({ - data: { - id: managedOrgId, - customerId: managerOrgBilling.customerId, - subscriptionId: managerOrgBilling.subscriptionId, - plan: managerOrgBilling.plan, - billingCycleStart: managerOrgBilling.billingCycleStart, - billingCycleEnd: managerOrgBilling.billingCycleEnd, - overdue: managerOrgBilling.overdue, - managerBillingId: managerOrgBilling.id, - }, - }); - } -} diff --git a/apps/api/v2/src/modules/billing/types.ts b/apps/api/v2/src/modules/billing/types.ts deleted file mode 100644 index 3576eb46c3e9eb..00000000000000 --- a/apps/api/v2/src/modules/billing/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -export enum PlatformPlan { - FREE = "FREE", - STARTER = "STARTER", - ESSENTIALS = "ESSENTIALS", - SCALE = "SCALE", - ENTERPRISE = "ENTERPRISE", - PER_ACTIVE_USER = "PER_ACTIVE_USER", -} - -export const orderedPlans = [ - "FREE", - "STARTER", - "ESSENTIALS", - "SCALE", - "PER_ACTIVE_USER", - "ENTERPRISE", -] as const; - -export type PlatformPlanType = (typeof orderedPlans)[number]; diff --git a/apps/api/v2/src/modules/cal-unified-calendars/cal-unified-calendars.module.ts b/apps/api/v2/src/modules/cal-unified-calendars/cal-unified-calendars.module.ts index c8d8e5f28c6cbe..e56e36fb72341b 100644 --- a/apps/api/v2/src/modules/cal-unified-calendars/cal-unified-calendars.module.ts +++ b/apps/api/v2/src/modules/cal-unified-calendars/cal-unified-calendars.module.ts @@ -1,9 +1,9 @@ import { Module } from "@nestjs/common"; -import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository"; -import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; -import { GoogleCalendarService as GCalService } from "@/ee/calendars/services/gcal.service"; +import { BookingReferencesRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/booking-references.repository"; +import { CalendarsRepository } from "@/platform/calendars/calendars.repository"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; +import { GoogleCalendarService as GCalService } from "@/platform/calendars/services/gcal.service"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { CalUnifiedCalendarsController } from "@/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller"; import { GoogleCalendarService } from "@/modules/cal-unified-calendars/services/google-calendar.service"; diff --git a/apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts b/apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts index 6f66a3f1720dfc..43d3cf5e8fdb2c 100644 --- a/apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts +++ b/apps/api/v2/src/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller.ts @@ -13,7 +13,7 @@ import { UseGuards, } from "@nestjs/common"; import { ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger"; -import { GetBusyTimesOutput } from "@/ee/calendars/outputs/busy-times.output"; +import { GetBusyTimesOutput } from "@/platform/calendars/outputs/busy-times.output"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; @@ -235,7 +235,7 @@ export class CalUnifiedCalendarsController { @ApiParam({ name: "eventUid", description: - "The Google Calendar event ID. You can retrieve this by getting booking references from the following endpoints:\n\n- For team events: https://cal.com/docs/api-reference/v2/orgs-teams-bookings/get-booking-references-for-a-booking\n\n- For user events: https://cal.com/docs/api-reference/v2/bookings/get-booking-references-for-a-booking", + "The Google Calendar event ID. You can retrieve this by getting booking references from the following endpoints:\n\n- For user events: GET /v2/bookings/{bookingUid}/references", type: String, }) @Get(["/:calendar/events/:eventUid", "/:calendar/event/:eventUid"]) @@ -263,7 +263,7 @@ export class CalUnifiedCalendarsController { @ApiParam({ name: "eventUid", description: - "The Google Calendar event ID. You can retrieve this by getting booking references from the following endpoints:\n\n- For team events: https://cal.com/docs/api-reference/v2/orgs-teams-bookings/get-booking-references-for-a-booking\n\n- For user events: https://cal.com/docs/api-reference/v2/bookings/get-booking-references-for-a-booking", + "The Google Calendar event ID. You can retrieve this by getting booking references from the following endpoints:\n\n- For user events: GET /v2/bookings/{bookingUid}/references", type: String, }) @Patch(["/:calendar/events/:eventUid", "/:calendar/event/:eventUid"]) diff --git a/apps/api/v2/src/modules/cal-unified-calendars/services/google-calendar.service.spec.ts b/apps/api/v2/src/modules/cal-unified-calendars/services/google-calendar.service.spec.ts index 9b8d576a7e3aeb..a6ea7154f840bb 100644 --- a/apps/api/v2/src/modules/cal-unified-calendars/services/google-calendar.service.spec.ts +++ b/apps/api/v2/src/modules/cal-unified-calendars/services/google-calendar.service.spec.ts @@ -44,8 +44,8 @@ import { GOOGLE_CALENDAR_TYPE } from "@calcom/platform-constants"; import { BadRequestException, NotFoundException, UnauthorizedException } from "@nestjs/common"; import { Test, TestingModule } from "@nestjs/testing"; import { GoogleCalendarService } from "./google-calendar.service"; -import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository"; -import { GoogleCalendarService as GCalService } from "@/ee/calendars/services/gcal.service"; +import { BookingReferencesRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/booking-references.repository"; +import { GoogleCalendarService as GCalService } from "@/platform/calendars/services/gcal.service"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; describe("GoogleCalendarService", () => { diff --git a/apps/api/v2/src/modules/cal-unified-calendars/services/google-calendar.service.ts b/apps/api/v2/src/modules/cal-unified-calendars/services/google-calendar.service.ts index 8f2b718375192d..8602f5312c7858 100644 --- a/apps/api/v2/src/modules/cal-unified-calendars/services/google-calendar.service.ts +++ b/apps/api/v2/src/modules/cal-unified-calendars/services/google-calendar.service.ts @@ -16,8 +16,8 @@ import { JWT } from "googleapis-common"; import type { CreateUnifiedCalendarEventInput } from "../inputs/create-unified-calendar-event.input"; import { UpdateUnifiedCalendarEventInput } from "../inputs/update-unified-calendar-event.input"; import { GoogleCalendarEventInputPipe } from "../pipes/google-calendar-event-input-pipe"; -import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository"; -import { GoogleCalendarService as GCalService } from "@/ee/calendars/services/gcal.service"; +import { BookingReferencesRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/booking-references.repository"; +import { GoogleCalendarService as GCalService } from "@/platform/calendars/services/gcal.service"; import { GoogleCalendarEventResponse } from "@/modules/cal-unified-calendars/pipes/get-calendar-event-details-output-pipe"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; diff --git a/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendar.service.ts b/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendar.service.ts index f0cfa921c5af52..f15aa30c1c7793 100644 --- a/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendar.service.ts +++ b/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendar.service.ts @@ -8,7 +8,7 @@ import { } from "@calcom/platform-constants"; import type { ConnectedDestinationCalendars } from "@calcom/platform-libraries"; import { BadRequestException, Injectable } from "@nestjs/common"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; import type { CreateUnifiedCalendarEventInput } from "@/modules/cal-unified-calendars/inputs/create-unified-calendar-event.input"; import type { UpdateUnifiedCalendarEventInput } from "@/modules/cal-unified-calendars/inputs/update-unified-calendar-event.input"; import { diff --git a/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.integration.spec.ts b/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.integration.spec.ts index 276e893bcd1daa..aa5b9e46b43b01 100644 --- a/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.integration.spec.ts +++ b/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.integration.spec.ts @@ -12,7 +12,7 @@ import { import type { ConnectedDestinationCalendars } from "@calcom/platform-libraries"; import { Test, TestingModule } from "@nestjs/testing"; import { UnifiedCalendarsFreebusyService } from "./unified-calendars-freebusy.service"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; describe("UnifiedCalendarsFreebusyService (integration with real platform-libraries types)", () => { let service: UnifiedCalendarsFreebusyService; diff --git a/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.service.spec.ts b/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.service.spec.ts index a67b62482e94b9..1c487798fe8721 100644 --- a/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.service.spec.ts +++ b/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.service.spec.ts @@ -22,7 +22,7 @@ import { import { NotFoundException } from "@nestjs/common"; import { Test, TestingModule } from "@nestjs/testing"; import { UnifiedCalendarsFreebusyService } from "./unified-calendars-freebusy.service"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; describe("UnifiedCalendarsFreebusyService", () => { let service: UnifiedCalendarsFreebusyService; diff --git a/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.service.ts b/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.service.ts index e3c513b1c878e4..b94dcb7a700d74 100644 --- a/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.service.ts +++ b/apps/api/v2/src/modules/cal-unified-calendars/services/unified-calendars-freebusy.service.ts @@ -1,7 +1,7 @@ import { GOOGLE_CALENDAR_TYPE } from "@calcom/platform-constants"; import type { ConnectedDestinationCalendars } from "@calcom/platform-libraries"; import { Injectable } from "@nestjs/common"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; type ConnectedCalendarsList = ConnectedDestinationCalendars["connectedCalendars"]; type CalendarToLoad = { credentialId: number; externalId: string }; diff --git a/apps/api/v2/src/modules/deployments/deployments.service.ts b/apps/api/v2/src/modules/deployments/deployments.service.ts index bbd3c5068e2cb2..ca3a3997bcf7b2 100644 --- a/apps/api/v2/src/modules/deployments/deployments.service.ts +++ b/apps/api/v2/src/modules/deployments/deployments.service.ts @@ -3,13 +3,6 @@ import { RedisService } from "@/modules/redis/redis.service"; import { Injectable } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -const CACHING_TIME = 86400000; // 24 hours in milliseconds - -const getLicenseCacheKey = (key: string) => `api-v2-license-key-goblin-url-${key}`; - -type LicenseCheckResponse = { - status: boolean; -}; @Injectable() export class DeploymentsService { constructor( @@ -18,30 +11,8 @@ export class DeploymentsService { private readonly redisService: RedisService ) {} + // Cal.diy is fully open source — no license key is required. async checkLicense() { - if (this.configService.get("e2e")) { - return true; - } - let licenseKey = this.configService.get("api.licenseKey"); - - if (!licenseKey) { - /** We try to check on DB only if env is undefined */ - const deployment = await this.deploymentsRepository.getDeployment(); - licenseKey = deployment?.licenseKey ?? undefined; - } - - if (!licenseKey) { - return false; - } - const licenseKeyUrl = this.configService.get("api.licenseKeyUrl") + `/${licenseKey}`; - const cachedData = await this.redisService.redis.get(getLicenseCacheKey(licenseKey)); - if (cachedData) { - return (JSON.parse(cachedData) as LicenseCheckResponse)?.status; - } - const response = await fetch(licenseKeyUrl, { mode: "cors" }); - const data = (await response.json()) as LicenseCheckResponse; - const cacheKey = getLicenseCacheKey(licenseKey); - this.redisService.redis.set(cacheKey, JSON.stringify(data), "EX", CACHING_TIME); - return data.status; + return true; } } diff --git a/apps/api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts b/apps/api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts index ea635b63490243..e22cc52c6d9a43 100644 --- a/apps/api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts @@ -12,7 +12,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { randomString } from "test/utils/randomString"; import { AppModule } from "@/app.module"; import { bootstrap } from "@/bootstrap"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { CalendarsService } from "@/platform/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"; @@ -117,7 +117,6 @@ describe("Platform Destination Calendar Endpoints", () => { userId: null, id: 0, delegationCredentialId: null, - domainWideDelegationCredentialId: null, createdAt: new Date(), updatedAt: new Date(), customCalendarReminder: 10, diff --git a/apps/api/v2/src/modules/destination-calendars/destination-calendars.module.ts b/apps/api/v2/src/modules/destination-calendars/destination-calendars.module.ts index 87f383bf915c7d..b81c0381d8777c 100644 --- a/apps/api/v2/src/modules/destination-calendars/destination-calendars.module.ts +++ b/apps/api/v2/src/modules/destination-calendars/destination-calendars.module.ts @@ -1,6 +1,6 @@ -import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { CalendarsRepository } from "@/platform/calendars/calendars.repository"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { DestinationCalendarsController } from "@/modules/destination-calendars/controllers/destination-calendars.controller"; diff --git a/apps/api/v2/src/modules/destination-calendars/services/destination-calendars.service.ts b/apps/api/v2/src/modules/destination-calendars/services/destination-calendars.service.ts index 828369ca1fcb3a..de42ee1972da64 100644 --- a/apps/api/v2/src/modules/destination-calendars/services/destination-calendars.service.ts +++ b/apps/api/v2/src/modules/destination-calendars/services/destination-calendars.service.ts @@ -1,6 +1,6 @@ -import { ConnectedCalendar, Calendar } from "@/ee/calendars/outputs/connected-calendars.output"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { ConnectedCalendar, Calendar } from "@/platform/calendars/outputs/connected-calendars.output"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository"; import { Injectable, NotFoundException } from "@nestjs/common"; diff --git a/apps/api/v2/src/modules/endpoints.module.ts b/apps/api/v2/src/modules/endpoints.module.ts index 8ea63997f2be7c..db4b82ff3c0d71 100644 --- a/apps/api/v2/src/modules/endpoints.module.ts +++ b/apps/api/v2/src/modules/endpoints.module.ts @@ -1,19 +1,11 @@ -import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module"; +import { PlatformEndpointsModule } from "@/platform/platform-endpoints-module"; import { AtomsModule } from "@/modules/atoms/atoms.module"; import { OAuth2Module } from "@/modules/auth/oauth2/oauth2.module"; -import { BillingModule } from "@/modules/billing/billing.module"; import { CalUnifiedCalendarsModule } from "@/modules/cal-unified-calendars/cal-unified-calendars.module"; import { ConferencingModule } from "@/modules/conferencing/conferencing.module"; import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module"; import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module"; -import { OrganizationsBookingsModule } from "@/modules/organizations/bookings/organizations.bookings.module"; -import { OrganizationsRoutingFormsModule } from "@/modules/organizations/routing-forms/organizations-routing-forms.module"; -import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.module"; -import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/bookings/organizations-users-bookings.module"; -import { RouterModule } from "@/modules/router/router.module"; import { StripeModule } from "@/modules/stripe/stripe.module"; -import { TeamsBookingsModule } from "@/modules/teams/bookings/teams-bookings.module"; -import { TeamsSchedulesModule } from "@/modules/teams/schedules/teams-schedules.module"; import { TimezoneModule } from "@/modules/timezones/timezones.module"; import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module"; import type { MiddlewareConsumer, NestModule } from "@nestjs/common"; @@ -26,7 +18,6 @@ import { WebhooksModule } from "./webhooks/webhooks.module"; imports: [ OAuth2Module, OAuthClientModule, - BillingModule, PlatformEndpointsModule, TimezoneModule, UsersModule, @@ -36,14 +27,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module"; StripeModule, ConferencingModule, CalUnifiedCalendarsModule, - OrganizationsTeamsBookingsModule, - OrganizationsUsersBookingsModule, - OrganizationsBookingsModule, - OrganizationsRoutingFormsModule, VerifiedResourcesModule, - RouterModule, - TeamsSchedulesModule, - TeamsBookingsModule, ], }) export class EndpointsModule implements NestModule { diff --git a/apps/api/v2/src/modules/event-types/guards/event-type-ownership.guard.ts b/apps/api/v2/src/modules/event-types/guards/event-type-ownership.guard.ts index ea745df74a6b13..38546c70feadf6 100644 --- a/apps/api/v2/src/modules/event-types/guards/event-type-ownership.guard.ts +++ b/apps/api/v2/src/modules/event-types/guards/event-type-ownership.guard.ts @@ -1,4 +1,4 @@ -import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; +import { EventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/event-types.service"; import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; import { BadRequestException, diff --git a/apps/api/v2/src/modules/event-types/services/event-type-access.service.ts b/apps/api/v2/src/modules/event-types/services/event-type-access.service.ts index 00de2547f1ae84..0f68a8a3cead03 100644 --- a/apps/api/v2/src/modules/event-types/services/event-type-access.service.ts +++ b/apps/api/v2/src/modules/event-types/services/event-type-access.service.ts @@ -1,4 +1,4 @@ -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { MembershipsService } from "@/modules/memberships/services/memberships.service"; diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts deleted file mode 100644 index 42d2fc489c3e7e..00000000000000 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts +++ /dev/null @@ -1,1043 +0,0 @@ -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"; -import { Locales } from "@/lib/enums/locales"; -import { CreateManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output"; -import { GetManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-user.output"; -import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output"; -import { KeysResponseDto } from "@/modules/oauth-clients/controllers/oauth-flow/responses/KeysResponse.dto"; -import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; -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"; - -const CLIENT_REDIRECT_URI = "http://localhost:4321"; - -describe("OAuth Client Users Endpoints", () => { - describe("Not authenticated", () => { - let app: INestApplication; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [PrismaExceptionFilter, HttpExceptionFilter], - imports: [AppModule, UsersModule], - }).compile(); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - describe("secret header not set", () => { - it(`/POST`, () => { - return request(app.getHttpServer()) - .post("/api/v2/oauth-clients/100/users") - .send({ email: "bob@gmail.com" }) - .expect(401); - }); - }); - - describe("Bearer access token not set", () => { - it(`/GET/:id`, () => { - return request(app.getHttpServer()).get("/api/v2/oauth-clients/100/users/200").expect(401); - }); - it(`/PUT/:id`, () => { - return request(app.getHttpServer()).patch("/api/v2/oauth-clients/100/users/200").expect(401); - }); - it(`/DELETE/:id`, () => { - return request(app.getHttpServer()).delete("/api/v2/oauth-clients/100/users/200").expect(401); - }); - }); - - afterAll(async () => { - await app.close(); - }); - }); - - describe("User Authenticated", () => { - let app: INestApplication; - - let oAuthClient: PlatformOAuthClient; - let oAuthClientEventTypesDisabled: PlatformOAuthClient; - let organization: Team; - let userRepositoryFixture: UserRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let schedulesRepositoryFixture: SchedulesRepositoryFixture; - let profilesRepositoryFixture: ProfileRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let postResponseData: CreateManagedUserOutput["data"]; - let postResponseData2: CreateManagedUserOutput["data"]; - - const platformAdminEmail = `oauth-client-users-admin-${randomString()}@api.com`; - let platformAdmin: User; - - const userEmail = `oauth-client-users-user-${randomString(5)}@api.com`; - const userTimeZone = "Europe/Rome"; - const userEmailTwo = `oauth-client-users-user-2-${randomString(5)}@api.com`; - const userTimeZoneTwo = "Europe/Rome"; - let postResponseDataTwo: CreateManagedUserOutput["data"]; - - const userEmail2 = `oauth-client-users-user2-${randomString()}@api.com`; - const userTimeZone2 = "America/New_York"; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [PrismaExceptionFilter, HttpExceptionFilter], - imports: [AppModule, UsersModule], - }).compile(); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef); - profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail }); - - organization = await teamRepositoryFixture.create({ - name: `oauth-client-users-organization-${randomString()}`, - isPlatform: true, - isOrganization: true, - }); - oAuthClient = await createOAuthClient(organization.id); - oAuthClientEventTypesDisabled = await createOAuthClient(organization.id, false); - - await profilesRepositoryFixture.create({ - uid: "asd1qwwqeqw-asddsadasd", - username: platformAdminEmail, - organization: { connect: { id: organization.id } }, - user: { - connect: { id: platformAdmin.id }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: platformAdmin.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - await app.init(); - }); - - async function createOAuthClient(organizationId: number, areDefaultEventTypesEnabled?: boolean) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: [CLIENT_REDIRECT_URI], - permissions: 32, - areDefaultEventTypesEnabled, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - it("should be defined", () => { - expect(oauthClientRepositoryFixture).toBeDefined(); - expect(userRepositoryFixture).toBeDefined(); - expect(oAuthClient).toBeDefined(); - }); - - it(`should fail /POST with incorrect timeZone`, async () => { - const requestBody: CreateManagedUserInput = { - email: userEmail, - timeZone: "incorrect-time-zone", - name: "Alice Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - }; - - await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(400); - }); - - it(`should fail /POST with incorrect timeFormat`, async () => { - const requestBody = { - email: userEmail, - timeZone: userTimeZone, - name: "Alice Smith", - timeFormat: 100, - }; - - await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(400); - }); - - it(`/POST with default event types`, async () => { - const requestBody: CreateManagedUserInput = { - email: userEmail, - timeZone: userTimeZone, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Alice Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - bio: "I am a bio", - metadata: { - key: "value", - }, - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - - postResponseData = responseBody.data; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email) - ); - expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone); - expect(responseBody.data.user.name).toEqual(requestBody.name); - expect(responseBody.data.user.weekStart).toEqual(requestBody.weekStart); - expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat); - expect(responseBody.data.user.locale).toEqual(requestBody.locale); - expect(responseBody.data.user.avatarUrl).toEqual(requestBody.avatarUrl); - expect(responseBody.data.user.bio).toEqual(requestBody.bio); - expect(responseBody.data.user.metadata).toEqual(requestBody.metadata); - const [emailUser, emailDomain] = responseBody.data.user.email.split("@"); - const [domainName, TLD] = emailDomain.split("."); - expect(responseBody.data.user.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`)); - - const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } = response.body.data; - expect(accessToken).toBeDefined(); - expect(refreshToken).toBeDefined(); - expect(accessTokenExpiresAt).toBeDefined(); - expect(refreshTokenExpiresAt).toBeDefined(); - - const jwtService = app.get(JwtService); - const decodedAccessToken = jwtService.decode(accessToken); - const decodedRefreshToken = jwtService.decode(refreshToken); - - expect(decodedAccessToken.clientId).toBe(oAuthClient.id); - expect(decodedAccessToken.ownerId).toBeDefined(); - expect(decodedAccessToken.type).toBe("access_token"); - expect(decodedAccessToken.expiresAt).toBe(new Date(accessTokenExpiresAt).valueOf()); - expect(decodedAccessToken.iat).toBeGreaterThan(0); - - expect(decodedRefreshToken.clientId).toBe(oAuthClient.id); - expect(decodedRefreshToken.ownerId).toBeDefined(); - expect(decodedRefreshToken.type).toBe("refresh_token"); - expect(decodedRefreshToken.expiresAt).toBe(new Date(refreshTokenExpiresAt).valueOf()); - expect(decodedRefreshToken.iat).toBeGreaterThan(0); - - await userConnectedToOAuth(oAuthClient.id, responseBody.data.user.email, 1); - await userHasDefaultEventTypes(responseBody.data.user.id); - await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId); - await userHasOnlyOneSchedule(responseBody.data.user.id); - }); - - it(`/POST with default event types`, async () => { - const requestBody: CreateManagedUserInput = { - email: userEmailTwo, - timeZone: userTimeZoneTwo, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Bob Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - bio: "I am a bio", - metadata: { - key: "value", - }, - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - - postResponseDataTwo = responseBody.data; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email) - ); - expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone); - expect(responseBody.data.user.name).toEqual(requestBody.name); - expect(responseBody.data.user.weekStart).toEqual(requestBody.weekStart); - expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat); - expect(responseBody.data.user.locale).toEqual(requestBody.locale); - expect(responseBody.data.user.avatarUrl).toEqual(requestBody.avatarUrl); - - const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } = response.body.data; - expect(accessToken).toBeDefined(); - expect(refreshToken).toBeDefined(); - expect(accessTokenExpiresAt).toBeDefined(); - expect(refreshTokenExpiresAt).toBeDefined(); - - const jwtService = app.get(JwtService); - const decodedAccessToken = jwtService.decode(accessToken); - const decodedRefreshToken = jwtService.decode(refreshToken); - - expect(decodedAccessToken.clientId).toBe(oAuthClient.id); - expect(decodedAccessToken.ownerId).toBeDefined(); - expect(decodedAccessToken.type).toBe("access_token"); - expect(decodedAccessToken.expiresAt).toBe(new Date(accessTokenExpiresAt).valueOf()); - expect(decodedAccessToken.iat).toBeGreaterThan(0); - - expect(decodedRefreshToken.clientId).toBe(oAuthClient.id); - expect(decodedRefreshToken.ownerId).toBeDefined(); - expect(decodedRefreshToken.type).toBe("refresh_token"); - expect(decodedRefreshToken.expiresAt).toBe(new Date(refreshTokenExpiresAt).valueOf()); - expect(decodedRefreshToken.iat).toBeGreaterThan(0); - - await userConnectedToOAuth(oAuthClient.id, responseBody.data.user.email, 2); - await userHasDefaultEventTypes(responseBody.data.user.id); - await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId); - await userHasOnlyOneSchedule(responseBody.data.user.id); - }); - - it(`/POST without default event types`, async () => { - const requestBody: CreateManagedUserInput = { - email: userEmail2, - timeZone: userTimeZone2, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Alice Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - bio: "I am a bio", - metadata: { - key: "value", - }, - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClientEventTypesDisabled.id}/users`) - .set("x-cal-secret-key", oAuthClientEventTypesDisabled.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - - postResponseData2 = responseBody.data; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClientEventTypesDisabled.id, requestBody.email) - ); - expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone); - expect(responseBody.data.user.name).toEqual(requestBody.name); - expect(responseBody.data.user.weekStart).toEqual(requestBody.weekStart); - expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat); - expect(responseBody.data.user.locale).toEqual(requestBody.locale); - expect(responseBody.data.user.avatarUrl).toEqual(requestBody.avatarUrl); - - const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } = response.body.data; - expect(accessToken).toBeDefined(); - expect(refreshToken).toBeDefined(); - expect(accessTokenExpiresAt).toBeDefined(); - expect(refreshTokenExpiresAt).toBeDefined(); - - const jwtService = app.get(JwtService); - const decodedAccessToken = jwtService.decode(accessToken); - const decodedRefreshToken = jwtService.decode(refreshToken); - - expect(decodedAccessToken.clientId).toBe(oAuthClientEventTypesDisabled.id); - expect(decodedAccessToken.ownerId).toBeDefined(); - expect(decodedAccessToken.type).toBe("access_token"); - expect(decodedAccessToken.expiresAt).toBe(new Date(accessTokenExpiresAt).valueOf()); - expect(decodedAccessToken.iat).toBeGreaterThan(0); - - expect(decodedRefreshToken.clientId).toBe(oAuthClientEventTypesDisabled.id); - expect(decodedRefreshToken.ownerId).toBeDefined(); - expect(decodedRefreshToken.type).toBe("refresh_token"); - expect(decodedRefreshToken.expiresAt).toBe(new Date(refreshTokenExpiresAt).valueOf()); - expect(decodedRefreshToken.iat).toBeGreaterThan(0); - - await userConnectedToOAuth(oAuthClientEventTypesDisabled.id, responseBody.data.user.email, 1); - await userDoesNotHaveDefaultEventTypes(responseBody.data.user.id); - await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId); - await userHasOnlyOneSchedule(responseBody.data.user.id); - }); - - async function userConnectedToOAuth(oAuthClientId: string, userEmail: string, usersCount: number) { - const oAuthUsers = await oauthClientRepositoryFixture.getUsers(oAuthClientId); - const newOAuthUser = oAuthUsers?.find((user) => user.email === userEmail); - - expect(oAuthUsers?.length).toEqual(usersCount); - expect(newOAuthUser?.email).toEqual(userEmail); - } - - async function userHasDefaultEventTypes(userId: number) { - const defaultEventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(userId); - - // note(Lauris): to determine count see default event types created in EventTypesService.createUserDefaultEventTypes - expect(defaultEventTypes?.length).toEqual(4); - expect( - defaultEventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.thirtyMinutes.slug) - ).toBeTruthy(); - expect( - defaultEventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.sixtyMinutes.slug) - ).toBeTruthy(); - expect( - defaultEventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.thirtyMinutesVideo.slug) - ).toBeTruthy(); - expect( - defaultEventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.sixtyMinutesVideo.slug) - ).toBeTruthy(); - } - - async function userDoesNotHaveDefaultEventTypes(userId: number) { - const defaultEventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(userId); - expect(defaultEventTypes?.length).toEqual(0); - } - - async function userHasDefaultSchedule(userId: number, scheduleId: number | null) { - expect(scheduleId).toBeDefined(); - expect(scheduleId).not.toBeNull(); - - const user = await userRepositoryFixture.get(userId); - expect(user?.defaultScheduleId).toEqual(scheduleId); - - const schedule = scheduleId ? await schedulesRepositoryFixture.getById(scheduleId) : null; - expect(schedule?.userId).toEqual(userId); - } - - async function userHasOnlyOneSchedule(userId: number) { - const schedules = await schedulesRepositoryFixture.getByUserId(userId); - expect(schedules?.length).toEqual(1); - } - - it(`should fail /POST using already used managed user email`, async () => { - const requestBody: CreateManagedUserInput = { - email: userEmail, - timeZone: userTimeZone, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Alice Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(409); - - const responseBody: CreateManagedUserOutput = response.body; - const error = responseBody.error; - expect(error).toBeDefined(); - expect(error?.message).toEqual( - `User with the provided e-mail already exists. Existing user ID=${postResponseData.user.id}` - ); - }); - - it(`/GET: return list of managed users`, async () => { - const response = await request(app.getHttpServer()) - .get(`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0`) - .set("x-cal-secret-key", oAuthClient.secret) - .set("Origin", `${CLIENT_REDIRECT_URI}`) - .expect(200); - - const responseBody: GetManagedUsersOutput = response.body; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data?.length).toBeGreaterThan(0); - expect(responseBody.data[0].email).toEqual(postResponseData.user.email); - expect(responseBody.data[0].name).toEqual(postResponseData.user.name); - expect(responseBody.data?.length).toEqual(2); - const userOne = responseBody.data.find((user) => user.email === postResponseData.user.email); - const userTwo = responseBody.data.find((user) => user.email === postResponseDataTwo.user.email); - expect(userOne?.email).toEqual(postResponseData.user.email); - expect(userOne?.name).toEqual(postResponseData.user.name); - expect(userTwo?.email).toEqual(postResponseDataTwo.user.email); - expect(userTwo?.name).toEqual(postResponseDataTwo.user.name); - expect(userOne?.bio).toEqual(postResponseData.user.bio); - expect(userOne?.metadata).toEqual(postResponseData.user.metadata); - expect(userTwo?.bio).toEqual(postResponseDataTwo.user.bio); - expect(userTwo?.metadata).toEqual(postResponseDataTwo.user.metadata); - }); - - it(`/GET: managed user by original email`, async () => { - const response = await request(app.getHttpServer()) - .get(`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0&emails=${userEmail}`) - .set("x-cal-secret-key", oAuthClient.secret) - .set("Origin", `${CLIENT_REDIRECT_URI}`) - .expect(200); - - const responseBody: GetManagedUsersOutput = response.body; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data?.length).toEqual(1); - const userOne = responseBody.data.find((user) => user.email === postResponseData.user.email); - expect(userOne?.email).toEqual(postResponseData.user.email); - expect(userOne?.name).toEqual(postResponseData.user.name); - expect(userOne?.bio).toEqual(postResponseData.user.bio); - expect(userOne?.metadata).toEqual(postResponseData.user.metadata); - }); - - it(`/GET: managed users by original emails`, async () => { - const response = await request(app.getHttpServer()) - .get( - `/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0&emails=${userEmail},${userEmailTwo}` - ) - .set("x-cal-secret-key", oAuthClient.secret) - .set("Origin", `${CLIENT_REDIRECT_URI}`) - .expect(200); - - const responseBody: GetManagedUsersOutput = response.body; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data?.length).toEqual(2); - const userOne = responseBody.data.find((user) => user.email === postResponseData.user.email); - const userTwo = responseBody.data.find((user) => user.email === postResponseDataTwo.user.email); - expect(userOne?.email).toEqual(postResponseData.user.email); - expect(userOne?.name).toEqual(postResponseData.user.name); - expect(userTwo?.email).toEqual(postResponseDataTwo.user.email); - expect(userTwo?.name).toEqual(postResponseDataTwo.user.name); - expect(userOne?.bio).toEqual(postResponseData.user.bio); - expect(userOne?.metadata).toEqual(postResponseData.user.metadata); - expect(userTwo?.bio).toEqual(postResponseDataTwo.user.bio); - expect(userTwo?.metadata).toEqual(postResponseDataTwo.user.metadata); - }); - - it(`/GET: managed user by oAuth email`, async () => { - const response = await request(app.getHttpServer()) - // note(Lauris): we use encodeURIComponent because email stored on our side includes "+" which without encoding becomes an empty space. - .get( - `/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0&emails=${encodeURIComponent( - postResponseData.user.email - )}` - ) - .set("x-cal-secret-key", oAuthClient.secret) - .set("Origin", `${CLIENT_REDIRECT_URI}`) - .expect(200); - - const responseBody: GetManagedUsersOutput = response.body; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data?.length).toEqual(1); - const userOne = responseBody.data.find((user) => user.email === postResponseData.user.email); - expect(userOne?.email).toEqual(postResponseData.user.email); - expect(userOne?.name).toEqual(postResponseData.user.name); - expect(userOne?.bio).toEqual(postResponseData.user.bio); - expect(userOne?.metadata).toEqual(postResponseData.user.metadata); - }); - - it(`should error /GET if managed user email is invalid`, async () => { - const invalidEmail = "invalid-email"; - const response = await request(app.getHttpServer()) - .get(`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0&emails=${invalidEmail}`) - .set("x-cal-secret-key", oAuthClient.secret) - .set("Origin", `${CLIENT_REDIRECT_URI}`) - .expect(400); - - expect(response.body?.error?.message).toEqual(`Invalid email ${invalidEmail}`); - }); - - it(`/GET: managed users by oAuth emails`, async () => { - const response = await request(app.getHttpServer()) - // note(Lauris): we use encodeURIComponent because email stored on our side includes "+" which without encoding becomes an empty space. - .get( - `/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0&emails=${encodeURIComponent( - postResponseData.user.email - )},${encodeURIComponent(postResponseDataTwo.user.email)}` - ) - .set("x-cal-secret-key", oAuthClient.secret) - .set("Origin", `${CLIENT_REDIRECT_URI}`) - .expect(200); - - const responseBody: GetManagedUsersOutput = response.body; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data?.length).toEqual(2); - const userOne = responseBody.data.find((user) => user.email === postResponseData.user.email); - const userTwo = responseBody.data.find((user) => user.email === postResponseDataTwo.user.email); - expect(userOne?.email).toEqual(postResponseData.user.email); - expect(userOne?.name).toEqual(postResponseData.user.name); - expect(userTwo?.email).toEqual(postResponseDataTwo.user.email); - expect(userTwo?.name).toEqual(postResponseDataTwo.user.name); - expect(userOne?.bio).toEqual(postResponseData.user.bio); - expect(userOne?.metadata).toEqual(postResponseData.user.metadata); - expect(userTwo?.bio).toEqual(postResponseDataTwo.user.bio); - expect(userTwo?.metadata).toEqual(postResponseDataTwo.user.metadata); - }); - - it(`/GET/:id`, async () => { - const response = await request(app.getHttpServer()) - .get(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`) - .set("x-cal-secret-key", oAuthClient.secret) - .set("Origin", `${CLIENT_REDIRECT_URI}`) - .expect(200); - - const responseBody: GetManagedUserOutput = response.body; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, userEmail) - ); - expect(responseBody.data.name).toEqual(postResponseData.user.name); - expect(responseBody.data.bio).toEqual(postResponseData.user.bio); - expect(responseBody.data.metadata).toEqual(postResponseData.user.metadata); - }); - - it(`/PATCH/:id`, async () => { - const userUpdatedEmail = "pineapple-pizza@gmail.com"; - const body: UpdateManagedUserInput = { email: userUpdatedEmail, locale: Locales.PT_BR }; - - const response = await request(app.getHttpServer()) - .patch(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`) - .set("x-cal-secret-key", oAuthClient.secret) - .set("Origin", `${CLIENT_REDIRECT_URI}`) - .send(body) - .expect(200); - - const responseBody: ApiSuccessResponse> = response.body; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, userUpdatedEmail) - ); - expect(responseBody.data.name).toEqual(postResponseData.user.name); - expect(responseBody.data.bio).toEqual(postResponseData.user.bio); - expect(responseBody.data.metadata).toEqual(postResponseData.user.metadata); - const [emailUser, emailDomain] = responseBody.data.email.split("@"); - const [domainName, TLD] = emailDomain.split("."); - expect(responseBody.data.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`)); - expect(responseBody.data.locale).toEqual(Locales.PT_BR); - - const profile = await profilesRepositoryFixture.findByOrgIdUserId( - organization.id, - responseBody.data.id - ); - expect(profile).toBeDefined(); - expect(profile?.username).toEqual(responseBody.data.username); - }); - - it("should force refresh tokens", async () => { - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}/force-refresh`) - .set("x-cal-secret-key", oAuthClient.secret) - .expect(200); - - const responseBody: KeysResponseDto = response.body; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.accessToken).toBeDefined(); - expect(responseBody.data.accessTokenExpiresAt).toBeDefined(); - - const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } = response.body.data; - expect(accessToken).toBeDefined(); - expect(refreshToken).toBeDefined(); - expect(accessTokenExpiresAt).toBeDefined(); - expect(refreshTokenExpiresAt).toBeDefined(); - - const jwtService = app.get(JwtService); - const decodedAccessToken = jwtService.decode(accessToken); - const decodedRefreshToken = jwtService.decode(refreshToken); - - expect(decodedAccessToken.clientId).toBe(oAuthClient.id); - expect(decodedAccessToken.ownerId).toBe(postResponseData.user.id); - expect(decodedAccessToken.type).toBe("access_token"); - expect(decodedAccessToken.expiresAt).toBe(new Date(accessTokenExpiresAt).valueOf()); - expect(decodedAccessToken.iat).toBeGreaterThan(0); - - expect(decodedRefreshToken.clientId).toBe(oAuthClient.id); - expect(decodedRefreshToken.ownerId).toBe(postResponseData.user.id); - expect(decodedRefreshToken.type).toBe("refresh_token"); - expect(decodedRefreshToken.expiresAt).toBe(new Date(refreshTokenExpiresAt).valueOf()); - expect(decodedRefreshToken.iat).toBeGreaterThan(0); - }); - - it(`/DELETE/:id`, () => { - return request(app.getHttpServer()) - .delete(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`) - .set("x-cal-secret-key", oAuthClient.secret) - .set("Origin", `${CLIENT_REDIRECT_URI}`) - .expect(200); - }); - - describe("managed user time zone", () => { - describe("negative tests", () => { - it("should not allow '' time zone", async () => { - const requestBody = { - email: "whatever2@gmail.com", - timeZone: "", - name: "Bob Smithson", - }; - - await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(400); - }); - - it("should not allow 'invalid-timezone' time zone", async () => { - const requestBody = { - email: "whatever2@gmail.com", - timeZone: "invalid-timezone", - name: "Bob Smithson", - }; - - await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(400); - }); - }); - - describe("positive tests", () => { - it("should allow null timezone", async () => { - const requestBody = { - email: "whatever1@gmail.com", - timeZone: null, - name: "Bob Smithson", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.data.user.timeZone).toEqual("Europe/London"); - await userRepositoryFixture.delete(responseBody.data.user.id); - }); - - it("should allow undefined time zone", async () => { - const requestBody = { - email: "whatever3@gmail.com", - timeZone: undefined, - name: "Bob Smithson", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.data.user.timeZone).toEqual("Europe/London"); - await userRepositoryFixture.delete(responseBody.data.user.id); - }); - - it("should allow valid time zone", async () => { - const requestBody = { - email: "whatever4@gmail.com", - timeZone: "Europe/Rome", - name: "Bob Smithson", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.data.user.timeZone).toBe("Europe/Rome"); - await userRepositoryFixture.delete(responseBody.data.user.id); - }); - - it("should allow without any time zone", async () => { - const requestBody = { - email: "whatever5@gmail.com", - name: "Bob Smithson", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set("x-cal-secret-key", oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.data.user.timeZone).toEqual("Europe/London"); - await userRepositoryFixture.delete(responseBody.data.user.id); - }); - }); - }); - - afterAll(async () => { - await oauthClientRepositoryFixture.delete(oAuthClient.id); - await oauthClientRepositoryFixture.delete(oAuthClientEventTypesDisabled.id); - await teamRepositoryFixture.delete(organization.id); - try { - await userRepositoryFixture.delete(postResponseData.user.id); - } catch (e) { - console.log(e); - } - try { - await userRepositoryFixture.delete(postResponseData2.user.id); - } catch (e) { - console.log(e); - } - try { - await userRepositoryFixture.delete(platformAdmin.id); - } catch (e) { - console.log(e); - } - await app.close(); - }); - }); - - describe("User team even-types", () => { - let app: INestApplication; - - let oAuthClient1: PlatformOAuthClient; - let oAuthClient2: PlatformOAuthClient; - - let organization: Team; - let team1: Team; - let team2: Team; - let owner: User; - - let userRepositoryFixture: UserRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let postResponseData: CreateManagedUserOutput["data"]; - - const userEmail = `oauth-client-users-user-${randomString(5)}@api.com`; - const userTimeZone = "Europe/Rome"; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [PrismaExceptionFilter, HttpExceptionFilter], - imports: [AppModule, UsersModule], - }).compile(); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - organization = await teamRepositoryFixture.create({ - name: `oauth-client-users-organization-${randomString()}`, - isPlatform: true, - isOrganization: true, - }); - - owner = await userRepositoryFixture.create({ - email: `oauth-client-users-admin-${randomString()}@api.com`, - username: `oauth-client-users-admin-${randomString()}@api.com`, - organization: { connect: { id: organization.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${owner.id}`, - username: `oauth-client-users-admin-${randomString()}@api.com`, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: owner.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: owner.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - oAuthClient1 = await createOAuthClient(organization.id); - oAuthClient2 = await createOAuthClient(organization.id); - - team1 = await teamRepositoryFixture.create({ - name: "Testy org team", - isOrganization: false, - parent: { connect: { id: organization.id } }, - createdByOAuthClient: { connect: { id: oAuthClient1.id } }, - }); - - team2 = await teamRepositoryFixture.create({ - name: "Testy org team 2", - isOrganization: false, - parent: { connect: { id: organization.id } }, - createdByOAuthClient: { connect: { id: oAuthClient2.id } }, - }); - - // note(Lauris): team1 team event-types - await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team1.id }, - }, - title: "Collective Event Type", - slug: "collective-event-type", - length: 30, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "MANAGED", - team: { - connect: { id: team1.id }, - }, - title: "Managed Event Type", - slug: "managed-event-type", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - // note(Lauris): team2 team event-types - await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team2.id }, - }, - title: "Collective Event Type team 2", - slug: "collective-event-type-team-2", - length: 30, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "MANAGED", - team: { - connect: { id: team2.id }, - }, - title: "Managed Event Type team 2", - slug: "managed-event-type-team-2", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - await app.init(); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: [CLIENT_REDIRECT_URI], - permissions: 32, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - it("should be defined", () => { - expect(oauthClientRepositoryFixture).toBeDefined(); - expect(userRepositoryFixture).toBeDefined(); - expect(oAuthClient1).toBeDefined(); - }); - - it(`should create managed user and update team event-types`, async () => { - const requestBody: CreateManagedUserInput = { - email: userEmail, - timeZone: userTimeZone, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Alice Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient1.id}/users`) - .set("x-cal-secret-key", oAuthClient1.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - postResponseData = responseBody.data; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - await teamHasCorrectEventTypes(team1.id); - expect(responseBody.data.user.name).toEqual(requestBody.name); - }); - - async function teamHasCorrectEventTypes(teamId: number) { - const eventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(teamId); - expect(eventTypes?.length).toEqual(2); - } - - afterAll(async () => { - await oauthClientRepositoryFixture.delete(oAuthClient1.id); - await teamRepositoryFixture.delete(organization.id); - await userRepositoryFixture.delete(owner.id); - try { - await userRepositoryFixture.delete(postResponseData.user.id); - } catch (e) { - console.log(e); - } - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts index 7d0e068a5f9f4a..83f0269b7f78a6 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts @@ -22,7 +22,6 @@ import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { GetOrgId } from "@/modules/auth/decorators/get-org-id/get-org-id.decorator"; import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard"; import { GetManagedUsersInput } from "@/modules/oauth-clients/controllers/oauth-client-users/inputs/get-managed-users.input"; import { CreateManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output"; import { GetManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-user.output"; @@ -42,7 +41,7 @@ import { UsersRepository } from "@/modules/users/users.repository"; path: "/v2/oauth-clients/:clientId/users", version: API_VERSIONS_VALUES, }) -@UseGuards(ApiAuthGuard, OAuthClientGuard, OrganizationRolesGuard) +@UseGuards(ApiAuthGuard, OAuthClientGuard) @DocsTags("Deprecated: Platform / Managed Users") @ApiHeader({ name: X_CAL_SECRET_KEY, diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller.ts index 85a7e3479f70bf..a1bf04aaef7120 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller.ts @@ -8,7 +8,6 @@ import { plainToClass } from "class-transformer"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard"; import { GetWebhook } from "@/modules/webhooks/decorators/get-webhook-decorator"; import { IsOAuthClientWebhookGuard } from "@/modules/webhooks/guards/is-oauth-client-webhook-guard"; import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input"; @@ -29,7 +28,7 @@ import { OAuthClientGuard } from "../../guards/oauth-client-guard"; path: "/v2/oauth-clients/:clientId/webhooks", version: API_VERSIONS_VALUES, }) -@UseGuards(ApiAuthGuard, OrganizationRolesGuard, OAuthClientGuard) +@UseGuards(ApiAuthGuard, OAuthClientGuard) @DocsTags("Deprecated: Platform / Webhooks") @ApiHeader({ name: X_CAL_SECRET_KEY, diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.e2e-spec.ts index 7feddb4c31aec1..bfdf1db99fb79b 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.e2e-spec.ts @@ -21,7 +21,6 @@ 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"; @@ -74,69 +73,10 @@ describe("OAuth Clients Endpoints", () => { }); }); - describe("Organization is not platform", () => { - let usersFixtures: UserRepositoryFixture; - let membershipFixtures: MembershipRepositoryFixture; - let teamFixtures: TeamRepositoryFixture; - let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture; - - let user: User; - let org: Team; - let app: INestApplication; - const userEmail = `oauth-clients-user-${randomString()}@api.com`; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - providers: [PrismaExceptionFilter, HttpExceptionFilter], - imports: [AppModule, OAuthClientModule, UsersModule, AuthModule, PrismaModule], - }) - ).compile(); - const strategy = moduleRef.get(ApiAuthStrategy); - expect(strategy).toBeInstanceOf(ApiAuthMockStrategy); - usersFixtures = new UserRepositoryFixture(moduleRef); - membershipFixtures = new MembershipRepositoryFixture(moduleRef); - teamFixtures = new TeamRepositoryFixture(moduleRef); - platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef); - - user = await usersFixtures.create({ - email: userEmail, - }); - org = await teamFixtures.create({ - name: `oauth-clients-organization-${randomString()}`, - isOrganization: true, - metadata: { - isOrganization: true, - orgAutoAcceptEmail: "api.com", - isOrganizationVerified: true, - isOrganizationConfigured: true, - }, - isPlatform: false, - }); - await membershipFixtures.addUserToOrg(user, org, "ADMIN", true); - await platformBillingRepositoryFixture.create(org.id); - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - it(`/GET`, () => { - return request(app.getHttpServer()).get("/api/v2/oauth-clients").expect(403); - }); - - afterAll(async () => { - await teamFixtures.delete(org.id); - await usersFixtures.delete(user.id); - await app.close(); - }); - }); - describe("User Is Authenticated", () => { let usersFixtures: UserRepositoryFixture; let membershipFixtures: MembershipRepositoryFixture; let teamFixtures: TeamRepositoryFixture; - let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture; let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture; let user: User; @@ -157,7 +97,6 @@ describe("OAuth Clients Endpoints", () => { usersFixtures = new UserRepositoryFixture(moduleRef); membershipFixtures = new MembershipRepositoryFixture(moduleRef); teamFixtures = new TeamRepositoryFixture(moduleRef); - platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef); oAuthClientsRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); user = await usersFixtures.create({ @@ -174,7 +113,6 @@ describe("OAuth Clients Endpoints", () => { }, isPlatform: true, }); - await platformBillingRepositoryFixture.create(org.id); app = moduleRef.createNestApplication(); bootstrap(app as NestExpressApplication); await app.init(); @@ -182,13 +120,13 @@ describe("OAuth Clients Endpoints", () => { describe("User is not part of an organization", () => { it(`/GET`, () => { - return request(app.getHttpServer()).get("/api/v2/oauth-clients").expect(403); + return request(app.getHttpServer()).get("/api/v2/oauth-clients").expect(401); }); it(`/GET/:id`, () => { return request(app.getHttpServer()).get("/api/v2/oauth-clients/1234").expect(403); }); it(`/POST`, () => { - return request(app.getHttpServer()).post("/api/v2/oauth-clients").expect(403); + return request(app.getHttpServer()).post("/api/v2/oauth-clients").expect(401); }); it(`/PUT/:id`, () => { return request(app.getHttpServer()).patch("/api/v2/oauth-clients/1234").expect(403); @@ -211,13 +149,13 @@ describe("OAuth Clients Endpoints", () => { return request(app.getHttpServer()).get("/api/v2/oauth-clients/1234").expect(404); }); it(`/POST`, () => { - return request(app.getHttpServer()).post("/api/v2/oauth-clients").expect(403); + return request(app.getHttpServer()).post("/api/v2/oauth-clients").expect(400); }); it(`/PUT/:id`, () => { - return request(app.getHttpServer()).patch("/api/v2/oauth-clients/1234").expect(403); + return request(app.getHttpServer()).patch("/api/v2/oauth-clients/1234").expect(404); }); it(`/DELETE/:id`, () => { - return request(app.getHttpServer()).delete("/api/v2/oauth-clients/1234").expect(403); + return request(app.getHttpServer()).delete("/api/v2/oauth-clients/1234").expect(404); }); afterAll(async () => { @@ -416,7 +354,6 @@ describe("OAuth Clients Endpoints", () => { afterAll(async () => { await teamFixtures.delete(org.id); await usersFixtures.delete(user.id); - await platformBillingRepositoryFixture.deleteSubscriptionForTeam(org.id); await app.close(); }); }); diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts index e382c36da9c0f5..fd23a012e085bd 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts @@ -28,7 +28,6 @@ import { API_KEY_HEADER } from "@/lib/docs/headers"; import { GetOrgId } from "@/modules/auth/decorators/get-org-id/get-org-id.decorator"; import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard"; import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output"; import { CreateOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/CreateOAuthClientResponse.dto"; import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto"; @@ -36,7 +35,6 @@ import { GetOAuthClientsResponseDto } from "@/modules/oauth-clients/controllers/ import { OAuthClientGuard } from "@/modules/oauth-clients/guards/oauth-client-guard"; import { OAuthClientsService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients.service"; import { OAuthClientUsersOutputService } from "@/modules/oauth-clients/services/oauth-clients-users-output.service"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; import { UsersRepository } from "@/modules/users/users.repository"; @Controller({ @@ -44,7 +42,7 @@ import { UsersRepository } from "@/modules/users/users.repository"; version: API_VERSIONS_VALUES, }) @ApiTags("Deprecated: Platform OAuth Clients") -@UseGuards(ApiAuthGuard, OrganizationRolesGuard) +@UseGuards(ApiAuthGuard) @ApiHeader(API_KEY_HEADER) export class OAuthClientsController { private readonly logger = new Logger("OAuthClientController"); @@ -52,8 +50,7 @@ export class OAuthClientsController { constructor( private readonly oAuthClientUsersOutputService: OAuthClientUsersOutputService, private readonly oAuthClientsService: OAuthClientsService, - private readonly userRepository: UsersRepository, - private readonly teamsRepository: OrganizationsRepository + private readonly userRepository: UsersRepository ) {} @Post("/") @@ -73,13 +70,6 @@ export class OAuthClientsController { ): Promise { this.logger.log(`Creating OAuth Client for organisation ${organizationId}`); - const organization = await this.teamsRepository.findByIdIncludeBilling(organizationId); - if (!organization?.platformBilling || !organization?.platformBilling?.subscriptionId) { - throw new BadRequestException( - "Team is not subscribed to platform plan, cannot create an OAuth Client." - ); - } - const oAuthClientCredentials = await this.oAuthClientsService.createOAuthClient(organizationId, body); return { diff --git a/apps/api/v2/src/modules/oauth-clients/oauth-client.module.ts b/apps/api/v2/src/modules/oauth-clients/oauth-client.module.ts index 2d9d20365cd95e..6e175523a3c9f8 100644 --- a/apps/api/v2/src/modules/oauth-clients/oauth-client.module.ts +++ b/apps/api/v2/src/modules/oauth-clients/oauth-client.module.ts @@ -1,11 +1,10 @@ -import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; -import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; +import { CalendarsRepository } from "@/platform/calendars/calendars.repository"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { EventTypesModule_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/event-types.module"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { AuthModule } from "@/modules/auth/auth.module"; -import { BillingModule } from "@/modules/billing/billing.module"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { OAuthClientUsersController } from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller"; @@ -19,7 +18,6 @@ import { OAuthClientsOutputService } from "@/modules/oauth-clients/services/oaut import { OAuthClientsService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients.service"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; import { OrganizationsModule } from "@/modules/organizations/organizations.module"; -import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { ProfilesModule } from "@/modules/profiles/profiles.module"; import { RedisModule } from "@/modules/redis/redis.module"; @@ -43,7 +41,6 @@ import { JwtService } from "@nestjs/jwt"; EventTypesModule_2024_04_15, OrganizationsModule, StripeModule, - BillingModule, SchedulesModule_2024_04_15, ProfilesModule, ], @@ -58,7 +55,6 @@ import { JwtService } from "@nestjs/jwt"; CalendarsRepository, SelectedCalendarsRepository, OAuthClientUsersService, - OrganizationsTeamsService, OAuthClientsService, OAuthClientsInputService, OAuthClientsOutputService, diff --git a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts index 27aaec6758cde7..8346b959d5fccd 100644 --- a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts +++ b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts @@ -1,6 +1,9 @@ -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service"; -import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; +import { CreationSource, createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries"; +import type { PlatformOAuthClient, User } from "@calcom/prisma/client"; +import { BadRequestException, ConflictException, Injectable, Logger } from "@nestjs/common"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { EventTypesService_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/services/event-types.service"; +import { SchedulesService_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/services/schedules.service"; import { Locales } from "@/lib/enums/locales"; import { GetManagedUsersInput } from "@/modules/oauth-clients/controllers/oauth-client-users/inputs/get-managed-users.input"; import { ProfilesRepository } from "@/modules/profiles/profiles.repository"; @@ -8,10 +11,6 @@ import { TokensRepository } from "@/modules/tokens/tokens.repository"; import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input"; import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input"; import { UsersRepository } from "@/modules/users/users.repository"; -import { BadRequestException, ConflictException, Injectable, Logger } from "@nestjs/common"; - -import { createNewUsersConnectToOrgIfExists, slugify, CreationSource } from "@calcom/platform-libraries"; -import type { User, PlatformOAuthClient } from "@calcom/prisma/client"; @Injectable() export class OAuthClientUsersService { diff --git a/apps/api/v2/src/modules/organizations/attributes/index/controllers/organization-attributes.e2e-spec.ts b/apps/api/v2/src/modules/organizations/attributes/index/controllers/organization-attributes.e2e-spec.ts deleted file mode 100644 index 2be306c69f946c..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/index/controllers/organization-attributes.e2e-spec.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { Membership, 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 { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { CreateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/create-organization-attribute.input"; -import { UpdateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/update-organization-attribute.input"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Attributes Endpoints", () => { - describe("User lacks required role", () => { - let app: INestApplication; - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipFixtures: MembershipRepositoryFixture; - - const userEmail = `organization-attributes-member-${randomString()}@api.com`; - let user: User; - let org: Team; - let membership: Membership; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipFixtures = new MembershipRepositoryFixture(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `organization-attributes-organization-${randomString()}`, - isOrganization: true, - }); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - organization: { connect: { id: org.id } }, - }); - - membership = await membershipFixtures.addUserToOrg(user, org, "MEMBER", true); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should not be able to create attribute for org", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/attributes`) - .send({ - key: "department", - value: "engineering", - }) - .expect(403); - }); - - it("should not be able to delete attribute for org", async () => { - return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/attributes/1`).expect(403); - }); - - afterAll(async () => { - await membershipFixtures.delete(membership.id); - await userRepositoryFixture.deleteByEmail(user.email); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); - - describe("User has required role", () => { - let app: INestApplication; - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipFixtures: MembershipRepositoryFixture; - - const userEmail = `organization-attributes-admin-${randomString()}@api.com`; - let user: User; - let org: Team; - let membership: Membership; - - let createdAttribute: any; - - const createAttributeInput: CreateOrganizationAttributeInput = { - name: "department", - slug: "department", - type: "TEXT", - options: [], - enabled: true, - }; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipFixtures = new MembershipRepositoryFixture(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `organization-attributes-admin-organization-${randomString()}`, - isOrganization: true, - }); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - organization: { connect: { id: org.id } }, - }); - - membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should create attribute for org", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/attributes`) - .send(createAttributeInput) - .expect(201) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - createdAttribute = response.body.data; - expect(createdAttribute.type).toEqual(createAttributeInput.type); - expect(createdAttribute.slug).toEqual(createAttributeInput.slug); - expect(createdAttribute.enabled).toEqual(createAttributeInput.enabled); - }); - }); - - it("should get org attributes", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/attributes`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - const attributes = response.body.data; - expect(attributes.length).toEqual(1); - expect(attributes[0].name).toEqual(createAttributeInput.name); - expect(attributes[0].slug).toEqual(createAttributeInput.slug); - expect(attributes[0].type).toEqual(createAttributeInput.type); - expect(attributes[0].enabled).toEqual(createAttributeInput.enabled); - }); - }); - - it("should get single org attribute", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/attributes/${createdAttribute.id}`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - const attribute = response.body.data; - expect(attribute.name).toEqual(createAttributeInput.name); - expect(attribute.slug).toEqual(createAttributeInput.slug); - expect(attribute.type).toEqual(createAttributeInput.type); - expect(attribute.enabled).toEqual(createAttributeInput.enabled); - }); - }); - - it("should update org attribute", async () => { - const updateAttributeInput: UpdateOrganizationAttributeInput = { - name: "marketing", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/attributes/${createdAttribute.id}`) - .send(updateAttributeInput) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - const updatedAttribute = response.body.data; - expect(updatedAttribute.name).toEqual(updateAttributeInput.name); - }); - }); - - it("should delete org attribute", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/attributes/${createdAttribute.id}`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - expect(response.body.data).toBeTruthy(); - }); - }); - - afterAll(async () => { - await membershipFixtures.delete(membership.id); - await userRepositoryFixture.deleteByEmail(user.email); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/attributes/index/controllers/organizations-attributes.controller.ts b/apps/api/v2/src/modules/organizations/attributes/index/controllers/organizations-attributes.controller.ts deleted file mode 100644 index d0052e9b7ed2f3..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/index/controllers/organizations-attributes.controller.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { CreateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/create-organization-attribute.input"; -import { UpdateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/update-organization-attribute.input"; -import { CreateOrganizationAttributesOutput } from "@/modules/organizations/attributes/index/outputs/create-organization-attributes.output"; -import { DeleteOrganizationAttributesOutput } from "@/modules/organizations/attributes/index/outputs/delete-organization-attributes.output"; -import { - GetOrganizationAttributesOutput, - GetSingleAttributeOutput, -} from "@/modules/organizations/attributes/index/outputs/get-organization-attributes.output"; -import { UpdateOrganizationAttributesOutput } from "@/modules/organizations/attributes/index/outputs/update-organization-attributes.output"; -import { OrganizationAttributesService } from "@/modules/organizations/attributes/index/services/organization-attributes.service"; -import { - Body, - Controller, - Delete, - Get, - Param, - ParseIntPipe, - Patch, - Post, - Query, - UseGuards, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { SkipTakePagination } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Attributes") -@ApiHeader(API_KEY_HEADER) -export class OrganizationsAttributesController { - constructor(private readonly organizationsAttributesService: OrganizationAttributesService) {} - // Gets all attributes for an organization - @Roles("ORG_MEMBER") - @PlatformPlan("ESSENTIALS") - @Get("/attributes") - @ApiOperation({ summary: "Get all attributes" }) - async getOrganizationAttributes( - @Param("orgId", ParseIntPipe) orgId: number, - @Query() queryParams: SkipTakePagination - ): Promise { - const { skip, take } = queryParams; - const attributes = await this.organizationsAttributesService.getOrganizationAttributes(orgId, skip, take); - - return { - status: SUCCESS_STATUS, - data: attributes, - }; - } - - // Gets a single attribute for an organization - @Roles("ORG_MEMBER") - @PlatformPlan("ESSENTIALS") - @Get("/attributes/:attributeId") - @ApiOperation({ summary: "Get an attribute" }) - async getOrganizationAttribute( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("attributeId") attributeId: string - ): Promise { - const attribute = await this.organizationsAttributesService.getOrganizationAttribute(orgId, attributeId); - return { - status: SUCCESS_STATUS, - data: attribute, - }; - } - - // Creates an attribute for an organization - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Post("/attributes") - @ApiOperation({ summary: "Create an attribute" }) - async createOrganizationAttribute( - @Param("orgId", ParseIntPipe) orgId: number, - @Body() bodyAttribute: CreateOrganizationAttributeInput - ): Promise { - const attribute = await this.organizationsAttributesService.createOrganizationAttribute( - orgId, - bodyAttribute - ); - return { - status: SUCCESS_STATUS, - data: attribute, - }; - } - - // Updates an attribute for an organization - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Patch("/attributes/:attributeId") - @ApiOperation({ summary: "Update an attribute" }) - async updateOrganizationAttribute( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("attributeId") attributeId: string, - @Body() bodyAttribute: UpdateOrganizationAttributeInput - ): Promise { - const attribute = await this.organizationsAttributesService.updateOrganizationAttribute( - orgId, - attributeId, - bodyAttribute - ); - return { - status: SUCCESS_STATUS, - data: attribute, - }; - } - - // Deletes an attribute for an organization - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Delete("/attributes/:attributeId") - @ApiOperation({ summary: "Delete an attribute" }) - async deleteOrganizationAttribute( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("attributeId") attributeId: string - ): Promise { - const attribute = await this.organizationsAttributesService.deleteOrganizationAttribute( - orgId, - attributeId - ); - return { - status: SUCCESS_STATUS, - data: attribute, - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/attributes/index/inputs/create-organization-attribute.input.ts b/apps/api/v2/src/modules/organizations/attributes/index/inputs/create-organization-attribute.input.ts deleted file mode 100644 index dd6e9f047b9815..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/index/inputs/create-organization-attribute.input.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input"; -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { - IsArray, - IsBoolean, - IsEnum, - IsNotEmpty, - IsOptional, - IsString, - ValidateNested, -} from "class-validator"; - -import { AttributeType } from "@calcom/platform-libraries"; - -export class CreateOrganizationAttributeInput { - @IsString() - @IsNotEmpty() - @ApiProperty() - readonly name!: string; - - @IsString() - @IsNotEmpty() - @ApiProperty() - readonly slug!: string; - - @IsEnum(AttributeType) - @IsNotEmpty() - @ApiProperty({ enum: AttributeType }) - readonly type!: AttributeType; - - @IsArray() - @ValidateNested({ each: true }) - @Type(() => CreateOrganizationAttributeOptionInput) - @ApiProperty({ type: [CreateOrganizationAttributeOptionInput] }) - readonly options!: CreateOrganizationAttributeOptionInput[]; - - @IsBoolean() - @IsOptional() - @ApiPropertyOptional() - readonly enabled?: boolean; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/index/inputs/update-organization-attribute.input.ts b/apps/api/v2/src/modules/organizations/attributes/index/inputs/update-organization-attribute.input.ts deleted file mode 100644 index 6a5ec4ae8c30a6..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/index/inputs/update-organization-attribute.input.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsBoolean, IsEnum, IsOptional, IsString } from "class-validator"; - -import { AttributeType } from "@calcom/platform-libraries"; - -export class UpdateOrganizationAttributeInput { - @IsString() - @IsOptional() - @ApiPropertyOptional() - readonly name?: string; - - @IsString() - @IsOptional() - @ApiPropertyOptional() - readonly slug?: string; - - @IsEnum(AttributeType) - @IsOptional() - @ApiPropertyOptional({ enum: AttributeType }) - readonly type?: AttributeType; - - @IsBoolean() - @IsOptional() - @ApiPropertyOptional() - readonly enabled?: boolean; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/index/organization-attributes.repository.ts b/apps/api/v2/src/modules/organizations/attributes/index/organization-attributes.repository.ts deleted file mode 100644 index 7a6c6d6a2b281a..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/index/organization-attributes.repository.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { CreateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/create-organization-attribute.input"; -import { UpdateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/update-organization-attribute.input"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrganizationAttributesRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async createOrganizationAttribute(organizationId: number, data: CreateOrganizationAttributeInput) { - const { options, ...attributeData } = data; - - const attribute = await this.dbWrite.prisma.attribute.create({ - data: { - ...attributeData, - teamId: organizationId, - }, - }); - - if (attribute.type === "SINGLE_SELECT" || attribute.type === "MULTI_SELECT") { - // TODO: move this to attribute option service - await this.dbWrite.prisma.attributeOption.createMany({ - data: options.map((option) => ({ - ...option, - attributeId: attribute.id, - })), - }); - } - return attribute; - } - - async getOrganizationAttribute(organizationId: number, attributeId: string) { - const attribute = await this.dbRead.prisma.attribute.findUnique({ - where: { - id: attributeId, - teamId: organizationId, - }, - }); - return attribute; - } - - async getOrganizationAttributes(organizationId: number, skip?: number, take?: number) { - const attributes = await this.dbRead.prisma.attribute.findMany({ - where: { - teamId: organizationId, - }, - skip, - take, - }); - return attributes; - } - async updateOrganizationAttribute( - organizationId: number, - attributeId: string, - data: UpdateOrganizationAttributeInput - ) { - const attribute = await this.dbWrite.prisma.attribute.update({ - where: { - id: attributeId, - teamId: organizationId, - }, - data, - }); - return attribute; - } - - async deleteOrganizationAttribute(organizationId: number, attributeId: string) { - const attribute = await this.dbWrite.prisma.attribute.delete({ - where: { - id: attributeId, - teamId: organizationId, - }, - }); - return attribute; - } -} diff --git a/apps/api/v2/src/modules/organizations/attributes/index/outputs/attribute.output.ts b/apps/api/v2/src/modules/organizations/attributes/index/outputs/attribute.output.ts deleted file mode 100644 index bdf05d3f4053f3..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/index/outputs/attribute.output.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsString, IsInt, IsEnum, IsBoolean } from "class-validator"; - -export const AttributeType = { - TEXT: "TEXT", - NUMBER: "NUMBER", - SINGLE_SELECT: "SINGLE_SELECT", - MULTI_SELECT: "MULTI_SELECT", -} as const; - -export type AttributeType = (typeof AttributeType)[keyof typeof AttributeType]; - -export class Attribute { - @IsString() - @ApiProperty({ type: String, required: true, description: "The ID of the attribute", example: "attr_123" }) - id!: string; - - @IsInt() - @ApiProperty({ - type: Number, - required: true, - description: "The team ID associated with the attribute", - example: 1, - }) - teamId!: number; - - @ApiProperty({ - type: String, - required: true, - description: "The type of the attribute", - enum: AttributeType, - }) - @IsEnum(AttributeType) - type!: AttributeType; - - @IsString() - @ApiProperty({ - type: String, - required: true, - description: "The name of the attribute", - example: "Attribute Name", - }) - name!: string; - - @IsString() - @ApiProperty({ - type: String, - required: true, - description: "The slug of the attribute", - example: "attribute-name", - }) - slug!: string; - - @IsBoolean() - @ApiProperty({ - type: Boolean, - required: true, - description: "Whether the attribute is enabled and displayed on their profile", - example: true, - }) - enabled!: boolean; - - @IsBoolean() - @ApiProperty({ - type: Boolean, - required: false, - description: "Whether users can edit the relation", - example: true, - }) - usersCanEditRelation!: boolean; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/index/outputs/base.output.ts b/apps/api/v2/src/modules/organizations/attributes/index/outputs/base.output.ts deleted file mode 100644 index 21bf06f1b769fc..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/index/outputs/base.output.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsEnum } from "class-validator"; - -import { ERROR_STATUS } from "@calcom/platform-constants"; -import { SUCCESS_STATUS } from "@calcom/platform-constants"; - -export class BaseOutputDTO { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/index/outputs/create-organization-attributes.output.ts b/apps/api/v2/src/modules/organizations/attributes/index/outputs/create-organization-attributes.output.ts deleted file mode 100644 index d0cafc9ae02730..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/index/outputs/create-organization-attributes.output.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Attribute } from "@/modules/organizations/attributes/index/outputs/attribute.output"; -import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output"; -import { Expose, Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -export class CreateOrganizationAttributesOutput extends BaseOutputDTO { - @Expose() - @ValidateNested() - @Type(() => Attribute) - data!: Attribute; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/index/outputs/delete-organization-attributes.output.ts b/apps/api/v2/src/modules/organizations/attributes/index/outputs/delete-organization-attributes.output.ts deleted file mode 100644 index e036a2666f5e6b..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/index/outputs/delete-organization-attributes.output.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Attribute } from "@/modules/organizations/attributes/index/outputs/attribute.output"; -import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output"; -import { Expose, Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -export class DeleteOrganizationAttributesOutput extends BaseOutputDTO { - @Expose() - @ValidateNested() - @Type(() => Attribute) - data!: Attribute; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/index/outputs/get-organization-attributes.output.ts b/apps/api/v2/src/modules/organizations/attributes/index/outputs/get-organization-attributes.output.ts deleted file mode 100644 index 28d85f4553e067..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/index/outputs/get-organization-attributes.output.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Attribute } from "@/modules/organizations/attributes/index/outputs/attribute.output"; -import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsOptional, ValidateNested } from "class-validator"; - -export class GetSingleAttributeOutput extends BaseOutputDTO { - @Expose() - @ValidateNested() - @IsOptional() - @Type(() => Attribute) - @ApiProperty({ type: Attribute, nullable: true }) - data!: Attribute | null; -} - -export class GetOrganizationAttributesOutput extends BaseOutputDTO { - @Expose() - @ValidateNested({ each: true }) - @Type(() => Attribute) - @ApiProperty({ type: [Attribute] }) - data!: Attribute[]; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/index/outputs/update-organization-attributes.output.ts b/apps/api/v2/src/modules/organizations/attributes/index/outputs/update-organization-attributes.output.ts deleted file mode 100644 index d5fb7e9a4ae4c1..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/index/outputs/update-organization-attributes.output.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Attribute } from "@/modules/organizations/attributes/index/outputs/attribute.output"; -import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output"; -import { Expose, Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -export class UpdateOrganizationAttributesOutput extends BaseOutputDTO { - @Expose() - @ValidateNested() - @Type(() => Attribute) - data!: Attribute; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/index/services/organization-attributes.service.ts b/apps/api/v2/src/modules/organizations/attributes/index/services/organization-attributes.service.ts deleted file mode 100644 index 97a2b9fc8ac9a7..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/index/services/organization-attributes.service.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { CreateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/create-organization-attribute.input"; -import { UpdateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/update-organization-attribute.input"; -import { OrganizationAttributesRepository } from "@/modules/organizations/attributes/index/organization-attributes.repository"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrganizationAttributesService { - constructor(private readonly organizationAttributesRepository: OrganizationAttributesRepository) {} - - async createOrganizationAttribute(organizationId: number, data: CreateOrganizationAttributeInput) { - const attribute = await this.organizationAttributesRepository.createOrganizationAttribute( - organizationId, - data - ); - return attribute; - } - - async getOrganizationAttribute(organizationId: number, attributeId: string) { - const attribute = await this.organizationAttributesRepository.getOrganizationAttribute( - organizationId, - attributeId - ); - return attribute; - } - - async getOrganizationAttributes(organizationId: number, skip?: number, take?: number) { - const attributes = await this.organizationAttributesRepository.getOrganizationAttributes( - organizationId, - skip, - take - ); - return attributes; - } - - async updateOrganizationAttribute( - organizationId: number, - attributeId: string, - data: UpdateOrganizationAttributeInput - ) { - const attribute = await this.organizationAttributesRepository.updateOrganizationAttribute( - organizationId, - attributeId, - data - ); - return attribute; - } - - async deleteOrganizationAttribute(organizationId: number, attributeId: string) { - const attribute = await this.organizationAttributesRepository.deleteOrganizationAttribute( - organizationId, - attributeId - ); - return attribute; - } -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input.ts b/apps/api/v2/src/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input.ts deleted file mode 100644 index e45142e8463ad3..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsNotEmpty, IsString } from "class-validator"; - -export class CreateOrganizationAttributeOptionInput { - @IsString() - @IsNotEmpty() - readonly value!: string; - - @IsString() - @IsNotEmpty() - readonly slug!: string; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/inputs/get-assigned-attribute-options.input.ts b/apps/api/v2/src/modules/organizations/attributes/options/inputs/get-assigned-attribute-options.input.ts deleted file mode 100644 index 71215c12d97e27..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/inputs/get-assigned-attribute-options.input.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { Transform } from "class-transformer"; -import { IsOptional, IsArray, ArrayMinSize, IsString, IsNumber } from "class-validator"; - -export class GetAssignedAttributeOptions { - @ApiPropertyOptional({ type: Number, description: "Number of responses to skip" }) - @Transform(({ value }) => value && parseInt(value)) - @IsOptional() - skip?: number; - - @ApiPropertyOptional({ type: Number, description: "Number of responses to take" }) - @Transform(({ value }) => value && parseInt(value)) - @IsOptional() - take?: number; - - @IsOptional() - @Transform(({ value }) => { - if (typeof value === "string") { - return value.split(",").map((optId: string) => optId); - } - return value; - }) - @ApiPropertyOptional({ - type: [String], - description: "Filter by assigned attribute option ids. ids must be separated by a comma.", - example: "?assignedOptionIds=aaaaaaaa-bbbb-cccc-dddd-eeeeee1eee,aaaaaaaa-bbbb-cccc-dddd-eeeeee2eee", - }) - @IsArray() - @IsString({ each: true }) - @ArrayMinSize(1, { message: "assignedOptionIds must contain at least 1 attribute option id" }) - assignedOptionIds?: string[]; - - @IsOptional() - @Transform(({ value }) => { - if (typeof value === "string") { - return value.split(",").map((teamId: string) => parseInt(teamId)); - } - return value; - }) - @ApiPropertyOptional({ - type: [Number], - description: "Filter by teamIds. Team ids must be separated by a comma.", - example: "?teamIds=100,200", - }) - @IsArray() - @IsNumber({}, { each: true }) - @ArrayMinSize(1, { message: "teamIds must contain at least 1 team id" }) - teamIds?: number[]; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/inputs/organizations-attributes-options-assign.input.ts b/apps/api/v2/src/modules/organizations/attributes/options/inputs/organizations-attributes-options-assign.input.ts deleted file mode 100644 index 56a8fe4c06fcfb..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/inputs/organizations-attributes-options-assign.input.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsNotEmpty, IsOptional, IsString } from "class-validator"; - -export class AssignOrganizationAttributeOptionToUserInput { - @IsOptional() - @IsString() - @IsNotEmpty() - @ApiPropertyOptional() - readonly value?: string; - - @IsOptional() - @IsString() - @IsNotEmpty() - @ApiPropertyOptional() - readonly attributeOptionId?: string; - - @IsNotEmpty() - @IsString() - @ApiProperty() - readonly attributeId!: string; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts.ts b/apps/api/v2/src/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts.ts deleted file mode 100644 index ffcf4156ad9ff1..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsString, IsOptional } from "class-validator"; - -export class UpdateOrganizationAttributeOptionInput { - @IsString() - @IsOptional() - @ApiPropertyOptional() - readonly value?: string; - - @IsString() - @IsOptional() - @ApiPropertyOptional() - readonly slug?: string; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/organization-attribute-options.repository.ts b/apps/api/v2/src/modules/organizations/attributes/options/organization-attribute-options.repository.ts deleted file mode 100644 index 458eedd269145c..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/organization-attribute-options.repository.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input"; -import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts"; -import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable, Logger, NotFoundException } from "@nestjs/common"; - -import { slugify } from "@calcom/platform-libraries"; - -import { GetOrganizationAttributeAssignedOptionsProp } from "./services/organization-attributes-option.service"; - -@Injectable() -export class OrganizationAttributeOptionRepository { - private readonly logger = new Logger("OrganizationAttributeOptionRepository"); - constructor( - private readonly dbRead: PrismaReadService, - private readonly dbWrite: PrismaWriteService, - private readonly organizationsMembershipsService: OrganizationsMembershipService - ) {} - - async createOrganizationAttributeOption( - organizationId: number, - attributeId: string, - data: CreateOrganizationAttributeOptionInput - ) { - return this.dbWrite.prisma.attributeOption.create({ - data: { - ...data, - attributeId, - }, - }); - } - - async deleteOrganizationAttributeOption(organizationId: number, attributeId: string, optionId: string) { - try { - const deletedAttributeOption = await this.dbWrite.prisma.attributeOption.delete({ - where: { - id: optionId, - attributeId, - }, - }); - return deletedAttributeOption; - } catch (error: any) { - if (error.code === "P2025") { - // P2025 is the Prisma error code for "Record to delete does not exist." - this.logger.warn(`Attribute option not found: ${optionId}`); - throw new NotFoundException("Attribute option not found"); - } - throw error; - } - } - - async updateOrganizationAttributeOption( - organizationId: number, - attributeId: string, - optionId: string, - data: UpdateOrganizationAttributeOptionInput - ) { - return this.dbWrite.prisma.attributeOption.update({ - where: { - id: optionId, - attributeId, - }, - data, - }); - } - - async getOrganizationAttributeOptions(organizationId: number, attributeId: string) { - return this.dbRead.prisma.attributeOption.findMany({ - where: { - attributeId, - }, - }); - } - - async getOrganizationAttributeOptionsForUser(organizationId: number, userId: number) { - const options = this.dbRead.prisma.attributeOption.findMany({ - where: { - attribute: { - teamId: organizationId, - }, - assignedUsers: { - some: { - member: { - userId, - }, - }, - }, - }, - }); - - return options; - } - - async getOrganizationAttributeAssignedOptions({ - attributeId, - attributeSlug, - filters, - organizationId, - skip, - take, - }: GetOrganizationAttributeAssignedOptionsProp) { - const options = await this.dbRead.prisma.attributeOption.findMany({ - where: { - attribute: { - ...(attributeId && { id: attributeId }), - ...(attributeSlug && { slug: attributeSlug }), - teamId: organizationId, - }, - assignedUsers: { - some: {}, // empty {} statement checks if option is assigned to at least one user - ...(filters?.teamIds && { - some: { member: { user: { teams: { some: { teamId: { in: filters.teamIds } } } } } }, - }), - }, - }, - include: { assignedUsers: { include: { member: true } } }, - skip, - take, - }); - - // only return options that are assigned to users alongside assignedOptionIds filter - if (filters?.assignedOptionIds) { - const filteredAssignedOptions = await this.dbRead.prisma.attributeOption.findMany({ - where: { - attribute: { teamId: organizationId }, - assignedUsers: { - every: { - attributeOptionId: { in: filters?.assignedOptionIds }, - }, - ...(filters?.teamIds && { - some: { member: { user: { teams: { some: { teamId: { in: filters.teamIds } } } } } }, - }), - }, - }, - include: { assignedUsers: { include: { member: true } } }, - }); - - if (!filteredAssignedOptions?.length) { - throw new NotFoundException( - "Options provided in assignedOptionIds are not assigned to anyone, or the users are not part of the teams specified in teamIds filter." - ); - } - - const matchingUserIds = filteredAssignedOptions.flatMap((opt) => - opt.assignedUsers.flatMap((assignedUser) => assignedUser.member.userId) - ); - // reduce remove options that are not assigned alongside assignedOptionIds filter - return options.reduce((acc, opt) => { - if (opt.assignedUsers.some((assignedUser) => matchingUserIds.includes(assignedUser.member.userId))) { - return [ - ...acc, - { - ...opt, - assignedUserIds: opt.assignedUsers.map((attributeToUser) => attributeToUser.member.userId), - }, - ]; - } - return acc; - }, [] as typeof options); - } - - return options.map((opt) => { - return { - ...opt, - assignedUserIds: opt.assignedUsers.map((attributeToUser) => attributeToUser.member.userId), - }; - }); - } - - async assignOrganizationAttributeOptionToUser({ - organizationId, - membershipId, - attributeId, - value, - attributeOptionId, - }: { - organizationId: number; - membershipId: number; - attributeId: string; - value?: string; - attributeOptionId?: string; - }) { - let _attributeOptionId = attributeOptionId; - - if (value && !attributeOptionId) { - _attributeOptionId = await this.createDynamicAttributeOption(organizationId, attributeId, value); - } - - if (!_attributeOptionId) throw new Error("Attribute option not found"); - - return this.dbWrite.prisma.attributeToUser.create({ - data: { - attributeOptionId: _attributeOptionId, - memberId: membershipId, - }, - }); - } - - private async createDynamicAttributeOption( - organizationId: number, - attributeId: string, - value: string - ): Promise { - const attributeOption = await this.createOrganizationAttributeOption(organizationId, attributeId, { - value: value, - slug: slugify(value), - }); - return attributeOption.id; - } - - async unassignOrganizationAttributeOptionFromUser( - organizationId: number, - userId: number, - attributeOptionId: string - ) { - const membership = await this.organizationsMembershipsService.getOrgMembershipByUserId( - organizationId, - userId - ); - - if (!membership) throw new Error("Membership not found"); - - try { - const deletedAttributeToUser = await this.dbWrite.prisma.attributeToUser.delete({ - where: { memberId_attributeOptionId: { memberId: membership.id, attributeOptionId } }, - }); - return deletedAttributeToUser; - } catch (error: any) { - if (error.code === "P2025") { - // P2025 is the Prisma error code for "Record to delete does not exist." - this.logger.warn(`Attribute option not found: ${attributeOptionId} for user ${userId}`); - throw new NotFoundException("Attribute does not belong to this user"); - } - throw error; - } - } -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/organization-attributes-options.e2e-spec.ts b/apps/api/v2/src/modules/organizations/attributes/options/organization-attributes-options.e2e-spec.ts deleted file mode 100644 index afda1173150414..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/organization-attributes-options.e2e-spec.ts +++ /dev/null @@ -1,475 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { Attribute, AttributeOption, Membership, 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 { AttributeRepositoryFixture } from "test/fixtures/repository/attributes.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; -import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; -import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; -import { randomString } from "test/utils/randomString"; -import { withApiAuth } from "test/utils/withApiAuth"; -import { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input"; -import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/attributes/options/inputs/organizations-attributes-options-assign.input"; -import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts"; -import { AssignedOptionOutput } from "@/modules/organizations/attributes/options/outputs/assigned-options.output"; -import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Attributes Options Endpoints", () => { - describe("User lacks required role", () => { - let app: INestApplication; - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipFixtures: MembershipRepositoryFixture; - - const userEmail = `organization-attributes-options-member-${randomString()}@api.com`; - let user: User; - let org: Team; - let membership: Membership; - let attributeId: string; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipFixtures = new MembershipRepositoryFixture(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `organization-attributes-options-organization-${randomString()}`, - isOrganization: true, - }); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - organization: { connect: { id: org.id } }, - }); - - membership = await membershipFixtures.addUserToOrg(user, org, "MEMBER", true); - - // Create an attribute for testing - attributeId = "test-attribute-id"; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should not be able to create attribute option", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/attributes/${attributeId}/options`) - .send({ - name: "Option 1", - value: "option1", - }) - .expect(403); - }); - - it("should not be able to delete attribute option", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/attributes/${attributeId}/options/1`) - .expect(403); - }); - - afterAll(async () => { - await membershipFixtures.delete(membership.id); - await userRepositoryFixture.deleteByEmail(user.email); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); - - describe("User has required role", () => { - let app: INestApplication; - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixtures: TeamRepositoryFixture; - - let membershipFixtures: MembershipRepositoryFixture; - let attributeRepositoryFixture: AttributeRepositoryFixture; - - const userEmail = `organization-attributes-options-admin-${randomString()}@api.com`; - const userEmail2 = `organization-attributes-options-member-${randomString()}@api.com`; - - let user: User; - let user2: User; - let org: Team; - let team: Team; - let membership: Membership; - let membership2: Membership; - - let attributeId: string; - let attributeSlug: string; - - let createdOption: any; - let createdOption2: any; - let attribute2: Attribute; - let attribute2Option: AttributeOption; - - const createOptionInput: CreateOrganizationAttributeOptionInput = { - value: "option1", - slug: `option1-${randomString()}`, - }; - - const createOption2Input: CreateOrganizationAttributeOptionInput = { - value: "option2", - slug: `option2${randomString()}`, - }; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixtures = new TeamRepositoryFixture(moduleRef); - membershipFixtures = new MembershipRepositoryFixture(moduleRef); - attributeRepositoryFixture = new AttributeRepositoryFixture(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `organization-attributes-options-admin-organization-${randomString()}`, - isOrganization: true, - }); - - team = await teamsRepositoryFixtures.create({ name: "org team", parent: { connect: { id: org.id } } }); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - organization: { connect: { id: org.id } }, - }); - - user2 = await userRepositoryFixture.create({ - email: userEmail2, - username: userEmail2, - organization: { connect: { id: org.id } }, - }); - - membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true); - membership2 = await membershipFixtures.addUserToOrg(user2, org, "ADMIN", true); - await membershipFixtures.create({ - role: "MEMBER", - accepted: true, - team: { connect: { id: team.id } }, - user: { connect: { id: user.id } }, - }); - - // Create an attribute for testing - const attribute = await attributeRepositoryFixture.create({ - name: "Test Attribute", - team: { connect: { id: org.id } }, - type: "TEXT", - slug: `test-attribute-${randomString()}`, - }); - - attribute2 = await attributeRepositoryFixture.create({ - name: "Test Attribute 2", - team: { connect: { id: org.id } }, - type: "TEXT", - slug: `test-attribute-2-${randomString()}`, - }); - attributeId = attribute.id; - attributeSlug = attribute.slug; - attribute2Option = await attributeRepositoryFixture.createOption({ - slug: `optionA-${randomString()}`, - value: "optionA", - attribute: { connect: { id: attribute2.id } }, - }); - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should create attribute option", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/attributes/${attributeId}/options`) - .send(createOptionInput) - .expect(201) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - createdOption = response.body.data; - expect(createdOption.value).toEqual(createOptionInput.value); - expect(createdOption.slug).toEqual(createOptionInput.slug); - }); - }); - - it("should create attribute option", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/attributes/${attributeId}/options`) - .send(createOption2Input) - .expect(201) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - createdOption2 = response.body.data; - expect(createdOption2.value).toEqual(createOption2Input.value); - expect(createdOption2.slug).toEqual(createOption2Input.slug); - }); - }); - - it("should get attribute options", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/attributes/${attributeId}/options`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - const options = response.body.data as OptionOutput[]; - expect(options.length).toEqual(2); - expect(options.find((opt) => opt.value === createOptionInput.value)).toBeDefined(); - expect(options.find((opt) => opt.slug === createOptionInput.slug)).toBeDefined(); - expect(options.find((opt) => opt.value === createOption2Input.value)).toBeDefined(); - expect(options.find((opt) => opt.slug === createOption2Input.slug)).toBeDefined(); - }); - }); - - it("should update attribute option", async () => { - const updateOptionInput: UpdateOrganizationAttributeOptionInput = { - value: "updated-option-value", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/attributes/${attributeId}/options/${createdOption.id}`) - .send(updateOptionInput) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - const updatedOption = response.body.data; - expect(updatedOption.value).toEqual(updateOptionInput.value); - }); - }); - - it("should assign attribute option to user", async () => { - const assignInput: AssignOrganizationAttributeOptionToUserInput = { - attributeId: attributeId, - attributeOptionId: createdOption.id, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/attributes/options/${user.id}`) - .send(assignInput) - .expect(201) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - expect(response.body.data).toBeTruthy(); - }); - }); - - it("should assign attribute option to user2", async () => { - const assignInput: AssignOrganizationAttributeOptionToUserInput = { - attributeId: attributeId, - attributeOptionId: createdOption2.id, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/attributes/options/${user2.id}`) - .send(assignInput) - .expect(201) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - expect(response.body.data).toBeTruthy(); - }); - }); - - it("should assign attribute option to user2", async () => { - const assignInput: AssignOrganizationAttributeOptionToUserInput = { - attributeId: attribute2.id, - attributeOptionId: attribute2Option.id, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/attributes/options/${user2.id}`) - .send(assignInput) - .expect(201) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - expect(response.body.data).toBeTruthy(); - }); - }); - - it("should get attribute options for user", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/attributes/options/${user.id}`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - const userOptions = response.body.data; - expect(userOptions.length).toEqual(1); - expect(userOptions[0].id).toEqual(createdOption.id); - }); - }); - - it("should get attribute options for user2", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/attributes/options/${user2.id}`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - const userOptions = response.body.data as AttributeOption[]; - expect(userOptions.length).toEqual(2); - expect(userOptions.find((opt) => opt.id === createdOption2.id)).toBeDefined(); - expect(userOptions.find((opt) => opt.id === attribute2Option.id)).toBeDefined(); - }); - }); - - it("should get attribute all assigned options", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/attributes/${attributeId}/options/assigned`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - const assignedOptions = response.body.data as AssignedOptionOutput[]; - expect(assignedOptions?.length).toEqual(2); - - expect(assignedOptions.find((opt) => createdOption.id === opt.id)).toBeDefined(); - expect( - assignedOptions - .find((opt) => createdOption.id === opt.id) - ?.assignedUserIds.find((id) => id === user.id) - ).toBeDefined(); - - expect(assignedOptions.find((opt) => createdOption2.id === opt.id)).toBeDefined(); - expect( - assignedOptions - .find((opt) => createdOption2.id === opt.id) - ?.assignedUserIds.find((id) => id === user2.id) - ).toBeDefined(); - }); - }); - - it("should get attribute all assigned options filtered by team id", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/attributes/${attributeId}/options/assigned?teamIds=${team.id}`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - const assignedOptions = response.body.data as AssignedOptionOutput[]; - expect(assignedOptions?.length).toEqual(1); - - expect(assignedOptions.find((opt) => createdOption.id === opt.id)).toBeDefined(); - expect( - assignedOptions - .find((opt) => createdOption.id === opt.id) - ?.assignedUserIds.find((id) => id === user.id) - ).toBeDefined(); - }); - }); - - it("should get attribute all assigned options by attribute slug", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/attributes/slugs/${attributeSlug}/options/assigned`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - const assignedOptions = response.body.data as AssignedOptionOutput[]; - expect(assignedOptions?.length).toEqual(2); - - expect(assignedOptions.find((opt) => createdOption.id === opt.id)).toBeDefined(); - expect( - assignedOptions - .find((opt) => createdOption.id === opt.id) - ?.assignedUserIds.find((id) => id === user.id) - ).toBeDefined(); - - expect(assignedOptions.find((opt) => createdOption2.id === opt.id)).toBeDefined(); - expect( - assignedOptions - .find((opt) => createdOption2.id === opt.id) - ?.assignedUserIds.find((id) => id === user2.id) - ).toBeDefined(); - }); - }); - - it("should get attribute all assigned options filtered by other assigned options", async () => { - return request(app.getHttpServer()) - .get( - `/v2/organizations/${org.id}/attributes/${attributeId}/options/assigned?assignedOptionIds=${attribute2Option.id}` - ) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - const assignedOptions = response.body.data as AssignedOptionOutput[]; - expect(assignedOptions?.length).toEqual(1); - expect(assignedOptions.find((opt) => createdOption2.id === opt.id)).toBeDefined(); - expect( - assignedOptions - .find((opt) => createdOption2.id === opt.id) - ?.assignedUserIds.find((id) => id === user2.id) - ).toBeDefined(); - }); - }); - - it("should not get attribute all assigned options filtered by other assigned options and teamIds in which the user is not part of", async () => { - return request(app.getHttpServer()) - .get( - `/v2/organizations/${org.id}/attributes/${attributeId}/options/assigned?assignedOptionIds=${attribute2Option.id}&teamIds=${team.id}` - ) - .expect(404); - }); - - it("should get attribute all assigned options filtered by other assigned options and teamIds", async () => { - return request(app.getHttpServer()) - .get( - `/v2/organizations/${org.id}/attributes/${attributeId}/options/assigned?assignedOptionIds=${createdOption.id}&teamIds=${team.id}` - ) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - const assignedOptions = response.body.data as AssignedOptionOutput[]; - expect(assignedOptions?.length).toEqual(1); - expect(assignedOptions.find((opt) => createdOption.id === opt.id)).toBeDefined(); - expect( - assignedOptions - .find((opt) => createdOption.id === opt.id) - ?.assignedUserIds.find((id) => id === user.id) - ).toBeDefined(); - }); - }); - - it("should unassign attribute option from user", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/attributes/options/${user.id}/${createdOption.id}`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - expect(response.body.data).toBeTruthy(); - }); - }); - - it("should delete attribute option", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/attributes/${attributeId}/options/${createdOption.id}`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - expect(response.body.data).toBeTruthy(); - }); - }); - - afterAll(async () => { - await membershipFixtures.delete(membership.id); - await membershipFixtures.delete(membership2.id); - await userRepositoryFixture.deleteByEmail(user.email); - await userRepositoryFixture.deleteByEmail(user2.email); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/attributes/options/organizations-attributes-options.controller.ts b/apps/api/v2/src/modules/organizations/attributes/options/organizations-attributes-options.controller.ts deleted file mode 100644 index 66afccd3882efd..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/organizations-attributes-options.controller.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input"; -import { GetAssignedAttributeOptions } from "@/modules/organizations/attributes/options/inputs/get-assigned-attribute-options.input"; -import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/attributes/options/inputs/organizations-attributes-options-assign.input"; -import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts"; -import { - AssignOptionUserOutput, - UnassignOptionUserOutput, -} from "@/modules/organizations/attributes/options/outputs/assign-option-user.output"; -import { GetAllAttributeAssignedOptionOutput } from "@/modules/organizations/attributes/options/outputs/assigned-options.output"; -import { CreateAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/create-option.output"; -import { DeleteAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/delete-option.output"; -import { GetOptionUserOutput } from "@/modules/organizations/attributes/options/outputs/get-option-user.output"; -import { GetAllAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/get-option.output"; -import { UpdateAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/update-option.output"; -import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service"; -import { - Body, - Controller, - Delete, - Get, - Param, - ParseIntPipe, - Patch, - Post, - Query, - UseGuards, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; - -@Controller({ - path: "/v2/organizations/:orgId", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Attributes / Options") -@ApiHeader(API_KEY_HEADER) -export class OrganizationsAttributesOptionsController { - constructor(private readonly organizationsAttributesOptionsService: OrganizationAttributeOptionService) {} - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Post("/attributes/:attributeId/options") - @ApiOperation({ summary: "Create an attribute option" }) - async createOrganizationAttributeOption( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("attributeId") attributeId: string, - @Body() bodyAttribute: CreateOrganizationAttributeOptionInput - ): Promise { - const attributeOption = - await this.organizationsAttributesOptionsService.createOrganizationAttributeOption( - orgId, - attributeId, - bodyAttribute - ); - return { - status: SUCCESS_STATUS, - data: attributeOption, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Delete("/attributes/:attributeId/options/:optionId") - @ApiOperation({ summary: "Delete an attribute option" }) - async deleteOrganizationAttributeOption( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("attributeId") attributeId: string, - @Param("optionId") optionId: string - ): Promise { - const attributeOption = - await this.organizationsAttributesOptionsService.deleteOrganizationAttributeOption( - orgId, - attributeId, - optionId - ); - return { - status: SUCCESS_STATUS, - data: attributeOption, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Patch("/attributes/:attributeId/options/:optionId") - @ApiOperation({ summary: "Update an attribute option" }) - async updateOrganizationAttributeOption( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("attributeId") attributeId: string, - @Param("optionId") optionId: string, - @Body() bodyAttribute: UpdateOrganizationAttributeOptionInput - ): Promise { - const attributeOption = - await this.organizationsAttributesOptionsService.updateOrganizationAttributeOption( - orgId, - attributeId, - optionId, - bodyAttribute - ); - return { - status: SUCCESS_STATUS, - data: attributeOption, - }; - } - - @Roles("ORG_MEMBER") - @PlatformPlan("ESSENTIALS") - @Get("/attributes/:attributeId/options") - @ApiOperation({ summary: "Get all attribute options" }) - async getOrganizationAttributeOptions( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("attributeId") attributeId: string - ): Promise { - const attributeOptions = await this.organizationsAttributesOptionsService.getOrganizationAttributeOptions( - orgId, - attributeId - ); - return { - status: SUCCESS_STATUS, - data: attributeOptions, - }; - } - - @Roles("ORG_MEMBER") - @PlatformPlan("ESSENTIALS") - @Get("/attributes/:attributeId/options/assigned") - @ApiOperation({ summary: "Get all assigned attribute options by attribute ID" }) - async getOrganizationAttributeAssignedOptions( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("attributeId") attributeId: string, - @Query() queryParams: GetAssignedAttributeOptions - ): Promise { - const { skip, take, ...rest } = queryParams; - const attributeOptions = - await this.organizationsAttributesOptionsService.getOrganizationAttributeAssignedOptions({ - organizationId: orgId, - attributeId, - skip: skip ?? 0, - take: take ?? 250, - filters: rest, - }); - return { - status: SUCCESS_STATUS, - data: attributeOptions, - }; - } - - @Roles("ORG_MEMBER") - @PlatformPlan("ESSENTIALS") - @Get("/attributes/slugs/:attributeSlug/options/assigned") - @ApiOperation({ summary: "Get all assigned attribute options by attribute slug" }) - async getOrganizationAttributeAssignedOptionsBySlug( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("attributeSlug") attributeSlug: string, - @Query() queryParams: GetAssignedAttributeOptions - ): Promise { - const { skip, take, ...rest } = queryParams; - const attributeOptions = - await this.organizationsAttributesOptionsService.getOrganizationAttributeAssignedOptions({ - organizationId: orgId, - attributeSlug, - skip: skip ?? 0, - take: take ?? 250, - filters: rest, - }); - return { - status: SUCCESS_STATUS, - data: attributeOptions, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Post("/attributes/options/:userId") - @ApiOperation({ summary: "Assign an attribute to a user" }) - async assignOrganizationAttributeOptionToUser( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("userId", ParseIntPipe) userId: number, - @Body() bodyAttribute: AssignOrganizationAttributeOptionToUserInput - ): Promise { - const attributeOption = - await this.organizationsAttributesOptionsService.assignOrganizationAttributeOptionToUser( - orgId, - userId, - bodyAttribute - ); - return { - status: SUCCESS_STATUS, - data: attributeOption, - }; - } - - @Roles("ORG_MEMBER") - @PlatformPlan("ESSENTIALS") - @Delete("/attributes/options/:userId/:attributeOptionId") - @ApiOperation({ summary: "Unassign an attribute from a user" }) - async unassignOrganizationAttributeOptionFromUser( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("userId", ParseIntPipe) userId: number, - @Param("attributeOptionId") attributeOptionId: string - ): Promise { - const attributeOption = - await this.organizationsAttributesOptionsService.unassignOrganizationAttributeOptionFromUser( - orgId, - userId, - attributeOptionId - ); - return { - status: SUCCESS_STATUS, - data: attributeOption, - }; - } - - @Roles("ORG_MEMBER") - @PlatformPlan("ESSENTIALS") - @Get("/attributes/options/:userId") - @ApiOperation({ summary: "Get all attribute options for a user" }) - async getOrganizationAttributeOptionsForUser( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("userId", ParseIntPipe) userId: number - ): Promise { - const attributeOptions = - await this.organizationsAttributesOptionsService.getOrganizationAttributeOptionsForUser(orgId, userId); - return { - status: SUCCESS_STATUS, - data: attributeOptions, - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/outputs/assign-option-user.output.ts b/apps/api/v2/src/modules/organizations/attributes/options/outputs/assign-option-user.output.ts deleted file mode 100644 index 7c4ecd05f20ca8..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/outputs/assign-option-user.output.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output"; -import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsString, ValidateNested } from "class-validator"; - -class AssignOptionUserOutputData { - @IsString() - @ApiProperty({ type: String, required: true, description: "The ID of the option assigned to the user" }) - id!: string; - - @IsString() - @ApiProperty({ type: Number, required: true, description: "The ID form the org membership for the user" }) - memberId!: number; - - @IsString() - @ApiProperty({ type: String, required: true, description: "The value of the option" }) - attributeOptionId!: string; -} - -export class AssignOptionUserOutput extends BaseOutputDTO { - @Expose() - @ValidateNested() - @Type(() => AssignOptionUserOutputData) - data!: AssignOptionUserOutputData; -} - -export class UnassignOptionUserOutput extends BaseOutputDTO { - @Expose() - @ValidateNested() - @Type(() => AssignOptionUserOutputData) - data!: AssignOptionUserOutputData; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/outputs/assigned-options.output.ts b/apps/api/v2/src/modules/organizations/attributes/options/outputs/assigned-options.output.ts deleted file mode 100644 index 8da3918bbf1ca8..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/outputs/assigned-options.output.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output"; -import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsArray, ValidateNested } from "class-validator"; - -export class AssignedOptionOutput extends OptionOutput { - @Expose() - @IsArray() - @ApiProperty({ - type: Array, - required: true, - description: "Ids of the users assigned to the attribute option.", - example: [124, 224], - }) - assignedUserIds!: number[]; -} - -export class GetAllAttributeAssignedOptionOutput extends BaseOutputDTO { - @Expose() - @ValidateNested() - @Type(() => OptionOutput) - data!: AssignedOptionOutput[]; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/outputs/create-option.output.ts b/apps/api/v2/src/modules/organizations/attributes/options/outputs/create-option.output.ts deleted file mode 100644 index edc8d7d9120942..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/outputs/create-option.output.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output"; -import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output"; -import { Expose, Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -export class CreateAttributeOptionOutput extends BaseOutputDTO { - @Expose() - @ValidateNested() - @Type(() => OptionOutput) - data!: OptionOutput; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/outputs/delete-option.output.ts b/apps/api/v2/src/modules/organizations/attributes/options/outputs/delete-option.output.ts deleted file mode 100644 index 6f6306494c009b..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/outputs/delete-option.output.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output"; -import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output"; -import { Expose, Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -export class DeleteAttributeOptionOutput extends BaseOutputDTO { - @Expose() - @ValidateNested() - @Type(() => OptionOutput) - data!: OptionOutput; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/outputs/get-option-user.output.ts b/apps/api/v2/src/modules/organizations/attributes/options/outputs/get-option-user.output.ts deleted file mode 100644 index 2e31b64bd6f914..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/outputs/get-option-user.output.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsString, ValidateNested } from "class-validator"; - -class GetOptionUserOutputData { - @IsString() - @ApiProperty({ type: String, required: true, description: "The ID of the option assigned to the user" }) - id!: string; - - @IsString() - @ApiProperty({ type: String, required: true, description: "The ID of the attribute" }) - attributeId!: string; - - @IsString() - @ApiProperty({ type: String, required: true, description: "The value of the option" }) - value!: string; - - @IsString() - @ApiProperty({ type: String, required: true, description: "The slug of the option" }) - slug!: string; -} - -export class GetOptionUserOutput extends BaseOutputDTO { - @Expose() - @ValidateNested() - @Type(() => GetOptionUserOutputData) - data!: GetOptionUserOutputData[]; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/outputs/get-option.output.ts b/apps/api/v2/src/modules/organizations/attributes/options/outputs/get-option.output.ts deleted file mode 100644 index 52a1e8800e9229..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/outputs/get-option.output.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output"; -import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output"; -import { Expose, Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -export class GetAllAttributeOptionOutput extends BaseOutputDTO { - @Expose() - @ValidateNested() - @Type(() => OptionOutput) - data!: OptionOutput[]; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/outputs/option.output.ts b/apps/api/v2/src/modules/organizations/attributes/options/outputs/option.output.ts deleted file mode 100644 index 08408a39d6f535..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/outputs/option.output.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Expose } from "class-transformer"; -import { IsString } from "class-validator"; - -export class OptionOutput { - @Expose() - @IsString() - @ApiProperty({ - type: String, - required: true, - description: "The ID of the option", - example: "attr_option_id", - }) - id!: string; - - @Expose() - @IsString() - @ApiProperty({ type: String, required: true, description: "The ID of the attribute", example: "attr_id" }) - attributeId!: string; - - @Expose() - @IsString() - @ApiProperty({ - type: String, - required: true, - description: "The value of the option", - example: "option_value", - }) - value!: string; - - @Expose() - @IsString() - @ApiProperty({ - type: String, - required: true, - description: "The slug of the option", - example: "option-slug", - }) - slug!: string; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/outputs/update-option.output.ts b/apps/api/v2/src/modules/organizations/attributes/options/outputs/update-option.output.ts deleted file mode 100644 index 9d1e94bfb9dd04..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/outputs/update-option.output.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output"; -import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output"; -import { Expose, Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -export class UpdateAttributeOptionOutput extends BaseOutputDTO { - @Expose() - @ValidateNested() - @Type(() => OptionOutput) - data!: OptionOutput; -} diff --git a/apps/api/v2/src/modules/organizations/attributes/options/services/organization-attributes-option.service.ts b/apps/api/v2/src/modules/organizations/attributes/options/services/organization-attributes-option.service.ts deleted file mode 100644 index 07b5c8937dd844..00000000000000 --- a/apps/api/v2/src/modules/organizations/attributes/options/services/organization-attributes-option.service.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { OrganizationAttributesService } from "@/modules/organizations/attributes/index/services/organization-attributes.service"; -import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input"; -import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/attributes/options/inputs/organizations-attributes-options-assign.input"; -import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts"; -import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository"; -import { AssignedOptionOutput } from "@/modules/organizations/attributes/options/outputs/assigned-options.output"; -import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service"; -import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; -import { plainToClass } from "class-transformer"; - -const TYPE_SUPPORTS_VALUE = new Set(["TEXT", "NUMBER"]); - -@Injectable() -export class OrganizationAttributeOptionService { - private readonly logger = new Logger("OrganizationAttributeOptionService"); - constructor( - private readonly organizationAttributeOptionRepository: OrganizationAttributeOptionRepository, - private readonly organizationAttributesService: OrganizationAttributesService, - private readonly organizationsMembershipsService: OrganizationsMembershipService - ) {} - - async createOrganizationAttributeOption( - organizationId: number, - attributeId: string, - data: CreateOrganizationAttributeOptionInput - ) { - return this.organizationAttributeOptionRepository.createOrganizationAttributeOption( - organizationId, - attributeId, - data - ); - } - - async deleteOrganizationAttributeOption(organizationId: number, attributeId: string, optionId: string) { - return this.organizationAttributeOptionRepository.deleteOrganizationAttributeOption( - organizationId, - attributeId, - optionId - ); - } - - async updateOrganizationAttributeOption( - organizationId: number, - attributeId: string, - optionId: string, - data: UpdateOrganizationAttributeOptionInput - ) { - return this.organizationAttributeOptionRepository.updateOrganizationAttributeOption( - organizationId, - attributeId, - optionId, - data - ); - } - - async getOrganizationAttributeOptions(organizationId: number, attributeId: string) { - return this.organizationAttributeOptionRepository.getOrganizationAttributeOptions( - organizationId, - attributeId - ); - } - - async assignOrganizationAttributeOptionToUser( - organizationId: number, - userId: number, - data: AssignOrganizationAttributeOptionToUserInput - ) { - const attribute = await this.organizationAttributesService.getOrganizationAttribute( - organizationId, - data.attributeId - ); - - if (!attribute) { - throw new NotFoundException("Attribute not found"); - } - - const membership = await this.organizationsMembershipsService.getOrgMembershipByUserId( - organizationId, - userId - ); - - if (!membership || !membership.accepted) - throw new NotFoundException("User is not a member of the organization"); - - if (!TYPE_SUPPORTS_VALUE.has(attribute.type) && data.value) { - throw new BadRequestException("Attribute type does not support value"); - } - - return this.organizationAttributeOptionRepository.assignOrganizationAttributeOptionToUser({ - organizationId, - membershipId: membership.id, - value: data.value, - attributeId: data.attributeId, - attributeOptionId: data.attributeOptionId, - }); - } - - async unassignOrganizationAttributeOptionFromUser( - organizationId: number, - userId: number, - attributeOptionId: string - ) { - return this.organizationAttributeOptionRepository.unassignOrganizationAttributeOptionFromUser( - organizationId, - userId, - attributeOptionId - ); - } - - async getOrganizationAttributeOptionsForUser(organizationId: number, userId: number) { - return this.organizationAttributeOptionRepository.getOrganizationAttributeOptionsForUser( - organizationId, - userId - ); - } - - async getOrganizationAttributeAssignedOptions({ - organizationId, - attributeId, - attributeSlug, - skip, - take, - filters, - }: GetOrganizationAttributeAssignedOptionsProp) { - const options = await this.organizationAttributeOptionRepository.getOrganizationAttributeAssignedOptions({ - organizationId, - ...(attributeId ? { attributeId } : { attributeSlug }), - skip, - take, - filters, - }); - return options.map((opt) => plainToClass(AssignedOptionOutput, opt, { strategy: "excludeAll" })); - } -} - -// Discriminative Union on attributeSlug / attributeId -export type GetOrganizationAttributeAssignedOptionsProp = - | GetOrganizationAttributeAssignedOptionsPropById - | GetOrganizationAttributeAssignedOptionsPropBySlug; - -type GetOrganizationAttributeAssignedOptionsPropById = { - organizationId: number; - attributeId: string; - attributeSlug?: undefined; - skip: number; - take: number; - filters?: { assignedOptionIds?: string[]; teamIds?: number[] }; -}; - -type GetOrganizationAttributeAssignedOptionsPropBySlug = { - organizationId: number; - attributeId?: undefined; - attributeSlug?: string; - skip: number; - take: number; - filters?: { assignedOptionIds?: string[]; teamIds?: number[] }; -}; diff --git a/apps/api/v2/src/modules/organizations/bookings/managed-organizations-bookings.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/bookings/managed-organizations-bookings.controller.e2e-spec.ts deleted file mode 100644 index cab7ec59ff1394..00000000000000 --- a/apps/api/v2/src/modules/organizations/bookings/managed-organizations-bookings.controller.e2e-spec.ts +++ /dev/null @@ -1,549 +0,0 @@ -import { randomString } from "@calcom/lib/random"; -import { - CAL_API_VERSION_HEADER, - SUCCESS_STATUS, - VERSION_2024_08_13, - X_CAL_CLIENT_ID, - X_CAL_SECRET_KEY, -} from "@calcom/platform-constants"; -import type { - BookingOutput_2024_08_13, - GetBookingsOutput_2024_08_13, - GetSeatedBookingOutput_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 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"; -import { ManagedOrganizationsRepositoryFixture } from "test/fixtures/repository/managed-organizations.repository.fixture"; -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 { TeamRepositoryFixture } from "test/fixtures/repository/team.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 { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; -import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; -import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.module"; - -describe("Organizations Bookings Endpoints 2024-08-13", () => { - describe("Manager and managed organizations bookings", () => { - let app: INestApplication; - let managerOrganization: Team; - let managedOrganization: Team; - - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let oAuthClient: PlatformOAuthClient; - let teamRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let hostsRepositoryFixture: HostsRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let managedOrganizationsRepositoryFixture: ManagedOrganizationsRepositoryFixture; - - const managerOrgUserEmail = "manager-org-user-1-bookings@api.com"; - const managerOrgUserEmail2 = "manager-org-user-2-bookings@api.com"; - const managedOrgUserEmail = "managed-org-user-1-bookings@api.com"; - const nonOrgUserEmail1 = "non-org-user-1-bookings@api.com"; - let managerOrgUser1: User; - let managerOrgUser2: User; - let managedOrgUser: User; - let nonOrgUser1: User; - let managerOrgTeam1: Team; - let managedOrgTeam1: Team; - - let managerOrgEventTypeId: number; - let managedOrgEventTypeId: number; - let nonOrgEventTypeId: number; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - managerOrgUserEmail, - Test.createTestingModule({ - imports: [AppModule, OrganizationsTeamsBookingsModule], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .overrideGuard(PlatformPlanGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - managedOrganizationsRepositoryFixture = new ManagedOrganizationsRepositoryFixture(moduleRef); - - await setupManagerOrganization(); - await setupManagedOrganization(); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - async function setupManagerOrganization() { - managerOrganization = await organizationsRepositoryFixture.create({ - name: "organization bookings", - isPlatform: true, - }); - managerOrgTeam1 = await teamRepositoryFixture.create({ - name: "team orgs booking 1", - isOrganization: false, - parent: { connect: { id: managerOrganization.id } }, - }); - oAuthClient = await createOAuthClient(managerOrganization.id); - - nonOrgUser1 = await userRepositoryFixture.create({ - email: nonOrgUserEmail1, - locale: "it", - name: "NonOrgUser1Bookings", - platformOAuthClients: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - managerOrgUser1 = await userRepositoryFixture.create({ - email: managerOrgUserEmail, - locale: "it", - name: "orgUser1Bookings", - platformOAuthClients: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - managerOrgUser2 = await userRepositoryFixture.create({ - email: managerOrgUserEmail2, - locale: "it", - name: "orgUser2Bookings", - platformOAuthClients: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: "working time", - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(managerOrgUser1.id, userSchedule); - await schedulesService.createUserSchedule(nonOrgUser1.id, userSchedule); - await schedulesService.createUserSchedule(managerOrgUser2.id, userSchedule); - - const orgEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: managerOrgTeam1.id }, - }, - title: "Collective Event Type", - slug: "manager-org-bookings-collective-event-type", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - await profileRepositoryFixture.create({ - uid: `usr-${managerOrgUser1.id}`, - username: managerOrgUserEmail, - organization: { - connect: { - id: managerOrganization.id, - }, - }, - user: { - connect: { - id: managerOrgUser1.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${managerOrgUser2.id}`, - username: managerOrgUserEmail2, - organization: { - connect: { - id: managerOrganization.id, - }, - }, - user: { - connect: { - id: managerOrgUser2.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: managerOrgUser1.id } }, - team: { connect: { id: managerOrganization.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: managerOrgUser2.id } }, - team: { connect: { id: managerOrganization.id } }, - accepted: true, - }); - - const nonOrgEventType = await eventTypesRepositoryFixture.create( - { - title: "Non Org Event Type", - slug: "non-org-event-type", - length: 60, - bookingFields: [], - locations: [], - }, - nonOrgUser1.id - ); - - managerOrgEventTypeId = orgEventType.id; - nonOrgEventTypeId = nonOrgEventType.id; - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: managerOrgUser2.id, - }, - }, - eventType: { - connect: { - id: managerOrgEventTypeId, - }, - }, - }); - - await bookingsRepositoryFixture.create({ - user: { - connect: { - id: managerOrgUser2.id, - }, - }, - startTime: new Date(Date.UTC(2030, 0, 9, 13, 0, 0)), - endTime: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)), - title: "Manager Org Collective Booking", - uid: `manager-org-collective-${randomString()}`, - eventType: { - connect: { - id: managerOrgEventTypeId, - }, - }, - location: "https://meet.google.com/abc-def-ghi", - customInputs: {}, - metadata: {}, - responses: { - name: "alice", - email: "alice@gmail.com", - }, - attendees: { - create: { - email: "alice@gmail.com", - name: "alice", - locale: "es", - timeZone: "Europe/Madrid", - }, - }, - }); - - await bookingsRepositoryFixture.create({ - user: { - connect: { - id: nonOrgUser1.id, - }, - }, - startTime: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)), - endTime: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)), - title: "Non-Org Booking 1", - uid: `non-org-booking-1-${randomString()}`, - eventType: { - connect: { - id: nonOrgEventTypeId, - }, - }, - location: "https://meet.google.com/jkl-mno-pqr", - customInputs: {}, - metadata: {}, - responses: { - name: managerOrgUser2.name ?? "", - email: managerOrgUserEmail2, - }, - attendees: { - create: { - email: managerOrgUserEmail2, - name: managerOrgUser2.name ?? "", - locale: "en", - timeZone: managerOrgUser2.timeZone ?? "Europe/Madrid", - }, - }, - }); - } - - async function setupManagedOrganization() { - managedOrganization = await organizationsRepositoryFixture.create({ - name: "organization bookings", - isPlatform: true, - }); - await managedOrganizationsRepositoryFixture.createManagedOrganization( - managerOrganization.id, - managedOrganization.id - ); - - managedOrgTeam1 = await teamRepositoryFixture.create({ - name: "managed org team 1", - isOrganization: false, - parent: { connect: { id: managedOrganization.id } }, - }); - - managedOrgUser = await userRepositoryFixture.create({ - email: managedOrgUserEmail, - locale: "it", - name: "ManagedOrgUser1Bookings", - platformOAuthClients: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${managedOrgUser.id}`, - username: managedOrgUserEmail, - organization: { - connect: { - id: managedOrganization.id, - }, - }, - user: { - connect: { - id: managedOrgUser.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${managerOrgUser1.id}`, - username: managerOrgUserEmail, - organization: { - connect: { - id: managedOrganization.id, - }, - }, - user: { - connect: { - id: managerOrgUser1.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: managedOrgUser.id } }, - team: { connect: { id: managedOrganization.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: managerOrgUser1.id } }, - team: { connect: { id: managedOrganization.id } }, - accepted: true, - }); - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: "working time", - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(managedOrgUser.id, userSchedule); - - const managedOrgEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: managedOrgTeam1.id }, - }, - title: "Collective Event Type", - slug: "managed-org-bookings-collective-event-type", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - managedOrgEventTypeId = managedOrgEventType.id; - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: managedOrgUser.id, - }, - }, - eventType: { - connect: { - id: managedOrgEventType.id, - }, - }, - }); - - await bookingsRepositoryFixture.create({ - user: { - connect: { - id: managedOrgUser.id, - }, - }, - startTime: new Date(Date.UTC(2030, 0, 11, 13, 0, 0)), - endTime: new Date(Date.UTC(2030, 0, 11, 14, 0, 0)), - title: "Managed Org Booking", - uid: `managed-org-booking-${randomString()}`, - eventType: { - connect: { - id: managedOrgEventTypeId, - }, - }, - location: "https://meet.google.com/ghi-jkl-mno", - customInputs: {}, - metadata: {}, - responses: { - name: "charlie", - email: "charlie@gmail.com", - }, - attendees: { - create: { - email: "charlie@gmail.com", - name: "charlie", - locale: "en", - timeZone: "Europe/Madrid", - }, - }, - }); - } - - describe("get manager organization bookings", () => { - it("should get bookings by organizationId for manager organization", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${managerOrganization.id}/bookings`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(2); - expect(data.map((booking) => booking.eventTypeId).sort()).toEqual( - [managerOrgEventTypeId, nonOrgEventTypeId].sort() - ); - }); - }); - - it("should get bookings by organizationId and userId", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${managerOrganization.id}/bookings?userIds=${managerOrgUser2.id}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(2); - expect(data.map((booking) => booking.eventTypeId).sort()).toEqual( - [managerOrgEventTypeId, nonOrgEventTypeId].sort() - ); - }); - }); - }); - - describe("get managed organization bookings", () => { - it("should get bookings by organizationId for managed organization", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${managedOrganization.id}/bookings`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].eventTypeId).toEqual(managedOrgEventTypeId); - }); - }); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: ["http://localhost:5555"], - permissions: 32, - areCalendarEventsEnabled: false, - areEmailsEnabled: false, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - afterAll(async () => { - await oauthClientRepositoryFixture.delete(oAuthClient.id); - await teamRepositoryFixture.delete(managerOrganization.id); - await userRepositoryFixture.deleteByEmail(managerOrgUser1.email); - await userRepositoryFixture.deleteByEmail(nonOrgUser1.email); - await bookingsRepositoryFixture.deleteAllBookings(managerOrgUser1.id, managerOrgUser1.email); - await bookingsRepositoryFixture.deleteAllBookings(nonOrgUser1.id, nonOrgUser1.email); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/bookings/organizations-bookings.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/bookings/organizations-bookings.controller.e2e-spec.ts deleted file mode 100644 index 5afb1b519b4618..00000000000000 --- a/apps/api/v2/src/modules/organizations/bookings/organizations-bookings.controller.e2e-spec.ts +++ /dev/null @@ -1,832 +0,0 @@ -import { - CAL_API_VERSION_HEADER, - SUCCESS_STATUS, - VERSION_2024_08_13, - X_CAL_CLIENT_ID, - X_CAL_SECRET_KEY, -} 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, -} 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 { 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 { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { 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 { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; -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 { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.module"; - -describe("Organizations Bookings Endpoints 2024-08-13", () => { - describe("Organization bookings", () => { - let app: INestApplication; - let organization: Team; - - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let oAuthClient: PlatformOAuthClient; - let teamRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let hostsRepositoryFixture: HostsRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - - const orgUserEmail = "org-user-1-bookings@api.com"; - const orgUserEmail2 = "org-user-2-bookings@api.com"; - const nonOrgUserEmail1 = "non-org-user-1-bookings@api.com"; - let orgUser: User; - let orgUser2: User; - let nonOrgUser1: User; - let team1: Team; - - let orgEventTypeId: number; - let orgEventTypeId2: number; - let nonOrgEventTypeId: number; - - let collectiveOrgBookingUid: string; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - orgUserEmail2, - Test.createTestingModule({ - imports: [AppModule, OrganizationsTeamsBookingsModule], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - - organization = await organizationsRepositoryFixture.create({ name: "organization bookings" }); - oAuthClient = await createOAuthClient(organization.id); - team1 = await teamRepositoryFixture.create({ - name: "team orgs booking 1", - isOrganization: false, - parent: { connect: { id: organization.id } }, - createdByOAuthClient: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - nonOrgUser1 = await userRepositoryFixture.create({ - email: nonOrgUserEmail1, - locale: "it", - name: "NonOrgUser1Bookings", - platformOAuthClients: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - orgUser = await userRepositoryFixture.create({ - email: orgUserEmail, - locale: "it", - name: "orgUser1Bookings", - platformOAuthClients: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - orgUser2 = await userRepositoryFixture.create({ - email: orgUserEmail2, - locale: "es", - name: "orgUser2Bookings", - platformOAuthClients: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: "working time", - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(orgUser.id, userSchedule); - await schedulesService.createUserSchedule(orgUser2.id, userSchedule); - await schedulesService.createUserSchedule(nonOrgUser1.id, userSchedule); - - const orgEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team1.id }, - }, - title: "Collective Event Type", - slug: "org-bookings-collective-event-type", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - const orgEventType2 = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: team1.id }, - }, - title: "Collective Event Type", - slug: "org-bookings-round-robin-event-type", - length: 60, - assignAllTeamMembers: false, - bookingFields: [], - locations: [], - }); - - orgEventTypeId2 = orgEventType2.id; - - await profileRepositoryFixture.create({ - uid: `usr-${orgUser.id}`, - username: orgUserEmail, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: orgUser.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${orgUser2.id}`, - username: orgUserEmail2, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: orgUser2.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: orgUser.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: orgUser2.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: orgUser.id } }, - team: { connect: { id: team1.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: orgUser2.id } }, - team: { connect: { id: team1.id } }, - accepted: true, - }); - - const nonOrgEventType = await eventTypesRepositoryFixture.create( - { - title: "Non Org Event Type", - slug: "non-org-event-type", - length: 60, - bookingFields: [], - locations: [], - }, - nonOrgUser1.id - ); - - orgEventTypeId = orgEventType.id; - nonOrgEventTypeId = nonOrgEventType.id; - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: orgUser.id, - }, - }, - eventType: { - connect: { - id: orgEventType.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: orgUser2.id, - }, - }, - eventType: { - connect: { - id: orgEventType.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: false, - user: { - connect: { - id: orgUser2.id, - }, - }, - eventType: { - connect: { - id: orgEventType2.id, - }, - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - describe("create organization bookings", () => { - it("should create an collective organization booking", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 9, 13, 0, 0)).toISOString(), - eventTypeId: orgEventTypeId, - attendee: { - name: "alice", - email: "alice@gmail.com", - timeZone: "Europe/Madrid", - language: "es", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - collectiveOrgBookingUid = data.uid; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(orgUser.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(orgEventTypeId); - expect(data.attendees.length).toEqual(2); - 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, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - - it("should create a round robin organization booking", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 10, 13, 0, 0)).toISOString(), - eventTypeId: orgEventTypeId2, - attendee: { - name: "alice", - email: "alice@gmail.com", - timeZone: "Europe/Madrid", - language: "es", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(orgUser2.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 10, 14, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(orgEventTypeId2); - expect(data.attendees.length).toEqual(1); - 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, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - - it("should create a non organization booking for org-user-1", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), - eventTypeId: nonOrgEventTypeId, - attendee: { - name: orgUser.name ?? "", - email: orgUserEmail, - timeZone: orgUser.timeZone ?? "Europe/Madrid", - language: "en", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(nonOrgUser1.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(nonOrgEventTypeId); - expect(data.attendees.length).toEqual(1); - expect(data.attendees[0]).toEqual({ - name: body.attendee.name, - email: orgUserEmail, - displayEmail: orgUserEmail, - timeZone: body.attendee.timeZone, - language: body.attendee.language, - absent: false, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - - it("should create a non organization booking for org-user-2", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 11, 13, 0, 0)).toISOString(), - eventTypeId: nonOrgEventTypeId, - attendee: { - name: orgUser2.name ?? "", - email: orgUserEmail2, - timeZone: orgUser2.timeZone ?? "Europe/Madrid", - language: "en", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(nonOrgUser1.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 11, 14, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(nonOrgEventTypeId); - expect(data.attendees.length).toEqual(1); - expect(data.attendees[0]).toEqual({ - name: body.attendee.name, - email: orgUserEmail2, - displayEmail: orgUserEmail2, - timeZone: body.attendee.timeZone, - language: body.attendee.language, - absent: false, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - }); - - describe("get organization bookings", () => { - it("should get bookings by organizationId", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/bookings`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(4); - expect(data.map((booking) => booking.eventTypeId).sort()).toEqual( - [orgEventTypeId, orgEventTypeId2, nonOrgEventTypeId, nonOrgEventTypeId].sort() - ); - }); - }); - - it("should get bookings by organizationId", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/bookings?userIds=${orgUser.id}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(2); - expect(data.map((booking) => booking.eventTypeId).sort()).toEqual( - [orgEventTypeId, nonOrgEventTypeId].sort() - ); - }); - }); - - it("should get bookings by organizationId and userIds", async () => { - return request(app.getHttpServer()) - .get( - `/v2/organizations/${organization.id}/bookings?userIds=${orgUser.id},${orgUser2.id}&skip=0&take=250` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(4); - expect(data.map((booking) => booking.eventTypeId).sort()).toEqual( - [orgEventTypeId, orgEventTypeId2, nonOrgEventTypeId, nonOrgEventTypeId].sort() - ); - }); - }); - - it("should get bookings by organizationId and userId", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/bookings?userIds=${orgUser2.id}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(3); - expect(data.map((booking) => booking.eventTypeId).sort()).toEqual( - [orgEventTypeId, orgEventTypeId2, nonOrgEventTypeId].sort() - ); - }); - }); - - it("should fail to get bookings by organizationId and Id of a user that does not exist", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/bookings?userIds=972930`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(400); - }); - - it("should fail to get bookings by organizationId and Id of a user that does not belong to the org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/bookings?userIds=${nonOrgUser1.id}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(403); - }); - - it("should get bookings by organizationId and non org event-type id", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/bookings?eventTypeIds=${nonOrgEventTypeId}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(2); - expect(data.map((booking) => booking.eventTypeId).sort()).toEqual( - [nonOrgEventTypeId, nonOrgEventTypeId].sort() - ); - }); - }); - - it("should get bookings by organizationId and org event-type id", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/bookings?eventTypeIds=${orgEventTypeId}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(1); - expect(data.map((booking) => booking.eventTypeId).sort()).toEqual([orgEventTypeId].sort()); - }); - }); - - it("should get bookings by organizationId and org + non org event-type ids", async () => { - return request(app.getHttpServer()) - .get( - `/v2/organizations/${organization.id}/bookings?eventTypeIds=${orgEventTypeId2},${nonOrgEventTypeId}` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(3); - expect(data.map((booking) => booking.eventTypeId).sort()).toEqual( - [orgEventTypeId2, nonOrgEventTypeId, nonOrgEventTypeId].sort() - ); - }); - }); - - describe("get by bookingUid param", () => { - it("should get a specific booking by bookingUid query param", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/bookings?bookingUid=${collectiveOrgBookingUid}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(1); - expect(responseDataIsBooking(data[0])).toBe(true); - - if (responseDataIsBooking(data[0])) { - const booking: BookingOutput_2024_08_13 = data[0]; - expect(booking.uid).toEqual(collectiveOrgBookingUid); - expect(booking.eventTypeId).toEqual(orgEventTypeId); - expect(booking.status).toEqual("accepted"); - expect(booking.attendees.length).toEqual(2); - expect(booking.attendees[0].name).toEqual("alice"); - expect(booking.attendees[0].email).toEqual("alice@gmail.com"); - } else { - throw new Error("Expected single booking but received different response type"); - } - }); - }); - - it("should return empty array for non-existent booking UID query param", async () => { - const nonExistentUid = "non-existent-booking-uid"; - - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/bookings?bookingUid=${nonExistentUid}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.length).toEqual(0); - }); - }); - - it("should return empty array for booking UID not belonging to the organization", async () => { - const regularUser = await userRepositoryFixture.create({ - email: `org-bookings-regular-user-${Math.floor(Math.random() * 10000)}@api.com`, - name: "Regular User", - }); - - const regularUserEventType = await eventTypesRepositoryFixture.create( - { - title: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`, - slug: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`, - length: 60, - bookingFields: [], - locations: [], - }, - regularUser.id - ); - - const regularUserBooking = await bookingsRepositoryFixture.create({ - user: { - connect: { - id: regularUser.id, - }, - }, - startTime: new Date(Date.UTC(2030, 0, 15, 13, 0, 0)), - endTime: new Date(Date.UTC(2030, 0, 15, 14, 0, 0)), - title: "Regular user booking", - uid: `regular-user-booking-${Math.floor(Math.random() * 10000)}`, - eventType: { - connect: { - id: regularUserEventType.id, - }, - }, - location: "https://meet.google.com/regular-user", - customInputs: {}, - metadata: {}, - status: "ACCEPTED", - responses: { - name: "Regular Attendee", - email: "regular@example.com", - }, - attendees: { - create: { - email: "regular@example.com", - name: "Regular Attendee", - locale: "en", - timeZone: "Europe/Rome", - }, - }, - }); - - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/bookings?bookingUid=${regularUserBooking.uid}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.length).toEqual(0); - - await userRepositoryFixture.delete(regularUser.id); - }); - }); - }); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: ["http://localhost:5555"], - permissions: 32, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { - return !Array.isArray(data) && typeof data === "object" && data && "id" in data; - } - - afterAll(async () => { - await oauthClientRepositoryFixture.delete(oAuthClient.id); - await teamRepositoryFixture.delete(organization.id); - await userRepositoryFixture.deleteByEmail(orgUser.email); - await userRepositoryFixture.deleteByEmail(orgUser2.email); - await userRepositoryFixture.deleteByEmail(nonOrgUser1.email); - await bookingsRepositoryFixture.deleteAllBookings(orgUser.id, orgUser.email); - await bookingsRepositoryFixture.deleteAllBookings(orgUser2.id, orgUser2.email); - await bookingsRepositoryFixture.deleteAllBookings(nonOrgUser1.id, nonOrgUser1.email); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/bookings/organizations-bookings.controller.ts b/apps/api/v2/src/modules/organizations/bookings/organizations-bookings.controller.ts deleted file mode 100644 index 31b8dc7e7d1f42..00000000000000 --- a/apps/api/v2/src/modules/organizations/bookings/organizations-bookings.controller.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, - OPTIONAL_API_KEY_HEADER, - OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { Controller, UseGuards, Get, Param, ParseIntPipe, Query, HttpStatus, HttpCode } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { GetBookingsOutput_2024_08_13, GetOrganizationsBookingsInput } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId/bookings", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Bookings") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER) -export class OrganizationsBookingsController { - constructor(private readonly bookingsService: BookingsService_2024_08_13) {} - - @Get("/") - @ApiOperation({ summary: "Get organization bookings" }) - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @HttpCode(HttpStatus.OK) - async getAllOrgTeamBookings( - @Query() queryParams: GetOrganizationsBookingsInput, - @Param("orgId", ParseIntPipe) orgId: number, - @GetUser() user: UserWithProfile - ): Promise { - const { userIds, ...restParams } = queryParams; - - const { bookings, pagination } = await this.bookingsService.getBookings( - { ...restParams }, - { email: user.email, id: user.id, orgId }, - userIds - ); - - return { - status: SUCCESS_STATUS, - data: bookings, - pagination, - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/bookings/organizations.bookings.module.ts b/apps/api/v2/src/modules/organizations/bookings/organizations.bookings.module.ts deleted file mode 100644 index 6b748637710b53..00000000000000 --- a/apps/api/v2/src/modules/organizations/bookings/organizations.bookings.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module"; -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OrganizationsUsersRepository } from "@/modules/organizations//users/index/organizations-users.repository"; -import { OrganizationsBookingsController } from "@/modules/organizations/bookings/organizations-bookings.controller"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { OrganizationsUsersService } from "@/modules/organizations/users/index/services/organizations-users-service"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [BookingsModule_2024_08_13, PrismaModule, StripeModule, RedisModule, MembershipsModule], - providers: [ - OrganizationsRepository, - OrganizationsTeamsRepository, - OrganizationsUsersService, - OrganizationsUsersRepository, - ], - controllers: [OrganizationsBookingsController], -}) -export class OrganizationsBookingsModule {} diff --git a/apps/api/v2/src/modules/organizations/conferencing/organizations-conferencing.controller.ts b/apps/api/v2/src/modules/organizations/conferencing/organizations-conferencing.controller.ts deleted file mode 100644 index 9f6a6f4925a49c..00000000000000 --- a/apps/api/v2/src/modules/organizations/conferencing/organizations-conferencing.controller.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { - ConferencingAppsOauthUrlOutputDto, - GetConferencingAppsOauthUrlResponseDto, -} from "@/modules/conferencing/outputs/get-conferencing-apps-oauth-url"; -import { - ConferencingAppsOutputResponseDto, - ConferencingAppOutputResponseDto, - ConferencingAppsOutputDto, - DisconnectConferencingAppOutputResponseDto, -} from "@/modules/conferencing/outputs/get-conferencing-apps.output"; -import { GetDefaultConferencingAppOutputResponseDto } from "@/modules/conferencing/outputs/get-default-conferencing-app.output"; -import { SetDefaultConferencingAppOutputResponseDto } from "@/modules/conferencing/outputs/set-default-conferencing-app.output"; -import { ConferencingService } from "@/modules/conferencing/services/conferencing.service"; -import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service"; -import { TokensRepository } from "@/modules/tokens/tokens.repository"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { - Controller, - Get, - Query, - HttpCode, - HttpStatus, - UseGuards, - Post, - Param, - Delete, - Headers, - Req, - ParseIntPipe, - BadRequestException, - Redirect, -} from "@nestjs/common"; -import { ApiOperation, ApiTags as DocsTags, ApiParam } from "@nestjs/swagger"; -import { plainToInstance } from "class-transformer"; -import { Request } from "express"; -import { stringify } from "querystring"; - -import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS, OFFICE_365_VIDEO, CAL_VIDEO } from "@calcom/platform-constants"; - -export type OAuthCallbackState = { - accessToken: string; - teamId?: string; - orgId?: string; - fromApp?: boolean; - returnTo?: string; - onErrorReturnTo?: string; -}; - -@Controller({ - path: "/v2/organizations/:orgId", - version: API_VERSIONS_VALUES, -}) -@DocsTags("Orgs / Teams / Conferencing") -@ApiParam({ name: "orgId", type: Number, required: true }) -export class OrganizationsConferencingController { - constructor( - private readonly conferencingService: ConferencingService, - private readonly organizationsConferencingService: OrganizationsConferencingService, - private readonly tokensRepository: TokensRepository - ) {} - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @ApiParam({ - name: "app", - description: "Conferencing application type", - enum: [GOOGLE_MEET], - required: true, - }) - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Post("/teams/:teamId/conferencing/:app/connect") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Connect your conferencing application to a team" }) - async connectTeamApp( - @GetUser() user: UserWithProfile, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("orgId", ParseIntPipe) orgId: number, - @Param("app") app: string - ): Promise { - const credential = await this.organizationsConferencingService.connectTeamNonOauthApps({ - teamId, - app, - }); - - return { status: SUCCESS_STATUS, data: plainToInstance(ConferencingAppsOutputDto, credential) }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @ApiParam({ - name: "app", - description: "Conferencing application type", - enum: [ZOOM, OFFICE_365_VIDEO], - required: true, - }) - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Get("/teams/:teamId/conferencing/:app/oauth/auth-url") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Get OAuth conferencing app's auth URL for a team" }) - async getTeamOAuthUrl( - @Req() req: Request, - @Headers("Authorization") authorization: string, - @Param("teamId") teamId: string, - @Param("orgId") orgId: string, - @Param("app") app: string, - @Query("returnTo") returnTo?: string, - @Query("onErrorReturnTo") onErrorReturnTo?: string - ): Promise { - const origin = req.headers.origin; - const accessToken = authorization.replace("Bearer ", ""); - - const state: OAuthCallbackState = { - returnTo: returnTo ?? origin, - onErrorReturnTo: onErrorReturnTo ?? origin, - fromApp: false, - accessToken, - teamId, - orgId, - }; - - const credential = await this.conferencingService.generateOAuthUrl(app, state); - - return { - status: SUCCESS_STATUS, - data: plainToInstance(ConferencingAppsOauthUrlOutputDto, credential), - }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Get("/teams/:teamId/conferencing") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "List team conferencing applications" }) - async listTeamConferencingApps( - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - const conferencingApps = await this.organizationsConferencingService.getConferencingApps({ - teamId, - }); - - return { - status: SUCCESS_STATUS, - data: conferencingApps.map((app) => plainToInstance(ConferencingAppsOutputDto, app)), - }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Post("/teams/:teamId/conferencing/:app/default") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Set team default conferencing application" }) - @ApiParam({ - name: "app", - description: "Conferencing application type", - enum: [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO, CAL_VIDEO], - required: true, - }) - async setTeamDefaultApp( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("app") app: string - ): Promise { - await this.organizationsConferencingService.setDefaultConferencingApp({ - teamId, - app, - }); - - return { status: SUCCESS_STATUS }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Get("/teams/:teamId/conferencing/default") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Get team default conferencing application" }) - async getTeamDefaultApp( - @GetUser() user: UserWithProfile, - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - const defaultConferencingApp = await this.organizationsConferencingService.getDefaultConferencingApp({ - teamId, - }); - - return { status: SUCCESS_STATUS, data: defaultConferencingApp }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Delete("/teams/:teamId/conferencing/:app/disconnect") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Disconnect team conferencing application" }) - @ApiParam({ - name: "app", - description: "Conferencing application type", - enum: [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO], - required: true, - }) - async disconnectTeamApp( - @GetUser() user: UserWithProfile, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("app") app: string - ): Promise { - await this.organizationsConferencingService.disconnectConferencingApp({ - teamId, - user, - app, - }); - - return { status: SUCCESS_STATUS }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Get("/teams/:teamId/conferencing/:app/oauth/callback") - @Redirect(undefined, 301) - @ApiOperation({ summary: "Save conferencing app OAuth credentials" }) - async saveTeamOauthCredentials( - @Query("state") state: string, - @Query("code") code: string, - @Query("error") error: string | undefined, - @Query("error_description") error_description: string | undefined, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("orgId", ParseIntPipe) orgId: number, - @Param("app") app: string - ): Promise<{ url: string }> { - if (!state) { - throw new BadRequestException("Missing `state` query param"); - } - - const decodedCallbackState: OAuthCallbackState = JSON.parse(state); - try { - return await this.organizationsConferencingService.connectTeamOauthApps({ - decodedCallbackState, - code, - app, - teamId, - }); - } catch (error) { - if (error instanceof Error) { - console.error(error.message); - } - return { - url: decodedCallbackState.onErrorReturnTo ?? "", - }; - } - } -} diff --git a/apps/api/v2/src/modules/organizations/conferencing/organizations-conferencing.module.ts b/apps/api/v2/src/modules/organizations/conferencing/organizations-conferencing.module.ts deleted file mode 100644 index 0d2b76df476cfb..00000000000000 --- a/apps/api/v2/src/modules/organizations/conferencing/organizations-conferencing.module.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { AppsRepository } from "@/modules/apps/apps.repository"; -import { ConferencingModule } from "@/modules/conferencing/conferencing.module"; -import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository"; -import { ConferencingService } from "@/modules/conferencing/services/conferencing.service"; -import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service"; -import { Office365VideoService } from "@/modules/conferencing/services/office365-video.service"; -import { ZoomVideoService } from "@/modules/conferencing/services/zoom-video.service"; -import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; -import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { OrganizationsConferencingController } from "@/modules/organizations/conferencing/organizations-conferencing.controller"; -import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisService } from "@/modules/redis/redis.service"; -import { StripeService } from "@/modules/stripe/stripe.service"; -import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; -import { TokensRepository } from "@/modules/tokens/tokens.repository"; -import { UsersRepository } from "@/modules/users/users.repository"; -import { Module } from "@nestjs/common"; -import { ConfigModule } from "@nestjs/config"; - -@Module({ - imports: [PrismaModule, ConfigModule, ConferencingModule], - providers: [ - ConferencingService, - ConferencingRepository, - GoogleMeetService, - UsersRepository, - TeamsRepository, - OrganizationsConferencingService, - ZoomVideoService, - Office365VideoService, - CredentialsRepository, - TokensRepository, - AppsRepository, - OrganizationsRepository, - StripeService, - MembershipsRepository, - RedisService, - OrganizationsTeamsRepository, - ], - exports: [OrganizationsConferencingService], - controllers: [OrganizationsConferencingController], -}) -export class OrganizationsConferencingModule {} diff --git a/apps/api/v2/src/modules/organizations/conferencing/services/organizations-conferencing.service.ts b/apps/api/v2/src/modules/organizations/conferencing/services/organizations-conferencing.service.ts deleted file mode 100644 index 80fc4e28932d40..00000000000000 --- a/apps/api/v2/src/modules/organizations/conferencing/services/organizations-conferencing.service.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { OAuthCallbackState } from "@/modules/conferencing/controllers/conferencing.controller"; -import { DefaultConferencingAppsOutputDto } from "@/modules/conferencing/outputs/get-default-conferencing-app.output"; -import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository"; -import { ConferencingService } from "@/modules/conferencing/services/conferencing.service"; -import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service"; -import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { UsersRepository } from "@/modules/users/users.repository"; -import { BadRequestException, InternalServerErrorException, Logger } from "@nestjs/common"; -import { Injectable } from "@nestjs/common"; - -import { GOOGLE_MEET } from "@calcom/platform-constants"; -import { CONFERENCING_APPS, CAL_VIDEO } from "@calcom/platform-constants"; -import { teamMetadataSchema } from "@calcom/platform-libraries"; -import { handleDeleteCredential } from "@calcom/platform-libraries/app-store"; - -@Injectable() -export class OrganizationsConferencingService { - private logger = new Logger("OrganizationsConferencingService"); - - constructor( - private readonly conferencingRepository: ConferencingRepository, - private teamsRepository: TeamsRepository, - private usersRepository: UsersRepository, - private readonly googleMeetService: GoogleMeetService, - private readonly conferencingService: ConferencingService - ) {} - - async connectTeamNonOauthApps({ teamId, app }: { teamId: number; app: string }): Promise { - switch (app) { - case GOOGLE_MEET: - return this.googleMeetService.connectGoogleMeetToTeam(teamId); - default: - throw new BadRequestException("Invalid conferencing app. Available apps: GOOGLE_MEET."); - } - } - - async connectTeamOauthApps({ - decodedCallbackState, - app, - code, - teamId, - }: { - app: string; - decodedCallbackState: OAuthCallbackState; - code: string; - teamId: number; - }) { - return this.conferencingService.connectOauthApps(app, code, decodedCallbackState, teamId); - } - - async getConferencingApps({ teamId }: { teamId: number }) { - return this.conferencingRepository.findTeamConferencingApps(teamId); - } - - async getDefaultConferencingApp({ - teamId, - }: { - teamId: number; - }): Promise { - const team = await this.teamsRepository.getById(teamId); - return teamMetadataSchema.parse(team?.metadata)?.defaultConferencingApp; - } - - async checkAppIsValidAndConnected(teamId: number, app: string) { - if (!CONFERENCING_APPS.includes(app)) { - throw new BadRequestException("Invalid app, available apps are: ", CONFERENCING_APPS.join(", ")); - } - const credential = await this.conferencingRepository.findTeamConferencingApp(teamId, app); - - if (!credential) { - throw new BadRequestException(`${app} not connected.`); - } - return credential; - } - - async disconnectConferencingApp({ - teamId, - user, - app, - }: { - teamId: number; - user: UserWithProfile; - app: string; - }) { - const credential = await this.checkAppIsValidAndConnected(teamId, app); - return handleDeleteCredential({ - userId: user.id, - teamId, - userMetadata: user?.metadata, - credentialId: credential.id, - }); - } - - async setDefaultConferencingApp({ teamId, app }: { teamId: number; app: string }) { - // cal-video is global, so we can skip this check - if (app !== CAL_VIDEO) { - await this.checkAppIsValidAndConnected(teamId, app); - } - const team = await this.teamsRepository.setDefaultConferencingApp(teamId, app); - const metadata = team.metadata as { defaultConferencingApp?: { appSlug?: string } }; - if (metadata?.defaultConferencingApp?.appSlug !== app) { - throw new InternalServerErrorException(`Could not set ${app} as default conferencing app`); - } - return true; - } -} diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/inputs/create-delegation-credential.input.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/inputs/create-delegation-credential.input.ts deleted file mode 100644 index ad177944d6cdb8..00000000000000 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/inputs/create-delegation-credential.input.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - GoogleServiceAccountKeyInput, - ServiceAccountKeyValidator, - MicrosoftServiceAccountKeyInput, -} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input"; -import { ApiProperty, getSchemaPath } from "@nestjs/swagger"; -import { ApiExtraModels } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsString, IsNotEmpty, Validate } from "class-validator"; - -@ApiExtraModels(GoogleServiceAccountKeyInput, MicrosoftServiceAccountKeyInput) -export class CreateDelegationCredentialInput { - @IsString() - @IsNotEmpty() - @ApiProperty() - @Expose() - workspacePlatformSlug!: string; - - @IsString() - @IsNotEmpty() - @ApiProperty() - @Expose() - domain!: string; - - @Validate(ServiceAccountKeyValidator) - @ApiProperty({ - type: [GoogleServiceAccountKeyInput, MicrosoftServiceAccountKeyInput], - oneOf: [ - { $ref: getSchemaPath(GoogleServiceAccountKeyInput) }, - { $ref: getSchemaPath(MicrosoftServiceAccountKeyInput) }, - ], - }) - @Expose() - @Type(() => Object) - serviceAccountKey!: (GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput) & { - [key: string]: unknown; - }; -} diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/inputs/service-account-key.input.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/inputs/service-account-key.input.ts deleted file mode 100644 index e505ddc16c8a1e..00000000000000 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/inputs/service-account-key.input.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Type, plainToClass, Expose } from "class-transformer"; -import { - IsString, - IsNotEmpty, - ValidationOptions, - ValidatorConstraint, - ValidatorConstraintInterface, - validateSync, -} from "class-validator"; - -export class MicrosoftServiceAccountKeyInput { - @Expose() - @IsString() - @IsNotEmpty() - @ApiProperty() - private_key!: string; - - @Expose() - @IsString() - @IsNotEmpty() - @ApiProperty() - tenant_id!: string; - - @Expose() - @IsString() - @IsNotEmpty() - @ApiProperty() - client_id!: string; -} - -export class GoogleServiceAccountKeyInput { - @Expose() - @IsString() - @IsNotEmpty() - @ApiProperty() - private_key!: string; - - @Expose() - @IsString() - @IsNotEmpty() - @ApiProperty() - client_email!: string; - - @Expose() - @IsString() - @IsNotEmpty() - @ApiProperty() - client_id!: string; -} - -@ValidatorConstraint({ name: "isServiceAccountKey", async: false }) -export class ServiceAccountKeyValidator implements ValidatorConstraintInterface { - validate(value: any) { - if (!value || typeof value !== "object") return false; - - if ("client_email" in value) { - const googleKey = plainToClass(GoogleServiceAccountKeyInput, value, { - excludeExtraneousValues: true, - }); - const googleErrors = validateSync(googleKey, { - whitelist: true, - forbidNonWhitelisted: true, - skipMissingProperties: false, - }); - - if (googleErrors.length === 0) return true; - this.errors = googleErrors; - return false; - } - - if ("tenant_id" in value) { - const msKey = plainToClass(MicrosoftServiceAccountKeyInput, value, { - excludeExtraneousValues: true, - }); - const msErrors = validateSync(msKey, { - whitelist: true, - forbidNonWhitelisted: true, - skipMissingProperties: false, - }); - - if (msErrors.length === 0) return true; - this.errors = msErrors; - return false; - } - - return false; - } - - private errors?: any[]; - - defaultMessage() { - if (this.errors?.length) { - return this.errors.map((error) => Object.values(error.constraints || {}).join(", ")).join("; "); - } - - return "Service account key must be either a Google service account key (with client_email, private_key, client_id) or Microsoft service account key (with tenant_id, private_key, client_id)"; - } -} diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/inputs/update-delegation-credential.input.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/inputs/update-delegation-credential.input.ts deleted file mode 100644 index eecb1343c8f0b1..00000000000000 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/inputs/update-delegation-credential.input.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - GoogleServiceAccountKeyInput, - MicrosoftServiceAccountKeyInput, - ServiceAccountKeyValidator, -} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input"; -import { ApiExtraModels, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsBoolean, IsOptional, Validate } from "class-validator"; - -@ApiExtraModels(GoogleServiceAccountKeyInput, MicrosoftServiceAccountKeyInput) -export class UpdateDelegationCredentialInput { - @IsBoolean() - @IsOptional() - @ApiPropertyOptional() - @Expose() - enabled?: boolean; - - @IsOptional() - @Validate(ServiceAccountKeyValidator) - @ApiPropertyOptional({ - type: [GoogleServiceAccountKeyInput, MicrosoftServiceAccountKeyInput], - oneOf: [ - { $ref: getSchemaPath(GoogleServiceAccountKeyInput) }, - { $ref: getSchemaPath(MicrosoftServiceAccountKeyInput) }, - ], - }) - @Expose() - @Type(() => Object) - serviceAccountKey?: GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput; -} diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.controller.e2e-spec.ts deleted file mode 100644 index 26d2ba7ad376fa..00000000000000 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.controller.e2e-spec.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { encryptServiceAccountKey } from "@calcom/platform-libraries"; -import type { Team, User } from "@calcom/prisma/client"; - -// Mock the toggleDelegationCredentialEnabled function to bypass Google API calls -const mockToggleDelegationCredentialEnabled = jest.fn(); -jest.mock("@calcom/platform-libraries/app-store", () => { - const actual = jest.requireActual("@calcom/platform-libraries/app-store"); - return { - ...actual, - toggleDelegationCredentialEnabled: (...args: unknown[]) => mockToggleDelegationCredentialEnabled(...args), - }; -}); - -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 { 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"; -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 { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { OrganizationsDelegationCredentialService } from "@/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; -import { UpdateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/update-delegation-credential.input"; -import { UpdateDelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/update-delegation-credential.output"; - -describe("Organizations Delegation Credentials Endpoints", () => { - describe("User Authentication - User is Org Admin", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipRepositoryFixture: MembershipRepositoryFixture; - let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let profilesRepositoryFixture: ProfileRepositoryFixture; - let prismaWriteService: PrismaWriteService; - - let org: Team; - let user: User; - let apiKey: string; - let delegationCredentialId: string; - let workspacePlatformId: number; - let ensureDefaultCalendarsSpy: jest.SpyInstance; - - const userEmail = `delegation-credentials-admin-${randomString()}@api.com`; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - prismaWriteService = moduleRef.get(PrismaWriteService); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - org = await organizationsRepositoryFixture.create({ - name: `delegation-credentials-organization-${randomString()}`, - isOrganization: true, - isPlatform: true, - }); - - await profilesRepositoryFixture.create({ - uid: `${randomString()}-uid`, - username: userEmail, - user: { connect: { id: user.id } }, - organization: { connect: { id: org.id } }, - movedFromUser: { connect: { id: user.id } }, - }); - - await platformBillingRepositoryFixture.create(org.id, "SCALE"); - - await membershipRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - accepted: true, - }); - - const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null, org.id); - apiKey = `cal_test_${keyString}`; - - const workspacePlatform = await prismaWriteService.prisma.workspacePlatform.create({ - data: { - slug: "google", - name: "Google Workspace", - description: "Google Workspace for testing", - defaultServiceAccountKey: { - type: "service_account", - project_id: "test-project", - private_key_id: "test-key-id", - private_key: "test-private-key", - client_email: "test@test-project.iam.gserviceaccount.com", - client_id: "123456789", - auth_uri: "https://accounts.google.com/o/oauth2/auth", - token_uri: "https://oauth2.googleapis.com/token", - auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs", - client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test", - }, - enabled: true, - }, - }); - workspacePlatformId = workspacePlatform.id; - - const testServiceAccountKey = { - type: "service_account" as const, - project_id: "test-project", - private_key_id: "test-key-id", - private_key: "test-private-key", - client_email: "test@test-project.iam.gserviceaccount.com", - client_id: "123456789", - auth_uri: "https://accounts.google.com/o/oauth2/auth", - token_uri: "https://oauth2.googleapis.com/token", - auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs", - client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test", - }; - - const encryptedServiceAccountKey = encryptServiceAccountKey(testServiceAccountKey); - - const delegationCredential = await prismaWriteService.prisma.delegationCredential.create({ - data: { - workspacePlatformId: workspacePlatform.id, - organizationId: org.id, - domain: "test-domain.com", - serviceAccountKey: encryptedServiceAccountKey, - enabled: false, - }, - }); - delegationCredentialId = delegationCredential.id; - - // Set up spy on prototype BEFORE app.init() - this is critical for NestJS - ensureDefaultCalendarsSpy = jest - .spyOn(OrganizationsDelegationCredentialService.prototype, "ensureDefaultCalendars") - .mockResolvedValue(undefined); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - afterEach(() => { - // Clear the spy call history after each test - ensureDefaultCalendarsSpy.mockClear(); - mockToggleDelegationCredentialEnabled.mockClear(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should call ensureDefaultCalendars when enabling delegation credentials", async () => { - await prismaWriteService.prisma.delegationCredential.update({ - where: { id: delegationCredentialId }, - data: { enabled: false }, - }); - - // Mock toggleDelegationCredentialEnabled to return a valid response - mockToggleDelegationCredentialEnabled.mockResolvedValue({ - id: delegationCredentialId, - enabled: true, - }); - - const response = await request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/delegation-credentials/${delegationCredentialId}`) - .set("Authorization", `Bearer ${apiKey}`) - .send({ - enabled: true, - } satisfies UpdateDelegationCredentialInput) - .expect(200); - - const responseBody: UpdateDelegationCredentialOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.enabled).toEqual(true); - - expect(ensureDefaultCalendarsSpy).toHaveBeenCalledWith(org.id, "test-domain.com"); - }); - - it("should not call ensureDefaultCalendars when disabling delegation credentials", async () => { - await prismaWriteService.prisma.delegationCredential.update({ - where: { id: delegationCredentialId }, - data: { enabled: true }, - }); - - // Mock toggleDelegationCredentialEnabled to return a valid response - mockToggleDelegationCredentialEnabled.mockResolvedValue({ - id: delegationCredentialId, - enabled: false, - }); - - const response = await request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/delegation-credentials/${delegationCredentialId}`) - .set("Authorization", `Bearer ${apiKey}`) - .send({ - enabled: false, - } satisfies UpdateDelegationCredentialInput) - .expect(200); - - const responseBody: UpdateDelegationCredentialOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.enabled).toEqual(false); - - expect(ensureDefaultCalendarsSpy).not.toHaveBeenCalled(); - }); - - it("should not call ensureDefaultCalendars when enabling already enabled delegation credentials", async () => { - await prismaWriteService.prisma.delegationCredential.update({ - where: { id: delegationCredentialId }, - data: { enabled: true }, - }); - - // Mock toggleDelegationCredentialEnabled to return a valid response - mockToggleDelegationCredentialEnabled.mockResolvedValue({ - id: delegationCredentialId, - enabled: true, - }); - - const response = await request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/delegation-credentials/${delegationCredentialId}`) - .set("Authorization", `Bearer ${apiKey}`) - .send({ - enabled: true, - } satisfies UpdateDelegationCredentialInput) - .expect(200); - - const responseBody: UpdateDelegationCredentialOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.enabled).toEqual(true); - - expect(ensureDefaultCalendarsSpy).not.toHaveBeenCalled(); - }); - - afterAll(async () => { - if (org?.id) { - await prismaWriteService.prisma.delegationCredential.deleteMany({ - where: { organizationId: org.id }, - }); - await organizationsRepositoryFixture.delete(org.id); - } - if (workspacePlatformId) { - await prismaWriteService.prisma.workspacePlatform.delete({ - where: { id: workspacePlatformId }, - }); - } - if (user?.email) { - await userRepositoryFixture.deleteByEmail(user.email); - } - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.controller.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.controller.ts deleted file mode 100644 index 0390e9ef556c77..00000000000000 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.controller.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, - OPTIONAL_API_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { UpdateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/update-delegation-credential.input"; -import { CreateDelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/create-delegation-credential.output"; -import { DelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/delegation-credential.output"; -import { UpdateDelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/update-delegation-credential.output"; -import { OrganizationsDelegationCredentialService } from "@/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service"; -import { - Controller, - UseGuards, - Param, - ParseIntPipe, - Post, - Body, - HttpCode, - HttpStatus, - Patch, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { User } from "@calcom/prisma/client"; - -import { CreateDelegationCredentialInput } from "./inputs/create-delegation-credential.input"; - -@Controller({ - path: "/v2/organizations/:orgId/delegation-credentials", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Delegation Credentials") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -export class OrganizationsDelegationCredentialController { - constructor(private readonly delegationCredentialService: OrganizationsDelegationCredentialService) {} - - @Post("/") - @HttpCode(HttpStatus.CREATED) - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @ApiOperation({ summary: "Save delegation credentials for your organization" }) - async createDelegationCredential( - @Param("orgId", ParseIntPipe) orgId: number, - @GetUser() delegatedServiceAccountUser: User, - @Body() body: CreateDelegationCredentialInput - ): Promise { - const delegationCredential = await this.delegationCredentialService.createDelegationCredential( - orgId, - delegatedServiceAccountUser, - body - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(DelegationCredentialOutput, delegationCredential, { strategy: "excludeAll" }), - }; - } - - @Patch("/:credentialId") - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @ApiOperation({ summary: "Update delegation credentials of your organization" }) - async updateDelegationCredential( - @Param("orgId", ParseIntPipe) orgId: number, - @GetUser() delegatedServiceAccountUser: User, - @Body() body: UpdateDelegationCredentialInput, - @Param("credentialId") credentialId: string - ): Promise { - const delegationCredential = await this.delegationCredentialService.updateDelegationCredential( - orgId, - credentialId, - delegatedServiceAccountUser, - body - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(DelegationCredentialOutput, delegationCredential, { strategy: "excludeAll" }), - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.module.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.module.ts deleted file mode 100644 index 7b5ecbad9fd5c2..00000000000000 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.module.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { BullModule } from "@nestjs/bull"; -import { Module } from "@nestjs/common"; -import { OrganizationsDelegationCredentialService } from "./services/organizations-delegation-credential.service"; -import { CalendarsModule } from "@/ee/calendars/calendars.module"; -import { CALENDARS_QUEUE, CalendarsProcessor } from "@/ee/calendars/processors/calendars.processor"; -import { CalendarsTaskerModule } from "@/lib/modules/calendars-tasker.module"; -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OrganizationsDelegationCredentialController } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.controller"; -import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; - -@Module({ - imports: [ - PrismaModule, - StripeModule, - RedisModule, - CalendarsModule, - MembershipsModule, - BullModule.registerQueue({ - name: CALENDARS_QUEUE, - limiter: { - max: 1, - duration: 1000, - }, - }), - CalendarsTaskerModule, - ], - providers: [ - OrganizationsDelegationCredentialService, - OrganizationsDelegationCredentialRepository, - OrganizationsRepository, - CalendarsProcessor, - ], - controllers: [OrganizationsDelegationCredentialController], - exports: [ - OrganizationsDelegationCredentialRepository, - OrganizationsDelegationCredentialService, - BullModule, - ], -}) -export class OrganizationsDelegationCredentialModule {} diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.repository.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.repository.ts deleted file mode 100644 index 840e0ed454927f..00000000000000 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.repository.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -import type { Prisma } from "@calcom/prisma/client"; - -@Injectable() -export class OrganizationsDelegationCredentialRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async findById(delegationCredentialId: string) { - return this.dbRead.prisma.delegationCredential.findUnique({ where: { id: delegationCredentialId } }); - } - - async findByIdWithWorkspacePlatform(delegationCredentialId: string) { - return this.dbRead.prisma.delegationCredential.findUnique({ - where: { id: delegationCredentialId }, - include: { workspacePlatform: true }, - }); - } - - async updateIncludeWorkspacePlatform( - delegationCredentialId: string, - data: Prisma.DelegationCredentialUncheckedUpdateInput - ) { - return this.dbWrite.prisma.delegationCredential.update({ - where: { id: delegationCredentialId }, - data, - include: { workspacePlatform: true }, - }); - } - - async findDelegatedUserProfiles(orgId: number, domain: string) { - return this.dbRead.prisma.profile.findMany({ - select: { - userId: true, - }, - where: { - organizationId: orgId, - user: { email: { endsWith: `@${domain}`, mode: "insensitive" } }, - }, - }); - } - - async findEnabledByOrgIdAndDomain(orgId: number, domain: string) { - return this.dbRead.prisma.delegationCredential.findFirst({ - where: { - organizationId: orgId, - domain, - enabled: true, - }, - select: { id: true }, - }); - } -} diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/outputs/create-delegation-credential.output.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/outputs/create-delegation-credential.output.ts deleted file mode 100644 index e0ca7b9c94db24..00000000000000 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/outputs/create-delegation-credential.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/delegation-credential.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class CreateDelegationCredentialOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: DelegationCredentialOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => DelegationCredentialOutput) - data!: DelegationCredentialOutput; -} diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/outputs/delegation-credential.output.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/outputs/delegation-credential.output.ts deleted file mode 100644 index c4a2fe54fadcd4..00000000000000 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/outputs/delegation-credential.output.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsBoolean, IsDate, IsInt, IsObject, IsString, ValidateNested } from "class-validator"; - -class WorkspacePlatformDto { - @ApiProperty() - @IsString() - @Expose() - name!: string; - - @ApiProperty() - @IsString() - @Expose() - slug!: string; -} - -export class DelegationCredentialOutput { - @ApiProperty() - @IsString() - @Expose() - id!: string; - - @ApiProperty() - @IsBoolean() - @Expose() - enabled!: boolean; - - @ApiProperty() - @IsString() - @Expose() - domain!: string; - - @ApiProperty() - @IsInt() - @Expose() - organizationId!: number; - - @ApiProperty({ type: WorkspacePlatformDto }) - @Type(() => WorkspacePlatformDto) - @IsObject() - @ValidateNested() - @Expose() - workspacePlatform!: WorkspacePlatformDto; - - @ApiProperty() - @Type(() => Date) - @IsDate() - @Expose() - createdAt!: Date; - - @ApiProperty() - @Type(() => Date) - @IsDate() - @Expose() - updatedAt!: Date; -} diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/outputs/update-delegation-credential.output.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/outputs/update-delegation-credential.output.ts deleted file mode 100644 index 5033e96a7a7dcb..00000000000000 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/outputs/update-delegation-credential.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/delegation-credential.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class UpdateDelegationCredentialOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: DelegationCredentialOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => DelegationCredentialOutput) - data!: DelegationCredentialOutput; -} diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.spec.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.spec.ts deleted file mode 100644 index 5e0f571a5e0cba..00000000000000 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.spec.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { - CALENDARS_QUEUE, - DEFAULT_CALENDARS_JOB, -} from "@/ee/calendars/processors/calendars.processor"; -import { CalendarsTasker } from "@/lib/services/tasker/calendars-tasker.service"; -import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository"; -import { OrganizationsDelegationCredentialService } from "@/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service"; -import { Logger } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import { getQueueToken } from "@nestjs/bull"; -import { Test, TestingModule } from "@nestjs/testing"; - -describe("OrganizationsDelegationCredentialService", () => { - let service: OrganizationsDelegationCredentialService; - let mockRepository: OrganizationsDelegationCredentialRepository; - let mockQueue: { getJob: jest.Mock; add: jest.Mock }; - let mockCalendarsTasker: { dispatch: jest.Mock }; - let mockConfigService: { get: jest.Mock }; - - const orgId = 1; - const domain = "example.com"; - - beforeEach(async () => { - mockQueue = { - getJob: jest.fn().mockResolvedValue(null), - add: jest.fn().mockResolvedValue(undefined), - }; - - mockCalendarsTasker = { - dispatch: jest.fn().mockResolvedValue({ runId: "test-run-id" }), - }; - - mockConfigService = { - get: jest.fn().mockReturnValue(false), - }; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - OrganizationsDelegationCredentialService, - { - provide: OrganizationsDelegationCredentialRepository, - useValue: { - findDelegatedUserProfiles: jest.fn().mockResolvedValue([]), - }, - }, - { - provide: getQueueToken(CALENDARS_QUEUE), - useValue: mockQueue, - }, - { - provide: CalendarsTasker, - useValue: mockCalendarsTasker, - }, - { - provide: ConfigService, - useValue: mockConfigService, - }, - ], - }).compile(); - - service = module.get( - OrganizationsDelegationCredentialService - ); - mockRepository = module.get( - OrganizationsDelegationCredentialRepository - ); - - jest.spyOn(Logger.prototype, "log").mockImplementation(); - jest.spyOn(Logger.prototype, "error").mockImplementation(); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - describe("ensureDefaultCalendars", () => { - it("adds calendar jobs for each delegated user profile", async () => { - (mockRepository.findDelegatedUserProfiles as jest.Mock).mockResolvedValue([ - { userId: 1 }, - { userId: 2 }, - ]); - - await service.ensureDefaultCalendars(orgId, domain); - - expect(mockRepository.findDelegatedUserProfiles).toHaveBeenCalledWith(orgId, domain); - expect(mockQueue.add).toHaveBeenCalledTimes(2); - expect(mockQueue.add).toHaveBeenCalledWith( - DEFAULT_CALENDARS_JOB, - { userId: 1 }, - { jobId: `${DEFAULT_CALENDARS_JOB}_1`, removeOnComplete: true } - ); - expect(mockQueue.add).toHaveBeenCalledWith( - DEFAULT_CALENDARS_JOB, - { userId: 2 }, - { jobId: `${DEFAULT_CALENDARS_JOB}_2`, removeOnComplete: true } - ); - }); - - it("removes existing job before adding new one", async () => { - const mockExistingJob = { remove: jest.fn().mockResolvedValue(undefined) }; - (mockRepository.findDelegatedUserProfiles as jest.Mock).mockResolvedValue([{ userId: 1 }]); - mockQueue.getJob.mockResolvedValue(mockExistingJob); - - await service.ensureDefaultCalendars(orgId, domain); - - expect(mockExistingJob.remove).toHaveBeenCalled(); - expect(mockQueue.add).toHaveBeenCalledTimes(1); - }); - - it("skips profiles without userId", async () => { - (mockRepository.findDelegatedUserProfiles as jest.Mock).mockResolvedValue([ - { userId: 1 }, - { userId: null }, - { userId: 3 }, - ]); - - await service.ensureDefaultCalendars(orgId, domain); - - expect(mockQueue.add).toHaveBeenCalledTimes(2); - expect(mockQueue.add).toHaveBeenCalledWith(DEFAULT_CALENDARS_JOB, { userId: 1 }, expect.any(Object)); - expect(mockQueue.add).toHaveBeenCalledWith(DEFAULT_CALENDARS_JOB, { userId: 3 }, expect.any(Object)); - }); - - it("does not add jobs when profiles list is empty", async () => { - (mockRepository.findDelegatedUserProfiles as jest.Mock).mockResolvedValue([]); - - await service.ensureDefaultCalendars(orgId, domain); - - expect(mockQueue.add).not.toHaveBeenCalled(); - }); - - it("processes all profiles even when some fail (Promise.allSettled)", async () => { - (mockRepository.findDelegatedUserProfiles as jest.Mock).mockResolvedValue([ - { userId: 1 }, - { userId: 2 }, - { userId: 3 }, - ]); - mockQueue.add - .mockResolvedValueOnce(undefined) - .mockRejectedValueOnce(new Error("Queue error")) - .mockResolvedValueOnce(undefined); - - await service.ensureDefaultCalendars(orgId, domain); - - // All 3 jobs were attempted despite the failure - expect(mockQueue.add).toHaveBeenCalledTimes(3); - }); - - it("does not throw when repository fails", async () => { - (mockRepository.findDelegatedUserProfiles as jest.Mock).mockRejectedValue( - new Error("Database error") - ); - - await expect(service.ensureDefaultCalendars(orgId, domain)).resolves.toBeUndefined(); - expect(mockQueue.add).not.toHaveBeenCalled(); - }); - }); - - describe("ensureDefaultCalendarsForUser", () => { - const userId = 123; - const userEmail = "user@example.com"; - - beforeEach(() => { - (mockRepository.findEnabledByOrgIdAndDomain as jest.Mock) = jest.fn(); - jest.spyOn(Logger.prototype, "warn").mockImplementation(); - }); - - it("adds calendar job when delegation credential exists for user domain", async () => { - (mockRepository.findEnabledByOrgIdAndDomain as jest.Mock).mockResolvedValue({ - id: "cred-1", - }); - - await service.ensureDefaultCalendarsForUser(orgId, userId, userEmail); - - expect(mockRepository.findEnabledByOrgIdAndDomain).toHaveBeenCalledWith(orgId, "example.com"); - expect(mockQueue.add).toHaveBeenCalledWith( - DEFAULT_CALENDARS_JOB, - { userId }, - { jobId: `${DEFAULT_CALENDARS_JOB}_${userId}`, removeOnComplete: true } - ); - }); - - it("removes existing job before adding new one", async () => { - const mockExistingJob = { remove: jest.fn().mockResolvedValue(undefined) }; - (mockRepository.findEnabledByOrgIdAndDomain as jest.Mock).mockResolvedValue({ - id: "cred-1", - }); - mockQueue.getJob.mockResolvedValue(mockExistingJob); - - await service.ensureDefaultCalendarsForUser(orgId, userId, userEmail); - - expect(mockExistingJob.remove).toHaveBeenCalled(); - expect(mockQueue.add).toHaveBeenCalledTimes(1); - }); - - it("does not add job when no delegation credential exists for domain", async () => { - (mockRepository.findEnabledByOrgIdAndDomain as jest.Mock).mockResolvedValue(null); - - await service.ensureDefaultCalendarsForUser(orgId, userId, userEmail); - - expect(mockRepository.findEnabledByOrgIdAndDomain).toHaveBeenCalledWith(orgId, "example.com"); - expect(mockQueue.add).not.toHaveBeenCalled(); - }); - - it("returns early and logs warning for invalid email without @ symbol", async () => { - await service.ensureDefaultCalendarsForUser(orgId, userId, "invalidemail"); - - expect(Logger.prototype.warn).toHaveBeenCalledWith(`Invalid email format for user ${userId}: missing domain`); - expect(mockRepository.findEnabledByOrgIdAndDomain).not.toHaveBeenCalled(); - expect(mockQueue.add).not.toHaveBeenCalled(); - }); - - it("returns early for email with @ but no domain part", async () => { - await service.ensureDefaultCalendarsForUser(orgId, userId, "user@"); - - expect(Logger.prototype.warn).toHaveBeenCalledWith(`Invalid email format for user ${userId}: missing domain`); - expect(mockRepository.findEnabledByOrgIdAndDomain).not.toHaveBeenCalled(); - expect(mockQueue.add).not.toHaveBeenCalled(); - }); - - it("does not throw when repository fails", async () => { - (mockRepository.findEnabledByOrgIdAndDomain as jest.Mock).mockRejectedValue( - new Error("Database error") - ); - - await expect(service.ensureDefaultCalendarsForUser(orgId, userId, userEmail)).resolves.toBeUndefined(); - expect(mockQueue.add).not.toHaveBeenCalled(); - }); - - it("does not throw when queue.add fails", async () => { - (mockRepository.findEnabledByOrgIdAndDomain as jest.Mock).mockResolvedValue({ - id: "cred-1", - }); - mockQueue.add.mockRejectedValue(new Error("Queue error")); - - await expect(service.ensureDefaultCalendarsForUser(orgId, userId, userEmail)).resolves.toBeUndefined(); - }); - - describe("when enableAsyncTasker is true", () => { - beforeEach(() => { - mockConfigService.get.mockReturnValue(true); - }); - - it("uses CalendarsTasker.dispatch instead of Bull queue", async () => { - (mockRepository.findEnabledByOrgIdAndDomain as jest.Mock).mockResolvedValue({ - id: "cred-1", - }); - - await service.ensureDefaultCalendarsForUser(orgId, userId, userEmail); - - expect(mockConfigService.get).toHaveBeenCalledWith("enableAsyncTasker"); - expect(mockCalendarsTasker.dispatch).toHaveBeenCalledWith( - "ensureDefaultCalendars", - { userId }, - { idempotencyKey: `${DEFAULT_CALENDARS_JOB}_${userId}`, idempotencyKeyTTL: "1h" } - ); - expect(mockQueue.add).not.toHaveBeenCalled(); - expect(mockQueue.getJob).not.toHaveBeenCalled(); - }); - - it("does not call CalendarsTasker when no delegation credential exists", async () => { - (mockRepository.findEnabledByOrgIdAndDomain as jest.Mock).mockResolvedValue(null); - - await service.ensureDefaultCalendarsForUser(orgId, userId, userEmail); - - expect(mockCalendarsTasker.dispatch).not.toHaveBeenCalled(); - expect(mockQueue.add).not.toHaveBeenCalled(); - }); - - it("does not throw when CalendarsTasker.dispatch fails", async () => { - (mockRepository.findEnabledByOrgIdAndDomain as jest.Mock).mockResolvedValue({ - id: "cred-1", - }); - mockCalendarsTasker.dispatch.mockRejectedValue(new Error("Tasker error")); - - await expect(service.ensureDefaultCalendarsForUser(orgId, userId, userEmail)).resolves.toBeUndefined(); - }); - - it("returns early for invalid email without calling tasker", async () => { - await service.ensureDefaultCalendarsForUser(orgId, userId, "invalidemail"); - - expect(Logger.prototype.warn).toHaveBeenCalledWith(`Invalid email format for user ${userId}: missing domain`); - expect(mockCalendarsTasker.dispatch).not.toHaveBeenCalled(); - expect(mockRepository.findEnabledByOrgIdAndDomain).not.toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.ts deleted file mode 100644 index 2e1e3725dd06e3..00000000000000 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { encryptServiceAccountKey } from "@calcom/platform-libraries"; -import { - addDelegationCredential, - type TServiceAccountKeySchema, - toggleDelegationCredentialEnabled, -} from "@calcom/platform-libraries/app-store"; -import type { DelegationCredential, Prisma, User } from "@calcom/prisma/client"; -import { InjectQueue } from "@nestjs/bull"; -import { Injectable, Logger, NotFoundException } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import { Queue } from "bull"; -import { AppConfig } from "@/config/type"; -import { - CALENDARS_QUEUE, - DEFAULT_CALENDARS_JOB, - DefaultCalendarsJobDataType, -} from "@/ee/calendars/processors/calendars.processor"; -import { CalendarsTasker } from "@/lib/services/tasker/calendars-tasker.service"; -import { CreateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/create-delegation-credential.input"; -import { - GoogleServiceAccountKeyInput, - MicrosoftServiceAccountKeyInput, -} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input"; -import { UpdateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/update-delegation-credential.input"; -import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository"; - -type DelegationCredentialWithWorkspacePlatform = { - workspacePlatform: { - name: string; - id: number; - enabled: boolean; - createdAt: Date; - updatedAt: Date; - slug: string; - description: string; - defaultServiceAccountKey: Prisma.JsonValue; - }; -} & { - id: string; - organizationId: number; - serviceAccountKey: Prisma.JsonValue; - enabled: boolean; - lastEnabledAt: Date | null; - lastDisabledAt: Date | null; - domain: string; - createdAt: Date; - updatedAt: Date; - workspacePlatformId: number; -}; - -@Injectable() -export class OrganizationsDelegationCredentialService { - private logger = new Logger("OrganizationsDelegationCredentialService"); - - constructor( - private readonly organizationsDelegationCredentialRepository: OrganizationsDelegationCredentialRepository, - private readonly calendarsTasker: CalendarsTasker, - private readonly configService: ConfigService, - @InjectQueue(CALENDARS_QUEUE) private readonly calendarsQueue: Queue - ) {} - async createDelegationCredential( - orgId: number, - delegatedServiceAccountUser: User, - body: CreateDelegationCredentialInput - ): Promise> | null | undefined> { - const delegationCredential = await addDelegationCredential({ - input: body, - ctx: { user: { id: delegatedServiceAccountUser.id, organizationId: orgId } }, - }); - return delegationCredential; - } - - async updateDelegationCredential( - orgId: number, - delegationCredentialId: string, - delegatedServiceAccountUser: User, - body: UpdateDelegationCredentialInput - ): Promise { - let delegationCredential = - await this.organizationsDelegationCredentialRepository.findByIdWithWorkspacePlatform( - delegationCredentialId - ); - - if (!delegationCredential) { - throw new NotFoundException(`DelegationCredential with id ${delegationCredentialId} not found`); - } - - if (body.serviceAccountKey !== undefined) { - const updatedDelegationCredential = await this.updateDelegationCredentialServiceAccountKey( - delegationCredential.id, - body.serviceAccountKey - ); - delegationCredential = updatedDelegationCredential ?? delegationCredential; - } - - if (body.enabled !== undefined) { - await this.updateDelegationCredentialEnabled( - orgId, - delegationCredentialId, - delegatedServiceAccountUser, - body.enabled - ); - } - - // once delegation credentials are enabled, slowly set all the destination calendars of delegated users - if (body.enabled === true && delegationCredential.enabled === false) { - await this.ensureDefaultCalendars(orgId, delegationCredential.domain); - } - - return { ...delegationCredential, enabled: body?.enabled ?? delegationCredential.enabled }; - } - - async ensureDefaultCalendars(orgId: number, domain: string): Promise { - try { - const delegatedUserProfiles = - await this.organizationsDelegationCredentialRepository.findDelegatedUserProfiles(orgId, domain); - - const results = await Promise.allSettled( - delegatedUserProfiles.map(async (profile) => { - if (profile.userId) { - if (this.configService.get("enableAsyncTasker")) { - this.logger.log(`Adding default calendar job for user with id: ${profile.userId}`); - await this.calendarsTasker.dispatch( - "ensureDefaultCalendars", - { - userId: profile.userId, - }, - { - idempotencyKey: `${DEFAULT_CALENDARS_JOB}_${profile.userId}`, - idempotencyKeyTTL: "1h", - tags: [`${DEFAULT_CALENDARS_JOB}_${profile.userId}`], - } - ); - return; - } - - const job = await this.calendarsQueue.getJob(`${DEFAULT_CALENDARS_JOB}_${profile.userId}`); - if (job) { - await job.remove(); - this.logger.log(`Removed default calendar job for user with id: ${profile.userId}`); - } - this.logger.log(`Adding default calendar job for user with id: ${profile.userId}`); - await this.calendarsQueue.add( - DEFAULT_CALENDARS_JOB, - { - userId: profile.userId, - } satisfies DefaultCalendarsJobDataType, - { jobId: `${DEFAULT_CALENDARS_JOB}_${profile.userId}`, removeOnComplete: true } - ); - } - }) - ); - - const failures = results.filter( - (result): result is PromiseRejectedResult => result.status === "rejected" - ); - if (failures.length > 0) { - this.logger.error( - `Failed to ensure default calendars for ${failures.length} users in org ${orgId}: ${failures.map((f) => f.reason).join(", ")}` - ); - } - } catch (err) { - this.logger.error( - err, - `Could not ensure default calendars for delegated users in org with id:${orgId}` - ); - } - } - - async ensureDefaultCalendarsForUser(orgId: number, userId: number, userEmail: string): Promise { - try { - const emailParts = userEmail.split("@"); - if (emailParts.length < 2 || !emailParts[1]) { - this.logger.warn(`Invalid email format for user ${userId}: missing domain`); - return; - } - const emailDomain = emailParts[1]; - - const delegationCredential = - await this.organizationsDelegationCredentialRepository.findEnabledByOrgIdAndDomain( - orgId, - emailDomain - ); - - if (!delegationCredential) { - return; - } - - if (this.configService.get("enableAsyncTasker")) { - this.logger.log(`Adding default calendar job for user with id: ${userId}`); - await this.calendarsTasker.dispatch( - "ensureDefaultCalendars", - { - userId, - }, - { idempotencyKey: `${DEFAULT_CALENDARS_JOB}_${userId}`, idempotencyKeyTTL: "1h" } - ); - return; - } - - const existingJob = await this.calendarsQueue.getJob(`${DEFAULT_CALENDARS_JOB}_${userId}`); - if (existingJob) { - await existingJob.remove(); - this.logger.log(`Removed existing default calendar job for user with id: ${userId}`); - } - this.logger.log(`Adding default calendar job for user with id: ${userId}`); - await this.calendarsQueue.add(DEFAULT_CALENDARS_JOB, { userId } satisfies DefaultCalendarsJobDataType, { - jobId: `${DEFAULT_CALENDARS_JOB}_${userId}`, - removeOnComplete: true, - }); - } catch (err) { - this.logger.error( - err, - `Could not ensure default calendars for user with id: ${userId} in org with id: ${orgId}` - ); - } - } - - async updateDelegationCredentialEnabled( - orgId: number, - delegationCredentialId: string, - delegatedServiceAccountUser: User, - enabled: boolean - ): Promise> | null | undefined> { - const handlerUser = { - id: delegatedServiceAccountUser.id, - email: delegatedServiceAccountUser.email, - organizationId: orgId, - }; - const handlerBody = { id: delegationCredentialId, enabled }; - const delegationCredential = await toggleDelegationCredentialEnabled(handlerUser, handlerBody); - return delegationCredential; - } - - async updateDelegationCredentialServiceAccountKey( - delegationCredentialId: string, - serviceAccountKey: GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput - ): Promise { - // First encrypt the service account key - const encryptedServiceAccountKey = encryptServiceAccountKey( - serviceAccountKey as TServiceAccountKeySchema - ); - const prismaJsonValue = JSON.parse(JSON.stringify(encryptedServiceAccountKey)); - - const delegationCredential = - await this.organizationsDelegationCredentialRepository.updateIncludeWorkspacePlatform( - delegationCredentialId, - { - serviceAccountKey: prismaJsonValue, - enabled: false, - } - ); - return delegationCredential; - } -} diff --git a/apps/api/v2/src/modules/organizations/event-types/assign-all-team-members.e2e-spec.ts b/apps/api/v2/src/modules/organizations/event-types/assign-all-team-members.e2e-spec.ts deleted file mode 100644 index acd24672af6216..00000000000000 --- a/apps/api/v2/src/modules/organizations/event-types/assign-all-team-members.e2e-spec.ts +++ /dev/null @@ -1,512 +0,0 @@ -import { SUCCESS_STATUS, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants"; -import type { - ApiSuccessResponse, - CreateTeamEventTypeInput_2024_06_14, - Host, - OrgTeamOutputDto, - TeamEventTypeOutput_2024_06_14, - UpdateTeamEventTypeInput_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 { 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 { HttpExceptionFilter } from "@/filters/http-exception.filter"; -import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter"; -import { Locales } from "@/lib/enums/locales"; -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 { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input"; -import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input"; -import { OrgTeamMembershipOutputResponseDto } from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output"; -import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input"; -import { UsersModule } from "@/modules/users/users.module"; - -const CLIENT_REDIRECT_URI = "http://localhost:4321"; - -describe("Assign all team members", () => { - let app: INestApplication; - - let oAuthClient: PlatformOAuthClient; - let organization: Team; - let userRepositoryFixture: UserRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let profilesRepositoryFixture: ProfileRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let hostsRepositoryFixture: HostsRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - - const platformAdminEmail = `assign-all-team-members-admin-${randomString()}@api.com`; - let platformAdmin: User; - - let managedTeam: OrgTeamOutputDto; - - const managedUsersTimeZone = "Europe/Rome"; - const firstManagedUserEmail = `managed-user-bookings-2024-04-15-first-user@api.com`; - const secondManagedUserEmail = `managed-user-bookings-2024-04-15-second-user@api.com`; - let firstManagedUser: CreateManagedUserData; - let secondManagedUser: CreateManagedUserData; - - let roundRobinEventType: TeamEventTypeOutput_2024_06_14; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [PrismaExceptionFilter, HttpExceptionFilter], - imports: [AppModule, UsersModule], - }).compile(); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - - platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail }); - - organization = await teamRepositoryFixture.create({ - name: `oauth-client-users-organization-${randomString()}`, - isPlatform: true, - isOrganization: true, - platformBilling: { - create: { - customerId: "cus_999", - plan: "ESSENTIALS", - subscriptionId: "sub_999", - }, - }, - }); - oAuthClient = await createOAuthClient(organization.id); - - await profilesRepositoryFixture.create({ - uid: randomString(), - username: platformAdminEmail, - organization: { connect: { id: organization.id } }, - user: { - connect: { id: platformAdmin.id }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: platformAdmin.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - await app.init(); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: [CLIENT_REDIRECT_URI], - permissions: 1023, - areDefaultEventTypesEnabled: false, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - describe("setup managed users", () => { - it("should create first managed user", async () => { - const requestBody: CreateManagedUserInput = { - email: firstManagedUserEmail, - timeZone: managedUsersTimeZone, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Alice Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email) - ); - expect(responseBody.data.accessToken).toBeDefined(); - expect(responseBody.data.refreshToken).toBeDefined(); - - firstManagedUser = responseBody.data; - }); - - it("should create second managed user", async () => { - const requestBody: CreateManagedUserInput = { - email: secondManagedUserEmail, - timeZone: managedUsersTimeZone, - weekStart: "Monday", - timeFormat: 24, - locale: Locales.FR, - name: "Bob Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.email).toEqual( - OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email) - ); - expect(responseBody.data.accessToken).toBeDefined(); - expect(responseBody.data.refreshToken).toBeDefined(); - - secondManagedUser = responseBody.data; - }); - }); - - describe("should setup managed team", () => { - it("should create managed team", async () => { - const body: CreateOrgTeamDto = { - name: `team-${randomString()}`, - }; - return request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/teams`) - .send(body) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .expect(201) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - managedTeam = responseBody.data; - }); - }); - }); - - describe("should setup memberships", () => { - it("should create first user's membership of the org's team", async () => { - const body: CreateOrgTeamMembershipDto = { - userId: firstManagedUser.user.id, - accepted: true, - role: "MEMBER", - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/memberships`) - .send(body) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .expect(201) - .then((response) => { - const responseBody: OrgTeamMembershipOutputResponseDto = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - }); - }); - - it("should create second user's membership of the org's team", async () => { - const body: CreateOrgTeamMembershipDto = { - userId: secondManagedUser.user.id, - accepted: true, - role: "MEMBER", - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/memberships`) - .send(body) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .expect(201) - .then((response) => { - const responseBody: OrgTeamMembershipOutputResponseDto = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - }); - }); - }); - - describe("should setup event types using assignAllTeamMembers true", () => { - it("should be able to setup team event type if no hosts nor assignAllTeamMembers provided", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation round robin", - slug: `organizations-event-types-round-robin-${randomString()}`, - lengthInMinutes: 60, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - schedulingType: "collective", - }; - - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`) - .send(body) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .expect(201); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(body.title); - expect(data.hosts).toEqual([]); - expect(data.schedulingType).toEqual("collective"); - const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id); - expect(eventTypeHosts.length).toEqual(0); - }); - - it("should setup collective event type assignAllTeamMembers true", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation collective", - slug: `assign-all-team-members-collective-${randomString()}`, - lengthInMinutes: 60, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - schedulingType: "collective", - assignAllTeamMembers: true, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`) - .send(body) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(body.title); - expect(data.hosts.length).toEqual(2); - expect(data.schedulingType).toEqual("collective"); - const dataFirstHost = data.hosts.find((host) => host.userId === firstManagedUser.user.id); - const dataSecondHost = data.hosts.find((host) => host.userId === secondManagedUser.user.id); - evaluateHost({ userId: firstManagedUser.user.id }, dataFirstHost); - evaluateHost({ userId: secondManagedUser.user.id }, dataSecondHost); - - const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id); - expect(eventTypeHosts.length).toEqual(2); - const firstHost = eventTypeHosts.find((host) => host.userId === firstManagedUser.user.id); - const secondHost = eventTypeHosts.find((host) => host.userId === secondManagedUser.user.id); - expect(firstHost).toBeDefined(); - expect(secondHost).toBeDefined(); - }); - }); - - it("should setup round robin event type assignAllTeamMembers true", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation round robin", - slug: `assign-all-team-members-round-robin-${randomString()}`, - lengthInMinutes: 60, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - schedulingType: "roundRobin", - assignAllTeamMembers: true, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`) - .send(body) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(body.title); - expect(data.hosts.length).toEqual(2); - expect(data.schedulingType).toEqual("roundRobin"); - const dataFirstHost = data.hosts.find((host) => host.userId === firstManagedUser.user.id); - const dataSecondHost = data.hosts.find((host) => host.userId === secondManagedUser.user.id); - evaluateHost( - { userId: firstManagedUser.user.id, mandatory: false, priority: "medium" }, - dataFirstHost - ); - evaluateHost( - { userId: secondManagedUser.user.id, mandatory: false, priority: "medium" }, - dataSecondHost - ); - - const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id); - expect(eventTypeHosts.length).toEqual(2); - const firstHost = eventTypeHosts.find((host) => host.userId === firstManagedUser.user.id); - const secondHost = eventTypeHosts.find((host) => host.userId === secondManagedUser.user.id); - expect(firstHost).toBeDefined(); - expect(secondHost).toBeDefined(); - roundRobinEventType = data; - }); - }); - - it("should update round robin event type", async () => { - if (!roundRobinEventType) { - const setupBody: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation round robin", - slug: `assign-all-team-members-round-robin-${randomString()}`, - lengthInMinutes: 60, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - schedulingType: "roundRobin", - assignAllTeamMembers: true, - }; - - const setupResponse = await request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`) - .send(setupBody) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .expect(201); - - const setupResponseBody: ApiSuccessResponse = setupResponse.body; - roundRobinEventType = setupResponseBody.data; - } - - const body: UpdateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation round robin updated", - }; - - return request(app.getHttpServer()) - .patch( - `/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types/${roundRobinEventType.id}` - ) - .send(body) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(body.title); - expect(data.hosts.length).toEqual(2); - expect(data.schedulingType).toEqual("roundRobin"); - const dataFirstHost = data.hosts.find((host) => host.userId === firstManagedUser.user.id); - const dataSecondHost = data.hosts.find((host) => host.userId === secondManagedUser.user.id); - evaluateHost( - { userId: firstManagedUser.user.id, mandatory: false, priority: "medium" }, - dataFirstHost - ); - evaluateHost( - { userId: secondManagedUser.user.id, mandatory: false, priority: "medium" }, - dataSecondHost - ); - - const eventTypeHosts = await hostsRepositoryFixture.getEventTypeHosts(data.id); - expect(eventTypeHosts.length).toEqual(2); - const firstHost = eventTypeHosts.find((host) => host.userId === firstManagedUser.user.id); - const secondHost = eventTypeHosts.find((host) => host.userId === secondManagedUser.user.id); - expect(firstHost).toBeDefined(); - expect(secondHost).toBeDefined(); - }); - }); - - it("should setup managed event type assignAllTeamMembers true", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation managed", - slug: `assign-all-team-members-managed-${randomString()}`, - lengthInMinutes: 60, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - schedulingType: "managed", - assignAllTeamMembers: true, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/teams/${managedTeam.id}/event-types`) - .send(body) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(3); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes( - firstManagedUser.user.id - ); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes( - secondManagedUser.user.id - ); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(managedTeam.id); - const managedTeamEventTypes = teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" - ); - - expect(teammate1EventTypes.length).toEqual(1); - expect(teammate2EventTypes.length).toEqual(1); - expect(managedTeamEventTypes.length).toEqual(1); - expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true); - - const responseTeamEvent = responseBody.data.find( - (eventType) => eventType.schedulingType === "managed" - ); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.teamId).toEqual(managedTeam.id); - expect(responseTeamEvent?.assignAllTeamMembers).toEqual(true); - - const responseTeammate1Event = responseBody.data.find( - (eventType) => eventType.ownerId === firstManagedUser.user.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - const responseTeammate2Event = responseBody.data.find( - (eventType) => eventType.ownerId === secondManagedUser.user.id - ); - expect(responseTeammate2Event).toBeDefined(); - expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - }); - }); - }); - - function evaluateHost(expected: Partial, received: Host | undefined) { - if (!received) { - throw new Error(`Host is undefined. Expected userId: ${expected.userId}`); - } - expect(expected.userId).toEqual(received.userId); - if (expected.mandatory !== undefined) { - expect(expected.mandatory).toEqual(received.mandatory); - } - if (expected.priority !== undefined) { - expect(expected.priority).toEqual(received.priority); - } - } - - afterAll(async () => { - await userRepositoryFixture.delete(firstManagedUser.user.id); - await userRepositoryFixture.delete(secondManagedUser.user.id); - await userRepositoryFixture.delete(platformAdmin.id); - await oauthClientRepositoryFixture.delete(oAuthClient.id); - await teamRepositoryFixture.delete(organization.id); - await app.close(); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/event-types/organizations-admin-not-team-member-event-types.e2e-spec.ts b/apps/api/v2/src/modules/organizations/event-types/organizations-admin-not-team-member-event-types.e2e-spec.ts deleted file mode 100644 index 2145f037eb0c9d..00000000000000 --- a/apps/api/v2/src/modules/organizations/event-types/organizations-admin-not-team-member-event-types.e2e-spec.ts +++ /dev/null @@ -1,1229 +0,0 @@ -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, - NoticeThresholdUnitEnum, -} from "@calcom/platform-enums"; -import { SchedulingType } from "@calcom/platform-libraries"; -import type { - ApiSuccessResponse, - CreateTeamEventTypeInput_2024_06_14, - EventTypeOutput_2024_06_14, - Host, - TeamEventTypeOutput_2024_06_14, - UpdateTeamEventTypeInput_2024_06_14, -} 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 { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { 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"; - -describe("Organizations Event Types Endpoints", () => { - describe("User Authentication - User is Org Admin but not team admin", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - - let org: Team; - let team: Team; - let falseTestOrg: Team; - let falseTestTeam: Team; - - const userEmail = `organizations-event-types-admin-${randomString()}@api.com`; - let userAdmin: User; - - const teammate1Email = `organizations-event-types-teammate1-${randomString()}@api.com`; - const teammate2Email = `organizations-event-types-teammate2-${randomString()}@api.com`; - const falseTestUserEmail = `organizations-event-types-false-user-${randomString()}@api.com`; - let teammate1: User; - let teammate2: User; - let falseTestUser: User; - - let collectiveEventType: TeamEventTypeOutput_2024_06_14; - let managedEventType: TeamEventTypeOutput_2024_06_14; - - const managedEventTypeSlug = `organizations-event-types-managed-${randomString()}`; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - - userAdmin = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - role: "ADMIN", - }); - - teammate1 = await userRepositoryFixture.create({ - email: teammate1Email, - username: teammate1Email, - name: "alice", - }); - - teammate2 = await userRepositoryFixture.create({ - email: teammate2Email, - username: teammate2Email, - name: "bob", - }); - - falseTestUser = await userRepositoryFixture.create({ - email: falseTestUserEmail, - username: falseTestUserEmail, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-event-types-organization-${randomString()}`, - isOrganization: true, - slug: `organizations-event-types-organization-${randomString()}`, - }); - - falseTestOrg = await organizationsRepositoryFixture.create({ - name: `organizations-event-types-false-org-${randomString()}`, - isOrganization: true, - }); - - team = await teamsRepositoryFixture.create({ - name: `organizations-event-types-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - falseTestTeam = await teamsRepositoryFixture.create({ - name: `organizations-event-types-false-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: falseTestOrg.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${userAdmin.id}`, - username: userEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: userAdmin.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teammate1.id}`, - username: teammate1Email, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: teammate1.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teammate2.id}`, - username: teammate2Email, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: teammate2.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: userAdmin.id } }, - team: { connect: { id: org.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammate1.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammate2.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: falseTestUser.id } }, - team: { connect: { id: falseTestTeam.id } }, - accepted: true, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(userAdmin).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should not be able to create event-type for team outside org", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation", - slug: "coding-consultation", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - { - type: "organizersDefaultApp", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teammate1.id, - mandatory: true, - priority: "high", - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types`) - .send(body) - .expect(404); - }); - - it("should not be able to create event-type for user outside org", async () => { - const userId = falseTestUser.id; - - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation", - slug: "coding-consultation", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId, - mandatory: true, - priority: "high", - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(404); - }); - - it("should create a collective team event-type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - successRedirectUrl: "https://masterchief.com/argentina/flan/video/1234", - title: "Coding consultation collective", - slug: `organizations-event-types-collective-${randomString()}`, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - bookingFields: [ - { - type: "select", - label: "select which language is your codebase in", - slug: "select-language", - required: true, - placeholder: "select language", - options: ["javascript", "python", "cobol"], - }, - ], - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - schedulingType: "collective", - hosts: [ - { - userId: teammate1.id, - }, - { - userId: teammate2.id, - }, - ], - bookingLimitsCount: { - day: 2, - week: 5, - }, - onlyShowFirstAvailableSlot: true, - bookingLimitsDuration: { - day: 60, - week: 100, - }, - offsetStart: 30, - bookingWindow: { - type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays, - value: 30, - rolling: true, - }, - bookerLayouts: { - enabledLayouts: [ - BookerLayoutsInputEnum_2024_06_14.column, - BookerLayoutsInputEnum_2024_06_14.month, - BookerLayoutsInputEnum_2024_06_14.week, - ], - defaultLayout: BookerLayoutsInputEnum_2024_06_14.month, - }, - - confirmationPolicy: { - type: ConfirmationPolicyEnum.TIME, - noticeThreshold: { - count: 60, - unit: NoticeThresholdUnitEnum.MINUTES, - }, - blockUnconfirmedBookingsInBooker: true, - }, - requiresBookerEmailVerification: true, - hideCalendarNotes: true, - hideCalendarEventDetails: true, - hideOrganizerEmail: true, - lockTimeZoneToggleOnBookingPage: true, - color: { - darkThemeHex: "#292929", - lightThemeHex: "#fafafa", - }, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(body.title); - expect(data.hosts.length).toEqual(2); - expect(data.schedulingType).toEqual("collective"); - evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]); - evaluateHost(body.hosts?.[1] || { userId: -1 }, data.hosts[1]); - expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount); - expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot); - expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration); - expect(data.offsetStart).toEqual(body.offsetStart); - expect(data.bookingWindow).toEqual(body.bookingWindow); - expect(data.bookerLayouts).toEqual(body.bookerLayouts); - expect(data.confirmationPolicy).toEqual(body.confirmationPolicy); - expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification); - expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes); - expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails); - expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail); - expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage); - expect(data.color).toEqual(body.color); - expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234"); - collectiveEventType = responseBody.data; - }); - }); - - it("should create a managed team event-type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation managed", - slug: managedEventTypeSlug, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "MANAGED", - hosts: [ - { - userId: teammate1.id, - mandatory: true, - priority: "high", - }, - { - userId: teammate2.id, - mandatory: false, - priority: "low", - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(3); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - - expect(teammate1EventTypes.length).toEqual(1); - expect(teammate1EventTypes[0].slug).toEqual(managedEventTypeSlug); - expect(teammate2EventTypes.length).toEqual(1); - expect(teammate2EventTypes[0].slug).toEqual(managedEventTypeSlug); - expect(teamEventTypes.filter((eventType) => eventType.schedulingType === "MANAGED").length).toEqual( - 1 - ); - - const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.hosts?.find((host) => host.userId === teammate1.id)).toEqual({ - userId: teammate1.id, - name: teammate1.name, - username: teammate1.username, - avatarUrl: teammate1.avatarUrl, - }); - - expect(responseTeamEvent?.hosts?.find((host) => host.userId === teammate2.id)).toEqual({ - userId: teammate2.id, - name: teammate2.name, - username: teammate2.username, - avatarUrl: teammate2.avatarUrl, - }); - - if (!responseTeamEvent) { - throw new Error("Team event not found"); - } - - const responseTeammate1Event = responseBody.data.find((event) => event.ownerId === teammate1.id); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - const responseTeammate2Event = responseBody.data.find((event) => event.ownerId === teammate2.id); - expect(responseTeammate2Event).toBeDefined(); - expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - managedEventType = responseTeamEvent; - }); - }); - - it("managed team event types should be returned when fetching event types of users", async () => { - return request(app.getHttpServer()) - .get(`/v2/event-types?username=${teammate1.username}&orgSlug=${org.slug}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].slug).toEqual(managedEventTypeSlug); - expect(data[0].ownerId).toEqual(teammate1.id); - expect(data[0].id).not.toEqual(managedEventType.id); - }); - }); - - it("managed team event type should be returned when fetching event types of users", async () => { - return request(app.getHttpServer()) - .get( - `/v2/event-types?username=${teammate1.username}&orgSlug=${org.slug}&eventSlug=${managedEventTypeSlug}` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].slug).toEqual(managedEventTypeSlug); - expect(data[0].ownerId).toEqual(teammate1.id); - expect(data[0].id).not.toEqual(managedEventType.id); - }); - }); - - it("should not get an event-type of team outside org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types/${collectiveEventType.id}`) - .expect(404); - }); - - it("should not get a non existing event-type", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/999999`) - .expect(404); - }); - - it("should get a team event-type", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(collectiveEventType.title); - expect(data.hosts.length).toEqual(2); - evaluateHost(collectiveEventType.hosts[0], data.hosts[0]); - evaluateHost(collectiveEventType.hosts[1], data.hosts[1]); - - collectiveEventType = responseBody.data; - }); - }); - - it("should not get event-types of team outside org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types`) - .expect(404); - }); - - it("should get team event-types", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(2); - - const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "collective"); - const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "managed"); - - expect(eventTypeCollective?.title).toEqual(collectiveEventType.title); - expect(eventTypeCollective?.hosts.length).toEqual(2); - - expect(eventTypeManaged?.title).toEqual(managedEventType.title); - expect(eventTypeManaged?.hosts.length).toEqual(2); - evaluateHost(collectiveEventType.hosts[0], eventTypeCollective?.hosts[0]); - evaluateHost(collectiveEventType.hosts[1], eventTypeCollective?.hosts[1]); - }); - }); - - it("should not be able to update event-type for incorrect team", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - title: "Clean code consultation", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types/${collectiveEventType.id}`) - .send(body) - .expect(404); - }); - - it("should not be able to update non existing event-type", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - title: "Clean code consultation", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/999999`) - .send(body) - .expect(400); - }); - - it("should update collective event-type", async () => { - const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [ - { - userId: teammate1.id, - }, - ]; - - const body: UpdateTeamEventTypeInput_2024_06_14 = { - hosts: newHosts, - successRedirectUrl: "https://new-url-success.com", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`) - .send(body) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const eventType = responseBody.data; - expect(eventType.successRedirectUrl).toEqual("https://new-url-success.com"); - expect(eventType.title).toEqual(collectiveEventType.title); - expect(eventType.hosts.length).toEqual(1); - evaluateHost(eventType.hosts[0], newHosts[0]); - }); - }); - - it("should update managed event-type", async () => { - const newTitle = "Coding consultation managed updated"; - const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [ - { - userId: teammate1.id, - mandatory: true, - priority: "medium", - }, - ]; - - const body: UpdateTeamEventTypeInput_2024_06_14 = { - title: newTitle, - hosts: newHosts, - successRedirectUrl: "https://new-url-success-managed.com", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${managedEventType.id}`) - .send(body) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(2); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - const managedTeamEventTypes = teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" - ); - - expect(teammate1EventTypes.length).toEqual(1); - expect(teammate1EventTypes[0].title).toEqual(newTitle); - expect(teammate2EventTypes.length).toEqual(0); - expect(managedTeamEventTypes.length).toEqual(1); - expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(false); - expect( - teamEventTypes.filter((eventType) => eventType.schedulingType === "MANAGED")?.[0]?.title - ).toEqual(newTitle); - - const responseTeamEvent = responseBody.data.find( - (eventType) => eventType.schedulingType === "managed" - ); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.title).toEqual(newTitle); - expect(responseTeamEvent?.assignAllTeamMembers).toEqual(false); - - const responseTeammate1Event = responseBody.data.find( - (eventType) => eventType.ownerId === teammate1.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.title).toEqual(newTitle); - - managedEventType = responseBody.data[0]; - expect(managedEventType.successRedirectUrl).toEqual("https://new-url-success-managed.com"); - }); - }); - - it("should be able to create phone-only event type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Phone coding consultation", - slug: "phone-coding-consultation", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - { - type: "organizersDefaultApp", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teammate1.id, - mandatory: true, - priority: "high", - }, - ], - bookingFields: [ - { - type: "email", - required: false, - label: "Email", - hidden: true, - }, - { - type: "phone", - slug: "attendeePhoneNumber", - required: true, - label: "Phone number", - hidden: false, - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data.bookingFields).toEqual([ - { - isDefault: true, - type: "name", - slug: "name", - label: "your_name", - required: true, - disableOnPrefill: false, - }, - { - isDefault: true, - type: "email", - slug: "email", - required: false, - label: "Email", - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - type: "radioInput", - slug: "location", - required: false, - hidden: false, - }, - { - isDefault: true, - type: "phone", - slug: "attendeePhoneNumber", - required: true, - hidden: false, - label: "Phone number", - disableOnPrefill: false, - }, - { - isDefault: true, - type: "text", - slug: "title", - required: true, - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - type: "textarea", - slug: "notes", - required: false, - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - type: "multiemail", - slug: "guests", - required: false, - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - type: "textarea", - slug: "rescheduleReason", - required: false, - disableOnPrefill: false, - hidden: false, - }, - ]); - }); - }); - - it("should be able to configure phone-only event type", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - bookingFields: [ - { - type: "email", - required: false, - label: "Email", - hidden: true, - }, - { - type: "phone", - slug: "attendeePhoneNumber", - required: true, - label: "Phone number", - hidden: false, - }, - ], - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`) - .send(body) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data.bookingFields).toEqual([ - { - isDefault: true, - type: "name", - slug: "name", - required: true, - label: "your_name", - disableOnPrefill: false, - }, - { - isDefault: true, - type: "email", - slug: "email", - required: false, - label: "Email", - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - type: "radioInput", - slug: "location", - required: false, - hidden: false, - }, - { - isDefault: true, - type: "phone", - slug: "attendeePhoneNumber", - required: true, - hidden: false, - label: "Phone number", - disableOnPrefill: false, - }, - { - isDefault: true, - type: "text", - slug: "title", - required: true, - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - type: "textarea", - slug: "notes", - required: false, - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - type: "multiemail", - slug: "guests", - required: false, - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - type: "textarea", - slug: "rescheduleReason", - required: false, - disableOnPrefill: false, - hidden: false, - }, - ]); - }); - }); - - it("should assign all members to managed event-type", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - assignAllTeamMembers: true, - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${managedEventType.id}`) - .send(body) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(3); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - const managedTeamEventTypes = teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" - ); - - expect(teammate1EventTypes.length).toEqual(1); - expect(teammate2EventTypes.length).toEqual(1); - expect(managedTeamEventTypes.length).toEqual(1); - expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true); - - const responseTeamEvent = responseBody.data.find( - (eventType) => eventType.schedulingType === "managed" - ); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.teamId).toEqual(team.id); - expect(responseTeamEvent?.assignAllTeamMembers).toEqual(true); - - const responseTeammate1Event = responseBody.data.find( - (eventType) => eventType.ownerId === teammate1.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - const responseTeammate2Event = responseBody.data.find( - (eventType) => eventType.ownerId === teammate2.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - if (responseTeamEvent) { - managedEventType = responseTeamEvent; - } - }); - }); - - it("should not delete event-type of team outside org", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types/${collectiveEventType.id}`) - .expect(404); - }); - - it("should delete event-type not part of the team", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${team.id}/event-types/99999`) - .expect(404); - }); - - it("should delete collective event-type", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`) - .expect(200); - }); - - it("should delete managed event-type", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${managedEventType.id}`) - .expect(200); - }); - - it("should return event type with default bookingFields if they are not defined", async () => { - const eventTypeInput = { - title: "unknown field event type two", - slug: `organizations-event-types-unknown-${randomString()}`, - description: "unknown field event type description two", - length: 40, - hidden: false, - locations: [], - schedulingType: SchedulingType.ROUND_ROBIN, - }; - const eventType = await eventTypesRepositoryFixture.createTeamEventType({ - ...eventTypeInput, - team: { connect: { id: team.id } }, - }); - - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}`) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - const fetchedEventType = responseBody.data; - - expect(fetchedEventType.bookingFields).toEqual([ - { isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false }, - { - isDefault: true, - required: true, - slug: "email", - type: "email", - disableOnPrefill: false, - hidden: false, - }, - { - disableOnPrefill: false, - isDefault: true, - type: "phone", - slug: "attendeePhoneNumber", - required: false, - hidden: true, - }, - { - isDefault: true, - type: "radioInput", - slug: "location", - required: false, - hidden: false, - }, - { - isDefault: true, - required: true, - slug: "title", - type: "text", - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - required: false, - slug: "notes", - type: "textarea", - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - required: false, - slug: "guests", - type: "multiemail", - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - required: false, - slug: "rescheduleReason", - type: "textarea", - disableOnPrefill: false, - hidden: false, - }, - ]); - }); - }); - - it("should create a round robin team event-type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - successRedirectUrl: "https://masterchief.com/argentina/flan/video/1234", - title: "Coding consultation round robin", - slug: `organizations-event-types-round-robin-${randomString()}`, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - bookingFields: [ - { - type: "select", - label: "select which language is your codebase in", - slug: "select-language", - required: true, - placeholder: "select language", - options: ["javascript", "python", "cobol"], - }, - ], - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - schedulingType: "roundRobin", - hosts: [ - { - userId: teammate1.id, - mandatory: true, - priority: "high", - }, - { - userId: teammate2.id, - mandatory: false, - priority: "medium", - }, - ], - bookingLimitsCount: { - day: 2, - week: 5, - }, - onlyShowFirstAvailableSlot: true, - bookingLimitsDuration: { - day: 60, - week: 100, - }, - offsetStart: 30, - bookingWindow: { - type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays, - value: 30, - rolling: true, - }, - bookerLayouts: { - enabledLayouts: [ - BookerLayoutsInputEnum_2024_06_14.column, - BookerLayoutsInputEnum_2024_06_14.month, - BookerLayoutsInputEnum_2024_06_14.week, - ], - defaultLayout: BookerLayoutsInputEnum_2024_06_14.month, - }, - - confirmationPolicy: { - type: ConfirmationPolicyEnum.TIME, - noticeThreshold: { - count: 60, - unit: NoticeThresholdUnitEnum.MINUTES, - }, - blockUnconfirmedBookingsInBooker: true, - }, - requiresBookerEmailVerification: true, - hideCalendarNotes: true, - hideCalendarEventDetails: true, - hideOrganizerEmail: true, - lockTimeZoneToggleOnBookingPage: true, - color: { - darkThemeHex: "#292929", - lightThemeHex: "#fafafa", - }, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(body.title); - expect(data.hosts.length).toEqual(2); - expect(data.schedulingType).toEqual("roundRobin"); - evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]); - evaluateHost(body.hosts?.[1] || { userId: -1 }, data.hosts[1]); - expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount); - expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot); - expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration); - expect(data.offsetStart).toEqual(body.offsetStart); - expect(data.bookingWindow).toEqual(body.bookingWindow); - expect(data.bookerLayouts).toEqual(body.bookerLayouts); - expect(data.confirmationPolicy).toEqual(body.confirmationPolicy); - expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification); - expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes); - expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails); - expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail); - expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage); - expect(data.color).toEqual(body.color); - expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234"); - collectiveEventType = responseBody.data; - }); - }); - - it("should create a managed team event-type without hosts", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation managed without hosts", - slug: "coding-consultation-managed-without-hosts", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "MANAGED", - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(1); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - - const teammate1HasThisEvent = teammate1EventTypes.some((eventType) => eventType.slug === body.slug); - const teammate2HasThisEvent = teammate2EventTypes.some((eventType) => eventType.slug === body.slug); - expect(teammate1HasThisEvent).toBe(false); - expect(teammate2HasThisEvent).toBe(false); - expect( - teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" && eventType.slug === body.slug - ).length - ).toEqual(1); - - const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.hosts).toEqual([]); - - if (!responseTeamEvent) { - throw new Error("Team event not found"); - } - }); - }); - - function evaluateHost(expected: Host, received: Host | undefined) { - expect(expected.userId).toEqual(received?.userId); - expect(expected.mandatory).toEqual(received?.mandatory); - expect(expected.priority).toEqual(received?.priority); - } - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(userAdmin.email); - await userRepositoryFixture.deleteByEmail(teammate1.email); - await userRepositoryFixture.deleteByEmail(teammate2.email); - await userRepositoryFixture.deleteByEmail(falseTestUser.email); - await teamsRepositoryFixture.delete(team.id); - await teamsRepositoryFixture.delete(falseTestTeam.id); - await organizationsRepositoryFixture.delete(org.id); - await organizationsRepositoryFixture.delete(falseTestOrg.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/event-types/organizations-event-types-private-links.e2e-spec.ts b/apps/api/v2/src/modules/organizations/event-types/organizations-event-types-private-links.e2e-spec.ts deleted file mode 100644 index a919b9c4540ba3..00000000000000 --- a/apps/api/v2/src/modules/organizations/event-types/organizations-event-types-private-links.e2e-spec.ts +++ /dev/null @@ -1,182 +0,0 @@ -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 { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; -import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; -import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; -import { randomString } from "test/utils/randomString"; -import { withApiAuth } from "test/utils/withApiAuth"; -import { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { HttpExceptionFilter } from "@/filters/http-exception.filter"; -import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations / Teams / Event Types / Private Links Endpoints", () => { - let app: INestApplication; - - let orgFixture: OrganizationRepositoryFixture; - let teamFixture: TeamRepositoryFixture; - let userFixture: UserRepositoryFixture; - let eventTypesFixture: EventTypesRepositoryFixture; - - let org: any; - let team: any; - let user: any; - let eventType: any; - - const userEmail = `org-private-links-user-${randomString()}@api.com`; - - beforeAll(async () => { - const testingModuleBuilder = withApiAuth( - userEmail, - Test.createTestingModule({ - providers: [PrismaExceptionFilter, HttpExceptionFilter], - imports: [AppModule, UsersModule, TokensModule], - }) - ) - // Bypass org admin plan and admin API checks and roles in this e2e - .overrideGuard(PlatformPlanGuard) - .useValue({ canActivate: () => true }) - .overrideGuard(IsAdminAPIEnabledGuard) - .useValue({ canActivate: () => true }) - .overrideGuard(RolesGuard) - .useValue({ canActivate: () => true }) - // Keep IsOrgGuard and IsTeamInOrg to validate org/team path integrity - .overrideGuard(ApiAuthGuard) - .useValue({ canActivate: () => true }); - - const moduleRef = await testingModuleBuilder.compile(); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - orgFixture = new OrganizationRepositoryFixture(moduleRef); - teamFixture = new TeamRepositoryFixture(moduleRef); - userFixture = new UserRepositoryFixture(moduleRef); - eventTypesFixture = new EventTypesRepositoryFixture(moduleRef); - - user = await userFixture.create({ - email: userEmail, - username: `org-private-links-user-${randomString()}`, - name: "Test User", - }); - - org = await orgFixture.create({ - name: `org-private-links-org-${randomString()}`, - slug: `org-private-links-org-${randomString()}`, - isOrganization: true, - }); - - team = await teamFixture.create({ - name: `org-private-links-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - // Create a team-owned event type - eventType = await eventTypesFixture.createTeamEventType({ - title: `org-private-links-event-type-${randomString()}`, - slug: `org-private-links-event-type-${randomString()}`, - length: 30, - locations: [], - team: { connect: { id: team.id } }, - }); - - await app.init(); - }); - - it("POST /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links - create", async () => { - const body: CreatePrivateLinkInput = { maxUsageCount: 5 }; - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`) - .set("Authorization", "Bearer test") - .send(body) - .expect(201); - - expect(response.body.status).toBe(SUCCESS_STATUS); - expect(response.body.data.linkId).toBeDefined(); - expect(response.body.data.maxUsageCount).toBe(5); - expect(response.body.data.usageCount).toBeDefined(); - }); - - it("GET /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links - list", async () => { - const response = await request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`) - .set("Authorization", "Bearer test") - .expect(200); - - expect(response.body.status).toBe(SUCCESS_STATUS); - expect(Array.isArray(response.body.data)).toBe(true); - }); - - it("PATCH /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links/:linkId - update", async () => { - // create first - const createResp = await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`) - .set("Authorization", "Bearer test") - .send({ maxUsageCount: 3 }) - .expect(201); - - const linkId = createResp.body.data.linkId as string; - - const response = await request(app.getHttpServer()) - .patch( - `/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links/${linkId}` - ) - .set("Authorization", "Bearer test") - .send({ maxUsageCount: 10 }) - .expect(200); - - expect(response.body.status).toBe(SUCCESS_STATUS); - expect(response.body.data.maxUsageCount).toBe(10); - }); - - it("DELETE /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links/:linkId - delete", async () => { - // create first - const createResp = await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`) - .set("Authorization", "Bearer test") - .send({ maxUsageCount: 2 }) - .expect(201); - - const linkId = createResp.body.data.linkId as string; - - const response = await request(app.getHttpServer()) - .delete( - `/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links/${linkId}` - ) - .set("Authorization", "Bearer test") - .expect(200); - - expect(response.body.status).toBe(SUCCESS_STATUS); - expect(response.body.data.linkId).toBe(linkId); - }); - - afterAll(async () => { - try { - if (eventType?.id) { - await eventTypesFixture.delete(eventType.id); - } - if (team?.id) { - await teamFixture.delete(team.id); - } - if (org?.id) { - await orgFixture.delete(org.id); - } - if (user?.email) { - await userFixture.deleteByEmail(user.email); - } - } catch {} - await app.close(); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/event-types/organizations-event-types.controller.ts b/apps/api/v2/src/modules/organizations/event-types/organizations-event-types.controller.ts deleted file mode 100644 index c9b09b43c87186..00000000000000 --- a/apps/api/v2/src/modules/organizations/event-types/organizations-event-types.controller.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input"; -import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer"; -import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service"; -import { OrganizationsEventTypesService } from "@/modules/organizations/event-types/services/organizations-event-types.service"; -import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service"; -import { CreateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/create-team-event-type.output"; -import { DeleteTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/delete-team-event-type.output"; -import { GetTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/get-team-event-type.output"; -import { GetTeamEventTypesOutput } from "@/modules/teams/event-types/outputs/get-team-event-types.output"; -import { UpdateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/update-team-event-type.output"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { - Controller, - UseGuards, - Get, - Post, - Param, - ParseIntPipe, - Body, - Patch, - Delete, - HttpCode, - HttpStatus, - NotFoundException, - Query, - Logger, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import { handleCreatePhoneCall } from "@calcom/platform-libraries"; -import { - CreateTeamEventTypeInput_2024_06_14, - GetOrganizationEventTypesQuery_2024_06_14, - GetTeamEventTypesQuery_2024_06_14, - TeamEventTypeOutput_2024_06_14, - UpdateTeamEventTypeInput_2024_06_14, -} from "@calcom/platform-types"; - -export type EventTypeHandlerResponse = { - data: DatabaseTeamEventType[] | DatabaseTeamEventType; - status: typeof SUCCESS_STATUS | typeof ERROR_STATUS; -}; - -@Controller({ - path: "/v2/organizations/:orgId", - version: API_VERSIONS_VALUES, -}) -@DocsTags("Orgs / Teams / Event Types") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -@ApiParam({ name: "orgId", type: Number, required: true }) -export class OrganizationsEventTypesController { - private readonly logger = new Logger("OrganizationsEventTypesController"); - - constructor( - private readonly organizationsEventTypesService: OrganizationsEventTypesService, - private readonly inputService: InputOrganizationsEventTypesService, - private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe - ) {} - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Post("/teams/:teamId/event-types") - @ApiOperation({ summary: "Create an event type" }) - async createTeamEventType( - @GetUser() user: UserWithProfile, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("orgId", ParseIntPipe) orgId: number, - @Body() bodyEventType: CreateTeamEventTypeInput_2024_06_14 - ): Promise { - const transformedBody = await this.inputService.transformAndValidateCreateTeamEventTypeInput( - user.id, - teamId, - bodyEventType - ); - this.logger.debug( - "nl debug - create org team event type - transformedBody", - JSON.stringify(transformedBody, null, 2) - ); - - const eventType = await this.organizationsEventTypesService.createOrganizationTeamEventType( - user, - teamId, - orgId, - transformedBody - ); - - return { - status: SUCCESS_STATUS, - data: await this.outputTeamEventTypesResponsePipe.transform(eventType), - }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Get("/teams/:teamId/event-types/:eventTypeId") - @ApiOperation({ summary: "Get an event type" }) - async getTeamEventType( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("eventTypeId") eventTypeId: number - ): Promise { - const eventType = await this.organizationsEventTypesService.getTeamEventType(teamId, eventTypeId); - - if (!eventType) { - throw new NotFoundException(`Event type with id ${eventTypeId} not found`); - } - - return { - status: SUCCESS_STATUS, - data: (await this.outputTeamEventTypesResponsePipe.transform( - eventType - )) as TeamEventTypeOutput_2024_06_14, - }; - } - - @Roles("TEAM_ADMIN") - @Post("/teams/:teamId/event-types/:eventTypeId/create-phone-call") - @UseGuards(ApiAuthGuard, IsOrgGuard, IsTeamInOrg, RolesGuard) - @ApiOperation({ summary: "Create a phone call" }) - @ApiParam({ name: "teamId", type: Number, required: true }) - async createPhoneCall( - @Param("eventTypeId") eventTypeId: number, - @Param("orgId", ParseIntPipe) orgId: number, - @Body() body: CreatePhoneCallInput, - @GetUser() user: UserWithProfile - ): Promise { - const data = await handleCreatePhoneCall({ - user: { - id: user.id, - timeZone: user.timeZone, - profile: { organization: { id: orgId } }, - }, - input: { ...body, eventTypeId }, - }); - - return { - status: SUCCESS_STATUS, - data, - }; - } - - @UseGuards(IsOrgGuard, IsTeamInOrg, IsAdminAPIEnabledGuard) - @Get("/teams/:teamId/event-types") - @ApiOperation({ - summary: "Get team event types", - description: - 'Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.', - }) - async getTeamEventTypes( - @Param("teamId", ParseIntPipe) teamId: number, - @Query() queryParams: GetTeamEventTypesQuery_2024_06_14 - ): Promise { - const { eventSlug, hostsLimit, sortCreatedAt } = queryParams; - - if (eventSlug) { - const eventType = await this.organizationsEventTypesService.getTeamEventTypeBySlug( - teamId, - eventSlug, - hostsLimit - ); - - return { - status: SUCCESS_STATUS, - data: await this.outputTeamEventTypesResponsePipe.transform(eventType ? [eventType] : []), - }; - } - - const eventTypes = await this.organizationsEventTypesService.getTeamEventTypes(teamId, sortCreatedAt); - - return { - status: SUCCESS_STATUS, - data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes), - }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Get("/teams/event-types") - @ApiOperation({ - summary: "Get all team event types", - description: - 'Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.', - }) - async getTeamsEventTypes( - @Param("orgId", ParseIntPipe) orgId: number, - @Query() queryParams: GetOrganizationEventTypesQuery_2024_06_14 - ): Promise { - const { skip, take, sortCreatedAt } = queryParams; - const eventTypes = await this.organizationsEventTypesService.getOrganizationsTeamsEventTypes( - orgId, - skip, - take, - sortCreatedAt - ); - - return { - status: SUCCESS_STATUS, - data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes), - }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Patch("/teams/:teamId/event-types/:eventTypeId") - @ApiOperation({ summary: "Update a team event type" }) - async updateTeamEventType( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("eventTypeId", ParseIntPipe) eventTypeId: number, - @GetUser() user: UserWithProfile, - @Body() bodyEventType: UpdateTeamEventTypeInput_2024_06_14 - ): Promise { - const transformedBody = await this.inputService.transformAndValidateUpdateTeamEventTypeInput( - user.id, - eventTypeId, - teamId, - bodyEventType - ); - - const eventType = await this.organizationsEventTypesService.updateOrganizationTeamEventType( - eventTypeId, - teamId, - transformedBody, - user - ); - - return { - status: SUCCESS_STATUS, - data: await this.outputTeamEventTypesResponsePipe.transform(eventType), - }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Delete("/teams/:teamId/event-types/:eventTypeId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Delete a team event type" }) - async deleteTeamEventType( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("eventTypeId", ParseIntPipe) eventTypeId: number - ): Promise { - const eventType = await this.organizationsEventTypesService.deleteTeamEventType(teamId, eventTypeId); - - return { - status: SUCCESS_STATUS, - data: { - id: eventTypeId, - title: eventType.title, - }, - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/event-types/organizations-event-types.repository.ts b/apps/api/v2/src/modules/organizations/event-types/organizations-event-types.repository.ts deleted file mode 100644 index 93f6cfd610937b..00000000000000 --- a/apps/api/v2/src/modules/organizations/event-types/organizations-event-types.repository.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { Injectable } from "@nestjs/common"; - -import type { SortOrderType } from "@calcom/platform-types"; - -@Injectable() -export class OrganizationsEventTypesRepository { - constructor(private readonly dbRead: PrismaReadService) {} - async getOrganizationTeamsEventTypes( - orgId: number, - skip: number, - take: number, - sortCreatedAt?: SortOrderType - ) { - return this.dbRead.prisma.eventType.findMany({ - where: { - team: { - parentId: orgId, - }, - }, - ...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }), - skip, - take, - include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, - }); - } -} diff --git a/apps/api/v2/src/modules/organizations/event-types/organizations-member-team-admin-event-types.e2e-spec.ts b/apps/api/v2/src/modules/organizations/event-types/organizations-member-team-admin-event-types.e2e-spec.ts deleted file mode 100644 index 816a37efe9bd27..00000000000000 --- a/apps/api/v2/src/modules/organizations/event-types/organizations-member-team-admin-event-types.e2e-spec.ts +++ /dev/null @@ -1,1401 +0,0 @@ -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, - NoticeThresholdUnitEnum, -} from "@calcom/platform-enums"; -import { SchedulingType } from "@calcom/platform-libraries"; -import type { - ApiSuccessResponse, - CreateTeamEventTypeInput_2024_06_14, - EventTypeOutput_2024_06_14, - Host, - TeamEventTypeOutput_2024_06_14, - UpdateTeamEventTypeInput_2024_06_14, -} 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 { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { 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"; - -describe("Organizations Event Types Endpoints", () => { - describe("User Authentication - User is Org member and team admin", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - - let org: Team; - let team: Team; - let falseTestOrg: Team; - let falseTestTeam: Team; - - const userEmail = `organizations-event-types-admin-${randomString()}@api.com`; - let userAdmin: User; - - const teammate1Email = `organizations-event-types-teammate1-${randomString()}@api.com`; - const teammate2Email = `organizations-event-types-teammate2-${randomString()}@api.com`; - const falseTestUserEmail = `organizations-event-types-false-user-${randomString()}@api.com`; - let teammate1: User; - let teammate2: User; - let falseTestUser: User; - - let collectiveEventType: TeamEventTypeOutput_2024_06_14; - let managedEventType: TeamEventTypeOutput_2024_06_14; - - let managedEventTypeSlug: string; - - beforeAll(async () => { - managedEventTypeSlug = `organizations-event-types-managed-${randomString()}`; - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - - userAdmin = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - role: "ADMIN", - }); - - teammate1 = await userRepositoryFixture.create({ - email: teammate1Email, - username: teammate1Email, - name: "alice", - }); - - teammate2 = await userRepositoryFixture.create({ - email: teammate2Email, - username: teammate2Email, - name: "bob", - }); - - falseTestUser = await userRepositoryFixture.create({ - email: falseTestUserEmail, - username: falseTestUserEmail, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-event-types-organization-${randomString()}`, - isOrganization: true, - slug: `organizations-event-types-organization-${randomString()}`, - }); - - falseTestOrg = await organizationsRepositoryFixture.create({ - name: `organizations-event-types-false-org-${randomString()}`, - isOrganization: true, - }); - - team = await teamsRepositoryFixture.create({ - name: `organizations-event-types-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - falseTestTeam = await teamsRepositoryFixture.create({ - name: `organizations-event-types-false-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: falseTestOrg.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${userAdmin.id}`, - username: userEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: userAdmin.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teammate1.id}`, - username: teammate1Email, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: teammate1.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teammate2.id}`, - username: teammate2Email, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: teammate2.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: userAdmin.id } }, - team: { connect: { id: org.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: userAdmin.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammate1.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammate2.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: falseTestUser.id } }, - team: { connect: { id: falseTestTeam.id } }, - accepted: true, - }); - - await eventTypesRepositoryFixture.deleteAllUserEventTypes(teammate1.id); - await eventTypesRepositoryFixture.deleteAllUserEventTypes(teammate2.id); - await eventTypesRepositoryFixture.deleteAllTeamEventTypes(team.id); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(userAdmin).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should not be able to create event-type for team outside org", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation", - slug: "coding-consultation", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - { - type: "organizersDefaultApp", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teammate1.id, - mandatory: true, - priority: "high", - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types`) - .send(body) - .expect(403); - }); - - it("should not be able to create event-type for user outside org", async () => { - const userId = falseTestUser.id; - - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation", - slug: "coding-consultation", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId, - mandatory: true, - priority: "high", - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(404); - }); - - it("should create a collective team event-type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - successRedirectUrl: "https://masterchief.com/argentina/flan/video/1234", - title: "Coding consultation collective", - slug: `organizations-event-types-collective-${randomString()}`, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - bookingFields: [ - { - type: "select", - label: "select which language is your codebase in", - slug: "select-language", - required: true, - placeholder: "select language", - options: ["javascript", "python", "cobol"], - }, - ], - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - schedulingType: "collective", - hosts: [ - { - userId: teammate1.id, - }, - { - userId: teammate2.id, - }, - ], - bookingLimitsCount: { - day: 2, - week: 5, - }, - onlyShowFirstAvailableSlot: true, - bookingLimitsDuration: { - day: 60, - week: 100, - }, - offsetStart: 30, - bookingWindow: { - type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays, - value: 30, - rolling: true, - }, - bookerLayouts: { - enabledLayouts: [ - BookerLayoutsInputEnum_2024_06_14.column, - BookerLayoutsInputEnum_2024_06_14.month, - BookerLayoutsInputEnum_2024_06_14.week, - ], - defaultLayout: BookerLayoutsInputEnum_2024_06_14.month, - }, - - confirmationPolicy: { - type: ConfirmationPolicyEnum.TIME, - noticeThreshold: { - count: 60, - unit: NoticeThresholdUnitEnum.MINUTES, - }, - blockUnconfirmedBookingsInBooker: true, - }, - requiresBookerEmailVerification: true, - hideCalendarNotes: true, - hideCalendarEventDetails: true, - hideOrganizerEmail: true, - lockTimeZoneToggleOnBookingPage: true, - color: { - darkThemeHex: "#292929", - lightThemeHex: "#fafafa", - }, - emailSettings: { - disableEmailsToAttendees: true, - disableEmailsToHosts: true, - }, - rescheduleWithSameRoundRobinHost: true, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(body.title); - expect(data.hosts.length).toEqual(2); - expect(data.schedulingType).toEqual("collective"); - evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]); - evaluateHost(body.hosts?.[1] || { userId: -1 }, data.hosts[1]); - expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount); - expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot); - expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration); - expect(data.offsetStart).toEqual(body.offsetStart); - expect(data.bookingWindow).toEqual(body.bookingWindow); - expect(data.bookerLayouts).toEqual(body.bookerLayouts); - expect(data.confirmationPolicy).toEqual(body.confirmationPolicy); - expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification); - expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes); - expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails); - expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail); - expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage); - expect(data.color).toEqual(body.color); - expect(data.rrHostSubsetEnabled).toEqual(false); - expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234"); - expect(data.emailSettings).toEqual(body.emailSettings); - collectiveEventType = responseBody.data; - }); - }); - - it("should create a managed team event-type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation managed", - slug: managedEventTypeSlug, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "MANAGED", - hosts: [ - { - userId: teammate1.id, - mandatory: true, - priority: "high", - }, - { - userId: teammate2.id, - mandatory: false, - priority: "low", - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(3); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - - expect(teammate1EventTypes.length).toEqual(1); - expect(teammate1EventTypes[0].slug).toEqual(managedEventTypeSlug); - expect(teammate2EventTypes.length).toEqual(1); - expect(teammate2EventTypes[0].slug).toEqual(managedEventTypeSlug); - expect(teamEventTypes.filter((eventType) => eventType.schedulingType === "MANAGED").length).toEqual( - 1 - ); - - const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.hosts).toEqual([ - { - userId: teammate1.id, - name: teammate1.name, - username: teammate1.username, - avatarUrl: teammate1.avatarUrl, - }, - { - userId: teammate2.id, - name: teammate2.name, - username: teammate2.username, - avatarUrl: teammate2.avatarUrl, - }, - ]); - - if (!responseTeamEvent) { - throw new Error("Team event not found"); - } - - const responseTeammate1Event = responseBody.data.find((event) => event.ownerId === teammate1.id); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - const responseTeammate2Event = responseBody.data.find((event) => event.ownerId === teammate2.id); - expect(responseTeammate2Event).toBeDefined(); - expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - managedEventType = responseTeamEvent; - }); - }); - - it("managed team event types should be returned when fetching event types of users", async () => { - return request(app.getHttpServer()) - .get(`/v2/event-types?username=${teammate1.username}&orgSlug=${org.slug}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].slug).toEqual(managedEventTypeSlug); - expect(data[0].ownerId).toEqual(teammate1.id); - expect(data[0].id).not.toEqual(managedEventType.id); - }); - }); - - it("managed team event type should be returned when fetching event types of users", async () => { - return request(app.getHttpServer()) - .get( - `/v2/event-types?username=${teammate1.username}&orgSlug=${org.slug}&eventSlug=${managedEventTypeSlug}` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].slug).toEqual(managedEventTypeSlug); - expect(data[0].ownerId).toEqual(teammate1.id); - expect(data[0].id).not.toEqual(managedEventType.id); - }); - }); - - it("should not get an event-type of team outside org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types/${collectiveEventType.id}`) - .expect(403); - }); - - it("should not get a non existing event-type", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/999999`) - .expect(404); - }); - - it("should get a team event-type", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(collectiveEventType.title); - expect(data.hosts.length).toEqual(2); - expect(data.rrHostSubsetEnabled).toEqual(false); - evaluateHost(collectiveEventType.hosts[0], data.hosts[0]); - evaluateHost(collectiveEventType.hosts[1], data.hosts[1]); - - collectiveEventType = responseBody.data; - }); - }); - - it("should not get event-types of team outside org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types`) - .expect(404); - }); - - it("should get team event-types", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(2); - - const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "collective"); - const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "managed"); - - expect(eventTypeCollective?.title).toEqual(collectiveEventType.title); - expect(eventTypeCollective?.hosts.length).toEqual(2); - - expect(eventTypeManaged?.title).toEqual(managedEventType.title); - expect(eventTypeManaged?.hosts.length).toEqual(2); - evaluateHost(collectiveEventType.hosts[0], eventTypeCollective?.hosts[0]); - evaluateHost(collectiveEventType.hosts[1], eventTypeCollective?.hosts[1]); - }); - }); - - it("should not be able to update event-type for incorrect team", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - title: "Clean code consultation", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types/${collectiveEventType.id}`) - .send(body) - .expect(403); - }); - - it("should not be able to update non existing event-type", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - title: "Clean code consultation", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/999999`) - .send(body) - .expect(400); - }); - - it("should update collective event-type", async () => { - const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [ - { - userId: teammate1.id, - }, - ]; - - const body: UpdateTeamEventTypeInput_2024_06_14 = { - hosts: newHosts, - successRedirectUrl: "https://new-url-success.com", - emailSettings: { - disableEmailsToAttendees: false, - disableEmailsToHosts: false, - }, - rescheduleWithSameRoundRobinHost: false, - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`) - .send(body) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const eventType = responseBody.data; - expect(eventType.successRedirectUrl).toEqual("https://new-url-success.com"); - expect(eventType.title).toEqual(collectiveEventType.title); - expect(eventType.hosts.length).toEqual(1); - expect(eventType.emailSettings).toEqual(body.emailSettings); - expect(eventType.rescheduleWithSameRoundRobinHost).toEqual(body.rescheduleWithSameRoundRobinHost); - evaluateHost(eventType.hosts[0], newHosts[0]); - }); - }); - - it("should update managed event-type", async () => { - const newTitle = "Coding consultation managed updated"; - const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [ - { - userId: teammate1.id, - mandatory: true, - priority: "medium", - }, - ]; - - const body: UpdateTeamEventTypeInput_2024_06_14 = { - title: newTitle, - hosts: newHosts, - successRedirectUrl: "https://new-url-success-managed.com", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${managedEventType.id}`) - .send(body) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(2); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - const managedTeamEventTypes = teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" - ); - - expect(teammate1EventTypes.length).toEqual(1); - expect(teammate1EventTypes[0].title).toEqual(newTitle); - expect(teammate2EventTypes.length).toEqual(0); - expect(managedTeamEventTypes.length).toEqual(1); - expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(false); - expect( - teamEventTypes.filter((eventType) => eventType.schedulingType === "MANAGED")?.[0]?.title - ).toEqual(newTitle); - - const responseTeamEvent = responseBody.data.find( - (eventType) => eventType.schedulingType === "managed" - ); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.title).toEqual(newTitle); - expect(responseTeamEvent?.assignAllTeamMembers).toEqual(false); - - const responseTeammate1Event = responseBody.data.find( - (eventType) => eventType.ownerId === teammate1.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.title).toEqual(newTitle); - - managedEventType = responseBody.data[0]; - expect(managedEventType.successRedirectUrl).toEqual("https://new-url-success-managed.com"); - }); - }); - - it("should be able to create phone-only event type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Phone coding consultation", - slug: "phone-coding-consultation", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - { - type: "organizersDefaultApp", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teammate1.id, - mandatory: true, - priority: "high", - }, - ], - bookingFields: [ - { - type: "email", - required: false, - label: "Email", - hidden: true, - }, - { - type: "phone", - slug: "attendeePhoneNumber", - required: true, - label: "Phone number", - hidden: false, - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data.bookingFields).toEqual([ - { - isDefault: true, - type: "name", - slug: "name", - label: "your_name", - required: true, - disableOnPrefill: false, - }, - { - isDefault: true, - type: "email", - slug: "email", - required: false, - label: "Email", - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - type: "radioInput", - slug: "location", - required: false, - hidden: false, - }, - { - isDefault: true, - type: "phone", - slug: "attendeePhoneNumber", - required: true, - hidden: false, - label: "Phone number", - disableOnPrefill: false, - }, - { - isDefault: true, - type: "text", - slug: "title", - required: true, - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - type: "textarea", - slug: "notes", - required: false, - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - type: "multiemail", - slug: "guests", - required: false, - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - type: "textarea", - slug: "rescheduleReason", - required: false, - disableOnPrefill: false, - hidden: false, - }, - ]); - }); - }); - - it("should be able to configure phone-only event type", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - bookingFields: [ - { - type: "email", - required: false, - label: "Email", - hidden: true, - }, - { - type: "phone", - slug: "attendeePhoneNumber", - required: true, - label: "Phone number", - hidden: false, - }, - ], - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`) - .send(body) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data.bookingFields).toEqual([ - { - isDefault: true, - type: "name", - slug: "name", - required: true, - label: "your_name", - disableOnPrefill: false, - }, - { - isDefault: true, - type: "email", - slug: "email", - required: false, - label: "Email", - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - type: "radioInput", - slug: "location", - required: false, - hidden: false, - }, - { - isDefault: true, - type: "phone", - slug: "attendeePhoneNumber", - required: true, - hidden: false, - label: "Phone number", - disableOnPrefill: false, - }, - { - isDefault: true, - type: "text", - slug: "title", - required: true, - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - type: "textarea", - slug: "notes", - required: false, - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - type: "multiemail", - slug: "guests", - required: false, - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - type: "textarea", - slug: "rescheduleReason", - required: false, - disableOnPrefill: false, - hidden: false, - }, - ]); - }); - }); - - it("should assign all members to managed event-type", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - assignAllTeamMembers: true, - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${managedEventType.id}`) - .send(body) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(4); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - const managedTeamEventTypes = teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" - ); - - expect(teammate1EventTypes.length).toEqual(1); - expect(teammate2EventTypes.length).toEqual(1); - expect(managedTeamEventTypes.length).toEqual(1); - expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true); - - const responseTeamEvent = responseBody.data.find( - (eventType) => eventType.schedulingType === "managed" - ); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.teamId).toEqual(team.id); - expect(responseTeamEvent?.assignAllTeamMembers).toEqual(true); - - const responseTeammate1Event = responseBody.data.find( - (eventType) => eventType.ownerId === teammate1.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - const responseTeammate2Event = responseBody.data.find( - (eventType) => eventType.ownerId === teammate2.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - if (responseTeamEvent) { - managedEventType = responseTeamEvent; - } - }); - }); - - it("should not delete event-type of team outside org", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types/${collectiveEventType.id}`) - .expect(403); - }); - - it("should delete event-type not part of the team", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${team.id}/event-types/99999`) - .expect(404); - }); - - it("should delete collective event-type", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`) - .expect(200); - }); - - it("should delete managed event-type", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${managedEventType.id}`) - .expect(200); - }); - - it("should return event type with default bookingFields if they are not defined", async () => { - const eventTypeInput = { - title: "unknown field event type two", - slug: `organizations-event-types-unknown-${randomString()}`, - description: "unknown field event type description two", - length: 40, - hidden: false, - locations: [], - schedulingType: SchedulingType.ROUND_ROBIN, - }; - const eventType = await eventTypesRepositoryFixture.createTeamEventType({ - ...eventTypeInput, - team: { connect: { id: team.id } }, - }); - - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}`) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - const fetchedEventType = responseBody.data; - - expect(fetchedEventType.bookingFields).toEqual([ - { isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false }, - { - isDefault: true, - required: true, - slug: "email", - type: "email", - disableOnPrefill: false, - hidden: false, - }, - { - disableOnPrefill: false, - isDefault: true, - type: "phone", - slug: "attendeePhoneNumber", - required: false, - hidden: true, - }, - { - isDefault: true, - type: "radioInput", - slug: "location", - required: false, - hidden: false, - }, - { - isDefault: true, - required: true, - slug: "title", - type: "text", - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - required: false, - slug: "notes", - type: "textarea", - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - required: false, - slug: "guests", - type: "multiemail", - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - required: false, - slug: "rescheduleReason", - type: "textarea", - disableOnPrefill: false, - hidden: false, - }, - ]); - }); - }); - - it("should create a round robin team event-type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - successRedirectUrl: "https://masterchief.com/argentina/flan/video/1234", - title: "Coding consultation round robin", - slug: `organizations-event-types-round-robin-${randomString()}`, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - bookingFields: [ - { - type: "select", - label: "select which language is your codebase in", - slug: "select-language", - required: true, - placeholder: "select language", - options: ["javascript", "python", "cobol"], - }, - ], - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - schedulingType: "roundRobin", - hosts: [ - { - userId: teammate1.id, - mandatory: true, - priority: "high", - }, - { - userId: teammate2.id, - mandatory: false, - priority: "medium", - }, - ], - bookingLimitsCount: { - day: 2, - week: 5, - }, - onlyShowFirstAvailableSlot: true, - bookingLimitsDuration: { - day: 60, - week: 100, - }, - offsetStart: 30, - bookingWindow: { - type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays, - value: 30, - rolling: true, - }, - bookerLayouts: { - enabledLayouts: [ - BookerLayoutsInputEnum_2024_06_14.column, - BookerLayoutsInputEnum_2024_06_14.month, - BookerLayoutsInputEnum_2024_06_14.week, - ], - defaultLayout: BookerLayoutsInputEnum_2024_06_14.month, - }, - - confirmationPolicy: { - type: ConfirmationPolicyEnum.TIME, - noticeThreshold: { - count: 60, - unit: NoticeThresholdUnitEnum.MINUTES, - }, - blockUnconfirmedBookingsInBooker: true, - }, - requiresBookerEmailVerification: true, - hideCalendarNotes: true, - hideCalendarEventDetails: true, - hideOrganizerEmail: true, - lockTimeZoneToggleOnBookingPage: true, - rrHostSubsetEnabled: true, - color: { - darkThemeHex: "#292929", - lightThemeHex: "#fafafa", - }, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(body.title); - expect(data.hosts.length).toEqual(2); - expect(data.schedulingType).toEqual("roundRobin"); - evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]); - evaluateHost(body.hosts?.[1] || { userId: -1 }, data.hosts[1]); - expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount); - expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot); - expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration); - expect(data.offsetStart).toEqual(body.offsetStart); - expect(data.bookingWindow).toEqual(body.bookingWindow); - expect(data.bookerLayouts).toEqual(body.bookerLayouts); - expect(data.confirmationPolicy).toEqual(body.confirmationPolicy); - expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification); - expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes); - expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails); - expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail); - expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage); - expect(data.color).toEqual(body.color); - expect(data.rrHostSubsetEnabled).toEqual(true); - expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234"); - collectiveEventType = responseBody.data; - }); - }); - - it("should create a managed team event-type without hosts", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation managed without hosts", - slug: "coding-consultation-managed-without-hosts", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "MANAGED", - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(1); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - - const teammate1HasThisEvent = teammate1EventTypes.some((eventType) => eventType.slug === body.slug); - const teammate2HasThisEvent = teammate2EventTypes.some((eventType) => eventType.slug === body.slug); - expect(teammate1HasThisEvent).toBe(false); - expect(teammate2HasThisEvent).toBe(false); - expect( - teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" && eventType.slug === body.slug - ).length - ).toEqual(1); - - const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.hosts).toEqual([]); - - if (!responseTeamEvent) { - throw new Error("Team event not found"); - } - }); - }); - - it("should preserve seatsPerTimeSlot when doing partial update", async () => { - const createBody: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation with seats", - slug: `organizations-event-types-seats-${randomString()}`, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teammate1.id, - }, - ], - seats: { - seatsPerTimeSlot: 5, - showAttendeeInfo: true, - showAvailabilityCount: true, - }, - }; - - const createResponse = await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(createBody) - .expect(201); - - const createdEventType: TeamEventTypeOutput_2024_06_14 = createResponse.body.data; - const createdSeats = createdEventType.seats; - expect(createdSeats).toBeDefined(); - expect(createdSeats && "seatsPerTimeSlot" in createdSeats).toBe(true); - if (createdSeats && "seatsPerTimeSlot" in createdSeats) { - expect(createdSeats.seatsPerTimeSlot).toEqual(5); - expect(createdSeats.showAttendeeInfo).toEqual(true); - expect(createdSeats.showAvailabilityCount).toEqual(true); - } - - // Now do a partial update that only changes a different field (not seats) - const updateBody: UpdateTeamEventTypeInput_2024_06_14 = { - bookingRequiresAuthentication: false, - }; - - const updateResponse = await request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${createdEventType.id}`) - .send(updateBody) - .expect(200); - - const updatedEventType: TeamEventTypeOutput_2024_06_14 = updateResponse.body.data; - - // Verify that seatsPerTimeSlot is preserved and not reset to null - const updatedSeats = updatedEventType.seats; - expect(updatedSeats).toBeDefined(); - expect(updatedSeats && "seatsPerTimeSlot" in updatedSeats).toBe(true); - if (updatedSeats && "seatsPerTimeSlot" in updatedSeats) { - expect(updatedSeats.seatsPerTimeSlot).toEqual(5); - expect(updatedSeats.showAttendeeInfo).toEqual(true); - expect(updatedSeats.showAvailabilityCount).toEqual(true); - } - expect(updatedEventType.bookingRequiresAuthentication).toEqual(false); - }); - - it("should preserve metadata fields when doing partial update", async () => { - const createBody: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation with metadata", - slug: `organizations-event-types-metadata-${randomString()}`, - description: "Our team will review your codebase.", - lengthInMinutes: 30, - lengthInMinutesOptions: [15, 30, 60], - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teammate1.id, - }, - ], - bookerLayouts: { - enabledLayouts: [ - BookerLayoutsInputEnum_2024_06_14.column, - BookerLayoutsInputEnum_2024_06_14.month, - BookerLayoutsInputEnum_2024_06_14.week, - ], - defaultLayout: BookerLayoutsInputEnum_2024_06_14.month, - }, - confirmationPolicy: { - type: ConfirmationPolicyEnum.TIME, - noticeThreshold: { - count: 60, - unit: NoticeThresholdUnitEnum.MINUTES, - }, - blockUnconfirmedBookingsInBooker: true, - }, - }; - - const createResponse = await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(createBody) - .expect(201); - - const createdEventType: TeamEventTypeOutput_2024_06_14 = createResponse.body.data; - - // Verify all metadata fields are set correctly on creation - expect(createdEventType.lengthInMinutesOptions).toBeDefined(); - expect(createdEventType.lengthInMinutesOptions).toEqual([15, 30, 60]); - expect(createdEventType.bookerLayouts).toBeDefined(); - expect(createdEventType.bookerLayouts).toEqual(createBody.bookerLayouts); - expect(createdEventType.confirmationPolicy).toBeDefined(); - expect(createdEventType.confirmationPolicy).toEqual(createBody.confirmationPolicy); - - // Now do a partial update that only changes a different field (not metadata fields) - const updateBody: UpdateTeamEventTypeInput_2024_06_14 = { - bookingRequiresAuthentication: false, - }; - - const updateResponse = await request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${createdEventType.id}`) - .send(updateBody) - .expect(200); - - const updatedEventType: TeamEventTypeOutput_2024_06_14 = updateResponse.body.data; - - // Verify that all metadata fields are preserved and not reset to undefined - expect(updatedEventType.lengthInMinutesOptions).toBeDefined(); - expect(updatedEventType.lengthInMinutesOptions).toEqual([15, 30, 60]); - expect(updatedEventType.bookerLayouts).toBeDefined(); - expect(updatedEventType.bookerLayouts).toEqual(createBody.bookerLayouts); - expect(updatedEventType.confirmationPolicy).toBeDefined(); - expect(updatedEventType.confirmationPolicy).toEqual(createBody.confirmationPolicy); - expect(updatedEventType.bookingRequiresAuthentication).toEqual(false); - }); - - function evaluateHost(expected: Host, received: Host | undefined) { - expect(expected.userId).toEqual(received?.userId); - expect(expected.mandatory).toEqual(received?.mandatory); - expect(expected.priority).toEqual(received?.priority); - } - - afterAll(async () => { - await eventTypesRepositoryFixture.deleteAllUserEventTypes(teammate1.id); - await eventTypesRepositoryFixture.deleteAllUserEventTypes(teammate2.id); - await eventTypesRepositoryFixture.deleteAllTeamEventTypes(team.id); - await eventTypesRepositoryFixture.deleteAllTeamEventTypes(falseTestTeam.id); - await userRepositoryFixture.deleteByEmail(userAdmin.email); - await userRepositoryFixture.deleteByEmail(teammate1.email); - await userRepositoryFixture.deleteByEmail(teammate2.email); - await userRepositoryFixture.deleteByEmail(falseTestUser.email); - await teamsRepositoryFixture.delete(team.id); - await teamsRepositoryFixture.delete(falseTestTeam.id); - await organizationsRepositoryFixture.delete(org.id); - await organizationsRepositoryFixture.delete(falseTestOrg.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/event-types/services/input.service.ts b/apps/api/v2/src/modules/organizations/event-types/services/input.service.ts deleted file mode 100644 index 64c429c4ef3e0f..00000000000000 --- a/apps/api/v2/src/modules/organizations/event-types/services/input.service.ts +++ /dev/null @@ -1,488 +0,0 @@ -import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service"; -import { transformTeamLocationsApiToInternal } from "@/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/locations"; -import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository"; -import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service"; -import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; -import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; -import { UsersRepository } from "@/modules/users/users.repository"; -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; - -import { SchedulingType } from "@calcom/platform-libraries"; -import { slugifyLenient } from "@calcom/platform-libraries"; -import { EventTypeMetadata } from "@calcom/platform-libraries/event-types"; -import { - CreateTeamEventTypeInput_2024_06_14, - UpdateTeamEventTypeInput_2024_06_14, - HostPriority, - EmailSettings_2024_06_14, -} from "@calcom/platform-types"; -import type { EventType } from "@calcom/prisma/client"; - -export const HOSTS_REQUIRED_WHEN_SWITCHING_SCHEDULING_TYPE_ERROR = - "Hosts required when switching schedulingType. Please provide 'hosts' or set 'assignAllTeamMembers: true' to specify how hosts should be configured for the new scheduling type."; - -export type TransformedCreateTeamEventTypeInput = Awaited< - ReturnType["transformInputCreateTeamEventType"]> ->; - -export type TransformedUpdateTeamEventTypeInput = Awaited< - ReturnType["transformInputUpdateTeamEventType"]> ->; -@Injectable() -export class InputOrganizationsEventTypesService { - constructor( - private readonly inputEventTypesService: InputEventTypesService_2024_06_14, - private readonly teamsRepository: TeamsRepository, - private readonly usersRepository: UsersRepository, - private readonly teamsEventTypesRepository: TeamsEventTypesRepository, - private readonly conferencingService: OrganizationsConferencingService, - private readonly conferencingRepository: ConferencingRepository - ) {} - async transformAndValidateCreateTeamEventTypeInput( - userId: number, - teamId: number, - inputEventType: CreateTeamEventTypeInput_2024_06_14 - ) { - const slugifiedInputEventType = { ...inputEventType, slug: slugifyLenient(inputEventType.slug) }; - - await this.validateInputLocations(teamId, inputEventType.locations); - await this.validateHosts(teamId, inputEventType.hosts); - await this.validateTeamEventTypeSlug(teamId, slugifiedInputEventType.slug); - - const transformedBody = await this.transformInputCreateTeamEventType(teamId, slugifiedInputEventType); - - await this.inputEventTypesService.validateEventTypeInputs({ - seatsPerTimeSlot: transformedBody.seatsPerTimeSlot, - locations: transformedBody.locations, - requiresConfirmation: transformedBody.requiresConfirmation, - eventName: transformedBody.eventName, - }); - - if (transformedBody.destinationCalendar) { - await this.inputEventTypesService.validateInputDestinationCalendar( - userId, - transformedBody.destinationCalendar - ); - } - - if (transformedBody.useEventTypeDestinationCalendarEmail) { - await this.inputEventTypesService.validateInputUseDestinationCalendarEmail(userId); - } - - return transformedBody; - } - - async transformAndValidateUpdateTeamEventTypeInput( - userId: number, - eventTypeId: number, - teamId: number, - inputEventType: UpdateTeamEventTypeInput_2024_06_14 - ) { - const slugifiedInputEventType = inputEventType.slug - ? { ...inputEventType, slug: slugifyLenient(inputEventType.slug) } - : inputEventType; - - await this.validateInputLocations(teamId, inputEventType.locations); - await this.validateHosts(teamId, inputEventType.hosts); - if (slugifiedInputEventType.slug) { - await this.validateTeamEventTypeSlug(teamId, slugifiedInputEventType.slug); - } - - const transformedBody = await this.transformInputUpdateTeamEventType( - eventTypeId, - teamId, - slugifiedInputEventType - ); - - await this.inputEventTypesService.validateEventTypeInputs({ - eventTypeId: eventTypeId, - seatsPerTimeSlot: transformedBody.seatsPerTimeSlot, - locations: transformedBody.locations, - requiresConfirmation: transformedBody.requiresConfirmation, - eventName: transformedBody.eventName, - }); - - if (transformedBody.destinationCalendar) { - await this.inputEventTypesService.validateInputDestinationCalendar( - userId, - transformedBody.destinationCalendar - ); - } - - if (transformedBody.useEventTypeDestinationCalendarEmail) { - await this.inputEventTypesService.validateInputUseDestinationCalendarEmail(userId); - } - - return transformedBody; - } - - async validateTeamEventTypeSlug(teamId: number, slug: string) { - const teamEventWithSlugExists = await this.teamsEventTypesRepository.getEventTypeByTeamIdAndSlug( - teamId, - slug - ); - - if (teamEventWithSlugExists) { - throw new BadRequestException("Team event type with this slug already exists"); - } - } - - async transformInputCreateTeamEventType( - teamId: number, - inputEventType: CreateTeamEventTypeInput_2024_06_14 - ) { - const { assignAllTeamMembers, locations, emailSettings, ...rest } = inputEventType; - - const eventType = this.inputEventTypesService.transformInputCreateEventType(rest); - - const isManagedEventType = rest.schedulingType === "MANAGED"; - - const defaultLocations: CreateTeamEventTypeInput_2024_06_14["locations"] = [ - { - type: "integration", - integration: "cal-video", - }, - ]; - - const children = isManagedEventType - ? await this.getChildEventTypesForManagedEventTypeCreate(inputEventType, teamId) - : undefined; - - let metadata = isManagedEventType - ? { managedEventConfig: {}, ...eventType.metadata } - : eventType.metadata; - - if (emailSettings) { - metadata = this.addEmailSettingsToMetadata(emailSettings, metadata); - } - - const teamEventType = { - ...eventType, - hosts: await this.transformInputCreateTeamEventTypeHosts(teamId, inputEventType), - assignAllTeamMembers, - locations: this.transformInputTeamLocations(locations || defaultLocations), - metadata, - children, - }; - - return teamEventType; - } - - private async transformInputCreateTeamEventTypeHosts( - teamId: number, - inputEventType: CreateTeamEventTypeInput_2024_06_14 - ) { - const { hosts, assignAllTeamMembers, schedulingType } = inputEventType; - - // note(Lauris): we don't populate hosts for managed event-types because they are handled by the children - each child managed event type is associated with - // a specific user and hosts property is only for team event types e.g round robin and collective. - if (schedulingType === "MANAGED") { - return undefined; - } - - if (assignAllTeamMembers) { - return await this.getAllTeamMembers(teamId, inputEventType.schedulingType); - } - return this.transformInputHosts(hosts, inputEventType.schedulingType); - } - - private addEmailSettingsToMetadata( - emailSettings: EmailSettings_2024_06_14, - metadata: NonNullable - ) { - if ( - emailSettings?.disableEmailsToAttendees === undefined && - emailSettings?.disableEmailsToHosts === undefined - ) { - return metadata; - } - - const clonedMetadata = structuredClone(metadata); - - if (!clonedMetadata.disableStandardEmails) { - clonedMetadata.disableStandardEmails = {}; - } - if (!clonedMetadata.disableStandardEmails.all) { - clonedMetadata.disableStandardEmails.all = {}; - } - - if (emailSettings?.disableEmailsToAttendees !== undefined) { - clonedMetadata.disableStandardEmails.all.attendee = emailSettings.disableEmailsToAttendees; - } - if (emailSettings?.disableEmailsToHosts !== undefined) { - clonedMetadata.disableStandardEmails.all.host = emailSettings.disableEmailsToHosts; - } - - return clonedMetadata; - } - - async transformInputUpdateTeamEventType( - eventTypeId: number, - teamId: number, - inputEventType: UpdateTeamEventTypeInput_2024_06_14 - ) { - const { assignAllTeamMembers, locations, emailSettings, ...rest } = inputEventType; - - const eventType = await this.inputEventTypesService.transformInputUpdateEventType(rest, eventTypeId); - const dbEventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId); - - if (!dbEventType) { - throw new BadRequestException("Event type to update not found"); - } - - const children = - dbEventType.schedulingType === "MANAGED" - ? await this.getChildEventTypesForManagedEventTypeUpdate(eventTypeId, inputEventType, teamId) - : undefined; - - let metadata = eventType.metadata; - - if (emailSettings) { - metadata = this.addEmailSettingsToMetadata(emailSettings, metadata); - } - - const teamEventType = { - ...eventType, - // note(Lauris): we don't populate hosts for managed event-types because they are handled by the children - hosts: await this.transformInputUpdateTeamEventTypeHosts( - teamId, - dbEventType.schedulingType, - inputEventType - ), - assignAllTeamMembers, - children, - locations: locations ? this.transformInputTeamLocations(locations) : undefined, - metadata, - }; - - return teamEventType; - } - - private async transformInputUpdateTeamEventTypeHosts( - teamId: number, - dbEventTypeSchedulingType: EventType["schedulingType"], - inputEventType: UpdateTeamEventTypeInput_2024_06_14 - ) { - const { hosts, assignAllTeamMembers } = inputEventType; - - if (dbEventTypeSchedulingType === "MANAGED") { - // note(Lauris): we don't populate hosts for managed event-types because they are handled by the event type children - return undefined; - } - - const isSchedulingTypeChanging = - inputEventType.schedulingType && inputEventType.schedulingType !== dbEventTypeSchedulingType; - - if (isSchedulingTypeChanging && !assignAllTeamMembers && !hosts) { - throw new BadRequestException(HOSTS_REQUIRED_WHEN_SWITCHING_SCHEDULING_TYPE_ERROR); - } - - const nextSchedulingType = inputEventType.schedulingType || dbEventTypeSchedulingType; - if (assignAllTeamMembers) { - return await this.getAllTeamMembers(teamId, nextSchedulingType); - } - - return this.transformInputHosts(hosts, nextSchedulingType); - } - - async getChildEventTypesForManagedEventTypeUpdate( - eventTypeId: number, - inputEventType: UpdateTeamEventTypeInput_2024_06_14, - teamId: number - ) { - const eventType = await this.teamsEventTypesRepository.getEventTypeByIdWithChildren(eventTypeId); - if (!eventType || eventType.schedulingType !== "MANAGED") { - return undefined; - } - - const ownersIds = await this.getOwnersIdsForManagedEventTypeUpdate(teamId, inputEventType, eventType); - const owners = await this.getOwnersForManagedEventType(ownersIds); - - return owners.map((owner) => { - return { - hidden: false, - owner, - }; - }); - } - - async getOwnersIdsForManagedEventTypeUpdate( - teamId: number, - inputEventType: UpdateTeamEventTypeInput_2024_06_14, - eventType: { children: { userId: number | null }[] } - ) { - if (inputEventType.assignAllTeamMembers) { - return await this.getTeamUsersIds(teamId); - } - - if (inputEventType.hosts) { - await this.validateHosts(teamId, inputEventType.hosts); - return inputEventType.hosts.map((host) => host.userId); - } - - // note(Lauris): when API user DOES NOT update managed event type users, but we still need existing managed event type users to know which event-types to update - // e.g if managed event type title is changed then all children managed event types should be updated as well. - const childrenOwnersIds: number[] = []; - for (const child of eventType.children) { - if (child.userId) { - childrenOwnersIds.push(child.userId); - } - } - return childrenOwnersIds; - } - - async getChildEventTypesForManagedEventTypeCreate( - inputEventType: Pick, - teamId: number - ) { - const ownersIds = await this.getOwnersIdsForManagedEventTypeCreate(teamId, inputEventType); - const owners = await this.getOwnersForManagedEventType(ownersIds); - - return owners.map((owner) => { - return { - hidden: false, - owner, - }; - }); - } - - async getOwnersIdsForManagedEventTypeCreate( - teamId: number, - inputEventType: Pick - ) { - if (inputEventType.assignAllTeamMembers) { - return await this.getTeamUsersIds(teamId); - } - - if (inputEventType.hosts) { - await this.validateHosts(teamId, inputEventType.hosts); - return inputEventType.hosts.map((host) => host.userId); - } - - return []; - } - - async getTeamUsersIds(teamId: number) { - const team = await this.teamsRepository.getById(teamId); - const isPlatformTeam = !!team?.createdByOAuthClientId; - if (isPlatformTeam) { - // note(Lauris): platform team creators have role "OWNER" but we don't want to assign them to team members marked as "assignAllTeamMembers: true" - // because they are not a managed user. - return await this.teamsRepository.getTeamManagedUsersIds(teamId); - } - return await this.teamsRepository.getTeamUsersIds(teamId); - } - - transformInputTeamLocations(inputLocations: CreateTeamEventTypeInput_2024_06_14["locations"]) { - return transformTeamLocationsApiToInternal(inputLocations); - } - - async getOwnersForManagedEventType(userIds: number[]) { - const users = userIds.length ? await this.usersRepository.findByIdsWithEventTypes(userIds) : []; - - return users.map((user) => { - const nonManagedEventTypes = user.eventTypes.filter((eventType) => !eventType.parentId); - return { - id: user.id, - name: user.name || user.email, - email: user.email, - // note(Lauris): managed event types slugs have to be excluded otherwise checkExistentEventTypes within handleChildrenEventTypes.ts will incorrectly delete managed user event type. - eventTypeSlugs: nonManagedEventTypes.map((eventType) => eventType.slug), - }; - }); - } - - async getAllTeamMembers(teamId: number, schedulingType: SchedulingType | null) { - const membersIds = await this.getTeamUsersIds(teamId); - const isFixed = schedulingType === "COLLECTIVE" ? true : false; - - return membersIds.map((id) => ({ - userId: id, - isFixed, - priority: 2, - })); - } - - transformInputHosts( - inputHosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined, - schedulingType: SchedulingType | null - ) { - if (!inputHosts) { - return undefined; - } - - const defaultPriority = "medium"; - const defaultIsFixed = false; - - return inputHosts.map((host) => ({ - userId: host.userId, - isFixed: schedulingType === "COLLECTIVE" ? true : host.mandatory || defaultIsFixed, - priority: getPriorityValue( - schedulingType === "COLLECTIVE" ? "medium" : host.priority || defaultPriority - ), - })); - } - - async validateHosts(teamId: number, hosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined) { - if (hosts && hosts.length) { - const membersIds = await this.getTeamUsersIds(teamId); - const invalidHosts = hosts.filter((host) => !membersIds.includes(host.userId)); - if (invalidHosts.length) { - throw new NotFoundException( - `Invalid hosts: ${invalidHosts - .map((host) => host.userId) - .join(", ")} are not members of team with id ${teamId}.` - ); - } - } - } - - async validateInputLocations( - teamId: number, - inputLocations?: CreateTeamEventTypeInput_2024_06_14["locations"] - ) { - await Promise.all( - inputLocations?.map(async (location) => { - if (location.type === "integration") { - // cal-video is global, so we can skip this check - if (location.integration !== "cal-video") { - await this.conferencingService.checkAppIsValidAndConnected(teamId, location.integration); - } - } - }) ?? [] - ); - } - - async checkAppIsValidAndConnected(teamId: number, app: string) { - const conferencingApps = ["google-meet", "office365-video", "zoom"]; - if (!conferencingApps.includes(app)) { - throw new BadRequestException("Invalid app, available apps are: ", conferencingApps.join(", ")); - } - if (app === "office365-video") { - app = "msteams"; - } - const credential = await this.conferencingRepository.findTeamConferencingApp(teamId, app); - - if (!credential) { - throw new BadRequestException(`${app} not connected.`); - } - return credential; - } -} - -function getPriorityValue(priority: keyof typeof HostPriority): number { - switch (priority) { - case "lowest": - return 0; - case "low": - return 1; - case "medium": - return 2; - case "high": - return 3; - case "highest": - return 4; - default: - throw new Error("Invalid HostPriority label"); - } -} diff --git a/apps/api/v2/src/modules/organizations/event-types/services/organizations-event-types.service.ts b/apps/api/v2/src/modules/organizations/event-types/services/organizations-event-types.service.ts deleted file mode 100644 index 10267b55e3beaf..00000000000000 --- a/apps/api/v2/src/modules/organizations/event-types/services/organizations-event-types.service.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; - -import type { SortOrderType } from "@calcom/platform-types"; -import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository"; -import { - TransformedCreateTeamEventTypeInput, - TransformedUpdateTeamEventTypeInput, -} from "@/modules/organizations/event-types/services/input.service"; -import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; -import { UsersService } from "@/modules/users/services/users.service"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable, Logger } from "@nestjs/common"; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -import { createEventType } from "@calcom/platform-libraries/event-types"; - -@Injectable() -export class OrganizationsEventTypesService { - private readonly logger = new Logger("OrganizationsEventTypesService"); - - constructor( - private readonly dbWrite: PrismaWriteService, - private readonly organizationEventTypesRepository: OrganizationsEventTypesRepository, - private readonly teamsEventTypesService: TeamsEventTypesService, - private readonly membershipsRepository: MembershipsRepository, - private readonly usersService: UsersService - ) {} - - async createOrganizationTeamEventType( - user: UserWithProfile, - teamId: number, - orgId: number, - body: TransformedCreateTeamEventTypeInput - ): Promise { - const eventTypeUser = await this.getUserToCreateTeamEvent(user, orgId); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { hosts, children, destinationCalendar, ...rest } = body; - const { eventType: eventTypeCreated } = await createEventType({ - input: { - teamId: teamId, - ...rest, - }, - ctx: { - user: eventTypeUser, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - prisma: this.dbWrite.prisma, - }, - }); - this.logger.debug( - "nl debug - create org team event type - eventTypeCreated", - JSON.stringify(eventTypeCreated, null, 2) - ); - - return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user, true); - } - - async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) { - const isOrgAdmin = await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId); - const profileId = - this.usersService.getUserProfileByOrgId(user, organizationId)?.id || - this.usersService.getUserMainProfile(user)?.id; - return { - id: user.id, - role: user.role, - organizationId: user.organizationId, - organization: { isOrgAdmin }, - profile: { id: profileId || null }, - metadata: user.metadata, - email: user.email, - }; - } - - async getTeamEventType(teamId: number, eventTypeId: number): Promise { - return this.teamsEventTypesService.getTeamEventType(teamId, eventTypeId); - } - - async getTeamEventTypeBySlug( - teamId: number, - eventTypeSlug: string, - hostsLimit?: number - ): Promise { - return this.teamsEventTypesService.getTeamEventTypeBySlug(teamId, eventTypeSlug, hostsLimit); - } - - async getTeamEventTypes(teamId: number, sortCreatedAt?: SortOrderType): Promise { - return await this.teamsEventTypesService.getTeamEventTypes(teamId, sortCreatedAt); - } - - async getOrganizationsTeamsEventTypes( - orgId: number, - skip = 0, - take = 250, - sortCreatedAt?: SortOrderType - ): Promise { - return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes( - orgId, - skip, - take, - sortCreatedAt - ); - } - - async updateOrganizationTeamEventType( - eventTypeId: number, - teamId: number, - body: TransformedUpdateTeamEventTypeInput, - user: UserWithProfile - ): Promise { - return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user, true); - } - - async deleteTeamEventType(teamId: number, eventTypeId: number) { - return this.teamsEventTypesService.deleteTeamEventType(teamId, eventTypeId); - } -} diff --git a/apps/api/v2/src/modules/organizations/index/organizations.repository.ts b/apps/api/v2/src/modules/organizations/index/organizations.repository.ts index ba9b0eecde294a..98b2752203e628 100644 --- a/apps/api/v2/src/modules/organizations/index/organizations.repository.ts +++ b/apps/api/v2/src/modules/organizations/index/organizations.repository.ts @@ -1,20 +1,23 @@ -import { PlatformPlan } from "@/modules/billing/types"; +import { Prisma } from "@calcom/prisma/client"; +import { Injectable } from "@nestjs/common"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { StripeService } from "@/modules/stripe/stripe.service"; -import { Injectable } from "@nestjs/common"; - -import { OrganizationRepository } from "@calcom/platform-libraries/organizations"; -import { Prisma } from "@calcom/prisma/client"; @Injectable() -export class OrganizationsRepository extends OrganizationRepository { +export class OrganizationsRepository { constructor( private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService, private readonly stripeService: StripeService - ) { - super({ prismaClient: dbWrite.prisma }); + ) {} + + async findById(args: { id: number }) { + return this.dbRead.prisma.team.findUnique({ + where: { + id: args.id, + }, + }); } async findByIds(organizationIds: number[]) { @@ -28,40 +31,6 @@ export class OrganizationsRepository extends OrganizationRepository { }); } - async findByIdIncludeBilling(orgId: number) { - return this.dbRead.prisma.team.findUnique({ - where: { - id: orgId, - }, - include: { - platformBilling: true, - }, - }); - } - - async createNewBillingRelation(orgId: number, plan?: PlatformPlan) { - const { id } = await this.stripeService.getStripe().customers.create({ - metadata: { - createdBy: "oauth_client_no_csid", // mark in case this is needed in the future. - }, - }); - - await this.dbWrite.prisma.team.update({ - where: { - id: orgId, - }, - data: { - platformBilling: { - create: { - customerId: id, - plan: plan ? plan.toString() : "none", - }, - }, - }, - }); - - return id; - } async findTeamIdAndSlugFromClientId(clientId: string) { return this.dbRead.prisma.team.findFirstOrThrow({ @@ -169,13 +138,4 @@ export class OrganizationsRepository extends OrganizationRepository { }); } - async findTeamByPlatformBillingId(billingId: number) { - return this.dbRead.prisma.team.findFirst({ - where: { - platformBilling: { - id: billingId, - }, - }, - }); - } } diff --git a/apps/api/v2/src/modules/organizations/index/organizations.service.ts b/apps/api/v2/src/modules/organizations/index/organizations.service.ts deleted file mode 100644 index b6e63c49271cbb..00000000000000 --- a/apps/api/v2/src/modules/organizations/index/organizations.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrganizationsService { - constructor(private readonly organizationsRepository: OrganizationsRepository) {} - - async isPlatform(organizationId: number) { - const organization = await this.organizationsRepository.findById({ id: organizationId }); - return organization?.isPlatform; - } -} diff --git a/apps/api/v2/src/modules/organizations/memberships/inputs/create-organization-membership.input.ts b/apps/api/v2/src/modules/organizations/memberships/inputs/create-organization-membership.input.ts deleted file mode 100644 index 9eec79d2431e41..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/inputs/create-organization-membership.input.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsBoolean, IsOptional, IsEnum, IsInt } from "class-validator"; - -import { MembershipRole } from "@calcom/platform-libraries"; - -export class CreateOrgMembershipDto { - @IsInt() - @ApiProperty() - readonly userId!: number; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ type: Boolean, default: false }) - readonly accepted?: boolean = false; - - @IsEnum(MembershipRole) - @ApiProperty({ - enum: ["MEMBER", "OWNER", "ADMIN"], - description: "If you are platform customer then managed users should only have MEMBER role.", - }) - readonly role: MembershipRole = MembershipRole.MEMBER; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ type: Boolean, default: false }) - readonly disableImpersonation?: boolean = false; -} diff --git a/apps/api/v2/src/modules/organizations/memberships/inputs/update-organization-membership.input.ts b/apps/api/v2/src/modules/organizations/memberships/inputs/update-organization-membership.input.ts deleted file mode 100644 index 527a883931f310..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/inputs/update-organization-membership.input.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsBoolean, IsOptional, IsEnum } from "class-validator"; - -import { MembershipRole } from "@calcom/platform-libraries"; - -export class UpdateOrgMembershipDto { - @IsOptional() - @IsBoolean() - @ApiPropertyOptional() - readonly accepted?: boolean; - - @IsOptional() - @IsEnum(MembershipRole) - @ApiPropertyOptional({ enum: ["MEMBER", "OWNER", "ADMIN"] }) - readonly role?: MembershipRole; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional() - readonly disableImpersonation?: boolean; -} diff --git a/apps/api/v2/src/modules/organizations/memberships/organizations-membership.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/memberships/organizations-membership.controller.e2e-spec.ts deleted file mode 100644 index 838764d1866040..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/organizations-membership.controller.e2e-spec.ts +++ /dev/null @@ -1,575 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { Attribute, AttributeOption, Membership, 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 { AttributeRepositoryFixture } from "test/fixtures/repository/attributes.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; -import { 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 { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input"; -import { UpdateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/update-organization-membership.input"; -import { CreateOrgMembershipOutput } from "@/modules/organizations/memberships/outputs/create-membership.output"; -import { DeleteOrgMembership } from "@/modules/organizations/memberships/outputs/delete-membership.output"; -import { GetAllOrgMemberships } from "@/modules/organizations/memberships/outputs/get-all-memberships.output"; -import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output"; -import { OrgUserAttribute } from "@/modules/organizations/memberships/outputs/organization-membership.output"; -import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output"; -import { OrganizationsDelegationCredentialService } from "@/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Memberships Endpoints", () => { - describe("User Authentication - User is Org Admin", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipRepositoryFixture: MembershipRepositoryFixture; - let attributesRepositoryFixture: AttributeRepositoryFixture; - - let org: Team; - let membership: Membership; - let membership2: Membership; - let membershipCreatedViaApi: TeamMembershipOutput; - - const userEmail = `organizations-memberships-admin-${randomString()}@api.com`; - const userEmail2 = `organizations-memberships-member-${randomString()}@api.com`; - const invitedUserEmail = `organizations-memberships-invited-${randomString()}@api.com`; - - let user: User; - let user2: User; - - let userToInviteViaApi: User; - - let textAttribute: Attribute; - let multiSelectAttribute: Attribute; - let numberAttribute: Attribute; - let singleSelectAttribute: Attribute; - - let textAttributeOption: AttributeOption; - let multiSelectAttributeOption: AttributeOption; - let multiSelectAttributeOption2: AttributeOption; - let numberAttributeOption: AttributeOption; - let singleSelectAttributeOption: AttributeOption; - - const metadata = { - some: "key", - }; - const bio = "This is a bio"; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - attributesRepositoryFixture = new AttributeRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - bio, - metadata, - }); - user2 = await userRepositoryFixture.create({ - email: userEmail2, - username: userEmail2, - bio, - metadata, - }); - - userToInviteViaApi = await userRepositoryFixture.create({ - email: invitedUserEmail, - username: invitedUserEmail, - bio, - metadata, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-memberships-organization-${randomString()}`, - isOrganization: true, - }); - - await setupAttributes(); - - membership = await membershipRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - membership2 = await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: user2.id } }, - team: { connect: { id: org.id } }, - AttributeToUser: { - create: [ - { - attributeOption: { connect: { id: textAttributeOption.id } }, - weight: 100, - }, - { - attributeOption: { connect: { id: multiSelectAttributeOption.id } }, - weight: 100, - }, - { - attributeOption: { connect: { id: multiSelectAttributeOption2.id } }, - weight: null, - }, - { - attributeOption: { connect: { id: numberAttributeOption.id } }, - weight: 100, - }, - { - attributeOption: { connect: { id: singleSelectAttributeOption.id } }, - weight: 100, - }, - ], - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - async function setupAttributes(): Promise { - textAttribute = await attributesRepositoryFixture.create({ - team: { connect: { id: org.id } }, - type: "TEXT", - name: "team", - slug: `team-${randomString()}`, - enabled: true, - usersCanEditRelation: false, - isWeightsEnabled: false, - isLocked: false, - }); - - multiSelectAttribute = await attributesRepositoryFixture.create({ - team: { connect: { id: org.id } }, - type: "MULTI_SELECT", - name: "skills", - slug: `skills-${randomString()}`, - enabled: true, - usersCanEditRelation: false, - isWeightsEnabled: false, - isLocked: false, - }); - - numberAttribute = await attributesRepositoryFixture.create({ - team: { connect: { id: org.id } }, - type: "NUMBER", - name: "age", - slug: `age-${randomString()}`, - enabled: true, - usersCanEditRelation: false, - isWeightsEnabled: false, - isLocked: false, - }); - - singleSelectAttribute = await attributesRepositoryFixture.create({ - team: { connect: { id: org.id } }, - type: "SINGLE_SELECT", - name: "frontend", - slug: `frontend-${randomString()}`, - enabled: true, - usersCanEditRelation: false, - isWeightsEnabled: false, - isLocked: false, - }); - - textAttributeOption = await attributesRepositoryFixture.createOption({ - attribute: { connect: { id: textAttribute.id } }, - value: "coders", - slug: "coders", - isGroup: false, - }); - - multiSelectAttributeOption = await attributesRepositoryFixture.createOption({ - attribute: { connect: { id: multiSelectAttribute.id } }, - value: "javascript", - slug: "javascript", - isGroup: false, - }); - - multiSelectAttributeOption2 = await attributesRepositoryFixture.createOption({ - attribute: { connect: { id: multiSelectAttribute.id } }, - value: "typescript", - slug: "typescript", - isGroup: false, - }); - - numberAttributeOption = await attributesRepositoryFixture.createOption({ - attribute: { connect: { id: numberAttribute.id } }, - value: "18", - slug: "18", - isGroup: false, - }); - - singleSelectAttributeOption = await attributesRepositoryFixture.createOption({ - attribute: { connect: { id: singleSelectAttribute.id } }, - value: "yes", - slug: "yes", - isGroup: false, - }); - } - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should get all the memberships of the org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/memberships`) - .expect(200) - .then((response) => { - const responseBody: GetAllOrgMemberships = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data[0].id).toEqual(membership.id); - expect(responseBody.data[0].userId).toEqual(user.id); - expect(responseBody.data[0].role).toEqual("ADMIN"); - expect(responseBody.data[0].user.bio).toEqual(bio); - expect(responseBody.data[0].user.metadata).toEqual(metadata); - expect(responseBody.data[0].user.email).toEqual(user.email); - expect(responseBody.data[0].user.username).toEqual(user.username); - expect(responseBody.data[0].teamId).toEqual(org.id); - expect(responseBody.data[1].id).toEqual(membership2.id); - expect(responseBody.data[1].userId).toEqual(user2.id); - expect(responseBody.data[1].role).toEqual("MEMBER"); - expect(responseBody.data[1].user.bio).toEqual(bio); - expect(responseBody.data[1].user.metadata).toEqual(metadata); - expect(responseBody.data[1].user.email).toEqual(user2.email); - expect(responseBody.data[1].user.username).toEqual(user2.username); - expect(responseBody.data[1].teamId).toEqual(org.id); - userHasCorrectAttributes(responseBody.data[1].attributes); - }); - }); - - function userHasCorrectAttributes(attributes: OrgUserAttribute[]): void { - expect(attributes.length).toEqual(4); - const responseNumberAttribute = attributes.find((attr) => attr.type === "number"); - const responseSingleSelectAttribute = attributes.find((attr) => attr.type === "singleSelect"); - const responseMultiSelectAttribute = attributes.find((attr) => attr.type === "multiSelect"); - const responseTextAttribute = attributes.find((attr) => attr.type === "text"); - expect(responseNumberAttribute).toEqual({ - id: numberAttribute.id, - name: numberAttribute.name, - optionId: numberAttributeOption.id, - option: +numberAttributeOption.value, - type: "number", - }); - expect(responseSingleSelectAttribute).toEqual({ - id: singleSelectAttribute.id, - name: singleSelectAttribute.name, - optionId: singleSelectAttributeOption.id, - option: singleSelectAttributeOption.value, - type: "singleSelect", - }); - expect(responseMultiSelectAttribute).toEqual({ - id: multiSelectAttribute.id, - name: multiSelectAttribute.name, - options: [ - { - optionId: multiSelectAttributeOption2.id, - option: multiSelectAttributeOption2.value, - }, - { - optionId: multiSelectAttributeOption.id, - option: multiSelectAttributeOption.value, - }, - ], - type: "multiSelect", - }); - expect(responseTextAttribute).toEqual({ - id: textAttribute.id, - name: textAttribute.name, - optionId: textAttributeOption.id, - option: textAttributeOption.value, - type: "text", - }); - } - - it("should get all the memberships of the org paginated", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/memberships?skip=1&take=1`) - .expect(200) - .then((response) => { - const responseBody: GetAllOrgMemberships = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data[0].id).toEqual(membership2.id); - expect(responseBody.data[0].role).toEqual("MEMBER"); - expect(responseBody.data[0].user.bio).toEqual(bio); - expect(responseBody.data[0].user.metadata).toEqual(metadata); - expect(responseBody.data[0].user.email).toEqual(user2.email); - expect(responseBody.data[0].user.username).toEqual(user2.username); - expect(responseBody.data[0].userId).toEqual(user2.id); - expect(responseBody.data[0].teamId).toEqual(org.id); - }); - }); - - it("should fail if org does not exist", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/120494059/memberships`).expect(403); - }); - - it("should get the membership of the org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/memberships/${membership.id}`) - .expect(200) - .then((response) => { - const responseBody: GetOrgMembership = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(membership.id); - expect(responseBody.data.userId).toEqual(user.id); - expect(responseBody.data.role).toEqual("ADMIN"); - expect(responseBody.data.user.bio).toEqual(bio); - expect(responseBody.data.user.metadata).toEqual(metadata); - expect(responseBody.data.user.email).toEqual(user.email); - expect(responseBody.data.user.username).toEqual(user.username); - expect(responseBody.data.teamId).toEqual(org.id); - }); - }); - - it("should get the membership of the org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/memberships/${membership2.id}`) - .expect(200) - .then((response) => { - const responseBody: GetOrgMembership = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(membership2.id); - expect(responseBody.data.userId).toEqual(user2.id); - expect(responseBody.data.role).toEqual("MEMBER"); - expect(responseBody.data.user.bio).toEqual(bio); - expect(responseBody.data.user.metadata).toEqual(metadata); - expect(responseBody.data.user.email).toEqual(user2.email); - expect(responseBody.data.user.username).toEqual(user2.username); - expect(responseBody.data.teamId).toEqual(org.id); - userHasCorrectAttributes(responseBody.data.attributes); - }); - }); - - it("should create the membership of the org", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/memberships`) - .send({ - userId: userToInviteViaApi.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgMembershipDto) - .expect(201) - .then((response) => { - const responseBody: CreateOrgMembershipOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - membershipCreatedViaApi = responseBody.data; - expect(membershipCreatedViaApi.teamId).toEqual(org.id); - expect(membershipCreatedViaApi.role).toEqual("MEMBER"); - expect(membershipCreatedViaApi.userId).toEqual(userToInviteViaApi.id); - expect(membershipCreatedViaApi.user.bio).toEqual(bio); - expect(membershipCreatedViaApi.user.metadata).toEqual(metadata); - expect(membershipCreatedViaApi.user.email).toEqual(userToInviteViaApi.email); - expect(membershipCreatedViaApi.user.username).toEqual(userToInviteViaApi.username); - }); - }); - - it("should call ensureDefaultCalendarsForUser when creating membership", async () => { - const newUserEmail = `organizations-memberships-calendars-${randomString()}@api.com`; - const newUser = await userRepositoryFixture.create({ - email: newUserEmail, - username: newUserEmail, - bio, - metadata, - }); - - const ensureDefaultCalendarsSpy = jest - .spyOn(OrganizationsDelegationCredentialService.prototype, "ensureDefaultCalendarsForUser") - .mockResolvedValue(undefined); - - try { - await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/memberships`) - .send({ - userId: newUser.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgMembershipDto) - .expect(201); - - expect(ensureDefaultCalendarsSpy).toHaveBeenCalledWith(org.id, newUser.id, newUserEmail); - } finally { - ensureDefaultCalendarsSpy.mockRestore(); - await membershipRepositoryFixture.delete( - (await membershipRepositoryFixture.getUserMembershipByTeamId(newUser.id, org.id))?.id ?? 0 - ); - await userRepositoryFixture.deleteByEmail(newUserEmail); - } - }); - - it("should update the membership of the org", async () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/memberships/${membershipCreatedViaApi.id}`) - .send({ - role: "OWNER", - } satisfies UpdateOrgMembershipDto) - .expect(200) - .then((response) => { - const responseBody: UpdateOrgMembership = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - membershipCreatedViaApi = responseBody.data; - expect(membershipCreatedViaApi.role).toEqual("OWNER"); - }); - }); - - it("should delete the membership of the org we created via api", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/memberships/${membershipCreatedViaApi.id}`) - .expect(200) - .then((response) => { - const responseBody: DeleteOrgMembership = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(membershipCreatedViaApi.id); - }); - }); - - it("should fail to get the membership of the org we just deleted", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/memberships/${membershipCreatedViaApi.id}`) - .expect(404); - }); - - it("should fail if the membership does not exist", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/memberships/123132145`) - .expect(404); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(user.email); - await userRepositoryFixture.deleteByEmail(user2.email); - await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); -}); - -describe("Organizations Memberships Endpoints", () => { - describe("User Authentication - User is Org Member", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipRepositoryFixture: MembershipRepositoryFixture; - - let org: Team; - let membership: Membership; - - const userEmail = `organizations-memberships-member-${randomString()}@api.com`; - let user: User; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-memberships-organization-${randomString()}`, - isOrganization: true, - }); - - membership = await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should deny get all the memberships of the org", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/memberships`).expect(403); - }); - - it("should deny get all the memberships of the org paginated", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/memberships?skip=1&take=1`) - .expect(403); - }); - - it("should deny get the membership of the org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/memberships/${membership.id}`) - .expect(403); - }); - - it("should deny create the membership for the org", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/memberships`) - .send({ - role: "OWNER", - userId: user.id, - accepted: true, - } satisfies CreateOrgMembershipDto) - .expect(403); - }); - - it("should deny update the membership of the org", async () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/memberships/${membership.id}`) - .send({ - role: "MEMBER", - } satisfies Partial) - .expect(403); - }); - - it("should deny delete the membership of the org we created via api", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/memberships/${membership.id}`) - .expect(403); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(user.email); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/memberships/organizations-membership.controller.ts b/apps/api/v2/src/modules/organizations/memberships/organizations-membership.controller.ts deleted file mode 100644 index 2cdf674294045f..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/organizations-membership.controller.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsMembershipInOrg } from "@/modules/auth/guards/memberships/is-membership-in-org.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input"; -import { UpdateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/update-organization-membership.input"; -import { CreateOrgMembershipOutput } from "@/modules/organizations/memberships/outputs/create-membership.output"; -import { DeleteOrgMembership } from "@/modules/organizations/memberships/outputs/delete-membership.output"; -import { GetAllOrgMemberships } from "@/modules/organizations/memberships/outputs/get-all-memberships.output"; -import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output"; -import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output"; -import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service"; -import { - Controller, - UseGuards, - Get, - Param, - ParseIntPipe, - Query, - Delete, - Patch, - Post, - Body, - HttpCode, - HttpStatus, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { SkipTakePagination } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId/memberships", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Memberships") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -export class OrganizationsMembershipsController { - constructor(private organizationsMembershipService: OrganizationsMembershipService) {} - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Get("/") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Get all memberships" }) - async getAllMemberships( - @Param("orgId", ParseIntPipe) orgId: number, - @Query() queryParams: SkipTakePagination - ): Promise { - const { skip, take } = queryParams; - const memberships = await this.organizationsMembershipService.getPaginatedOrgMemberships( - orgId, - skip ?? 0, - take ?? 250 - ); - return { - status: SUCCESS_STATUS, - data: memberships, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Post("/") - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: "Create a membership" }) - async createMembership( - @Param("orgId", ParseIntPipe) orgId: number, - @Body() body: CreateOrgMembershipDto - ): Promise { - const membership = await this.organizationsMembershipService.createOrgMembership(orgId, body); - return { - status: SUCCESS_STATUS, - data: membership, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsMembershipInOrg) - @Get("/:membershipId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Get a membership" }) - async getOrgMembership( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("membershipId", ParseIntPipe) membershipId: number - ): Promise { - const membership = await this.organizationsMembershipService.getOrgMembership(orgId, membershipId); - return { - status: SUCCESS_STATUS, - data: membership, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsMembershipInOrg) - @Delete("/:membershipId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Delete a membership" }) - async deleteMembership( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("membershipId", ParseIntPipe) membershipId: number - ): Promise { - const membership = await this.organizationsMembershipService.deleteOrgMembership(orgId, membershipId); - return { - status: SUCCESS_STATUS, - data: membership, - }; - } - - @UseGuards(IsMembershipInOrg) - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Patch("/:membershipId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Update a membership" }) - async updateMembership( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("membershipId", ParseIntPipe) membershipId: number, - @Body() body: UpdateOrgMembershipDto - ): Promise { - const membership = await this.organizationsMembershipService.updateOrgMembership( - orgId, - membershipId, - body - ); - return { - status: SUCCESS_STATUS, - data: membership, - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/memberships/organizations-membership.repository.ts b/apps/api/v2/src/modules/organizations/memberships/organizations-membership.repository.ts deleted file mode 100644 index 9c294a5f46852b..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/organizations-membership.repository.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { MembershipUserSelect } from "@/modules/teams/memberships/teams-memberships.repository"; -import { Injectable } from "@nestjs/common"; - -import type { Prisma } from "@calcom/prisma/client"; - -import type { UpdateOrgMembershipDto } from "./inputs/update-organization-membership.input"; - -export type DbOrgMembership = Awaited>; - -const attributeToUserSelect = { - createdByDSyncId: true, - weight: true, - attributeOption: { - select: { - id: true, - value: true, - slug: true, - attribute: { - select: { - id: true, - name: true, - type: true, - }, - }, - }, - }, -} as const satisfies Prisma.AttributeToUserSelect; - -@Injectable() -export class OrganizationsMembershipRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async findOrgMembership(organizationId: number, membershipId: number) { - return this.dbRead.prisma.membership.findUnique({ - where: { - id: membershipId, - teamId: organizationId, - }, - include: { - user: { select: MembershipUserSelect }, - AttributeToUser: { - select: attributeToUserSelect, - }, - }, - }); - } - - async findOrgMembershipByUserId(organizationId: number, userId: number) { - return this.dbRead.prisma.membership.findFirst({ - where: { - teamId: organizationId, - userId, - }, - include: { - user: { select: MembershipUserSelect }, - AttributeToUser: { - select: attributeToUserSelect, - }, - }, - }); - } - - async deleteOrgMembership(organizationId: number, membershipId: number) { - return this.dbWrite.prisma.membership.delete({ - where: { - id: membershipId, - teamId: organizationId, - }, - include: { - user: { select: MembershipUserSelect }, - AttributeToUser: { - select: attributeToUserSelect, - }, - }, - }); - } - - async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) { - return this.dbWrite.prisma.membership.upsert({ - create: { ...data, teamId: organizationId }, - update: { role: data.role, accepted: data.accepted, disableImpersonation: data.disableImpersonation }, - where: { userId_teamId: { userId: data.userId, teamId: organizationId } }, - include: { - user: { select: MembershipUserSelect }, - AttributeToUser: { - select: attributeToUserSelect, - }, - }, - }); - } - async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) { - return this.dbWrite.prisma.membership.update({ - data: { ...data }, - where: { id: membershipId, teamId: organizationId }, - include: { - user: { select: MembershipUserSelect }, - AttributeToUser: { - select: attributeToUserSelect, - }, - }, - }); - } - - async findOrgMembershipsPaginated(organizationId: number, skip: number, take: number) { - return this.dbRead.prisma.membership.findMany({ - where: { - teamId: organizationId, - }, - include: { - user: { select: MembershipUserSelect }, - AttributeToUser: { - select: attributeToUserSelect, - }, - }, - skip, - take, - }); - } -} diff --git a/apps/api/v2/src/modules/organizations/memberships/outputs/create-membership.output.ts b/apps/api/v2/src/modules/organizations/memberships/outputs/create-membership.output.ts deleted file mode 100644 index 601092563f6b21..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/outputs/create-membership.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class CreateOrgMembershipOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: OrganizationMembershipOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => OrganizationMembershipOutput) - data!: OrganizationMembershipOutput; -} diff --git a/apps/api/v2/src/modules/organizations/memberships/outputs/delete-membership.output.ts b/apps/api/v2/src/modules/organizations/memberships/outputs/delete-membership.output.ts deleted file mode 100644 index a0e6cfc7550648..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/outputs/delete-membership.output.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { IsEnum } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class DeleteOrgMembership { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - data!: OrganizationMembershipOutput; -} diff --git a/apps/api/v2/src/modules/organizations/memberships/outputs/get-all-memberships.output.ts b/apps/api/v2/src/modules/organizations/memberships/outputs/get-all-memberships.output.ts deleted file mode 100644 index e1fb6b053bfcd6..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/outputs/get-all-memberships.output.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested, IsArray } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class GetAllOrgMemberships { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: OrganizationMembershipOutput, - }) - @IsNotEmptyObject() - @ValidateNested({ each: true }) - @Type(() => OrganizationMembershipOutput) - @IsArray() - data!: OrganizationMembershipOutput[]; -} diff --git a/apps/api/v2/src/modules/organizations/memberships/outputs/get-membership.output.ts b/apps/api/v2/src/modules/organizations/memberships/outputs/get-membership.output.ts deleted file mode 100644 index 69103cb3681098..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/outputs/get-membership.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class GetOrgMembership { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: OrganizationMembershipOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => OrganizationMembershipOutput) - data!: OrganizationMembershipOutput; -} diff --git a/apps/api/v2/src/modules/organizations/memberships/outputs/organization-membership.output.ts b/apps/api/v2/src/modules/organizations/memberships/outputs/organization-membership.output.ts deleted file mode 100644 index 371247ccb08d38..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/outputs/organization-membership.output.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; -import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsArray, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator"; - -class BaseAttribute { - @IsString() - @Expose() - @ApiProperty() - id!: string; - - @IsString() - @Expose() - @ApiProperty() - name!: string; -} - -export class TextAttribute extends BaseAttribute { - @IsString() - @Expose() - @ApiProperty() - type!: "text"; - - @IsString() - @Expose() - @ApiProperty() - option!: string; - - @IsString() - @Expose() - @ApiProperty() - optionId!: string; -} - -export class NumberAttribute extends BaseAttribute { - @IsString() - @Expose() - @ApiProperty() - type!: "number"; - - @IsNumber() - @Expose() - @ApiProperty() - option!: number; - - @IsString() - @Expose() - @ApiProperty() - optionId!: string; -} - -export class SingleSelectAttribute extends BaseAttribute { - @IsString() - @IsString() - @Expose() - @ApiProperty() - type!: "singleSelect"; - - @IsString() - @Expose() - @ApiProperty() - option!: string; - - @IsString() - @Expose() - @ApiProperty() - optionId!: string; -} - -class MultiSelectAttributeOption { - @IsString() - @Expose() - @ApiProperty() - optionId!: string; - - @IsString() - @Expose() - @ApiProperty() - option!: string; -} - -export class MultiSelectAttribute extends BaseAttribute { - @IsString() - @Expose() - @ApiProperty() - type!: "multiSelect"; - - @IsArray() - @ValidateNested({ each: true }) - @Expose() - @ApiProperty() - options!: MultiSelectAttributeOption[]; -} - -export type OrgUserAttribute = TextAttribute | NumberAttribute | SingleSelectAttribute | MultiSelectAttribute; - -@ApiExtraModels(BaseAttribute, TextAttribute, NumberAttribute, SingleSelectAttribute, MultiSelectAttribute) -export class OrganizationMembershipOutput extends TeamMembershipOutput { - @IsArray() - @ValidateNested({ each: true }) - @Expose() - @ApiProperty({ - required: true, - oneOf: [ - { $ref: getSchemaPath(TextAttribute) }, - { $ref: getSchemaPath(NumberAttribute) }, - { $ref: getSchemaPath(SingleSelectAttribute) }, - { $ref: getSchemaPath(MultiSelectAttribute) }, - ], - type: "array", - }) - @Type(() => Object) - attributes!: OrgUserAttribute[]; -} diff --git a/apps/api/v2/src/modules/organizations/memberships/outputs/update-membership.output.ts b/apps/api/v2/src/modules/organizations/memberships/outputs/update-membership.output.ts deleted file mode 100644 index 39549ad943c461..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/outputs/update-membership.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class UpdateOrgMembership { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: OrganizationMembershipOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => OrganizationMembershipOutput) - data!: OrganizationMembershipOutput; -} diff --git a/apps/api/v2/src/modules/organizations/memberships/services/organizations-membership-output.service.ts b/apps/api/v2/src/modules/organizations/memberships/services/organizations-membership-output.service.ts deleted file mode 100644 index 64eb03a7762e05..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/services/organizations-membership-output.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { DbOrgMembership } from "@/modules/organizations/memberships/organizations-membership.repository"; -import { - MultiSelectAttribute, - NumberAttribute, - OrganizationMembershipOutput, - OrgUserAttribute, - SingleSelectAttribute, - TextAttribute, -} from "@/modules/organizations/memberships/outputs/organization-membership.output"; -import { Injectable } from "@nestjs/common"; -import { plainToClass } from "class-transformer"; - -import { groupMembershipAttributes } from "@calcom/platform-libraries"; -import type { GroupedAttribute } from "@calcom/platform-libraries"; - -@Injectable() -export class OrganizationsMembershipOutputService { - getOrgMembershipsOutput(memberships: NonNullable[]) { - return memberships.map((membership) => this.getOrgMembershipOutput(membership)); - } - - getOrgMembershipOutput(organizationMembership: NonNullable) { - const { AttributeToUser, ...orgMembership } = organizationMembership; - - const groupedMembershipAttributes: GroupedAttribute[] = groupMembershipAttributes(AttributeToUser); - const attributesOutput = this.getAttributesOutput(groupedMembershipAttributes); - - return plainToClass(OrganizationMembershipOutput, { ...orgMembership, attributes: attributesOutput }); - } - - private getAttributesOutput(attributes: GroupedAttribute[]): OrgUserAttribute[] { - return attributes.map((attribute) => { - switch (attribute.type) { - case "TEXT": - return this.getTextAttributeOutput(attribute); - case "NUMBER": - return this.getNumberAttributeOutput(attribute); - case "SINGLE_SELECT": - return this.getSingleSelectAttributeOutput(attribute); - case "MULTI_SELECT": - return this.getMultiSelectAttributeOutput(attribute); - default: - return this.getTextAttributeOutput(attribute); - } - }); - } - - private getTextAttributeOutput(attribute: GroupedAttribute): TextAttribute { - return { - id: attribute.id, - name: attribute.name, - optionId: attribute.options[0].id, - option: attribute.options[0].value, - type: "text", - }; - } - - private getNumberAttributeOutput(attribute: GroupedAttribute): NumberAttribute { - return { - id: attribute.id, - name: attribute.name, - optionId: attribute.options[0].id, - option: +attribute.options[0].value, - type: "number", - }; - } - - private getSingleSelectAttributeOutput(attribute: GroupedAttribute): SingleSelectAttribute { - return { - id: attribute.id, - name: attribute.name, - optionId: attribute.options[0].id, - option: attribute.options[0].value, - type: "singleSelect", - }; - } - - private getMultiSelectAttributeOutput(attribute: GroupedAttribute): MultiSelectAttribute { - return { - id: attribute.id, - name: attribute.name, - options: attribute.options.map((option: GroupedAttribute["options"][number]) => ({ - optionId: option.id, - option: option.value, - })), - type: "multiSelect", - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/memberships/services/organizations-membership.service.ts b/apps/api/v2/src/modules/organizations/memberships/services/organizations-membership.service.ts deleted file mode 100644 index a1f614dbc1edfe..00000000000000 --- a/apps/api/v2/src/modules/organizations/memberships/services/organizations-membership.service.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { TeamService } from "@calcom/platform-libraries"; -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input"; -import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service"; -import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; -import { OrganizationsDelegationCredentialService } from "@/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service"; -import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input"; -import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository"; -import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output"; - -export const PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR = `Can't add user to organization - the user is platform managed user but organization is not because organization probably was not created using OAuth credentials.`; -export const REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR = `Can't add user to organization - the user is not platform managed user but organization is platform managed. Both have to be created using OAuth credentials.`; -export const MANAGED_USER_AND_MANAGED_ORG_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR = `Can't add user to organization - managed user and organization were created using different OAuth clients.`; - -@Injectable() -export class OrganizationsMembershipService { - constructor( - private readonly organizationsMembershipRepository: OrganizationsMembershipRepository, - private readonly organizationsMembershipOutputService: OrganizationsMembershipOutputService, - private readonly oAuthClientsRepository: OAuthClientRepository, - private readonly delegationCredentialService: OrganizationsDelegationCredentialService - ) {} - - async getOrgMembership( - organizationId: number, - membershipId: number - ): Promise { - const membership = await this.organizationsMembershipRepository.findOrgMembership( - organizationId, - membershipId - ); - - if (!membership) { - throw new NotFoundException( - `Membership with id ${membershipId} within organization id ${organizationId} not found` - ); - } - - return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership); - } - - async isOrgAdminOrOwner(organizationId: number, userId: number): Promise { - const membership = await this.organizationsMembershipRepository.findOrgMembershipByUserId( - organizationId, - userId - ); - if (!membership) { - return false; - } - return membership.role === "ADMIN" || membership.role === "OWNER"; - } - - async getOrgMembershipByUserId( - organizationId: number, - userId: number - ): Promise { - const membership = await this.organizationsMembershipRepository.findOrgMembershipByUserId( - organizationId, - userId - ); - if (!membership) { - throw new NotFoundException( - `Membership for user with id ${userId} within organization id ${organizationId} not found` - ); - } - - return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership); - } - - async getPaginatedOrgMemberships( - organizationId: number, - skip = 0, - take = 250 - ): Promise { - const memberships = await this.organizationsMembershipRepository.findOrgMembershipsPaginated( - organizationId, - skip, - take - ); - return this.organizationsMembershipOutputService.getOrgMembershipsOutput(memberships); - } - - async deleteOrgMembership( - organizationId: number, - membershipId: number - ): Promise { - // Get the membership first to get the userId - const membership = await this.organizationsMembershipRepository.findOrgMembership( - organizationId, - membershipId - ); - - if (!membership) { - throw new NotFoundException( - `Membership with id ${membershipId} within organization id ${organizationId} not found` - ); - } - - await TeamService.removeMembers({ - teamIds: [organizationId], - userIds: [membership.userId], - isOrg: true, - }); - - return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership); - } - - async updateOrgMembership( - organizationId: number, - membershipId: number, - data: UpdateOrgMembershipDto - ): Promise { - const membership = await this.organizationsMembershipRepository.updateOrgMembership( - organizationId, - membershipId, - data - ); - return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership); - } - - async createOrgMembership( - organizationId: number, - data: CreateOrgMembershipDto - ): Promise { - await this.canUserBeAddedToOrg(data.userId, organizationId); - const membership = await this.organizationsMembershipRepository.createOrgMembership(organizationId, data); - - if (membership.user.email) { - await this.delegationCredentialService.ensureDefaultCalendarsForUser( - organizationId, - data.userId, - membership.user.email - ); - } - - return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership); - } - - async canUserBeAddedToOrg(userId: number, orgId: number): Promise { - const [userOAuthClient, orgOAuthClients] = await Promise.all([ - this.oAuthClientsRepository.getByUserId(userId), - this.oAuthClientsRepository.getByOrgId(orgId), - ]); - - if (!userOAuthClient && orgOAuthClients.length === 0) { - return true; - } - - if (userOAuthClient && orgOAuthClients.some((orgClient) => orgClient.id === userOAuthClient.id)) { - return true; - } - - if (!userOAuthClient) { - throw new BadRequestException(REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR); - } - - if (orgOAuthClients.length === 0) { - throw new BadRequestException(PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR); - } - - throw new BadRequestException(MANAGED_USER_AND_MANAGED_ORG_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR); - } -} diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index 0e0a6906c18cbb..9b693fc9ed5a79 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -1,11 +1,9 @@ import { Logger, Module } from "@nestjs/common"; -import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; -import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller"; -import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module"; -import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module"; -import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service"; -import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; -import { OrganizationMembershipService } from "@/lib/services/organization-membership.service"; +import { EventTypesModule_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.module"; +import { EventTypesPrivateLinksModule } from "@/platform/event-types-private-links/event-types-private-links.module"; +import { SchedulesModule_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/schedules.module"; +import { InputSchedulesService_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/services/input-schedules.service"; +import { SchedulesService_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/services/schedules.service"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository"; import { ConferencingService } from "@/modules/conferencing/services/conferencing.service"; @@ -19,72 +17,14 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository"; import { UserOOOService } from "@/modules/ooo/services/ooo.service"; -import { OrganizationsAttributesController } from "@/modules/organizations/attributes/index/controllers/organizations-attributes.controller"; -import { OrganizationAttributesRepository } from "@/modules/organizations/attributes/index/organization-attributes.repository"; -import { OrganizationAttributesService } from "@/modules/organizations/attributes/index/services/organization-attributes.service"; -import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository"; -import { OrganizationsAttributesOptionsController } from "@/modules/organizations/attributes/options/organizations-attributes-options.controller"; -import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service"; -import { OrganizationsConferencingModule } from "@/modules/organizations/conferencing/organizations-conferencing.module"; -import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service"; -import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module"; -import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller"; -import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository"; -import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer"; -import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service"; -import { OrganizationsEventTypesService } from "@/modules/organizations/event-types/services/organizations-event-types.service"; -import { OutputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/output.service"; import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsService } from "@/modules/organizations/index/organizations.service"; -import { OrganizationsMembershipsController } from "@/modules/organizations/memberships/organizations-membership.controller"; -import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository"; -import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service"; -import { OrganizationsMembershipOutputService } from "@/modules/organizations/memberships/services/organizations-membership-output.service"; -import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module"; -import { OrganizationsRolesModule } from "@/modules/organizations/roles/organizations-roles.module"; -import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller"; -import { OrganizationsSchedulesService } from "@/modules/organizations/schedules/services/organizations-schedules.service"; -import { OrganizationsStripeModule } from "@/modules/organizations/stripe/organizations-stripe.module"; -import { OrganizationsStripeService } from "@/modules/organizations/stripe/services/organizations-stripe.service"; -import { OrganizationsTeamsController } from "@/modules/organizations/teams/index/organizations-teams.controller"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service"; -import { OrganizationsTeamsInviteController } from "@/modules/organizations/teams/invite/organizations-teams-invite.controller"; -import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.controller"; -import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository"; -import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service"; -import { OrganizationsTeamsRolesModule } from "@/modules/organizations/teams/roles/organizations-teams-roles.module"; -import { OrganizationsTeamsRoutingFormsModule } from "@/modules/organizations/teams/routing-forms/organizations-teams-routing-forms.module"; -import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/teams/schedules/organizations-teams-schedules.controller"; -import { OrganizationTeamWorkflowsController } from "@/modules/organizations/teams/workflows/controllers/org-team-workflows.controller"; -import { OrganizationsUsersController } from "@/modules/organizations/users/index/controllers/organizations-users.controller"; -import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository"; -import { OrganizationsUsersService } from "@/modules/organizations/users/index/services/organizations-users-service"; -import { OrganizationsUsersOOOController } from "@/modules/organizations/users/ooo/controllers/organizations-users-ooo.controller"; -import { OrgUsersOOORepository } from "@/modules/organizations/users/ooo/organizations-users-ooo.repository"; -import { OrgUsersOOOService } from "@/modules/organizations/users/ooo/services/organization-users-ooo.service"; -import { OrganizationsWebhooksController } from "@/modules/organizations/webhooks/controllers/organizations-webhooks.controller"; -import { OrganizationsWebhooksRepository } from "@/modules/organizations/webhooks/organizations-webhooks.repository"; -import { OrganizationsWebhooksService } from "@/modules/organizations/webhooks/services/organizations-webhooks.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; import { RedisService } from "@/modules/redis/redis.service"; import { StripeModule } from "@/modules/stripe/stripe.module"; -import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; -import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service"; -import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository"; -import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service"; -import { TeamsModule } from "@/modules/teams/teams/teams.module"; import { TokensRepository } from "@/modules/tokens/tokens.repository"; import { UsersModule } from "@/modules/users/users.module"; import { TeamsVerifiedResourcesRepository } from "@/modules/verified-resources/teams-verified-resources.repository"; -import { WebhooksService } from "@/modules/webhooks/services/webhooks.service"; -import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository"; -import { TeamEventTypeWorkflowsService } from "@/modules/workflows/services/team-event-type-workflows.service"; -import { TeamRoutingFormWorkflowsService } from "@/modules/workflows/services/team-routing-form-workflows.service"; -import { WorkflowsInputService } from "@/modules/workflows/services/workflows.input.service"; -import { WorkflowsOutputService } from "@/modules/workflows/services/workflows.output.service"; -import { WorkflowsRepository } from "@/modules/workflows/workflows.repository"; @Module({ imports: [ @@ -95,52 +35,14 @@ import { WorkflowsRepository } from "@/modules/workflows/workflows.repository"; RedisModule, EmailModule, EventTypesModule_2024_06_14, - TeamsEventTypesModule, - TeamsModule, - OrganizationsDelegationCredentialModule, - OrganizationsOrganizationsModule, - OrganizationsRolesModule, - OrganizationsTeamsRolesModule, - OrganizationsStripeModule, - OrganizationsTeamsRoutingFormsModule, MembershipsModule, - OrganizationsConferencingModule, EventTypesPrivateLinksModule, ], providers: [ OrganizationsRepository, - OrganizationMembershipService, - OrganizationsTeamsRepository, - OrganizationsService, - OrganizationsTeamsService, - OrganizationsSchedulesService, - OrganizationsUsersRepository, - OrganizationsUsersService, EmailService, - OrganizationsMembershipRepository, - OrganizationsMembershipService, - OrganizationsMembershipOutputService, - OrganizationsEventTypesService, - InputOrganizationsEventTypesService, - OutputOrganizationsEventTypesService, - OrganizationsEventTypesRepository, - OrganizationsTeamsMembershipsRepository, - OrganizationsTeamsMembershipsService, - OrganizationAttributesService, - OrganizationAttributeOptionService, - OrganizationAttributeOptionRepository, - OrganizationAttributesRepository, - OrganizationsWebhooksRepository, - OrganizationsWebhooksService, - WebhooksRepository, - WebhooksService, - OutputTeamEventTypesResponsePipe, UserOOOService, UserOOORepository, - OrgUsersOOOService, - OrgUsersOOORepository, - OrganizationsConferencingService, - OrganizationsStripeService, CredentialsRepository, AppsRepository, RedisService, @@ -151,57 +53,13 @@ import { WorkflowsRepository } from "@/modules/workflows/workflows.repository"; Office365VideoService, TokensRepository, TeamsVerifiedResourcesRepository, - WorkflowsRepository, - TeamEventTypeWorkflowsService, - TeamRoutingFormWorkflowsService, - WorkflowsInputService, - WorkflowsOutputService, - TeamsSchedulesService, SchedulesService_2024_06_11, InputSchedulesService_2024_06_11, - TeamsMembershipsService, - TeamsMembershipsRepository, OAuthClientRepository, Logger, ], exports: [ - OrganizationsService, OrganizationsRepository, - OrganizationsTeamsRepository, - OrganizationsUsersRepository, - OrganizationsUsersService, - OrganizationsMembershipRepository, - OrganizationsMembershipService, - OrganizationsTeamsMembershipsRepository, - OrganizationsTeamsMembershipsService, - OrganizationAttributesService, - OrganizationAttributeOptionService, - OrganizationAttributeOptionRepository, - OrganizationAttributesRepository, - OrganizationsWebhooksRepository, - OrganizationsWebhooksService, - WebhooksRepository, - WebhooksService, - OrganizationsEventTypesService, - OrganizationsConferencingService, - OrganizationsStripeService, - OrganizationMembershipService, - ], - controllers: [ - OrganizationsTeamsController, - OrganizationsSchedulesController, - OrganizationsUsersController, - OrganizationsMembershipsController, - OrganizationsEventTypesController, - OrganizationsTeamsMembershipsController, - OrganizationsTeamsInviteController, - OrganizationsAttributesController, - OrganizationsAttributesOptionsController, - OrganizationsWebhooksController, - OrganizationsTeamsSchedulesController, - OrganizationsUsersOOOController, - OrganizationTeamWorkflowsController, - OrganizationsEventTypesPrivateLinksController, ], }) export class OrganizationsModule {} diff --git a/apps/api/v2/src/modules/organizations/organizations/inputs/create-managed-organization.input.ts b/apps/api/v2/src/modules/organizations/organizations/inputs/create-managed-organization.input.ts deleted file mode 100644 index 2708d1ccb1b499..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/inputs/create-managed-organization.input.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { RefreshApiKeyInput } from "@/modules/api-keys/inputs/refresh-api-key.input"; -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsObject, IsOptional, IsString, Length } from "class-validator"; - -import { Metadata, METADATA_DOCS, ValidateMetadata } from "@calcom/platform-types"; - -export class CreateOrganizationInput extends RefreshApiKeyInput { - @IsString() - @Length(1) - @ApiProperty({ description: "Name of the organization", example: "CalTeam" }) - readonly name!: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ - type: String, - description: - "Organization slug in kebab-case - if not provided will be generated automatically based on name.", - example: "cal-tel", - }) - readonly slug?: string; - - @ApiPropertyOptional({ - type: Object, - description: METADATA_DOCS, - example: { key: "value" }, - }) - @IsObject() - @IsOptional() - @ValidateMetadata() - metadata?: Metadata; -} diff --git a/apps/api/v2/src/modules/organizations/organizations/inputs/get-managed-organizations.input.ts b/apps/api/v2/src/modules/organizations/organizations/inputs/get-managed-organizations.input.ts deleted file mode 100644 index 7bd45ac94b7e9b..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/inputs/get-managed-organizations.input.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsOptional, IsString } from "class-validator"; - -import { SkipTakePagination } from "@calcom/platform-types"; - -export class GetManagedOrganizationsInput_2024_08_13 extends SkipTakePagination { - @IsOptional() - @IsString() - @ApiProperty({ example: "organization-slug", description: "The slug of the managed organization" }) - slug?: string; - - @IsOptional() - @IsString() - @ApiProperty({ - example: "metadata-key", - description: - "The key of the metadata - it is case sensitive so provide exactly as stored. If you provide it then you must also provide metadataValue", - }) - metadataKey?: string; - - @IsOptional() - @IsString() - @ApiProperty({ - example: "metadata-value", - description: - "The value of the metadata - it is case sensitive so provide exactly as stored. If you provide it then you must also provide metadataKey", - }) - metadataValue?: string; -} diff --git a/apps/api/v2/src/modules/organizations/organizations/inputs/update-managed-organization.input.ts b/apps/api/v2/src/modules/organizations/organizations/inputs/update-managed-organization.input.ts deleted file mode 100644 index 9a291ceb352ed7..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/inputs/update-managed-organization.input.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsObject, IsOptional, IsString, Length } from "class-validator"; - -import { Metadata, METADATA_DOCS, ValidateMetadata } from "@calcom/platform-types"; - -export class UpdateOrganizationInput { - @IsString() - @IsOptional() - @Length(1) - @ApiPropertyOptional({ description: "Name of the organization", example: "CalTeam" }) - readonly name?: string; - - @ApiPropertyOptional({ - type: Object, - description: METADATA_DOCS, - example: { key: "value" }, - }) - @IsObject() - @IsOptional() - @ValidateMetadata() - metadata?: Metadata; -} diff --git a/apps/api/v2/src/modules/organizations/organizations/managed-organizations.repository.ts b/apps/api/v2/src/modules/organizations/organizations/managed-organizations.repository.ts deleted file mode 100644 index bd5adbdf5e6a80..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/managed-organizations.repository.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { GetManagedOrganizationsInput_2024_08_13 } from "@/modules/organizations/organizations/inputs/get-managed-organizations.input"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -import type { Prisma } from "@calcom/prisma/client"; - -@Injectable() -export class ManagedOrganizationsRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async createManagedOrganization(managerOrganizationId: number, data: Prisma.TeamCreateInput) { - return this.dbWrite.prisma.team.create({ - data: { - ...data, - managedOrganization: { - create: { - managerOrganization: { - connect: { id: managerOrganizationId }, - }, - }, - }, - }, - }); - } - - async getManagedOrganizationBySlug(managerOrganizationId: number, managedOrganizationSlug: string) { - return this.dbRead.prisma.managedOrganization.findFirst({ - where: { - managerOrganizationId, - managedOrganization: { - slug: managedOrganizationSlug, - }, - }, - }); - } - - async getByManagerManagedIds(managerOrganizationId: number, managedOrganizationId: number) { - return this.dbRead.prisma.managedOrganization.findUnique({ - where: { - managerOrganizationId_managedOrganizationId: { - managerOrganizationId, - managedOrganizationId, - }, - }, - }); - } - - async getByManagerOrganizationIdPaginated( - managerOrganizationId: number, - query: GetManagedOrganizationsInput_2024_08_13 - ) { - const { skip, take, slug, metadataKey, metadataValue } = query; - - const managedOrganizationFilter: Prisma.TeamWhereInput = { - slug, - }; - - if (metadataKey && metadataValue) { - managedOrganizationFilter.metadata = { - path: [metadataKey], - equals: metadataValue, - }; - } - - const where: Prisma.ManagedOrganizationWhereInput = { - managerOrganizationId, - managedOrganization: managedOrganizationFilter, - }; - - const [totalItems, linkRows] = await Promise.all([ - this.dbRead.prisma.managedOrganization.count({ where }), - this.dbRead.prisma.managedOrganization.findMany({ - where, - skip, - take, - orderBy: { managedOrganizationId: "asc" }, - include: { managedOrganization: true }, - }), - ]); - - const items = linkRows.map((l) => l.managedOrganization); - - return { totalItems, items }; - } -} diff --git a/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.e2e-spec.ts deleted file mode 100644 index 1b7f4e3282a98f..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.e2e-spec.ts +++ /dev/null @@ -1,720 +0,0 @@ -import { - APPS_READ, - APPS_WRITE, - BOOKING_READ, - BOOKING_WRITE, - EVENT_TYPE_READ, - EVENT_TYPE_WRITE, - PROFILE_READ, - PROFILE_WRITE, - SCHEDULE_READ, - SCHEDULE_WRITE, - SUCCESS_STATUS, - X_CAL_SECRET_KEY, -} from "@calcom/platform-constants"; -import type { ApiSuccessResponse, CreateOAuthClientInput } from "@calcom/platform-types"; -import type { PlatformBilling, Team, User } 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 { DateTime } from "luxon"; -import request from "supertest"; -import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture"; -import { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture"; -import { ManagedOrganizationsRepositoryFixture } from "test/fixtures/repository/managed-organizations.repository.fixture"; -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 { mockThrottlerGuard } from "test/utils/withNoThrottler"; -import { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { getEnv } from "@/env"; -import { sha256Hash, stripApiKey } from "@/lib/api-key"; -import { RefreshApiKeyOutput } from "@/modules/api-keys/outputs/refresh-api-key.output"; -import { CreateOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/CreateOAuthClientResponse.dto"; -import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto"; -import { CreateOrganizationInput } from "@/modules/organizations/organizations/inputs/create-managed-organization.input"; -import { UpdateOrganizationInput } from "@/modules/organizations/organizations/inputs/update-managed-organization.input"; -import { GetManagedOrganizationsOutput } from "@/modules/organizations/organizations/outputs/get-managed-organizations.output"; -import { - ManagedOrganizationOutput, - ManagedOrganizationWithApiKeyOutput, -} from "@/modules/organizations/organizations/outputs/managed-organization.output"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Organizations Endpoints", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture; - let managedOrganizationsRepositoryFixture: ManagedOrganizationsRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture; - let profilesRepositoryFixture: ProfileRepositoryFixture; - - let managerOrg: Team; - let payPerUserPlanManagerOrg: Team; - let essentialsPlanManagerOrg: Team; - - let managedOrg: ManagedOrganizationWithApiKeyOutput; - let managedOrg2: ManagedOrganizationWithApiKeyOutput; - - const managerOrgAdminEmail = `organizations-organizations-admin-${randomString()}@api.com`; - const payPerUserPlanManagerOrgAdminEmail = `organizations-organizations-admin-${randomString()}@api.com`; - const essentialsPlanManagerOrgAdminEmail = `organizations-organizations-admin-${randomString()}@api.com`; - - let managerOrgAdmin: User; - let payPerUserPlanManagerOrgAdmin: User; - let essentialsPlanManagerOrgAdmin: User; - - let managerOrgAdminApiKey: string; - let payPerUserPlanManagerOrgAdminApiKey: string; - let essentialsPlanManagerOrgAdminApiKey: string; - - let managerOrgBilling: PlatformBilling; - - let managedOrgApiKey: string; - let managedOrgOAuthClientId: string; - let managedOrgOAuthClientSecret: string; - const createOAuthClientBody: CreateOAuthClientInput = { - name: "OAuth client for managed organization", - redirectUris: ["http://localhost:4321"], - permissions: ["*"], - }; - - const newDate = new Date(2035, 0, 9, 15, 0, 0); - - beforeAll(async () => { - mockThrottlerGuard(); - - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef); - managedOrganizationsRepositoryFixture = new ManagedOrganizationsRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - oAuthClientsRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - - // Setup manager organization with SCALE plan - const managerSetup = await setupTestOrganization( - managerOrgAdminEmail, - `organizations-organizations-organization-${randomString()}`, - "SCALE" - ); - managerOrgAdmin = managerSetup.admin; - managerOrg = managerSetup.org; - managerOrgBilling = managerSetup.billing; - managerOrgAdminApiKey = managerSetup.apiKey; - - // Setup pay-per-user organization - const payPerUserSetup = await setupTestOrganization( - payPerUserPlanManagerOrgAdminEmail, - `pay-per-user-plan-organization-${randomString()}`, - "PER_ACTIVE_USER" - ); - payPerUserPlanManagerOrgAdmin = payPerUserSetup.admin; - payPerUserPlanManagerOrg = payPerUserSetup.org; - payPerUserPlanManagerOrgAdminApiKey = payPerUserSetup.apiKey; - - // Setup essentials organization - const essentialsSetup = await setupTestOrganization( - essentialsPlanManagerOrgAdminEmail, - `essentials-per-user-plan-organization-${randomString()}`, - "ESSENTIALS" - ); - essentialsPlanManagerOrgAdmin = essentialsSetup.admin; - essentialsPlanManagerOrg = essentialsSetup.org; - essentialsPlanManagerOrgAdminApiKey = essentialsSetup.apiKey; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - advanceTo(newDate); - - await app.init(); - }); - - async function setupTestOrganization( - adminEmail: string, - orgName: string, - plan: "SCALE" | "PER_ACTIVE_USER" | "ESSENTIALS" - ) { - const admin = await userRepositoryFixture.create({ - email: adminEmail, - username: adminEmail, - }); - - const org = await organizationsRepositoryFixture.create({ - name: orgName, - isOrganization: true, - isPlatform: true, - }); - - await profilesRepositoryFixture.create({ - uid: `${randomString()}-uid`, - username: adminEmail, - user: { connect: { id: admin.id } }, - organization: { connect: { id: org.id } }, - movedFromUser: { connect: { id: admin.id } }, - }); - - const billing = await platformBillingRepositoryFixture.create(org.id, plan); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: admin.id } }, - team: { connect: { id: org.id } }, - accepted: true, - }); - - const { keyString } = await apiKeysRepositoryFixture.createApiKey(admin.id, null, org.id); - const apiKey = `cal_test_${keyString}`; - - return { admin, org, billing, apiKey }; - } - - function createManagedOrgInput( - namePrefix: string, - metadata?: Record - ): CreateOrganizationInput { - const suffix = randomString(5); - return { - name: `${namePrefix} ${suffix}`, - slug: `${namePrefix.toLowerCase().replace(/\s+/g, "-")}-${suffix}`, - metadata: metadata || { key: "value" }, - }; - } - - afterAll(() => { - clear(); - }); - - it("should not create managed organization with string metadata", async () => { - const suffix = randomString(); - - const body = { - name: `organizations organizations org ${suffix}`, - metadata: JSON.stringify({ key: "value" }), - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${managerOrg.id}/organizations`) - .set("Authorization", `Bearer ${managerOrgAdminApiKey}`) - .send(body) - .expect(400); - }); - - const metadataKey = "first-org-metadata-key"; - const metadataValue = "first-org-metadata-value"; - const createManagedOrganizationBody: CreateOrganizationInput = createManagedOrgInput("org", { - [metadataKey]: metadataValue, - }); - - const createManagedOrganizationBodySecond: CreateOrganizationInput = createManagedOrgInput("org2"); - - const createManagedOrganizationBodyThird: CreateOrganizationInput = createManagedOrgInput("org3"); - - it("should not allow to create managed organization if plan is below SCALE", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${essentialsPlanManagerOrg.id}/organizations`) - .set("Authorization", `Bearer ${essentialsPlanManagerOrgAdminApiKey}`) - .send(createManagedOrganizationBody) - .expect(403); - expect(response.body.error.message).toBe( - `PlatformPlanGuard - organization with id=${essentialsPlanManagerOrg.id} does not have required plan for this operation. Minimum plan is SCALE while the organization has ESSENTIALS.` - ); - }); - - it("should allow to create managed organization if plan is SCALE or above", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${payPerUserPlanManagerOrg.id}/organizations`) - .set("Authorization", `Bearer ${payPerUserPlanManagerOrgAdminApiKey}`) - .send(createManagedOrganizationBodyThird) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const managedOrg3 = responseBody.data; - expect(managedOrg3?.id).toBeDefined(); - expect(managedOrg3?.name).toEqual(createManagedOrganizationBodyThird.name); - expect(managedOrg3?.slug).toEqual(createManagedOrganizationBodyThird.slug); - expect(managedOrg3?.metadata).toEqual(createManagedOrganizationBodyThird.metadata); - expect(managedOrg3?.apiKey).toBeDefined(); - }); - }); - - it("should create managed organization", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${managerOrg.id}/organizations`) - .set("Authorization", `Bearer ${managerOrgAdminApiKey}`) - .send(createManagedOrganizationBody) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - managedOrg = responseBody.data; - expect(managedOrg?.id).toBeDefined(); - expect(managedOrg?.name).toEqual(createManagedOrganizationBody.name); - expect(managedOrg?.slug).toEqual(createManagedOrganizationBody.slug); - expect(managedOrg?.metadata).toEqual(createManagedOrganizationBody.metadata); - expect(managedOrg?.apiKey).toBeDefined(); - - // note(Lauris): check that managed organization is correctly setup in database - const managedOrgInDb = - await managedOrganizationsRepositoryFixture.getOrganizationWithManagedOrganizations(managedOrg.id); - expect(managedOrgInDb).toBeDefined(); - expect(managedOrgInDb?.id).toEqual(managedOrg.id); - expect(managedOrgInDb?.isPlatform).toEqual(true); - expect(managedOrgInDb?.isOrganization).toEqual(true); - expect(managedOrgInDb?.managedOrganization?.managedOrganizationId).toEqual(managedOrg.id); - expect(managedOrgInDb?.managedOrganization?.managerOrganizationId).toEqual(managerOrg.id); - // note(Lauris): check that manager organization is correctly setup in database - const managerOrgInDb = - await managedOrganizationsRepositoryFixture.getOrganizationWithManagedOrganizations(managerOrg.id); - expect(managerOrgInDb).toBeDefined(); - expect(managerOrgInDb?.id).toEqual(managerOrg.id); - expect(managerOrgInDb?.isPlatform).toEqual(true); - expect(managerOrgInDb?.isOrganization).toEqual(true); - expect(managerOrgInDb?.managedOrganization).toEqual(null); - expect(managerOrgInDb?.managedOrganizations?.length).toEqual(1); - expect(managerOrgInDb?.managedOrganizations?.[0]?.managedOrganizationId).toEqual(managedOrg.id); - expect(managerOrgInDb?.managedOrganizations?.[0]?.managerOrganizationId).toEqual(managerOrg.id); - - // note(Lauris): test that auth user who made request to create managed organization is OWNER of it - const membership = await membershipsRepositoryFixture.getUserMembershipByTeamId( - managerOrgAdmin.id, - managedOrg.id - ); - expect(membership?.role ?? "").toEqual("OWNER"); - expect(membership?.accepted).toEqual(true); - // note(Lauris): test that auth user who made request to create managed organization has profile in it - const managedOrgProfile = await profilesRepositoryFixture.findByOrgIdUserId( - managedOrg.id, - managerOrgAdmin.id - ); - expect(managedOrgProfile).toBeDefined(); - expect(managedOrgProfile?.id).toBeDefined(); - expect(managedOrgProfile?.username).toEqual(managerOrgAdmin.username); - // note(Lauris): test that auth user who made request to create managed organization has profile in it - const managerOrgProfile = await profilesRepositoryFixture.findByOrgIdUserId( - managerOrg.id, - managerOrgAdmin.id - ); - expect(managerOrgProfile).toBeDefined(); - expect(managerOrgProfile?.id).toBeDefined(); - expect(managerOrgProfile?.username).toEqual(managerOrgAdmin.username); - // note(Lauris): test that auth user who made request to create managed organization has movedToProfileId pointing to manager org - const user = await userRepositoryFixture.get(managerOrgAdmin.id); - expect(user?.movedToProfileId).toEqual(managerOrgProfile?.id); - // note(Lauris): check that platform billing is setup correctly for manager and managed orgs - const managerOrgBilling = await platformBillingRepositoryFixture.get(managerOrg.id); - expect(managerOrgBilling).toBeDefined(); - expect(managerOrgBilling?.id).toBeDefined(); - const customerId = managerOrgBilling?.customerId; - expect(customerId).toBeDefined(); - if (!customerId) { - throw new Error( - "organizations-organizations.controller.e2e-spec.ts: PlatformBilling customerId is not defined" - ); - } - const subscriptionId = managerOrgBilling?.subscriptionId; - expect(subscriptionId).toBeDefined(); - if (!subscriptionId) { - throw new Error( - "organizations-organizations.controller.e2e-spec.ts: PlatformBilling subscriptionId is not defined" - ); - } - const plan = managerOrgBilling?.plan; - expect(plan).toEqual("SCALE"); - - const managedOrgBilling = await platformBillingRepositoryFixture.get(managedOrg.id); - expect(managedOrgBilling).toBeDefined(); - expect(managedOrgBilling?.customerId).toEqual(customerId); - expect(managedOrgBilling?.subscriptionId).toEqual(subscriptionId); - expect(managedOrgBilling?.plan).toEqual(plan); - expect(managedOrgBilling?.managerBillingId).toEqual(managerOrgBilling?.id); - - expect(managerOrgBilling?.managedBillings?.length).toEqual(1); - expect(managerOrgBilling?.managedBillings?.[0]?.id).toEqual(managedOrgBilling?.id); - - const billings = await platformBillingRepositoryFixture.getByCustomerSubscriptionIds( - customerId, - subscriptionId - ); - expect(billings).toBeDefined(); - // note(Lauris): manager and manager organizaitons billings because managed organization client and subscription ids are same as manager organization billing row. - expect(billings?.length).toEqual(2); - - // note(Lauris): check that in database api key is generated for managed org - const managedOrgApiKeys = await apiKeysRepositoryFixture.getTeamApiKeys(managedOrg.id); - expect(managedOrgApiKeys?.length).toEqual(1); - expect(managedOrgApiKeys?.[0]?.id).toBeDefined(); - const apiKeyPrefix = getEnv("API_KEY_PREFIX", "cal_"); - const hashedApiKey = `${sha256Hash(stripApiKey(managedOrg?.apiKey, apiKeyPrefix))}`; - expect(managedOrgApiKeys?.[0]?.hashedKey).toEqual(hashedApiKey); - const expectedExpiresAt = DateTime.fromJSDate(newDate).setZone("utc").plus({ days: 30 }).toJSDate(); - expect(managedOrgApiKeys?.[0]?.expiresAt).toEqual(expectedExpiresAt); - expect(managedOrgApiKeys?.[0]?.note).toEqual( - `Managed organization API key. ManagerOrgId: ${managerOrg.id}. ManagedOrgId: ${managedOrg.id}` - ); - managedOrgApiKey = managedOrg?.apiKey; - }); - }); - - it("should not create managed organization if slug already exists", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${managerOrg.id}/organizations`) - .set("Authorization", `Bearer ${managerOrgAdminApiKey}`) - .send(createManagedOrganizationBody) - .expect(409); - - expect(response.body.error.message).toBe( - `Organization with slug '${createManagedOrganizationBody.slug}' already exists. Please, either provide a different slug or change name so that the automatically generated slug is different.` - ); - }); - - it("should create second managed organization", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${managerOrg.id}/organizations`) - .set("Authorization", `Bearer ${managerOrgAdminApiKey}`) - .send(createManagedOrganizationBodySecond) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - managedOrg2 = responseBody.data; - expect(managedOrg2?.id).toBeDefined(); - expect(managedOrg2?.name).toEqual(createManagedOrganizationBodySecond.name); - expect(managedOrg2?.slug).toEqual(createManagedOrganizationBodySecond.slug); - expect(managedOrg2?.metadata).toEqual(createManagedOrganizationBodySecond.metadata); - expect(managedOrg2?.apiKey).toBeDefined(); - }); - }); - - it("should get managed organization", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${managerOrg.id}/organizations/${managedOrg.id}`) - .set("Authorization", `Bearer ${managerOrgAdminApiKey}`) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseManagedOrg = responseBody.data; - expect(responseManagedOrg?.id).toBeDefined(); - expect(responseManagedOrg?.name).toEqual(managedOrg.name); - expect(responseManagedOrg?.metadata).toEqual(managedOrg.metadata); - }); - }); - - it("should get managed organizations", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${managerOrg.id}/organizations`) - .set("Authorization", `Bearer ${managerOrgAdminApiKey}`) - .expect(200) - .then(async (response) => { - const responseBody: GetManagedOrganizationsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseManagedOrgs = responseBody.data; - expect(responseManagedOrgs?.length).toEqual(2); - const responseManagedOrg = responseManagedOrgs.find((org) => org.id === managedOrg.id); - expect(responseManagedOrg?.id).toBeDefined(); - expect(responseManagedOrg?.name).toEqual(managedOrg.name); - expect(responseManagedOrg?.metadata).toEqual(managedOrg.metadata); - - const responseManagedOrg2 = responseManagedOrgs.find((org) => org.id === managedOrg2.id); - expect(responseManagedOrg2?.id).toBeDefined(); - expect(responseManagedOrg2?.name).toEqual(managedOrg2.name); - expect(responseManagedOrg2?.metadata).toEqual(managedOrg2.metadata); - - expect(responseBody.pagination).toBeDefined(); - expect(responseBody.pagination.totalItems).toEqual(2); - expect(responseBody.pagination.remainingItems).toEqual(0); - expect(responseBody.pagination.returnedItems).toEqual(2); - expect(responseBody.pagination.itemsPerPage).toEqual(250); - expect(responseBody.pagination.currentPage).toEqual(1); - expect(responseBody.pagination.totalPages).toEqual(1); - }); - }); - - it("should get managed organization by slug", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${managerOrg.id}/organizations?slug=${managedOrg.slug}`) - .set("Authorization", `Bearer ${managerOrgAdminApiKey}`) - .expect(200) - .then(async (response) => { - const responseBody: GetManagedOrganizationsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseManagedOrgs = responseBody.data; - expect(responseManagedOrgs?.length).toEqual(1); - const responseManagedOrg = responseManagedOrgs.find((org) => org.id === managedOrg.id); - expect(responseManagedOrg?.id).toBeDefined(); - expect(responseManagedOrg?.name).toEqual(managedOrg.name); - expect(responseManagedOrg?.metadata).toEqual(managedOrg.metadata); - - expect(responseBody.pagination).toBeDefined(); - expect(responseBody.pagination.totalItems).toEqual(1); - expect(responseBody.pagination.remainingItems).toEqual(0); - expect(responseBody.pagination.returnedItems).toEqual(1); - expect(responseBody.pagination.itemsPerPage).toEqual(250); - expect(responseBody.pagination.currentPage).toEqual(1); - expect(responseBody.pagination.totalPages).toEqual(1); - }); - }); - - it("should get managed organization by metadata key", async () => { - return request(app.getHttpServer()) - .get( - `/v2/organizations/${managerOrg.id}/organizations?metadataKey=${metadataKey}&metadataValue=${metadataValue}` - ) - .set("Authorization", `Bearer ${managerOrgAdminApiKey}`) - .expect(200) - .then(async (response) => { - const responseBody: GetManagedOrganizationsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseManagedOrgs = responseBody.data; - expect(responseManagedOrgs?.length).toEqual(1); - const responseManagedOrg = responseManagedOrgs.find((org) => org.id === managedOrg.id); - expect(responseManagedOrg?.id).toBeDefined(); - expect(responseManagedOrg?.name).toEqual(managedOrg.name); - expect(responseManagedOrg?.metadata).toEqual(managedOrg.metadata); - - expect(responseBody.pagination).toBeDefined(); - expect(responseBody.pagination.totalItems).toEqual(1); - expect(responseBody.pagination.remainingItems).toEqual(0); - expect(responseBody.pagination.returnedItems).toEqual(1); - expect(responseBody.pagination.itemsPerPage).toEqual(250); - expect(responseBody.pagination.currentPage).toEqual(1); - expect(responseBody.pagination.totalPages).toEqual(1); - }); - }); - - it("should not update managed organization with string metadata", async () => { - const body = { - metadata: JSON.stringify({ key: "value" }), - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${managerOrg.id}/organizations/${managedOrg.id}`) - .set("Authorization", `Bearer ${managerOrgAdminApiKey}`) - .send(body) - .expect(400); - }); - - it("should update managed organization ", async () => { - const name = `new organizations organizations org ${randomString()}`; - const metadata = { - updatedKey: "updatedValue", - }; - const body: UpdateOrganizationInput = { - name, - metadata, - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${managerOrg.id}/organizations/${managedOrg.id}`) - .set("Authorization", `Bearer ${managerOrgAdminApiKey}`) - .send(body) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseManagedOrg = responseBody.data; - expect(responseManagedOrg?.id).toBeDefined(); - expect(responseManagedOrg?.name).toEqual(name); - expect(responseManagedOrg?.metadata).toEqual(metadata); - - const managedOrgInDb = - await managedOrganizationsRepositoryFixture.getOrganizationWithManagedOrganizations(managedOrg.id); - expect(managedOrgInDb).toBeDefined(); - expect(managedOrgInDb?.name).toEqual(name); - expect(managedOrgInDb?.metadata).toEqual(metadata); - - managedOrg = { ...managedOrg, name, metadata }; - }); - }); - - it("should refresh api key for managed organization with a custom duration", async () => { - return request(app.getHttpServer()) - .post(`/v2/api-keys/refresh`) - .send({ apiKeyDaysValid: 60 }) - .set("Authorization", `Bearer ${managedOrgApiKey}`) - .expect(200) - .then(async (response) => { - const responseBody: RefreshApiKeyOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseData = responseBody.data; - const newApiKey = responseData?.apiKey; - expect(newApiKey).toBeDefined(); - expect(newApiKey).not.toEqual(managedOrgApiKey); - - const managedOrgApiKeys = await apiKeysRepositoryFixture.getTeamApiKeys(managedOrg.id); - expect(managedOrgApiKeys?.length).toEqual(1); - expect(managedOrgApiKeys?.[0]?.id).toBeDefined(); - const apiKeyPrefix = getEnv("API_KEY_PREFIX", "cal_"); - const hashedApiKey = `${sha256Hash(stripApiKey(newApiKey, apiKeyPrefix))}`; - expect(managedOrgApiKeys?.[0]?.hashedKey).toEqual(hashedApiKey); - const expectedExpiresAt = DateTime.fromJSDate(newDate).setZone("utc").plus({ days: 60 }).toJSDate(); - expect(managedOrgApiKeys?.[0]?.expiresAt).toEqual(expectedExpiresAt); - expect(managedOrgApiKeys?.[0]?.note).toEqual( - `Managed organization API key. ManagerOrgId: ${managerOrg.id}. ManagedOrgId: ${managedOrg.id}` - ); - managedOrgApiKey = newApiKey; - }); - }); - - it("should create OAuth client for managed organization", async () => { - return request(app.getHttpServer()) - .post(`/v2/oauth-clients`) - .send(createOAuthClientBody) - .set("Authorization", `Bearer ${managedOrgApiKey}`) - .expect(201) - .then(async (response) => { - const responseBody: CreateOAuthClientResponseDto = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseData = responseBody.data; - const clientId = responseData?.clientId; - const clientSecret = responseData?.clientSecret; - expect(clientId).toBeDefined(); - expect(clientSecret).toBeDefined(); - - const managedOrgOAuthClients = await oAuthClientsRepositoryFixture.getByOrgId(managedOrg.id); - expect(managedOrgOAuthClients?.length).toEqual(1); - expect(managedOrgOAuthClients?.[0]?.id).toBeDefined(); - expect(managedOrgOAuthClients?.[0]?.id).toEqual(clientId); - expect(managedOrgOAuthClients?.[0]?.secret).toEqual(clientSecret); - expect(managedOrgOAuthClients?.[0]?.name).toEqual(createOAuthClientBody.name); - expect(managedOrgOAuthClients?.[0]?.redirectUris).toEqual(createOAuthClientBody.redirectUris); - expect(managedOrgOAuthClients?.[0]?.permissions).toEqual( - EVENT_TYPE_READ + - EVENT_TYPE_WRITE + - BOOKING_READ + - BOOKING_WRITE + - SCHEDULE_READ + - SCHEDULE_WRITE + - APPS_READ + - APPS_WRITE + - PROFILE_READ + - PROFILE_WRITE - ); - managedOrgOAuthClientId = clientId; - managedOrgOAuthClientSecret = clientSecret; - }); - }); - - it("should fetch OAuth client for managed organization", async () => { - return request(app.getHttpServer()) - .get(`/v2/oauth-clients/${managedOrgOAuthClientId}`) - .set(X_CAL_SECRET_KEY, managedOrgOAuthClientSecret) - .expect(200) - .then(async (response) => { - const responseBody: GetOAuthClientResponseDto = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseData = responseBody.data; - expect(responseData?.id).toEqual(managedOrgOAuthClientId); - expect(responseData?.secret).toEqual(managedOrgOAuthClientSecret); - expect(responseData?.name).toEqual(createOAuthClientBody.name); - expect(responseData?.redirectUris).toEqual(createOAuthClientBody.redirectUris); - expect(responseData?.permissions).toEqual([ - "EVENT_TYPE_READ", - "EVENT_TYPE_WRITE", - "BOOKING_READ", - "BOOKING_WRITE", - "SCHEDULE_READ", - "SCHEDULE_WRITE", - "APPS_READ", - "APPS_WRITE", - "PROFILE_READ", - "PROFILE_WRITE", - ]); - }); - }); - - it("should delete managed organization", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${managerOrg.id}/organizations/${managedOrg2.id}`) - .set("Authorization", `Bearer ${managerOrgAdminApiKey}`) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseManagedOrg = responseBody.data; - expect(responseManagedOrg?.id).toBeDefined(); - expect(responseManagedOrg?.id).toEqual(managedOrg2.id); - expect(responseManagedOrg?.name).toEqual(managedOrg2.name); - - const managedOrgInDb = - await managedOrganizationsRepositoryFixture.getOrganizationWithManagedOrganizations(managedOrg2.id); - expect(managedOrgInDb).toEqual(null); - - const billings = await platformBillingRepositoryFixture.getByCustomerSubscriptionIds( - managerOrgBilling.customerId, - managerOrgBilling.subscriptionId! - ); - expect(billings).toBeDefined(); - // note(Lauris): manager billing is left and other managed org - expect(billings?.length).toEqual(2); - - const managerOrgInDb = - await managedOrganizationsRepositoryFixture.getOrganizationWithManagedOrganizations(managerOrg.id); - expect(managerOrgInDb).toBeDefined(); - expect(managerOrgInDb?.id).toEqual(managerOrg.id); - expect(managerOrgInDb?.managedOrganizations?.length).toEqual(1); - }); - }); - - it("should delete managed organization", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${managerOrg.id}/organizations/${managedOrg.id}`) - .set("Authorization", `Bearer ${managerOrgAdminApiKey}`) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseManagedOrg = responseBody.data; - expect(responseManagedOrg?.id).toBeDefined(); - expect(responseManagedOrg?.id).toEqual(managedOrg.id); - expect(responseManagedOrg?.name).toEqual(managedOrg.name); - - const managedOrgInDb = - await managedOrganizationsRepositoryFixture.getOrganizationWithManagedOrganizations(managedOrg.id); - expect(managedOrgInDb).toEqual(null); - - const billings = await platformBillingRepositoryFixture.getByCustomerSubscriptionIds( - managerOrgBilling.customerId, - managerOrgBilling.subscriptionId! - ); - expect(billings).toBeDefined(); - // note(Lauris): only manager billing is left - expect(billings?.length).toEqual(1); - - const managerOrgInDb = - await managedOrganizationsRepositoryFixture.getOrganizationWithManagedOrganizations(managerOrg.id); - expect(managerOrgInDb).toBeDefined(); - expect(managerOrgInDb?.id).toEqual(managerOrg.id); - expect(managerOrgInDb?.managedOrganizations?.length).toEqual(0); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(managerOrgAdmin.email); - await userRepositoryFixture.deleteByEmail(payPerUserPlanManagerOrgAdmin.email); - await userRepositoryFixture.deleteByEmail(essentialsPlanManagerOrgAdmin.email); - await organizationsRepositoryFixture.delete(managerOrg.id); - await organizationsRepositoryFixture.delete(payPerUserPlanManagerOrg.id); - await organizationsRepositoryFixture.delete(essentialsPlanManagerOrg.id); - await app.close(); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.ts b/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.ts deleted file mode 100644 index b23686c1504af8..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { SkipTakePagination } from "@calcom/platform-types"; -import { - Body, - Controller, - Delete, - Get, - HttpCode, - HttpStatus, - Param, - ParseIntPipe, - Patch, - Post, - Query, - UseGuards, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { X_CAL_CLIENT_ID_HEADER, X_CAL_SECRET_KEY_HEADER } from "@/lib/docs/headers"; -import { Throttle } from "@/lib/endpoint-throttler-decorator"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsManagedOrgInManagerOrg } from "@/modules/auth/guards/organizations/is-managed-org-in-manager-org.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; -import { CreateOrganizationInput } from "@/modules/organizations/organizations/inputs/create-managed-organization.input"; -import { GetManagedOrganizationsInput_2024_08_13 } from "@/modules/organizations/organizations/inputs/get-managed-organizations.input"; -import { UpdateOrganizationInput } from "@/modules/organizations/organizations/inputs/update-managed-organization.input"; -import { CreateManagedOrganizationOutput } from "@/modules/organizations/organizations/outputs/create-managed-organization.output"; -import { GetManagedOrganizationOutput } from "@/modules/organizations/organizations/outputs/get-managed-organization.output"; -import { GetManagedOrganizationsOutput } from "@/modules/organizations/organizations/outputs/get-managed-organizations.output"; -import { ManagedOrganizationsService } from "@/modules/organizations/organizations/services/managed-organizations.service"; - -const SCALE = "SCALE"; - -@Controller({ - path: "/v2/organizations/:orgId/organizations", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Managed Orgs") -@ApiHeader(X_CAL_CLIENT_ID_HEADER) -@ApiHeader(X_CAL_SECRET_KEY_HEADER) -@ApiParam({ name: "orgId", type: Number, required: true }) -export class OrganizationsOrganizationsController { - constructor(private readonly managedOrganizationsService: ManagedOrganizationsService) {} - - @Post() - @Roles("ORG_ADMIN") - @PlatformPlan(SCALE) - @ApiOperation({ - summary: "Create an organization within an organization", - description: - "Requires the user to have at least the 'ORG_ADMIN' role within the organization. Additionally, for platform, the plan must be 'SCALE' or higher to access this endpoint.", - }) - async createOrganization( - @Param("orgId", ParseIntPipe) managerOrganizationId: number, - @GetUser() authUser: ApiAuthGuardUser, - @Body() body: CreateOrganizationInput - ): Promise { - const organization = await this.managedOrganizationsService.createManagedOrganization( - authUser, - managerOrganizationId, - body - ); - - return { - status: SUCCESS_STATUS, - data: organization, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan(SCALE) - @Get("/:managedOrganizationId") - @ApiOperation({ - summary: "Get an organization within an organization", - description: - "Requires the user to have at least the 'ORG_ADMIN' role within the organization. Additionally, for platform, the plan must be 'SCALE' or higher to access this endpoint.", - }) - @UseGuards(IsManagedOrgInManagerOrg) - async getOrganization( - @Param("managedOrganizationId", ParseIntPipe) managedOrganizationId: number - ): Promise { - const organization = await this.managedOrganizationsService.getManagedOrganization(managedOrganizationId); - return { - status: SUCCESS_STATUS, - data: organization, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan(SCALE) - @Get("/") - @ApiOperation({ - summary: "Get all organizations within an organization", - description: - "Requires the user to have at least the 'ORG_ADMIN' role within the organization. Additionally, for platform, the plan must be 'SCALE' or higher to access this endpoint.", - }) - async getOrganizations( - @Param("orgId", ParseIntPipe) managerOrganizationId: number, - @Query() query: GetManagedOrganizationsInput_2024_08_13 - ): Promise { - const { organizations, pagination: responsePagination } = - await this.managedOrganizationsService.getManagedOrganizations(managerOrganizationId, query); - return { - status: SUCCESS_STATUS, - data: organizations, - pagination: responsePagination, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan(SCALE) - @Patch("/:managedOrganizationId") - @ApiOperation({ - summary: "Update an organization within an organization", - description: - "Requires the user to have at least the 'ORG_ADMIN' role within the organization. Additionally, for platform, the plan must be 'SCALE' or higher to access this endpoint.", - }) - @UseGuards(IsManagedOrgInManagerOrg) - @HttpCode(HttpStatus.OK) - async updateOrganization( - @Param("orgId", ParseIntPipe) managerOrganizationId: number, - @Param("managedOrganizationId", ParseIntPipe) managedOrganizationId: number, - @Body() body: UpdateOrganizationInput - ): Promise { - const organization = await this.managedOrganizationsService.updateManagedOrganization( - managedOrganizationId, - body - ); - return { - status: SUCCESS_STATUS, - data: organization, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan(SCALE) - @Delete("/:managedOrganizationId") - @Throttle({ limit: 1, ttl: 1000, blockDuration: 1000, name: "organizations_delete" }) - @ApiOperation({ - summary: "Delete an organization within an organization", - description: - "Requires the user to have at least the 'ORG_ADMIN' role within the organization. Additionally, for platform, the plan must be 'SCALE' or higher to access this endpoint.", - }) - @UseGuards(IsManagedOrgInManagerOrg) - async deleteOrganization( - @Param("managedOrganizationId", ParseIntPipe) managedOrganizationId: number - ): Promise { - const organization = - await this.managedOrganizationsService.deleteManagedOrganization(managedOrganizationId); - return { - status: SUCCESS_STATUS, - data: organization, - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.module.ts deleted file mode 100644 index 0f7224f480e960..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.module.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { BullModule } from "@nestjs/bull"; -import { Module } from "@nestjs/common"; -import { CALENDARS_QUEUE } from "@/ee/calendars/processors/calendars.processor"; -import { CalendarsTaskerModule } from "@/lib/modules/calendars-tasker.module"; -import { ApiKeysModule } from "@/modules/api-keys/api-keys.module"; -import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.billing.service"; -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository"; -import { OrganizationsDelegationCredentialService } from "@/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository"; -import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service"; -import { OrganizationsMembershipOutputService } from "@/modules/organizations/memberships/services/organizations-membership-output.service"; -import { ManagedOrganizationsRepository } from "@/modules/organizations/organizations/managed-organizations.repository"; -import { OrganizationsOrganizationsController } from "@/modules/organizations/organizations/organizations-organizations.controller"; -import { ManagedOrganizationsService } from "@/modules/organizations/organizations/services/managed-organizations.service"; -import { ManagedOrganizationsOutputService } from "@/modules/organizations/organizations/services/managed-organizations-output.service"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { ProfilesModule } from "@/modules/profiles/profiles.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { UsersRepository } from "@/modules/users/users.repository"; - -@Module({ - imports: [ - PrismaModule, - RedisModule, - StripeModule, - MembershipsModule, - ApiKeysModule, - ProfilesModule, - BullModule.registerQueue({ - name: CALENDARS_QUEUE, - limiter: { - max: 1, - duration: 1000, - }, - }), - CalendarsTaskerModule, - ], - providers: [ - ManagedOrganizationsService, - ManagedOrganizationsRepository, - ManagedOrganizationsBillingService, - OrganizationsRepository, - UsersRepository, - OrganizationsDelegationCredentialRepository, - OrganizationsDelegationCredentialService, - OrganizationsMembershipService, - OrganizationsMembershipOutputService, - OrganizationsMembershipRepository, - ManagedOrganizationsOutputService, - ], - controllers: [OrganizationsOrganizationsController], -}) -export class OrganizationsOrganizationsModule {} diff --git a/apps/api/v2/src/modules/organizations/organizations/outputs/create-managed-organization.output.ts b/apps/api/v2/src/modules/organizations/organizations/outputs/create-managed-organization.output.ts deleted file mode 100644 index 60be617211fbde..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/outputs/create-managed-organization.output.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ManagedOrganizationWithApiKeyOutput } from "@/modules/organizations/organizations/outputs/managed-organization.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum, ValidateNested } from "class-validator"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; - -export class CreateManagedOrganizationOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ type: ManagedOrganizationWithApiKeyOutput }) - @Expose() - @ValidateNested() - @Type(() => ManagedOrganizationWithApiKeyOutput) - data!: ManagedOrganizationWithApiKeyOutput; -} diff --git a/apps/api/v2/src/modules/organizations/organizations/outputs/get-managed-organization.output.ts b/apps/api/v2/src/modules/organizations/organizations/outputs/get-managed-organization.output.ts deleted file mode 100644 index 651f19c5634179..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/outputs/get-managed-organization.output.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ManagedOrganizationOutput } from "@/modules/organizations/organizations/outputs/managed-organization.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum, ValidateNested } from "class-validator"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; - -export class GetManagedOrganizationOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ type: ManagedOrganizationOutput }) - @Expose() - @ValidateNested() - @Type(() => ManagedOrganizationOutput) - data!: ManagedOrganizationOutput; -} diff --git a/apps/api/v2/src/modules/organizations/organizations/outputs/get-managed-organizations.output.ts b/apps/api/v2/src/modules/organizations/organizations/outputs/get-managed-organizations.output.ts deleted file mode 100644 index cf52f8d674acd8..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/outputs/get-managed-organizations.output.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ManagedOrganizationOutput } from "@/modules/organizations/organizations/outputs/managed-organization.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum, ValidateNested } from "class-validator"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import { PaginationMetaDto } from "@calcom/platform-types"; - -export class GetManagedOrganizationsOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: [ManagedOrganizationOutput], - }) - @Expose() - @ValidateNested({ each: true }) - @Type(() => ManagedOrganizationOutput) - data!: ManagedOrganizationOutput[]; - - @ApiProperty({ type: () => PaginationMetaDto }) - @Type(() => PaginationMetaDto) - @ValidateNested() - pagination!: PaginationMetaDto; -} diff --git a/apps/api/v2/src/modules/organizations/organizations/outputs/managed-organization.output.ts b/apps/api/v2/src/modules/organizations/organizations/outputs/managed-organization.output.ts deleted file mode 100644 index 4dd313e2c88e0c..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/outputs/managed-organization.output.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { Expose, Transform, Type } from "class-transformer"; -import { IsBoolean, IsInt, IsObject, IsOptional, IsString, Length, ValidateNested } from "class-validator"; - -import { Metadata } from "@calcom/platform-types"; - -export class ManagedOrganizationOutput { - @IsInt() - @Expose() - @ApiProperty() - readonly id!: number; - - @IsString() - @Length(1) - @Expose() - @ApiProperty() - readonly name!: string; - - @IsString() - @IsOptional() - @Expose() - @ApiPropertyOptional() - readonly slug?: string; - - @ApiPropertyOptional({ - type: Object, - example: { key: "value" }, - }) - @IsObject() - @IsOptional() - @Expose() - @Transform( - // note(Lauris): added this transform because without it metadata is removed for some reason - ({ obj }: { obj: { metadata: Metadata | null | undefined } }) => { - return obj.metadata || undefined; - } - ) - metadata?: Metadata; -} - -export class ManagedOrganizationWithApiKeyOutput extends ManagedOrganizationOutput { - @IsString() - @Expose() - @ApiProperty() - readonly apiKey!: string; -} diff --git a/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations-output.service.ts b/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations-output.service.ts deleted file mode 100644 index d688678fc4f3d1..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations-output.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ManagedOrganizationOutput } from "@/modules/organizations/organizations/outputs/managed-organization.output"; -import { Injectable } from "@nestjs/common"; -import { plainToClass } from "class-transformer"; - -import type { Team } from "@calcom/prisma/client"; - -@Injectable() -export class ManagedOrganizationsOutputService { - getOutputManagedOrganization(managedOrganization: Team): ManagedOrganizationOutput { - return plainToClass(ManagedOrganizationOutput, managedOrganization, { strategy: "excludeAll" }); - } -} diff --git a/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations.service.ts b/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations.service.ts deleted file mode 100644 index f423fb35acb1ba..00000000000000 --- a/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations.service.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { getPagination } from "@/lib/pagination/pagination"; -import { ApiKeysService } from "@/modules/api-keys/services/api-keys.service"; -import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; -import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.billing.service"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service"; -import { CreateOrganizationInput } from "@/modules/organizations/organizations/inputs/create-managed-organization.input"; -import { GetManagedOrganizationsInput_2024_08_13 } from "@/modules/organizations/organizations/inputs/get-managed-organizations.input"; -import { UpdateOrganizationInput } from "@/modules/organizations/organizations/inputs/update-managed-organization.input"; -import { ManagedOrganizationsRepository } from "@/modules/organizations/organizations/managed-organizations.repository"; -import { ManagedOrganizationsOutputService } from "@/modules/organizations/organizations/services/managed-organizations-output.service"; -import { ProfilesRepository } from "@/modules/profiles/profiles.repository"; -import { ConflictException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; - -import { slugify } from "@calcom/platform-libraries"; - -@Injectable() -export class ManagedOrganizationsService { - constructor( - private readonly managedOrganizationsRepository: ManagedOrganizationsRepository, - private readonly organizationsRepository: OrganizationsRepository, - private readonly managedOrganizationsBillingService: ManagedOrganizationsBillingService, - private readonly organizationsMembershipService: OrganizationsMembershipService, - private readonly apiKeysService: ApiKeysService, - private readonly managedOrganizationsOutputService: ManagedOrganizationsOutputService, - private readonly profilesRepository: ProfilesRepository - ) {} - - async createManagedOrganization( - authUser: ApiAuthGuardUser, - managerOrganizationId: number, - organizationInput: CreateOrganizationInput - ) { - const isManagerOrganizationPlatform = await this.isManagerOrganizationPlatform(managerOrganizationId); - if (!isManagerOrganizationPlatform) { - throw new ForbiddenException( - "Manager organization must be a platform organization. Normal organizations can't create managed organizations yet." - ); - } - - const { apiKeyDaysValid, apiKeyNeverExpires, ...organizationData } = organizationInput; - - const effectiveSlug = organizationData.slug || slugify(organizationData.name); - - if (!organizationData.slug) { - organizationData.slug = effectiveSlug; - } - - const existingManagedOrganization = - await this.managedOrganizationsRepository.getManagedOrganizationBySlug( - managerOrganizationId, - effectiveSlug - ); - - if (existingManagedOrganization) { - throw new ConflictException( - `Organization with slug '${organizationData.slug}' already exists. Please, either provide a different slug or change name so that the automatically generated slug is different.` - ); - } - - const organization = await this.managedOrganizationsRepository.createManagedOrganization( - managerOrganizationId, - { - ...organizationData, - isOrganization: true, - isPlatform: true, - metadata: organizationData.metadata, - } - ); - - await this.organizationsMembershipService.createOrgMembership(organization.id, { - userId: authUser.id, - accepted: true, - role: "OWNER", - }); - - const defaultProfileUsername = `${organization.name}-${authUser.id}`; - await this.profilesRepository.createProfile( - organization.id, - authUser.id, - authUser.username || defaultProfileUsername - ); - - await this.managedOrganizationsBillingService.createManagedOrganizationBilling( - managerOrganizationId, - organization.id - ); - - const apiKey = await this.apiKeysService.createApiKey(authUser.id, { - apiKeyDaysValid, - apiKeyNeverExpires, - note: `Managed organization API key. ManagerOrgId: ${managerOrganizationId}. ManagedOrgId: ${organization.id}`, - teamId: organization.id, - }); - - const outputOrganization = - this.managedOrganizationsOutputService.getOutputManagedOrganization(organization); - - return { - ...outputOrganization, - apiKey, - }; - } - - private async isManagerOrganizationPlatform(managerOrganizationId: number) { - const organization = await this.organizationsRepository.findById({ id: managerOrganizationId }); - return !!organization?.isPlatform; - } - - async getManagedOrganization(managedOrganizationId: number) { - const organization = await this.organizationsRepository.findById({ id: managedOrganizationId }); - if (!organization) { - throw new NotFoundException(`Managed organization with id=${managedOrganizationId} does not exist.`); - } - return this.managedOrganizationsOutputService.getOutputManagedOrganization(organization); - } - - async getManagedOrganizations( - managerOrganizationId: number, - query: GetManagedOrganizationsInput_2024_08_13 - ) { - const { items: managedOrganizations, totalItems } = - await this.managedOrganizationsRepository.getByManagerOrganizationIdPaginated( - managerOrganizationId, - query - ); - - return { - organizations: managedOrganizations.map((managedOrganization) => - this.managedOrganizationsOutputService.getOutputManagedOrganization(managedOrganization) - ), - pagination: getPagination({ - ...query, - totalCount: totalItems, - }), - }; - } - - async updateManagedOrganization(managedOrganizationId: number, body: UpdateOrganizationInput) { - const organization = await this.organizationsRepository.update(managedOrganizationId, body); - return this.managedOrganizationsOutputService.getOutputManagedOrganization(organization); - } - - async deleteManagedOrganization(managedOrganizationId: number) { - const organization = await this.organizationsRepository.delete(managedOrganizationId); - return this.managedOrganizationsOutputService.getOutputManagedOrganization(organization); - } -} diff --git a/apps/api/v2/src/modules/organizations/roles/inputs/base-org-role.input.ts b/apps/api/v2/src/modules/organizations/roles/inputs/base-org-role.input.ts deleted file mode 100644 index aee4f5cb3f9fac..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/inputs/base-org-role.input.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsArray, IsOptional, IsString, Validate } from "class-validator"; - -import type { PermissionString } from "@calcom/platform-libraries/pbac"; -import { getAllPermissionStringsForScope, Scope } from "@calcom/platform-libraries/pbac"; - -import { OrgPermissionStringValidator } from "../permissions/inputs/validators/org-permission-string.validator"; - -export const orgPermissionEnum = [...getAllPermissionStringsForScope(Scope.Organization)] as const; - -export class BaseOrgRoleInput { - @ApiPropertyOptional({ description: "Color for the role (hex code)" }) - @IsString() - @IsOptional() - color?: string; - - @ApiPropertyOptional({ description: "Description of the role" }) - @IsString() - @IsOptional() - description?: string; - - @ApiPropertyOptional({ - description: - "Permissions for this role (format: resource.action). On update, this field replaces the entire permission set for the role (full replace). Use granular permission endpoints for one-by-one changes.", - enum: orgPermissionEnum, - isArray: true, - example: ["eventType.read", "eventType.create", "booking.read"], - }) - @IsArray() - @IsString({ each: true }) - @Validate(OrgPermissionStringValidator, { each: true }) - @IsOptional() - permissions?: PermissionString[]; -} diff --git a/apps/api/v2/src/modules/organizations/roles/inputs/create-org-role.input.ts b/apps/api/v2/src/modules/organizations/roles/inputs/create-org-role.input.ts deleted file mode 100644 index 75b8397a9338b9..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/inputs/create-org-role.input.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsString, MinLength } from "class-validator"; - -import { BaseOrgRoleInput } from "./base-org-role.input"; - -export class CreateOrgRoleInput extends BaseOrgRoleInput { - @ApiProperty({ description: "Name of the role", minLength: 1 }) - @IsString() - @MinLength(1) - name!: string; -} diff --git a/apps/api/v2/src/modules/organizations/roles/inputs/update-org-role.input.ts b/apps/api/v2/src/modules/organizations/roles/inputs/update-org-role.input.ts deleted file mode 100644 index 3d9840187a3e37..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/inputs/update-org-role.input.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsString, IsOptional, MinLength } from "class-validator"; - -import { BaseOrgRoleInput } from "./base-org-role.input"; - -export class UpdateOrgRoleInput extends BaseOrgRoleInput { - @ApiPropertyOptional({ description: "Name of the role", minLength: 1 }) - @IsString() - @MinLength(1) - @IsOptional() - name?: string; -} diff --git a/apps/api/v2/src/modules/organizations/roles/organizations-roles.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/roles/organizations-roles.controller.e2e-spec.ts deleted file mode 100644 index c76024b5dd5e15..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/organizations-roles.controller.e2e-spec.ts +++ /dev/null @@ -1,635 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { RoleService } from "@calcom/platform-libraries/pbac"; -import type { Team, User } from "@calcom/prisma/client"; -import { MembershipRole } from "@calcom/prisma/enums"; -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 { FeaturesRepositoryFixture } from "test/fixtures/repository/features.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { PbacGuard } from "@/modules/auth/guards/pbac/pbac.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { CreateOrgRoleInput } from "@/modules/organizations/roles/inputs/create-org-role.input"; -import { UpdateOrgRoleInput } from "@/modules/organizations/roles/inputs/update-org-role.input"; -import { CreateOrgRoleOutput } from "@/modules/organizations/roles/outputs/create-org-role.output"; -import { DeleteOrgRoleOutput } from "@/modules/organizations/roles/outputs/delete-org-role.output"; -import { GetAllOrgRolesOutput } from "@/modules/organizations/roles/outputs/get-all-org-roles.output"; -import { GetOrgRoleOutput } from "@/modules/organizations/roles/outputs/get-org-role.output"; -import { UpdateOrgRoleOutput } from "@/modules/organizations/roles/outputs/update-org-role.output"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Roles Endpoints", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipRepositoryFixture: MembershipRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let featuresRepositoryFixture: FeaturesRepositoryFixture; - let roleService: RoleService; - - // Test users - let legacyOrgAdminUser: User; - let legacyOrgMemberUser: User; - let pbacOrgUserWithRolePermission: User; - let pbacOrgUserWithoutRolePermission: User; - let nonOrgUser: User; - - // API Keys - let legacyOrgAdminApiKey: string; - let legacyOrgMemberApiKey: string; - let pbacOrgUserWithRolePermissionApiKey: string; - let pbacOrgUserWithoutRolePermissionApiKey: string; - let nonOrgUserApiKey: string; - - // Organization - let organization: Team; - let pbacEnabledOrganization: Team; - - const legacyOrgAdminEmail = `legacy-org-admin-${randomString()}@api.com`; - const legacyOrgMemberEmail = `legacy-org-member-${randomString()}@api.com`; - const pbacOrgUserWithRolePermissionEmail = `pbac-org-user-with-role-permission-${randomString()}@api.com`; - const pbacOrgUserWithoutRolePermissionEmail = `pbac-org-user-without-role-permission-${randomString()}@api.com`; - const nonOrgUserEmail = `non-org-user-${randomString()}@api.com`; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - featuresRepositoryFixture = new FeaturesRepositoryFixture(moduleRef); - roleService = new RoleService(); - - // Create test users - legacyOrgAdminUser = await userRepositoryFixture.create({ - email: legacyOrgAdminEmail, - username: legacyOrgAdminEmail, - }); - - legacyOrgMemberUser = await userRepositoryFixture.create({ - email: legacyOrgMemberEmail, - username: legacyOrgMemberEmail, - }); - - pbacOrgUserWithRolePermission = await userRepositoryFixture.create({ - email: pbacOrgUserWithRolePermissionEmail, - username: pbacOrgUserWithRolePermissionEmail, - }); - - pbacOrgUserWithoutRolePermission = await userRepositoryFixture.create({ - email: pbacOrgUserWithoutRolePermissionEmail, - username: pbacOrgUserWithoutRolePermissionEmail, - }); - - nonOrgUser = await userRepositoryFixture.create({ - email: nonOrgUserEmail, - username: nonOrgUserEmail, - }); - - // Create organizations - organization = await organizationsRepositoryFixture.create({ - name: `org-roles-test-${randomString()}`, - isOrganization: true, - }); - - pbacEnabledOrganization = await organizationsRepositoryFixture.create({ - name: `pbac-org-roles-test-${randomString()}`, - isOrganization: true, - }); - - await featuresRepositoryFixture.create({ slug: "pbac", enabled: true }); - await featuresRepositoryFixture.setTeamFeatureState({ - teamId: pbacEnabledOrganization.id, - featureId: "pbac", - state: "enabled", - }); - - // Create memberships - await membershipRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: legacyOrgAdminUser.id } }, - team: { connect: { id: organization.id } }, - }); - - await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: legacyOrgMemberUser.id } }, - team: { connect: { id: organization.id } }, - }); - - const pbacOrgUserWithRolePermissionMembership = await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: pbacOrgUserWithRolePermission.id } }, - team: { connect: { id: pbacEnabledOrganization.id } }, - }); - - const pbacOrgUserWithoutRolePermissionMembership = await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: pbacOrgUserWithoutRolePermission.id } }, - team: { connect: { id: pbacEnabledOrganization.id } }, - }); - - const roleWithPermission = await roleService.createRole({ - name: "Role Manager", - teamId: pbacEnabledOrganization.id, - permissions: ["role.create", "role.read", "role.update", "role.delete"], - type: "CUSTOM", - }); - - const roleWithoutPermission = await roleService.createRole({ - name: "Basic Role", - teamId: pbacEnabledOrganization.id, - permissions: ["booking.read"], - type: "CUSTOM", - }); - - await roleService.assignRoleToMember(roleWithPermission.id, pbacOrgUserWithRolePermissionMembership.id); - await roleService.assignRoleToMember( - roleWithoutPermission.id, - pbacOrgUserWithoutRolePermissionMembership.id - ); - - const { keyString: legacyOrgAdminKeyString } = await apiKeysRepositoryFixture.createApiKey( - legacyOrgAdminUser.id, - null - ); - legacyOrgAdminApiKey = `cal_test_${legacyOrgAdminKeyString}`; - - const { keyString: legacyOrgMemberKeyString } = await apiKeysRepositoryFixture.createApiKey( - legacyOrgMemberUser.id, - null - ); - legacyOrgMemberApiKey = `cal_test_${legacyOrgMemberKeyString}`; - - const { keyString: pbacOrgUserWithRolePermissionKeyString } = await apiKeysRepositoryFixture.createApiKey( - pbacOrgUserWithRolePermission.id, - null - ); - pbacOrgUserWithRolePermissionApiKey = `cal_test_${pbacOrgUserWithRolePermissionKeyString}`; - - const { keyString: pbacOrgUserWithoutRolePermissionKeyString } = - await apiKeysRepositoryFixture.createApiKey(pbacOrgUserWithoutRolePermission.id, null); - pbacOrgUserWithoutRolePermissionApiKey = `cal_test_${pbacOrgUserWithoutRolePermissionKeyString}`; - - const { keyString: nonOrgUserKeyString } = await apiKeysRepositoryFixture.createApiKey( - nonOrgUser.id, - null - ); - nonOrgUserApiKey = `cal_test_${nonOrgUserKeyString}`; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - describe("Role Creation Authorization", () => { - beforeEach(() => { - jest.restoreAllMocks(); - }); - - describe("Positive Tests", () => { - it("should allow role creation when organization has PBAC enabled and user has a create permission", async () => { - const createRoleInput: CreateOrgRoleInput = { - name: "Test Role PBAC", - permissions: ["booking.read", "eventType.create"], - }; - - const pbacSpyCanActivate = jest.spyOn(PbacGuard.prototype, "canActivate"); - const pbacSpyHasPbacEnabled = jest.spyOn( - PbacGuard.prototype as unknown as PbacGuard, - "hasPbacEnabled" - ); - - const rolesSpyCanActivate = jest.spyOn(RolesGuard.prototype, "canActivate"); - const rolesSpyCheckUserRoleAccess = jest.spyOn( - RolesGuard.prototype as unknown as RolesGuard, - "checkUserRoleAccess" - ); - - return request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(createRoleInput) - .expect(201) - .then(async (response) => { - const responseBody: CreateOrgRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.name).toEqual(createRoleInput.name); - expect(responseBody.data.permissions).toEqual(createRoleInput.permissions); - expect(responseBody.data.organizationId).toEqual(pbacEnabledOrganization.id); - - expect(pbacSpyCanActivate).toHaveBeenCalled(); - expect(pbacSpyHasPbacEnabled).toHaveBeenCalled(); - expect(rolesSpyCanActivate).toHaveBeenCalled(); - const pbacCanActivateResult = await pbacSpyCanActivate.mock.results[0].value; - const rolesCanActivateResult = await rolesSpyCanActivate.mock.results[0].value; - const hasPbac = await pbacSpyHasPbacEnabled.mock.results[0].value; - expect(pbacCanActivateResult).toBe(true); - expect(rolesCanActivateResult).toBe(true); - expect(hasPbac).toBe(true); - expect(rolesSpyCheckUserRoleAccess).not.toHaveBeenCalled(); - }); - }); - - it("should allow role creation when organization does not have PBAC enabled and user is org admin", async () => { - const createRoleInput: CreateOrgRoleInput = { - name: "Test Role Legacy Admin", - permissions: ["booking.read"], - }; - - const pbacSpyCanActivate = jest.spyOn(PbacGuard.prototype, "canActivate"); - const pbacSpyHasPbacEnabled = jest.spyOn( - PbacGuard.prototype as unknown as PbacGuard, - "hasPbacEnabled" - ); - const rolesSpyCanActivate = jest.spyOn(RolesGuard.prototype, "canActivate"); - const rolesSpyCheckUserRoleAccess = jest.spyOn( - RolesGuard.prototype as unknown as RolesGuard, - "checkUserRoleAccess" - ); - - return request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/roles`) - .set("Authorization", `Bearer ${legacyOrgAdminApiKey}`) - .send(createRoleInput) - .expect(201) - .then(async (response) => { - const responseBody: CreateOrgRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.name).toEqual(createRoleInput.name); - expect(responseBody.data.organizationId).toEqual(organization.id); - - expect(pbacSpyCanActivate).toHaveBeenCalled(); - expect(rolesSpyCanActivate).toHaveBeenCalled(); - expect(pbacSpyHasPbacEnabled).toHaveBeenCalled(); - const pbacCanActivateResult = await pbacSpyCanActivate.mock.results[0].value; - const rolesCanActivateResult = await rolesSpyCanActivate.mock.results[0].value; - const hasPbac = await pbacSpyHasPbacEnabled.mock.results[0].value; - expect(pbacCanActivateResult).toBe(true); - expect(rolesCanActivateResult).toBe(true); - expect(hasPbac).toBe(false); - expect(rolesSpyCheckUserRoleAccess).toHaveBeenCalled(); - }); - }); - }); - - describe("Negative Tests", () => { - it("should not allow role creation when organization has PBAC enabled but user has no role assigned", async () => { - const createRoleInput: CreateOrgRoleInput = { - name: "Test Role No Role", - permissions: ["booking.read"], - }; - - const userWithNoRole = await userRepositoryFixture.create({ - email: `no-role-user-${randomString()}@api.com`, - username: `no-role-user-${randomString()}@api.com`, - }); - - await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: userWithNoRole.id } }, - team: { connect: { id: pbacEnabledOrganization.id } }, - }); - - const { keyString: noRoleKeyString } = await apiKeysRepositoryFixture.createApiKey( - userWithNoRole.id, - null - ); - const noRoleApiKey = `cal_test_${noRoleKeyString}`; - - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${noRoleApiKey}`) - .send(createRoleInput); - expect(response.status).toBe(403); - expect(response.body.error.message).toBe( - `RolesGuard - user with id=${userWithNoRole.id} does not have the minimum required role=ORG_ADMIN within organization with id=${pbacEnabledOrganization.id}.` - ); - }); - - it("should not allow role creation when organization has PBAC enabled but user role lacks required permission", async () => { - const createRoleInput: CreateOrgRoleInput = { - name: "Test Role No Permission", - permissions: ["booking.read"], - }; - - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithoutRolePermissionApiKey}`) - .send(createRoleInput); - expect(response.status).toBe(403); - expect(response.body.error.message).toBe( - `RolesGuard - user with id=${pbacOrgUserWithoutRolePermission.id} does not have the minimum required role=ORG_ADMIN within organization with id=${pbacEnabledOrganization.id}.` - ); - }); - - it("should not allow role creation when organization does not have PBAC enabled and user has no membership", async () => { - const createRoleInput: CreateOrgRoleInput = { - name: "Test Role No Membership", - permissions: ["booking.read"], - }; - - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/roles`) - .set("Authorization", `Bearer ${nonOrgUserApiKey}`) - .send(createRoleInput); - expect(response.status).toBe(403); - expect(response.body.error.message).toBe( - `RolesGuard - User is not a member of the organization with id=${organization.id}.` - ); - }); - - it("should not allow role creation when organization does not have PBAC enabled and user has member membership (not admin)", async () => { - const createRoleInput: CreateOrgRoleInput = { - name: "Test Role Member Only", - permissions: ["booking.read"], - }; - - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/roles`) - .set("Authorization", `Bearer ${legacyOrgMemberApiKey}`) - .send(createRoleInput); - expect(response.status).toBe(403); - expect(response.body.error.message).toBe( - `RolesGuard - user with id=${legacyOrgMemberUser.id} does not have the minimum required role=ORG_ADMIN within organization with id=${organization.id}.` - ); - }); - }); - - describe("CRUD Role Endpoints", () => { - let createdRoleId: string; - - it("should create a role", async () => { - const createRoleInput: CreateOrgRoleInput = { - name: "CRUD Test Role", - permissions: ["booking.read", "eventType.create"], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(createRoleInput) - .expect(201) - .then((response) => { - const responseBody: CreateOrgRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.name).toEqual(createRoleInput.name); - expect(responseBody.data.permissions).toEqual(createRoleInput.permissions); - expect(responseBody.data.organizationId).toEqual(pbacEnabledOrganization.id); - createdRoleId = responseBody.data.id; - }); - }); - - it("should update role permissions and name", async () => { - const updateRoleInput: UpdateOrgRoleInput = { - name: "CRUD Test Role Updated", - permissions: ["booking.read", "eventType.read"], - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${createdRoleId}`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(updateRoleInput) - .expect(200) - .then((response) => { - const responseBody: UpdateOrgRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(createdRoleId); - expect(responseBody.data.name).toEqual(updateRoleInput.name); - expect(responseBody.data.permissions).toEqual(updateRoleInput.permissions); - expect(responseBody.data.organizationId).toEqual(pbacEnabledOrganization.id); - }); - }); - - it("should fetch the role", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${createdRoleId}`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(200) - .then((response) => { - const responseBody: GetOrgRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(createdRoleId); - expect(responseBody.data.organizationId).toEqual(pbacEnabledOrganization.id); - }); - }); - - it("should fetch all roles", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(200) - .then((response) => { - const responseBody: GetAllOrgRolesOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(Array.isArray(responseBody.data)).toBe(true); - expect(responseBody.data.find((r) => r.id === createdRoleId)).toBeDefined(); - const created = responseBody.data.find((r) => r.id === createdRoleId); - expect(created?.organizationId).toEqual(pbacEnabledOrganization.id); - }); - }); - - it("should delete the role", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${createdRoleId}`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(200) - .then((response) => { - const responseBody: DeleteOrgRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(createdRoleId); - expect(responseBody.data.organizationId).toEqual(pbacEnabledOrganization.id); - }); - }); - - describe("Negative error cases", () => { - it("should fail to create a role with a duplicate name (400)", async () => { - const name = `dup-role-${randomString()}`; - - const firstCreate: CreateOrgRoleInput = { name, permissions: ["booking.read"] }; - await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(firstCreate) - .expect(201); - - const secondCreate: CreateOrgRoleInput = { name, permissions: ["booking.read"] }; - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(secondCreate); - expect(response.status).toBe(400); - expect(response.body.error.message).toBe(`Role with name "${name}" already exists`); - }); - - it("should fail to create a role with invalid permissions (400)", async () => { - const createRoleInput = { - name: `invalid-perms-${randomString()}`, - permissions: ["invalid"], - }; - - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(createRoleInput); - expect(response.status).toBe(400); - expect(response.body.error.message).toBe( - "Permission 'invalid' must be a valid permission string in format 'resource.action' (e.g., 'eventType.read', 'booking.create')" - ); - }); - - it("should return 404 when updating a role not belonging to the organization", async () => { - const defaultAdminRoleId = await roleService.getDefaultRoleId(MembershipRole.ADMIN); - const updateRoleInput: UpdateOrgRoleInput = { - name: `no-update-default-${randomString()}`, - permissions: ["booking.read"], - }; - - const response = await request(app.getHttpServer()) - .patch(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${defaultAdminRoleId}`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(updateRoleInput); - expect(response.status).toBe(404); - expect(response.body.error.message).toBe( - `Role with id ${defaultAdminRoleId} within team id ${pbacEnabledOrganization.id} not found` - ); - }); - - it("should return 404 when updating a default (system) role not belonging to the organization", async () => { - const defaultAdminRoleId = await roleService.getDefaultRoleId(MembershipRole.ADMIN); - const updateRoleInput: UpdateOrgRoleInput = { - name: `no-update-default-${randomString()}`, - permissions: ["booking.read"], - }; - - const response = await request(app.getHttpServer()) - .patch(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${defaultAdminRoleId}`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(updateRoleInput); - expect(response.status).toBe(404); - expect(response.body.error.message).toBe( - `Role with id ${defaultAdminRoleId} within team id ${pbacEnabledOrganization.id} not found` - ); - }); - - it("should fail to update with invalid permissions (400)", async () => { - // Create a fresh role to update - const { body: createRes } = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send({ name: `update-invalid-${randomString()}` as string, permissions: ["booking.read"] }) - .expect(201); - const roleId: string = createRes.data.id; - - const updateRoleInput = { - name: `update-invalid-${randomString()}`, - permissions: ["invalid"], - }; - - const response = await request(app.getHttpServer()) - .patch(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${roleId}`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(updateRoleInput); - expect(response.status).toBe(400); - expect(response.body.error.message).toBe( - "Permission 'invalid' must be a valid permission string in format 'resource.action' (e.g., 'eventType.read', 'booking.create')" - ); - }); - - it("should return 404 when updating a role from a different organization", async () => { - // Create a role in a different org (legacy/non-PBAC org) - const foreignRole = await roleService.createRole({ - name: `foreign-update-${randomString()}`, - teamId: organization.id, - permissions: ["booking.read"], - type: "CUSTOM", - }); - - const response = await request(app.getHttpServer()) - .patch(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${foreignRole.id}`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send({ permissions: ["eventType.read"] }); - expect(response.status).toBe(404); - expect(response.body.error.message).toBe( - `Role with id ${foreignRole.id} within team id ${pbacEnabledOrganization.id} not found` - ); - }); - - it("should return 404 when deleting a role not belonging to the organization", async () => { - const defaultMemberRoleId = await roleService.getDefaultRoleId(MembershipRole.MEMBER); - const response = await request(app.getHttpServer()) - .delete(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${defaultMemberRoleId}`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`); - expect(response.status).toBe(404); - expect(response.body.error.message).toBe( - `Role with id ${defaultMemberRoleId} within team id ${pbacEnabledOrganization.id} not found` - ); - }); - - it("should return 404 when deleting a default (system) role not belonging to the organization", async () => { - const defaultMemberRoleId = await roleService.getDefaultRoleId(MembershipRole.MEMBER); - const response = await request(app.getHttpServer()) - .delete(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${defaultMemberRoleId}`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`); - expect(response.status).toBe(404); - expect(response.body.error.message).toBe( - `Role with id ${defaultMemberRoleId} within team id ${pbacEnabledOrganization.id} not found` - ); - }); - - it("should return 404 when deleting a role from a different organization", async () => { - // Create a role in a different org - const foreignRole = await roleService.createRole({ - name: `foreign-delete-${randomString()}`, - teamId: organization.id, - permissions: ["booking.read"], - type: "CUSTOM", - }); - - const response = await request(app.getHttpServer()) - .delete(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${foreignRole.id}`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`); - expect(response.status).toBe(404); - expect(response.body.error.message).toBe( - `Role with id ${foreignRole.id} within team id ${pbacEnabledOrganization.id} not found` - ); - }); - }); - }); - }); - - afterAll(async () => { - try { - await featuresRepositoryFixture.deleteTeamFeature(pbacEnabledOrganization.id, "pbac"); - - await organizationsRepositoryFixture.delete(organization.id); - await organizationsRepositoryFixture.delete(pbacEnabledOrganization.id); - - await userRepositoryFixture.deleteByEmail(legacyOrgAdminUser.email); - await userRepositoryFixture.deleteByEmail(legacyOrgMemberUser.email); - await userRepositoryFixture.deleteByEmail(pbacOrgUserWithRolePermission.email); - await userRepositoryFixture.deleteByEmail(pbacOrgUserWithoutRolePermission.email); - await userRepositoryFixture.deleteByEmail(nonOrgUser.email); - } catch (error) { - console.error("Cleanup error:", error); - } finally { - await app.close(); - } - }); -}); diff --git a/apps/api/v2/src/modules/organizations/roles/organizations-roles.controller.ts b/apps/api/v2/src/modules/organizations/roles/organizations-roles.controller.ts deleted file mode 100644 index 8a95eab6a100e0..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/organizations-roles.controller.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Pbac } from "@/modules/auth/decorators/pbac/pbac.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { PbacGuard } from "@/modules/auth/guards/pbac/pbac.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { CreateOrgRoleInput } from "@/modules/organizations/roles/inputs/create-org-role.input"; -import { UpdateOrgRoleInput } from "@/modules/organizations/roles/inputs/update-org-role.input"; -import { CreateOrgRoleOutput } from "@/modules/organizations/roles/outputs/create-org-role.output"; -import { DeleteOrgRoleOutput } from "@/modules/organizations/roles/outputs/delete-org-role.output"; -import { GetAllOrgRolesOutput } from "@/modules/organizations/roles/outputs/get-all-org-roles.output"; -import { GetOrgRoleOutput } from "@/modules/organizations/roles/outputs/get-org-role.output"; -import { UpdateOrgRoleOutput } from "@/modules/organizations/roles/outputs/update-org-role.output"; -import { OrganizationsRolesOutputService } from "@/modules/organizations/roles/services/organizations-roles-output.service"; -import { RolesService } from "@/modules/roles/services/roles.service"; -import { - Controller, - UseGuards, - Get, - Param, - ParseIntPipe, - Query, - Delete, - Patch, - Post, - Body, - HttpCode, - HttpStatus, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { SkipTakePagination } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId/roles", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, PbacGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Roles") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -export class OrganizationsRolesController { - constructor( - private readonly rolesService: RolesService, - private readonly rolesOutputService: OrganizationsRolesOutputService - ) {} - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.create"]) - @Post("/") - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: "Create a new organization role" }) - async createRole( - @Param("orgId", ParseIntPipe) orgId: number, - @Body() body: CreateOrgRoleInput - ): Promise { - const role = await this.rolesService.createRole(orgId, body); - return { - status: SUCCESS_STATUS, - data: this.rolesOutputService.getOrganizationRoleOutput(role), - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.read"]) - @Get("/:roleId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Get a specific organization role" }) - async getRole( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("roleId") roleId: string - ): Promise { - const role = await this.rolesService.getRole(orgId, roleId); - return { - status: SUCCESS_STATUS, - data: this.rolesOutputService.getOrganizationRoleOutput(role), - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.read"]) - @Get("/") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Get all organization roles" }) - async getAllRoles( - @Param("orgId", ParseIntPipe) orgId: number, - @Query() queryParams: SkipTakePagination - ): Promise { - const { skip, take } = queryParams; - const roles = await this.rolesService.getTeamRoles(orgId, skip ?? 0, take ?? 250); - return { - status: SUCCESS_STATUS, - data: this.rolesOutputService.getOrganizationRolesOutput(roles), - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.update"]) - @Patch("/:roleId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Update an organization role" }) - async updateRole( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("roleId") roleId: string, - @Body() body: UpdateOrgRoleInput - ): Promise { - const role = await this.rolesService.updateRole(orgId, roleId, body); - return { - status: SUCCESS_STATUS, - data: this.rolesOutputService.getOrganizationRoleOutput(role), - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.delete"]) - @Delete("/:roleId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Delete an organization role" }) - async deleteRole( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("roleId") roleId: string - ): Promise { - const role = await this.rolesService.deleteRole(orgId, roleId); - return { - status: SUCCESS_STATUS, - data: this.rolesOutputService.getOrganizationRoleOutput(role), - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/roles/organizations-roles.module.ts b/apps/api/v2/src/modules/organizations/roles/organizations-roles.module.ts deleted file mode 100644 index a9e1d5ecf87969..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/organizations-roles.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsRolesPermissionsController } from "@/modules/organizations/roles/permissions/organizations-roles-permissions.controller"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { RolesModule } from "@/modules/roles/roles.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { Module } from "@nestjs/common"; - -import { OrganizationsRolesController } from "./organizations-roles.controller"; - -@Module({ - imports: [StripeModule, PrismaModule, RedisModule, MembershipsModule, RolesModule], - providers: [OrganizationsRepository], - controllers: [OrganizationsRolesController, OrganizationsRolesPermissionsController], -}) -export class OrganizationsRolesModule {} diff --git a/apps/api/v2/src/modules/organizations/roles/outputs/create-org-role.output.ts b/apps/api/v2/src/modules/organizations/roles/outputs/create-org-role.output.ts deleted file mode 100644 index 3a6d06293384b5..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/outputs/create-org-role.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OrgRoleOutput } from "@/modules/organizations/roles/outputs/org-role.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class CreateOrgRoleOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: OrgRoleOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => OrgRoleOutput) - data!: OrgRoleOutput; -} diff --git a/apps/api/v2/src/modules/organizations/roles/outputs/delete-org-role.output.ts b/apps/api/v2/src/modules/organizations/roles/outputs/delete-org-role.output.ts deleted file mode 100644 index 191b712ce13e87..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/outputs/delete-org-role.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OrgRoleOutput } from "@/modules/organizations/roles/outputs/org-role.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class DeleteOrgRoleOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: OrgRoleOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => OrgRoleOutput) - data!: OrgRoleOutput; -} diff --git a/apps/api/v2/src/modules/organizations/roles/outputs/get-all-org-roles.output.ts b/apps/api/v2/src/modules/organizations/roles/outputs/get-all-org-roles.output.ts deleted file mode 100644 index 62936ee32a1ee5..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/outputs/get-all-org-roles.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OrgRoleOutput } from "@/modules/organizations/roles/outputs/org-role.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsArray, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class GetAllOrgRolesOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: [OrgRoleOutput], - }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => OrgRoleOutput) - data!: OrgRoleOutput[]; -} diff --git a/apps/api/v2/src/modules/organizations/roles/outputs/get-org-role.output.ts b/apps/api/v2/src/modules/organizations/roles/outputs/get-org-role.output.ts deleted file mode 100644 index 481047d8789489..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/outputs/get-org-role.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OrgRoleOutput } from "@/modules/organizations/roles/outputs/org-role.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class GetOrgRoleOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: OrgRoleOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => OrgRoleOutput) - data!: OrgRoleOutput; -} diff --git a/apps/api/v2/src/modules/organizations/roles/outputs/org-role.output.ts b/apps/api/v2/src/modules/organizations/roles/outputs/org-role.output.ts deleted file mode 100644 index c2226241ddc812..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/outputs/org-role.output.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { Expose } from "class-transformer"; -import { IsString, IsOptional, IsArray, IsEnum, IsDateString, IsNumber } from "class-validator"; - -import { orgPermissionEnum } from "../inputs/base-org-role.input"; - -enum RoleTypeEnum { - SYSTEM = "SYSTEM", - CUSTOM = "CUSTOM", -} - -type RoleType = keyof typeof RoleTypeEnum; - -export class OrgRoleOutput { - @ApiProperty({ description: "Unique identifier for the role" }) - @IsString() - @Expose() - id!: string; - - @ApiProperty({ description: "Name of the role" }) - @IsString() - @Expose() - name!: string; - - @ApiPropertyOptional({ description: "Color for the role (hex code)" }) - @IsString() - @IsOptional() - @Expose() - color?: string | null; - - @ApiPropertyOptional({ description: "Description of the role" }) - @IsString() - @IsOptional() - @Expose() - description?: string | null; - - @ApiPropertyOptional({ description: "Organization ID this role belongs to" }) - @IsNumber() - @IsOptional() - @Expose() - organizationId?: number | null; - - @ApiProperty({ - description: "Type of role", - enum: RoleTypeEnum, - }) - @IsEnum(RoleTypeEnum) - @Expose() - type!: RoleType; - - @ApiProperty({ - description: "Permissions assigned to this role in 'resource.action' format.", - enum: orgPermissionEnum, - isArray: true, - example: ["booking.read", "eventType.create"], - }) - @IsArray() - @IsString({ each: true }) - @Expose() - permissions!: string[]; - - @ApiProperty({ description: "When the role was created" }) - @IsDateString() - @Expose() - createdAt!: string; - - @ApiProperty({ description: "When the role was last updated" }) - @IsDateString() - @Expose() - updatedAt!: string; -} diff --git a/apps/api/v2/src/modules/organizations/roles/outputs/update-org-role.output.ts b/apps/api/v2/src/modules/organizations/roles/outputs/update-org-role.output.ts deleted file mode 100644 index 5288cc55e1da4f..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/outputs/update-org-role.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OrgRoleOutput } from "@/modules/organizations/roles/outputs/org-role.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class UpdateOrgRoleOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: OrgRoleOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => OrgRoleOutput) - data!: OrgRoleOutput; -} diff --git a/apps/api/v2/src/modules/organizations/roles/permissions/inputs/create-org-role-permissions.input.ts b/apps/api/v2/src/modules/organizations/roles/permissions/inputs/create-org-role-permissions.input.ts deleted file mode 100644 index 62f733411316f3..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/permissions/inputs/create-org-role-permissions.input.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, IsString, Validate } from "class-validator"; - -import type { PermissionString } from "@calcom/platform-libraries/pbac"; - -import { orgPermissionEnum } from "../../inputs/base-org-role.input"; -import { OrgPermissionStringValidator } from "./validators/org-permission-string.validator"; - -export class CreateOrgRolePermissionsInput { - @ApiProperty({ - description: "Permissions to add (format: resource.action)", - enum: orgPermissionEnum, - isArray: true, - example: ["eventType.read", "booking.read"], - }) - @IsArray() - @IsString({ each: true }) - @Validate(OrgPermissionStringValidator, { each: true }) - permissions!: PermissionString[]; -} diff --git a/apps/api/v2/src/modules/organizations/roles/permissions/inputs/delete-org-role-permissions.query.ts b/apps/api/v2/src/modules/organizations/roles/permissions/inputs/delete-org-role-permissions.query.ts deleted file mode 100644 index 118b4ef18fb72d..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/permissions/inputs/delete-org-role-permissions.query.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { Transform } from "class-transformer"; -import { ArrayNotEmpty, IsArray, IsOptional, IsString, Validate } from "class-validator"; - -import type { PermissionString } from "@calcom/platform-libraries/pbac"; - -import { orgPermissionEnum } from "../../inputs/base-org-role.input"; -import { OrgPermissionStringValidator } from "./validators/org-permission-string.validator"; - -export class DeleteOrgRolePermissionsQuery { - @ApiPropertyOptional({ - description: - "Permissions to remove (format: resource.action). Supports comma-separated values as well as repeated query params.", - example: "?permissions=eventType.read,booking.read", - enum: orgPermissionEnum, - isArray: true, - }) - @IsOptional() - @Transform(({ value }) => { - if (typeof value === "string") { - return value - .split(",") - .map((p: string) => p.trim()) - .filter((p: string) => p.length > 0); - } - return value; - }) - @IsArray() - @ArrayNotEmpty({ message: "permissions cannot be empty." }) - @IsString({ each: true }) - @Validate(OrgPermissionStringValidator, { each: true }) - permissions?: PermissionString[]; -} diff --git a/apps/api/v2/src/modules/organizations/roles/permissions/inputs/validators/org-permission-string.validator.spec.ts b/apps/api/v2/src/modules/organizations/roles/permissions/inputs/validators/org-permission-string.validator.spec.ts deleted file mode 100644 index 61c23b7fd0c6bf..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/permissions/inputs/validators/org-permission-string.validator.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { BadRequestException } from "@nestjs/common"; - -import { OrgPermissionStringValidator } from "./org-permission-string.validator"; - -describe("PermissionStringValidator", () => { - let validator: OrgPermissionStringValidator; - - beforeEach(() => { - validator = new OrgPermissionStringValidator(); - }); - - describe("validate", () => { - it("should return true for valid permission strings", () => { - const validPermissions = [ - "eventType.read", - "eventType.create", - "eventType.update", - "eventType.delete", - "booking.read", - "booking.update", - "role.create", - "role.read", - "role.update", - "role.delete", - "team.read", - "team.invite", - "organization.read", - "organization.listMembers", - // no wildcards here - ]; - - validPermissions.forEach((permission) => { - expect(validator.validate(permission)).toBe(true); - }); - }); - - it("should throw for invalid permission strings", () => { - const invalidPermissions = [ - "invalid", // no dot - "invalid.", // no action - ".invalid", // no resource - "invalid.action", // invalid resource - "eventType.invalid", // invalid action - "event-type.read", // wrong format (should be eventType) - "", // empty string - "eventType..read", // double dot - "eventType.read.extra", // too many parts - ]; - - invalidPermissions.forEach((permission) => { - expect(() => validator.validate(permission)).toThrow(BadRequestException); - }); - }); - - it("should throw for wildcard permission strings", () => { - const wildcardPermissions = ["*", "*.read", "eventType.*"]; - - wildcardPermissions.forEach((permission) => { - expect(() => validator.validate(permission)).toThrow(BadRequestException); - }); - }); - }); - - describe("defaultMessage", () => { - it("should return appropriate error message", () => { - const message = validator.defaultMessage(); - expect(message).toContain("Permission must be a valid permission string"); - expect(message).toContain("resource.action"); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/roles/permissions/inputs/validators/org-permission-string.validator.ts b/apps/api/v2/src/modules/organizations/roles/permissions/inputs/validators/org-permission-string.validator.ts deleted file mode 100644 index 6bd8f8c061e071..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/permissions/inputs/validators/org-permission-string.validator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BadRequestException } from "@nestjs/common"; -import type { ValidatorConstraintInterface } from "class-validator"; -import { ValidatorConstraint } from "class-validator"; - -import { isValidPermissionStringForScope } from "@calcom/platform-libraries/pbac"; -import { Scope } from "@calcom/platform-libraries/pbac"; - -@ValidatorConstraint({ name: "orgPermissionStringValidator", async: false }) -export class OrgPermissionStringValidator implements ValidatorConstraintInterface { - validate(permission: string) { - const isValid = isValidPermissionStringForScope(permission, Scope.Organization); - if (!isValid) { - throw new BadRequestException( - `Permission '${permission}' must be a valid permission string in format 'resource.action' (e.g., 'eventType.read', 'booking.create')` - ); - } - return true; - } - - defaultMessage() { - return "Permission must be a valid permission string in format 'resource.action' (e.g., 'eventType.read', 'booking.create')"; - } -} diff --git a/apps/api/v2/src/modules/organizations/roles/permissions/organizations-roles-permissions.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/roles/permissions/organizations-roles-permissions.controller.e2e-spec.ts deleted file mode 100644 index 7ab90e20442b8e..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/permissions/organizations-roles-permissions.controller.e2e-spec.ts +++ /dev/null @@ -1,391 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { PermissionString } from "@calcom/platform-libraries/pbac"; -import { RoleService } from "@calcom/platform-libraries/pbac"; -import type { Team, User } from "@calcom/prisma/client"; -import { MembershipRole } from "@calcom/prisma/enums"; -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 { FeaturesRepositoryFixture } from "test/fixtures/repository/features.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { CreateTeamRoleInput } from "@/modules/organizations/teams/roles/inputs/create-team-role.input"; -import type { CreateTeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/create-team-role.output"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Roles Permissions Endpoints", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipRepositoryFixture: MembershipRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let featuresRepositoryFixture: FeaturesRepositoryFixture; - let roleService: RoleService; - - let pbacOrgUserWithRolePermission: User; - let pbacOrgUserWithRolePermissionApiKey: string; - - let pbacEnabledOrganization: Team; - - const pbacUserEmail = `pbac-org-user-with-permissions-${randomString()}@api.com`; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - featuresRepositoryFixture = new FeaturesRepositoryFixture(moduleRef); - roleService = new RoleService(); - - // Create PBAC org - pbacEnabledOrganization = await organizationsRepositoryFixture.create({ - name: `pbac-org-role-perm-test-${randomString()}`, - isOrganization: true, - }); - - await featuresRepositoryFixture.create({ slug: "pbac", enabled: true }); - await featuresRepositoryFixture.setTeamFeatureState({ - teamId: pbacEnabledOrganization.id, - featureId: "pbac", - state: "enabled", - }); - - // Create user + membership in org - pbacOrgUserWithRolePermission = await userRepositoryFixture.create({ - email: pbacUserEmail, - username: pbacUserEmail, - }); - - const membership = await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: pbacOrgUserWithRolePermission.id } }, - team: { connect: { id: pbacEnabledOrganization.id } }, - }); - - // Create a role that allows role.read and role.update, assign to user - const managerRole = await roleService.createRole({ - name: `role-manager-${randomString()}`, - teamId: pbacEnabledOrganization.id, - permissions: ["role.create", "role.read", "role.update"], - type: "CUSTOM", - }); - await roleService.assignRoleToMember(managerRole.id, membership.id); - - // API key for user - const { keyString } = await apiKeysRepositoryFixture.createApiKey(pbacOrgUserWithRolePermission.id, null); - pbacOrgUserWithRolePermissionApiKey = `cal_test_${keyString}`; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - describe("Negative Tests", () => { - it("rejects invalid permission on add (400)", async () => { - const baseRoleInput: CreateTeamRoleInput = { - name: `neg-add-invalid-${randomString()}`, - permissions: ["booking.read"], - }; - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const roleId = createRes.body.data.id as string; - - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${roleId}/permissions`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send({ permissions: ["invalid"] }); - expect(response.status).toBe(400); - expect(response.body.error.message).toBe( - "Permission 'invalid' must be a valid permission string in format 'resource.action' (e.g., 'eventType.read', 'booking.create')" - ); - }); - - it("rejects invalid permission on replace (400)", async () => { - const baseRoleInput: CreateTeamRoleInput = { - name: `neg-put-invalid-${randomString()}`, - permissions: ["booking.read"], - }; - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const roleId = createRes.body.data.id as string; - - const response = await request(app.getHttpServer()) - .put(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${roleId}/permissions`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send({ permissions: ["invalid"] }); - expect(response.status).toBe(400); - expect(response.body.error.message).toBe( - "Permission 'invalid' must be a valid permission string in format 'resource.action' (e.g., 'eventType.read', 'booking.create')" - ); - }); - - it("rejects invalid permission on delete single (400)", async () => { - const baseRoleInput: CreateTeamRoleInput = { - name: `neg-delone-invalid-${randomString()}`, - permissions: ["booking.read"], - }; - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const roleId = createRes.body.data.id as string; - - const response = await request(app.getHttpServer()) - .delete(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${roleId}/permissions/${"invalid"}`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`); - expect(response.status).toBe(400); - expect(response.body.error.message).toBe( - "Permission 'invalid' must be a valid permission string in format 'resource.action' (e.g., 'eventType.read', 'booking.create')" - ); - }); - - it("rejects invalid permission on bulk delete (400)", async () => { - const baseRoleInput: CreateTeamRoleInput = { - name: `neg-delmany-invalid-${randomString()}`, - permissions: ["booking.read", "eventType.create"], - }; - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const roleId = createRes.body.data.id as string; - - const invalid = "invalid"; - const res = await request(app.getHttpServer()) - .delete( - `/v2/organizations/${ - pbacEnabledOrganization.id - }/roles/${roleId}/permissions?permissions=${encodeURIComponent(`booking.read,${invalid}`)}` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`); - - expect(res.status).toBe(400); - expect(res.body.error.message).toBe( - "Permission 'invalid' must be a valid permission string in format 'resource.action' (e.g., 'eventType.read', 'booking.create')" - ); - }); - - it("returns 404 when modifying permissions for role not belonging to organization", async () => { - // Create foreign organization and role - const foreignOrg = await organizationsRepositoryFixture.create({ - name: `foreign-org-${randomString()}`, - isOrganization: true, - }); - const foreignRole = await roleService.createRole({ - name: `foreign-role-${randomString()}`, - teamId: foreignOrg.id, - permissions: ["booking.read"], - type: "CUSTOM", - }); - - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${foreignRole.id}/permissions`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send({ permissions: ["eventType.read"] }); - expect(response.status).toBe(404); - expect(response.body.error.message).toBe( - `Role with id ${foreignRole.id} within team id ${pbacEnabledOrganization.id} not found` - ); - - await organizationsRepositoryFixture.delete(foreignOrg.id); - }); - - it("returns 404 when modifying permissions of a default (system) role not belonging to organization", async () => { - const defaultMemberRoleId = await roleService.getDefaultRoleId(MembershipRole.MEMBER); - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${defaultMemberRoleId}/permissions`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send({ permissions: ["eventType.read"] }); - expect(response.status).toBe(404); - expect(response.body.error.message).toBe( - `Role with id ${defaultMemberRoleId} within team id ${pbacEnabledOrganization.id} not found` - ); - }); - }); - - it("lists permissions for a role (GET /)", async () => { - const initialPermissions = ["booking.read"] as const; - const baseRoleInput: CreateTeamRoleInput = { - name: `perm-target-role-list-${randomString()}`, - permissions: [...initialPermissions], - }; - - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const responseBody: CreateTeamRoleOutput = createRes.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.permissions).toEqual(baseRoleInput.permissions); - const roleId = responseBody.data.id; - - const listRes = await request(app.getHttpServer()) - .get(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${roleId}/permissions`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(200); - expect(listRes.body.status).toEqual(SUCCESS_STATUS); - expect(listRes.body.data).toEqual(initialPermissions); - }); - - it("adds permissions (POST /)", async () => { - const initialPermissions = ["booking.read"] as const; - const toAdd = ["eventType.create", "eventType.read"] as const; - const expected = [...initialPermissions, ...toAdd] as string[]; - - const baseRoleInput: CreateTeamRoleInput = { - name: `perm-target-role-add-${randomString()}`, - permissions: [...initialPermissions], - }; - - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const responseBody: CreateTeamRoleOutput = createRes.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.permissions).toEqual(baseRoleInput.permissions); - const roleId = responseBody.data.id; - - const addRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${roleId}/permissions`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send({ permissions: toAdd }) - .expect(200); - expect(addRes.body.status).toEqual(SUCCESS_STATUS); - expect(addRes.body.data).toEqual(expected); - }); - - it("bulk removes permissions via query (DELETE /)", async () => { - const initialPermissions: PermissionString[] = ["booking.read", "eventType.create", "eventType.read"]; - const toRemove: PermissionString[] = ["eventType.create", "eventType.read"]; - const expected: PermissionString[] = ["booking.read"]; - - const baseRoleInput: CreateTeamRoleInput = { - name: `perm-target-role-delmany-${randomString()}`, - permissions: initialPermissions, - }; - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const responseBody: CreateTeamRoleOutput = createRes.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.permissions).toEqual(baseRoleInput.permissions); - const roleId = responseBody.data.id; - - await request(app.getHttpServer()) - .delete( - `/v2/organizations/${ - pbacEnabledOrganization.id - }/roles/${roleId}/permissions?permissions=${toRemove.join(",")}` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(204); - - const listRes = await request(app.getHttpServer()) - .get(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${roleId}/permissions`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(200); - expect(listRes.body.data).toEqual(expected); - }); - - it("replaces all permissions (PUT /)", async () => { - const initialPermissions = ["booking.read"] as const; - const replacement = ["booking.read", "eventType.update"] as const; - - const baseRoleInput: CreateTeamRoleInput = { - name: `perm-target-role-put-${randomString()}`, - permissions: [...initialPermissions], - }; - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const responseBody: CreateTeamRoleOutput = createRes.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.permissions).toEqual(baseRoleInput.permissions); - const roleId = responseBody.data.id; - - const putRes = await request(app.getHttpServer()) - .put(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${roleId}/permissions`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send({ permissions: replacement }) - .expect(200); - - expect(putRes.body.status).toEqual(SUCCESS_STATUS); - expect(putRes.body.data).toEqual(replacement); - }); - - it("removes a single permission (DELETE /:permission)", async () => { - const initialPermissions = ["booking.read", "eventType.update"] as const; - const toRemove = "eventType.update" as const; - const expected = ["booking.read"] as const; - - const baseRoleInput: CreateTeamRoleInput = { - name: `perm-target-role-delone-${randomString()}`, - permissions: [...initialPermissions], - }; - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const responseBody: CreateTeamRoleOutput = createRes.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.permissions).toEqual(baseRoleInput.permissions); - const roleId = responseBody.data.id; - - await request(app.getHttpServer()) - .delete(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${roleId}/permissions/${toRemove}`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(204); - - const listRes = await request(app.getHttpServer()) - .get(`/v2/organizations/${pbacEnabledOrganization.id}/roles/${roleId}/permissions`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(200); - expect(listRes.body.data).toEqual(expected); - }); - - afterAll(async () => { - try { - // Clean up feature flag from organization - await featuresRepositoryFixture.deleteTeamFeature(pbacEnabledOrganization.id, "pbac"); - - // Clean up org - await organizationsRepositoryFixture.delete(pbacEnabledOrganization.id); - - // Clean up user - await userRepositoryFixture.deleteByEmail(pbacOrgUserWithRolePermission.email); - } catch (err) { - console.log(err); - } finally { - await app.close(); - } - }); -}); diff --git a/apps/api/v2/src/modules/organizations/roles/permissions/organizations-roles-permissions.controller.ts b/apps/api/v2/src/modules/organizations/roles/permissions/organizations-roles-permissions.controller.ts deleted file mode 100644 index 97eb19c9ae6027..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/permissions/organizations-roles-permissions.controller.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Pbac } from "@/modules/auth/decorators/pbac/pbac.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { PbacGuard } from "@/modules/auth/guards/pbac/pbac.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { CreateOrgRolePermissionsInput } from "@/modules/organizations/roles/permissions/inputs/create-org-role-permissions.input"; -import { DeleteOrgRolePermissionsQuery } from "@/modules/organizations/roles/permissions/inputs/delete-org-role-permissions.query"; -import { GetOrgRolePermissionsOutput } from "@/modules/organizations/roles/permissions/outputs/get-org-role-permissions.output"; -import { RolesPermissionsService } from "@/modules/roles/permissions/services/roles-permissions.service"; -import { - Body, - Controller, - Delete, - Get, - HttpCode, - HttpStatus, - Param, - ParseIntPipe, - Put, - Query, - Post, - UseGuards, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { PermissionString } from "@calcom/platform-libraries/pbac"; - -@Controller({ - path: "/v2/organizations/:orgId/roles/:roleId/permissions", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, PbacGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Roles / Permissions") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -export class OrganizationsRolesPermissionsController { - constructor(private readonly rolePermissionsService: RolesPermissionsService) {} - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.update"]) - @Post("/") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Add permissions to an organization role (single or batch)" }) - async addPermissions( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("roleId") roleId: string, - @Body() body: CreateOrgRolePermissionsInput - ): Promise { - const permissions = await this.rolePermissionsService.addRolePermissions(orgId, roleId, body.permissions); - return { status: SUCCESS_STATUS, data: permissions }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.read"]) - @Get("/") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "List permissions for an organization role" }) - async listPermissions( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("roleId") roleId: string - ): Promise { - const permissions = await this.rolePermissionsService.getRolePermissions(orgId, roleId); - return { status: SUCCESS_STATUS, data: permissions }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.update"]) - @Put("/") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Replace all permissions for an organization role" }) - async setPermissions( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("roleId") roleId: string, - @Body() body: CreateOrgRolePermissionsInput - ): Promise { - const permissions = await this.rolePermissionsService.setRolePermissions( - orgId, - roleId, - body.permissions || [] - ); - return { status: SUCCESS_STATUS, data: permissions }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.update"]) - @Delete("/:permission") - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: "Remove a permission from an organization role" }) - async removePermission( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("roleId") roleId: string, - @Param("permission") permission: PermissionString - ): Promise { - await this.rolePermissionsService.removeRolePermission(orgId, roleId, permission); - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.update"]) - @Delete("/") - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: "Remove multiple permissions from an organization role" }) - async removePermissions( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("roleId") roleId: string, - @Query() query: DeleteOrgRolePermissionsQuery - ): Promise { - await this.rolePermissionsService.removeRolePermissions(orgId, roleId, query.permissions || []); - } -} diff --git a/apps/api/v2/src/modules/organizations/roles/permissions/outputs/get-org-role-permissions.output.ts b/apps/api/v2/src/modules/organizations/roles/permissions/outputs/get-org-role-permissions.output.ts deleted file mode 100644 index 2927eb636de574..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/permissions/outputs/get-org-role-permissions.output.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, IsString } from "class-validator"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; - -export class GetOrgRolePermissionsOutput { - @ApiProperty({ example: SUCCESS_STATUS }) - status!: typeof SUCCESS_STATUS; - - @ApiProperty({ type: [String] }) - @IsArray() - @IsString({ each: true }) - data!: string[]; -} diff --git a/apps/api/v2/src/modules/organizations/roles/services/organizations-roles-output.service.ts b/apps/api/v2/src/modules/organizations/roles/services/organizations-roles-output.service.ts deleted file mode 100644 index 1a929d9caeb475..00000000000000 --- a/apps/api/v2/src/modules/organizations/roles/services/organizations-roles-output.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { OrgRoleOutput } from "@/modules/organizations/roles/outputs/org-role.output"; -import { TeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/team-role.output"; -import { Injectable } from "@nestjs/common"; - -import type { Role } from "@calcom/platform-libraries/pbac"; - -@Injectable() -export class OrganizationsRolesOutputService { - getOrganizationRoleOutput(role: Role): OrgRoleOutput { - return { - id: role.id, - name: role.name, - color: role.color || null, - description: role.description || null, - organizationId: role.teamId || null, - type: role.type, - permissions: role.permissions.map((permission) => `${permission.resource}.${permission.action}`), - createdAt: role.createdAt.toISOString(), - updatedAt: role.updatedAt.toISOString(), - }; - } - - getOrganizationRolesOutput(roles: Role[]): TeamRoleOutput[] { - return roles.map((role) => this.getOrganizationRoleOutput(role)); - } -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms-responses.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms-responses.controller.e2e-spec.ts deleted file mode 100644 index 8e8682e752dd3d..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms-responses.controller.e2e-spec.ts +++ /dev/null @@ -1,946 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { App_RoutingForms_Form, App_RoutingForms_FormResponse, 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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { PrismaModule } from "@/modules/prisma/prisma.module"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("OrganizationsRoutingFormsResponsesController", () => { - let app: INestApplication; - let prismaWriteService: PrismaWriteService; - let org: Team; - - // Grouped data structures - let orgAdminData: { - user: User; - apiKey: string; - eventType: { - id: number; - slug: string | null; - teamId: number | null; - userId: number | null; - title: string; - }; - routingForm: App_RoutingForms_Form; - }; - - let teamData: { - team: Team; - routingForm: App_RoutingForms_Form; - routingFormResponse1: App_RoutingForms_FormResponse; - routingFormResponse2: App_RoutingForms_FormResponse; - }; - - let nonOrgAdminData: { - user: User; - apiKey: string; - eventType: { - id: number; - slug: string | null; - teamId: number | null; - userId: number | null; - title: string; - }; - routingForm: App_RoutingForms_Form; - }; - - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - - const userEmail = `OrganizationsRoutingFormsResponsesController-key-bookings-2024-08-13-user-${randomString()}@api.com`; - let profileRepositoryFixture: ProfileRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); - - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - - prismaWriteService = moduleRef.get(PrismaWriteService); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - org = await organizationsRepositoryFixture.create({ - name: `OrganizationsRoutingFormsResponsesController-teams-memberships-organization-${randomString()}`, - isOrganization: true, - }); - - // Initialize grouped data structures - orgAdminData = {} as any; - teamData = {} as any; - nonOrgAdminData = {} as any; - - teamData.team = await teamRepositoryFixture.create({ - name: "OrganizationsRoutingFormsResponsesController orgs booking 1", - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - orgAdminData.user = await userRepositoryFixture.create({ - email: userEmail, - }); - - nonOrgAdminData.user = await userRepositoryFixture.create({ - email: `non-org-admin-user-${randomString()}@api.com`, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: orgAdminData.user.id } }, - team: { connect: { id: org.id } }, - accepted: true, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${orgAdminData.user.id}`, - username: userEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: orgAdminData.user.id, - }, - }, - }); - const now = new Date(); - now.setDate(now.getDate() + 1); - const { keyString } = await apiKeysRepositoryFixture.createApiKey(orgAdminData.user.id, null); - orgAdminData.apiKey = `${keyString}`; - - const { keyString: _nonOrgAdminUserApiKey } = await apiKeysRepositoryFixture.createApiKey( - nonOrgAdminData.user.id, - null - ); - nonOrgAdminData.apiKey = `${_nonOrgAdminUserApiKey}`; - - // Create an event type for routing form to route to - orgAdminData.eventType = await prismaWriteService.prisma.eventType.create({ - data: { - title: "Test Event Type", - slug: "test-event-type", - length: 30, - userId: orgAdminData.user.id, - teamId: null, - }, - }); - - nonOrgAdminData.eventType = await prismaWriteService.prisma.eventType.create({ - data: { - title: "Test Event Type", - slug: "test-event-type", - length: 30, - userId: nonOrgAdminData.user.id, - teamId: null, - }, - }); - - const routingFormData = { - name: "Test Routing Form", - description: "Test Description", - disabled: false, - routes: [ - { - id: "route-1", - queryValue: { - id: "route-1", - type: "group", - children1: { - "rule-1": { - type: "rule", - properties: { - field: "question1", - operator: "equal", - value: ["answer1"], - valueSrc: ["value"], - valueType: ["text"], - }, - }, - }, - }, - action: null, - isFallback: false, - }, - { - id: "fallback-route", - action: { type: "customPageMessage", value: "Fallback Message" }, - isFallback: true, - queryValue: { id: "fallback-route", type: "group" }, - }, - ], - fields: [ - { - id: "question1", - type: "text", - label: "Question 1", - required: true, - identifier: "question1", - }, - { - id: "question2", - type: "text", - label: "Question 2", - required: false, - identifier: "question2", - }, - ], - settings: { - emailOwnerOnSubmission: false, - }, - }; - - orgAdminData.routingForm = await prismaWriteService.prisma.app_RoutingForms_Form.create({ - data: { - ...routingFormData, - routes: [ - { - ...routingFormData.routes[0], - action: { - type: "eventTypeRedirectUrl", - eventTypeId: orgAdminData.eventType.id, - value: `${orgAdminData.user.username}/${orgAdminData.eventType.slug}`, - }, - }, - routingFormData.routes[1], - ], - userId: orgAdminData.user.id, - // User Routing Form has teamId=null - teamId: null, - }, - }); - - nonOrgAdminData.routingForm = await prismaWriteService.prisma.app_RoutingForms_Form.create({ - data: { - ...routingFormData, - routes: [ - { - ...routingFormData.routes[0], - action: { - type: "eventTypeRedirectUrl", - eventTypeId: nonOrgAdminData.eventType.id, - value: `${nonOrgAdminData.user.username}/${nonOrgAdminData.eventType.slug}`, - }, - }, - routingFormData.routes[1], - ], - userId: nonOrgAdminData.user.id, - teamId: null, - }, - }); - - // Patch response and get Responses endpoints right now work for teams only - // We need to fix them in a followup PR - teamData.routingForm = await prismaWriteService.prisma.app_RoutingForms_Form.create({ - data: { - ...routingFormData, - routes: [ - { - ...routingFormData.routes[0], - action: { - type: "eventTypeRedirectUrl", - eventTypeId: orgAdminData.eventType.id, - value: `team/${teamData.team.slug}/${orgAdminData.eventType.slug}`, - }, - }, - routingFormData.routes[1], - ], - userId: orgAdminData.user.id, - teamId: teamData.team.id, - }, - }); - - teamData.routingFormResponse1 = await prismaWriteService.prisma.app_RoutingForms_FormResponse.create({ - data: { - formId: teamData.routingForm.id, - response: JSON.stringify({ question1: "answer1", question2: "answer2" }), - }, - }); - - teamData.routingFormResponse2 = await prismaWriteService.prisma.app_RoutingForms_FormResponse.create({ - data: { - formId: teamData.routingForm.id, - response: { question1: "answer1", question2: "answer2" }, - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - describe(`GET /v2/organizations/:orgId/routing-forms/:routingFormId/responses`, () => { - it("should not get routing form responses for non existing org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/99999/routing-forms/${teamData.routingForm.id}/responses`) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .expect(403); - }); - - it("should not get routing form responses for non existing form", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/routing-forms/non-existent-id/responses`) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .expect(404); - }); - - it("should not get routing form responses without authentication", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/routing-forms/${teamData.routingForm.id}/responses`) - .expect(401); - }); - - it("should get routing form responses", async () => { - const createdAt = new Date(teamData.routingFormResponse1.createdAt); - createdAt.setHours(createdAt.getHours() - 1); - const isoStringCreatedAt = createdAt.toISOString(); - return request(app.getHttpServer()) - .get( - `/v2/organizations/${org.id}/routing-forms/${teamData.routingForm.id}/responses?skip=0&take=2&sortUpdatedAt=asc&sortCreatedAt=desc&afterCreatedAt=${isoStringCreatedAt}` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .expect(200) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responses = responseBody.data as App_RoutingForms_FormResponse[]; - expect(responses).toBeDefined(); - expect(responses.length).toBeGreaterThan(0); - expect( - responses.find((response) => response.id === teamData.routingFormResponse1.id) - ).toBeDefined(); - expect( - responses.find((response) => response.id === teamData.routingFormResponse1.id)?.formId - ).toEqual(teamData.routingFormResponse1.formId); - expect( - responses.find((response) => response.id === teamData.routingFormResponse2.id) - ).toBeDefined(); - expect( - responses.find((response) => response.id === teamData.routingFormResponse2.id)?.formId - ).toEqual(teamData.routingFormResponse2.formId); - }); - }); - }); - - describe(`POST /v2/organizations/:orgId/routing-forms/:routingFormId/responses`, () => { - it("should return 403 when organization does not exist", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/99999/routing-forms/${orgAdminData.routingForm.id}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ - question1: "answer1", - }) - .expect(403); - }); - - it("should return 404 when routing form does not exist", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/non-existent-id/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ - question1: "answer1", - }) - .expect(404); - }); - - it("should return 401 when authentication token is missing", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/${orgAdminData.routingForm.id}/responses?start=2050-09-05&end=2050-09-06` - ) - .send({ - question1: "answer1", - }) - .expect(401); - }); - - it("should create response and return available slots when routing to event type", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/${orgAdminData.routingForm.id}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ - question1: "answer1", // This matches the route condition - question2: "answer2", - }) - .expect(201) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data).toBeDefined(); - expect(data.routing?.responseId).toBeDefined(); - expect(typeof data.routing?.responseId).toBe("number"); - expect(data.eventTypeId).toEqual(orgAdminData.eventType.id); - expect(data.slots).toBeDefined(); - expect(typeof data.slots).toBe("object"); - }); - }); - - it("should return 400 when required form fields are missing", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/${orgAdminData.routingForm.id}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ - question2: "answer2", // Missing required question1 - }) - .expect(400); - }); - - it("should create response and return custom message if the routing is to custom page", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/${orgAdminData.routingForm.id}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ - question1: "different-answer", // This won't match any route - question2: "answer2", - }) - .expect(201) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data).toBeDefined(); - expect(data.routingCustomMessage).toBeDefined(); - expect(data.routingCustomMessage).toBe("Fallback Message"); - }); - }); - - it("should return 400 when required slot query parameters are missing", async () => { - // Missing start parameter - await request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/${orgAdminData.routingForm.id}/responses?end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ - question1: "answer1", - question2: "answer2", - }) - .expect(400); - - // Missing end parameter - await request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/${orgAdminData.routingForm.id}/responses?start=2050-09-05` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ - question1: "answer1", - question2: "answer2", - }) - .expect(400); - }); - - it("should return 400 when date parameters have invalid format", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/${orgAdminData.routingForm.id}/responses?start=invalid-date&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ - question1: "answer1", - }) - .expect(400); - }); - - it("should return 400 when end date is before start date", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/${orgAdminData.routingForm.id}/responses?start=2050-09-10&end=2050-09-05` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ - question1: "answer1", - }) - .expect(400); - }); - - it("should return 403 when user lacks permission to access organization", async () => { - // Create a new user without organization access - const unauthorizedUser = await userRepositoryFixture.create({ - email: `unauthorized-user-${randomString()}@api.com`, - }); - - const { keyString: unauthorizedApiKey } = await apiKeysRepositoryFixture.createApiKey( - unauthorizedUser.id, - null - ); - - const response = await request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/${orgAdminData.routingForm.id}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${unauthorizedApiKey}` }) - .send({ - question1: "answer1", - }); - - expect(response.status).toBe(403); - - // Clean up - await prismaWriteService.prisma.user.delete({ - where: { id: unauthorizedUser.id }, - }); - }); - - it("should handle queued response creation", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/${orgAdminData.routingForm.id}/responses?start=2050-09-05&end=2050-09-06&queueResponse=true` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ - question1: "answer1", - question2: "answer2", - }) - .expect(201) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data.routing?.queuedResponseId).toBeDefined(); - }); - }); - - it("should return 500 when event type is not found", async () => { - // Create a routing form with an invalid eventTypeId - const orgAdminUserRoutingFormWithInvalidEventType = - await prismaWriteService.prisma.app_RoutingForms_Form.create({ - data: { - name: "Test Routing Form with Invalid Event Type", - description: "Test Description", - disabled: false, - routes: [ - { - id: "route-1", - queryValue: { - id: "route-1", - type: "group", - children1: { - "rule-1": { - type: "rule", - properties: { - field: "question1", - operator: "equal", - value: ["answer1"], - valueSrc: ["value"], - valueType: ["text"], - }, - }, - }, - }, - action: { - type: "eventTypeRedirectUrl", - eventTypeId: 99999, // Invalid event type ID - value: `team/${teamData.team.slug}/non-existent-event-type`, - }, - isFallback: false, - }, - { - id: "fallback-route", - action: { type: "customPageMessage", value: "Fallback Message" }, - isFallback: true, - queryValue: { id: "fallback-route", type: "group" }, - }, - ], - fields: [ - { - id: "question1", - type: "text", - label: "Question 1", - required: true, - identifier: "question1", - }, - ], - settings: { - emailOwnerOnSubmission: false, - }, - - teamId: null, - userId: orgAdminData.user.id, - }, - }); - - // Try to create a response for the form with invalid event type - const response = await request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/${orgAdminUserRoutingFormWithInvalidEventType.id}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ - question1: "answer1", - }); - - expect(response.status).toBe(500); - - // Clean up the form - await prismaWriteService.prisma.app_RoutingForms_Form.delete({ - where: { id: orgAdminUserRoutingFormWithInvalidEventType.id }, - }); - }); - - it("should return external redirect URL when routing to external URL", async () => { - // Create a routing form with external redirect action - const externalRoutingForm = await prismaWriteService.prisma.app_RoutingForms_Form.create({ - data: { - name: "Test External Routing Form", - description: "Test Description for External Redirect", - disabled: false, - routes: [ - { - id: "external-route-1", - queryValue: { - id: "external-route-1", - type: "group", - children1: { - "rule-1": { - type: "rule", - properties: { - field: "question1", - operator: "equal", - value: ["external"], - valueSrc: ["value"], - valueType: ["text"], - }, - }, - }, - }, - action: { - type: "externalRedirectUrl", - value: "https://example.com/external-booking", - }, - isFallback: false, - }, - { - id: "fallback-route", - action: { type: "customPageMessage", value: "Fallback Message" }, - isFallback: true, - queryValue: { id: "fallback-route", type: "group" }, - }, - ], - fields: [ - { - id: "question1", - type: "text", - label: "Question 1", - required: true, - identifier: "question1", - }, - ], - settings: { - emailOwnerOnSubmission: false, - }, - teamId: null, - userId: orgAdminData.user.id, - }, - }); - - const response = await request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/routing-forms/${externalRoutingForm.id}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ - question1: "external", // This matches the route condition for external redirect - }) - .expect(201); - - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data).toBeDefined(); - expect(data.routingExternalRedirectUrl).toBeDefined(); - expect(data.routingExternalRedirectUrl).toContain("https://example.com/external-booking"); - expect(data.routingExternalRedirectUrl).toContain("cal.action=externalRedirectUrl"); - - // Verify that it doesn't contain event type routing data - expect(data.eventTypeId).toBeUndefined(); - expect(data.slots).toBeUndefined(); - expect(data.routing).toBeUndefined(); - expect(data.routingCustomMessage).toBeUndefined(); - - // Clean up the external routing form - await prismaWriteService.prisma.app_RoutingForms_Form.delete({ - where: { id: externalRoutingForm.id }, - }); - }); - }); - - describe(`POST /v2/organizations/:orgId/routing-forms/:routingFormId/responses - restrictions check`, () => { - // Helper functions to centralize API version header setting - const createAuthenticatedRequest = (method: "get" | "post" | "patch", url: string, apiKey: string) => { - return request(app.getHttpServer()) - [method](url) - .set({ - Authorization: `Bearer cal_test_${apiKey}`, - }); - }; - - it("should allow non-org-admin user to access their own user routing form", async () => { - return createAuthenticatedRequest( - "post", - `/v2/organizations/${org.id}/routing-forms/${nonOrgAdminData.routingForm.id}/responses?start=2050-09-05&end=2050-09-06`, - nonOrgAdminData.apiKey - ) - .send({ - question1: "answer1", - question2: "answer2", - }) - .expect(201) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data).toBeDefined(); - expect(data.routing?.responseId).toBeDefined(); - expect(typeof data.routing?.responseId).toBe("number"); - expect(data.eventTypeId).toEqual(nonOrgAdminData.eventType.id); - expect(data.slots).toBeDefined(); - expect(typeof data.slots).toBe("object"); - }); - }); - - it("should return 403 when non-org-admin user tries to access routing form they don't own", async () => { - // Create a second user - const otherUser = await userRepositoryFixture.create({ - email: `other-user-${randomString()}@api.com`, - }); - - // Create a routing form that belongs to the other user - const otherorgAdminUserRoutingForm = await prismaWriteService.prisma.app_RoutingForms_Form.create({ - data: { - name: "Other User's Routing Form", - description: "Test Description", - disabled: false, - routes: [ - { - id: "route-1", - queryValue: { - id: "route-1", - type: "group", - children1: { - "rule-1": { - type: "rule", - properties: { - field: "question1", - operator: "equal", - value: ["answer1"], - valueSrc: ["value"], - valueType: ["text"], - }, - }, - }, - }, - action: { - type: "eventTypeRedirectUrl", - eventTypeId: orgAdminData.eventType.id, - value: `team/${teamData.team.slug}/${orgAdminData.eventType.slug}`, - }, - isFallback: false, - }, - ], - fields: [ - { - id: "question1", - type: "text", - label: "Question 1", - required: true, - identifier: "question1", - }, - ], - settings: { - emailOwnerOnSubmission: false, - }, - // User Routing Form has teamId=null - teamId: null, - userId: otherUser.id, // This form belongs to otherUser, not the authenticated user - }, - }); - - // Try to access the routing form that belongs to the other user - const response = await createAuthenticatedRequest( - "post", - `/v2/organizations/${org.id}/routing-forms/${otherorgAdminUserRoutingForm.id}/responses?start=2050-09-05&end=2050-09-06`, - nonOrgAdminData.apiKey - ).send({ - question1: "answer1", - }); - - expect(response.status).toBe(403); - expect(response.body.error.message).toContain( - `Routing Form with id=${otherorgAdminUserRoutingForm.id} is not a user Routing Form owned by user with id=${nonOrgAdminData.user.id}.` - ); - - // Clean up - await prismaWriteService.prisma.app_RoutingForms_Form.delete({ - where: { id: otherorgAdminUserRoutingForm.id }, - }); - await prismaWriteService.prisma.user.delete({ - where: { id: otherUser.id }, - }); - }); - - it("should allow org admin to access any routing form using RolesGuard", async () => { - return createAuthenticatedRequest( - "post", - `/v2/organizations/${org.id}/routing-forms/${orgAdminData.routingForm.id}/responses?start=2050-09-05&end=2050-09-06`, - orgAdminData.apiKey - ) - .send({ - question1: "answer1", - question2: "answer2", - }) - .expect(201) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data).toBeDefined(); - expect(data.routing?.responseId).toBeDefined(); - expect(typeof data.routing?.responseId).toBe("number"); - }); - }); - - afterAll(async () => { - // Clean up user routing form - await prismaWriteService.prisma.app_RoutingForms_Form.delete({ - where: { id: orgAdminData.routingForm.id }, - }); - - // Clean up non-org-admin user - await prismaWriteService.prisma.user.delete({ - where: { id: nonOrgAdminData.user.id }, - }); - }); - }); - - describe(`PATCH /v2/organizations/:orgId/routing-forms/:routingFormId/responses/:responseId`, () => { - it("should not update routing form response for non existing org", async () => { - return request(app.getHttpServer()) - .patch( - `/v2/organizations/99999/routing-forms/${teamData.routingForm.id}/responses/${teamData.routingFormResponse1.id}` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ response: JSON.stringify({ question1: "updated_answer1" }) }) - .expect(403); - }); - - it("should not update routing form response for non existing form", async () => { - return request(app.getHttpServer()) - .patch( - `/v2/organizations/${org.id}/routing-forms/non-existent-id/responses/${teamData.routingFormResponse1.id}` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ response: JSON.stringify({ question1: "updated_answer1" }) }) - .expect(404); - }); - - it("should not update routing form response for non existing response", async () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/routing-forms/${teamData.routingForm.id}/responses/99999`) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ response: JSON.stringify({ question1: "updated_answer1" }) }) - .expect(404); - }); - - it("should not update routing form response without authentication", async () => { - return request(app.getHttpServer()) - .patch( - `/v2/organizations/${org.id}/routing-forms/${teamData.routingForm.id}/responses/${teamData.routingFormResponse1.id}` - ) - .send({ response: JSON.stringify({ question1: "updated_answer1" }) }) - .expect(401); - }); - - it("should update routing form response", async () => { - const updatedResponse = { question1: "updated_answer1", question2: "updated_answer2" }; - return request(app.getHttpServer()) - .patch( - `/v2/organizations/${org.id}/routing-forms/${teamData.routingForm.id}/responses/${teamData.routingFormResponse1.id}` - ) - .set({ Authorization: `Bearer cal_test_${orgAdminData.apiKey}` }) - .send({ response: updatedResponse }) - .expect(200) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data).toBeDefined(); - expect(data.id).toEqual(teamData.routingFormResponse1.id); - expect(data.formId).toEqual(teamData.routingFormResponse1.formId); - expect(data.response).toEqual(updatedResponse); - }); - }); - }); - - afterAll(async () => { - await prismaWriteService.prisma.app_RoutingForms_FormResponse.delete({ - where: { - id: teamData.routingFormResponse1.id, - }, - }); - await prismaWriteService.prisma.app_RoutingForms_FormResponse.delete({ - where: { - id: teamData.routingFormResponse2.id, - }, - }); - await prismaWriteService.prisma.app_RoutingForms_Form.deleteMany({ - where: { - teamId: org.id, - }, - }); - await prismaWriteService.prisma.apiKey.deleteMany({ - where: { - teamId: org.id, - }, - }); - await prismaWriteService.prisma.team.delete({ - where: { - id: teamData.team.id, - }, - }); - await prismaWriteService.prisma.team.delete({ - where: { - id: org.id, - }, - }); - await prismaWriteService.prisma.user.delete({ - where: { - id: orgAdminData.user.id, - }, - }); - - await app.close(); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms-responses.controller.ts b/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms-responses.controller.ts deleted file mode 100644 index fadb51dc11ea39..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms-responses.controller.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { Or } from "@/modules/auth/guards/or-guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { IsUserRoutingForm } from "@/modules/auth/guards/organizations/is-user-routing-form.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { GetRoutingFormResponsesOutput } from "@/modules/organizations/routing-forms/outputs/get-routing-form-responses.output"; -import { OrganizationsRoutingFormsResponsesService } from "@/modules/organizations/routing-forms/services/organizations-routing-forms-responses.service"; -import { - Body, - Controller, - Get, - Param, - Patch, - Post, - Query, - UseGuards, - ParseIntPipe, - Req, - Version, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger"; -import { Request } from "express"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; - -import { CreateRoutingFormResponseInput } from "../inputs/create-routing-form-response.input"; -import { GetRoutingFormResponsesParams } from "../inputs/get-routing-form-responses-params.input"; -import { UpdateRoutingFormResponseInput } from "../inputs/update-routing-form-response.input"; -import { CreateRoutingFormResponseOutput } from "../outputs/create-routing-form-response.output"; -import { UpdateRoutingFormResponseOutput } from "../outputs/update-routing-form-response.output"; - -@Controller({ - path: "/v2/organizations/:orgId/routing-forms/:routingFormId/responses", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@ApiTags("Orgs / Routing forms") -@ApiHeader(API_KEY_HEADER) -export class OrganizationsRoutingFormsResponsesController { - constructor( - private readonly organizationsRoutingFormsResponsesService: OrganizationsRoutingFormsResponsesService - ) {} - - @Get("/") - @ApiOperation({ summary: "Get routing form responses" }) - @Roles("ORG_ADMIN") - @UseGuards(RolesGuard) - @PlatformPlan("ESSENTIALS") - async getRoutingFormResponses( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("routingFormId") routingFormId: string, - @Query() queryParams: GetRoutingFormResponsesParams - ): Promise { - const { skip, take, ...filters } = queryParams; - - const responses = - await this.organizationsRoutingFormsResponsesService.getOrganizationRoutingFormResponses( - orgId, - routingFormId, - skip ?? 0, - take ?? 250, - filters - ); - - return { - status: SUCCESS_STATUS, - data: responses, - }; - } - - @Post("/") - @ApiOperation({ summary: "Create routing form response and get available slots" }) - @Roles("ORG_ADMIN") - @UseGuards(Or([RolesGuard, IsUserRoutingForm])) - @PlatformPlan("ESSENTIALS") - async createRoutingFormResponse( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("routingFormId") routingFormId: string, - @Query() query: CreateRoutingFormResponseInput, - @Req() request: Request - ): Promise { - const result = await this.organizationsRoutingFormsResponsesService.createRoutingFormResponseWithSlots( - routingFormId, - query, - request - ); - - return { - status: SUCCESS_STATUS, - data: result, - }; - } - - @Patch("/:responseId") - @ApiOperation({ summary: "Update routing form response" }) - @Roles("ORG_ADMIN") - @UseGuards(RolesGuard) - @PlatformPlan("ESSENTIALS") - async updateRoutingFormResponse( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("routingFormId") routingFormId: string, - @Param("responseId", ParseIntPipe) responseId: number, - @Body() updateRoutingFormResponseInput: UpdateRoutingFormResponseInput - ): Promise { - const updatedResponse = await this.organizationsRoutingFormsResponsesService.updateRoutingFormResponse( - orgId, - routingFormId, - responseId, - updateRoutingFormResponseInput - ); - - return { - status: SUCCESS_STATUS, - data: updatedResponse, - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms.controller.e2e-spec.ts deleted file mode 100644 index cdb68d497c5fde..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms.controller.e2e-spec.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { App_RoutingForms_Form, 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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { GetRoutingFormsOutput } from "@/modules/organizations/routing-forms/outputs/get-routing-forms.output"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("OrganizationsRoutingFormController", () => { - let app: INestApplication; - let prismaWriteService: PrismaWriteService; - let org: Team; - let team: Team; - let apiKeyString: string; - let routingForm: App_RoutingForms_Form; - - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - - let user: User; - const userEmail = `OrganizationsRoutingFormsResponsesController-key-bookings-2024-08-13-user-${randomString()}@api.com`; - let profileRepositoryFixture: ProfileRepositoryFixture; - - let membershipsRepositoryFixture: MembershipRepositoryFixture; - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); - - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - - prismaWriteService = moduleRef.get(PrismaWriteService); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - org = await organizationsRepositoryFixture.create({ - name: `OrganizationsRoutingFormsResponsesController-teams-memberships-organization-${randomString()}`, - isOrganization: true, - }); - - team = await teamRepositoryFixture.create({ - name: "OrganizationsRoutingFormsResponsesController orgs booking 1", - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - user = await userRepositoryFixture.create({ - email: userEmail, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: userEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); - apiKeyString = `${keyString}`; - - routingForm = await prismaWriteService.prisma.app_RoutingForms_Form.create({ - data: { - name: "Test Routing Form", - description: "Test Description", - disabled: false, - routes: [{ redirect: "http://google.com" }], - fields: [{ territory: "input" }], - settings: { test: "true" }, - teamId: team.id, - userId: user.id, - }, - }); - - await prismaWriteService.prisma.app_RoutingForms_FormResponse.create({ - data: { - formId: routingForm.id, - response: JSON.stringify({ question1: "answer1", question2: "answer2" }), - }, - }); - - await prismaWriteService.prisma.app_RoutingForms_FormResponse.create({ - data: { - formId: routingForm.id, - response: JSON.stringify({ question1: "answer1", question2: "answer2" }), - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - afterAll(async () => { - await prismaWriteService.prisma.app_RoutingForms_Form.deleteMany({ - where: { - teamId: org.id, - }, - }); - await prismaWriteService.prisma.apiKey.deleteMany({ - where: { - teamId: org.id, - }, - }); - await prismaWriteService.prisma.team.delete({ - where: { - id: team.id, - }, - }); - await prismaWriteService.prisma.team.delete({ - where: { - id: org.id, - }, - }); - await app.close(); - }); - - describe(`GET /v2/organizations/:orgId/routing-forms`, () => { - it("should not get routing forms for non existing org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/99999/routing-forms`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(403); - }); - - it("should not get routing forms without authentication", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/routing-forms`).expect(401); - }); - - it("should get organization routing forms", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/routing-forms?skip=0&take=1`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - const responseBody: GetRoutingFormsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const routingForms = responseBody.data; - expect(routingForms).toBeDefined(); - expect(routingForms.length).toBeGreaterThan(0); - expect(routingForms[0].id).toEqual(routingForm.id); - expect(routingForms[0].name).toEqual(routingForm.name); - expect(routingForms[0].description).toEqual(routingForm.description); - expect(routingForms[0].disabled).toEqual(routingForm.disabled); - }); - }); - - it("should filter routing forms by name", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/routing-forms?name=Test`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - const responseBody: GetRoutingFormsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const routingForms = responseBody.data; - expect(routingForms).toBeDefined(); - expect(routingForms.length).toBeGreaterThan(0); - expect(routingForms[0].name).toContain("Test"); - }); - }); - - it("should filter routing forms by disabled status", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/routing-forms?disabled=false`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - const responseBody: GetRoutingFormsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const routingForms = responseBody.data; - expect(routingForms).toBeDefined(); - expect(routingForms.length).toBeGreaterThan(0); - expect(routingForms[0].disabled).toEqual(false); - expect(routingForms[0].fields?.[0]).toEqual({ territory: "input" }); - expect(routingForms[0].routes?.[0]).toEqual({ redirect: "http://google.com" }); - expect(routingForms[0].settings).toEqual({ test: "true" }); - }); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms.controller.ts b/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms.controller.ts deleted file mode 100644 index dbbb61a4dba1b2..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms.controller.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { GetRoutingFormsParams } from "@/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input"; -import { - GetRoutingFormsOutput, - RoutingFormOutput, -} from "@/modules/organizations/routing-forms/outputs/get-routing-forms.output"; -import { OrganizationsRoutingFormsService } from "@/modules/organizations/routing-forms/services/organizations-routing-forms.service"; -import { Controller, Get, Param, Query, UseGuards, ParseIntPipe } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; - -@Controller({ - path: "/v2/organizations/:orgId/routing-forms", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@ApiTags("Orgs / Routing forms") -@ApiHeader(API_KEY_HEADER) -export class OrganizationsRoutingFormsController { - constructor(private readonly organizationsRoutingFormsService: OrganizationsRoutingFormsService) {} - - @Get() - @ApiOperation({ summary: "Get organization routing forms" }) - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - async getOrganizationRoutingForms( - @Param("orgId", ParseIntPipe) orgId: number, - @Query() queryParams: GetRoutingFormsParams - ): Promise { - const { skip, take, ...filters } = queryParams; - - const routingForms = await this.organizationsRoutingFormsService.getOrganizationRoutingForms( - orgId, - skip ?? 0, - take ?? 250, - filters - ); - - return { - status: SUCCESS_STATUS, - data: routingForms.map((form) => plainToClass(RoutingFormOutput, form)), - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/inputs/create-routing-form-response.input.ts b/apps/api/v2/src/modules/organizations/routing-forms/inputs/create-routing-form-response.input.ts deleted file mode 100644 index 2eb1c49f2c7d92..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/inputs/create-routing-form-response.input.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { Transform } from "class-transformer"; -import { IsBoolean, IsOptional } from "class-validator"; - -import { GetAvailableSlotsInput_2024_09_04 } from "@calcom/platform-types"; - -export class CreateRoutingFormResponseInput extends GetAvailableSlotsInput_2024_09_04 { - @Transform(({ value }: { value: string | boolean }) => { - if (typeof value === "boolean") return value; - if (typeof value === "string") { - return value.toLowerCase() === "true"; - } - return undefined; - }) - @IsBoolean() - @IsOptional() - @ApiPropertyOptional({ - type: Boolean, - description: "Whether to queue the form response.", - example: true, - }) - queueResponse?: boolean; -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input.ts b/apps/api/v2/src/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input.ts deleted file mode 100644 index 3b210e0659b8fd..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { Transform } from "class-transformer"; -import { IsOptional, IsString, IsEnum, IsISO8601, IsNumber, IsArray, ArrayMinSize } from "class-validator"; - -enum SortOrder { - ASC = "asc", - DESC = "desc", -} - -export class GetRoutingFormResponsesParams { - @ApiPropertyOptional({ type: Number, description: "Number of responses to skip" }) - @Transform(({ value }) => value && parseInt(value)) - @IsOptional() - skip?: number; - - @ApiPropertyOptional({ type: Number, description: "Number of responses to take" }) - @Transform(({ value }) => value && parseInt(value)) - @IsOptional() - take?: number; - - @ApiPropertyOptional({ enum: SortOrder, description: "Sort by creation time" }) - @IsOptional() - @IsEnum(SortOrder) - sortCreatedAt?: "asc" | "desc"; - - @ApiPropertyOptional({ enum: SortOrder, description: "Sort by update time" }) - @IsOptional() - @IsEnum(SortOrder) - sortUpdatedAt?: "asc" | "desc"; - - @ApiPropertyOptional({ - type: String, - format: "date-time", - description: "Filter by responses created after this date", - }) - @IsOptional() - @IsISO8601() - afterCreatedAt?: string; - - @ApiPropertyOptional({ - type: String, - format: "date-time", - description: "Filter by responses created before this date", - }) - @IsOptional() - @IsISO8601() - beforeCreatedAt?: string; - - @ApiPropertyOptional({ - type: String, - format: "date-time", - description: "Filter by responses created after this date", - }) - @IsOptional() - @IsISO8601() - afterUpdatedAt?: string; - - @ApiPropertyOptional({ - type: String, - format: "date-time", - description: "Filter by responses updated before this date", - }) - @IsOptional() - @IsISO8601() - beforeUpdatedAt?: string; - - @ApiPropertyOptional({ type: String, description: "Filter by responses routed to a specific booking" }) - @IsOptional() - @IsString() - routedToBookingUid?: string; -} - -export class GetRoutingFormsParams extends GetRoutingFormResponsesParams { - @IsOptional() - @Transform(({ value }) => { - if (typeof value === "string") { - return value.split(",").map((teamId: string) => parseInt(teamId)); - } - return value; - }) - @ApiPropertyOptional({ - type: [Number], - description: "Filter by teamIds. Team ids must be separated by a comma.", - example: "?teamIds=100,200", - }) - @IsArray() - @IsNumber({}, { each: true }) - @ArrayMinSize(1, { message: "teamIds must contain at least 1 team id" }) - teamIds?: number[]; -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/inputs/get-routing-forms-params.input.ts b/apps/api/v2/src/modules/organizations/routing-forms/inputs/get-routing-forms-params.input.ts deleted file mode 100644 index 9b3daa145a0668..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/inputs/get-routing-forms-params.input.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { Transform } from "class-transformer"; -import { IsOptional, IsBoolean, IsString, IsEnum, IsDate, IsISO8601 } from "class-validator"; - -enum SortOrder { - ASC = "asc", - DESC = "desc", -} - -export class GetRoutingFormsParams { - @ApiPropertyOptional({ type: Number, description: "Number of routing forms to skip" }) - @Transform(({ value }) => value && parseInt(value)) - @IsOptional() - skip?: number; - - @ApiPropertyOptional({ type: Number, description: "Number of routing forms to take" }) - @Transform(({ value }) => value && parseInt(value)) - @IsOptional() - take?: number; - - @ApiPropertyOptional({ enum: SortOrder, description: "Sort by creation time" }) - @IsOptional() - @IsEnum(SortOrder) - sortCreatedAt?: "asc" | "desc"; - - @ApiPropertyOptional({ enum: SortOrder, description: "Sort by update time" }) - @IsOptional() - @IsEnum(SortOrder) - sortUpdatedAt?: "asc" | "desc"; - - @ApiPropertyOptional({ type: Boolean, description: "Filter by disabled status" }) - @IsOptional() - @Transform(({ value }) => { - if (value === "true") return true; - if (value === "false") return false; - return value; - }) - @IsBoolean() - disabled?: boolean; - - @ApiPropertyOptional({ type: String, description: "Filter by name" }) - @IsOptional() - @IsString() - name?: string; - - @ApiPropertyOptional({ - type: String, - format: "date-time", - description: "Filter by forms created after this date", - }) - @IsOptional() - @IsISO8601() - @Transform(({ value }) => value && new Date(value)) - @IsDate() - afterCreatedAt?: Date; - - @ApiPropertyOptional({ - type: String, - format: "date-time", - description: "Filter by forms created before this date", - }) - @IsOptional() - @IsISO8601() - @Transform(({ value }) => value && new Date(value)) - @IsDate() - beforeCreatedAt?: Date; - - @ApiPropertyOptional({ - type: String, - format: "date-time", - description: "Filter by forms updated after this date", - }) - @IsOptional() - @IsISO8601() - @Transform(({ value }) => value && new Date(value)) - @IsDate() - afterUpdatedAt?: Date; - - @ApiPropertyOptional({ - type: String, - format: "date-time", - description: "Filter by forms updated before this date", - }) - @IsOptional() - @IsISO8601() - @Transform(({ value }) => value && new Date(value)) - @IsDate() - beforeUpdatedAt?: Date; -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/inputs/update-routing-form-response.input.ts b/apps/api/v2/src/modules/organizations/routing-forms/inputs/update-routing-form-response.input.ts deleted file mode 100644 index 7e4de86edd853b..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/inputs/update-routing-form-response.input.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsOptional } from "class-validator"; - -export class UpdateRoutingFormResponseInput { - @ApiPropertyOptional({ type: Object, description: "The updated response data" }) - @IsOptional() - response?: Record; -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/organizations-routing-forms.module.ts b/apps/api/v2/src/modules/organizations/routing-forms/organizations-routing-forms.module.ts deleted file mode 100644 index d329b59b84128f..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/organizations-routing-forms.module.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { IsUserRoutingForm } from "@/modules/auth/guards/organizations/is-user-routing-form.guard"; -import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsTeamsRoutingFormsResponsesOutputService } from "@/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { RoutingFormsModule } from "@/modules/routing-forms/routing-forms.module"; -import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; -import { Module } from "@nestjs/common"; - -import { OrganizationsRoutingFormsResponsesController } from "./controllers/organizations-routing-forms-responses.controller"; -import { OrganizationsRoutingFormsController } from "./controllers/organizations-routing-forms.controller"; -import { OrganizationsRoutingFormsRepository } from "./organizations-routing-forms.repository"; -import { OrganizationsRoutingFormsResponsesService } from "./services/organizations-routing-forms-responses.service"; -import { OrganizationsRoutingFormsService } from "./services/organizations-routing-forms.service"; -import { SharedRoutingFormResponseService } from "./services/shared-routing-form-response.service"; - -@Module({ - imports: [PrismaModule, StripeModule, RedisModule, RoutingFormsModule, SlotsModule_2024_09_04], - providers: [ - IsUserRoutingForm, - MembershipsRepository, - OrganizationsRepository, - OrganizationsRoutingFormsRepository, - OrganizationsRoutingFormsService, - OrganizationsRoutingFormsResponsesService, - SharedRoutingFormResponseService, - OrganizationsTeamsRoutingFormsResponsesOutputService, - TeamsEventTypesRepository, - EventTypesRepository_2024_06_14, - ], - controllers: [OrganizationsRoutingFormsController, OrganizationsRoutingFormsResponsesController], -}) -export class OrganizationsRoutingFormsModule {} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/organizations-routing-forms.repository.ts b/apps/api/v2/src/modules/organizations/routing-forms/organizations-routing-forms.repository.ts deleted file mode 100644 index 66fe05d83f05d8..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/organizations-routing-forms.repository.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrganizationsRoutingFormsRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async getOrganizationRoutingForms( - orgId: number, - skip: number, - take: number, - options?: { - disabled?: boolean; - name?: string; - sortCreatedAt?: "asc" | "desc"; - sortUpdatedAt?: "asc" | "desc"; - afterCreatedAt?: string; - beforeCreatedAt?: string; - afterUpdatedAt?: string; - beforeUpdatedAt?: string; - teamIds?: number[]; - } - ) { - const { - disabled, - name, - sortCreatedAt, - sortUpdatedAt, - afterCreatedAt, - beforeCreatedAt, - afterUpdatedAt, - beforeUpdatedAt, - teamIds, - } = options || {}; - - return this.dbRead.prisma.app_RoutingForms_Form.findMany({ - where: { - team: { parentId: orgId, ...(teamIds?.length ? { id: { in: teamIds } } : {}) }, - ...(disabled !== undefined && { disabled }), - ...(name && { name: { contains: name, mode: "insensitive" } }), - ...(afterCreatedAt && { createdAt: { gte: afterCreatedAt } }), - ...(beforeCreatedAt && { createdAt: { lte: beforeCreatedAt } }), - ...(afterUpdatedAt && { updatedAt: { gte: afterUpdatedAt } }), - ...(beforeUpdatedAt && { updatedAt: { lte: beforeUpdatedAt } }), - }, - orderBy: [ - ...(sortCreatedAt ? [{ createdAt: sortCreatedAt }] : []), - ...(sortUpdatedAt ? [{ updatedAt: sortUpdatedAt }] : []), - ], - skip, - take, - }); - } - - async getOrganizationRoutingFormResponses( - orgId: number, - routingFormId: string, - skip: number, - take: number, - options?: { - sortCreatedAt?: "asc" | "desc"; - sortUpdatedAt?: "asc" | "desc"; - afterCreatedAt?: string; - beforeCreatedAt?: string; - afterUpdatedAt?: string; - beforeUpdatedAt?: string; - routedToBookingUid?: string; - } - ) { - const { - sortCreatedAt, - sortUpdatedAt, - afterCreatedAt, - beforeCreatedAt, - routedToBookingUid, - afterUpdatedAt, - beforeUpdatedAt, - } = options || {}; - await this.dbRead.prisma.app_RoutingForms_Form.findFirstOrThrow({ - where: { - team: { parentId: orgId }, - id: routingFormId, - }, - }); - - return this.dbRead.prisma.app_RoutingForms_FormResponse.findMany({ - where: { - formId: routingFormId, - ...(afterCreatedAt && { createdAt: { gte: afterCreatedAt } }), - ...(beforeCreatedAt && { createdAt: { lte: beforeCreatedAt } }), - ...(afterUpdatedAt && { updatedAt: { gte: afterUpdatedAt } }), - ...(beforeUpdatedAt && { updatedAt: { lte: beforeUpdatedAt } }), - ...(routedToBookingUid && { routedToBookingUid }), - }, - orderBy: [ - ...(sortCreatedAt ? [{ createdAt: sortCreatedAt }] : []), - ...(sortUpdatedAt ? [{ updatedAt: sortUpdatedAt }] : []), - ], - skip, - take, - }); - } - - async updateRoutingFormResponse( - orgId: number, - routingFormId: string, - responseId: number, - data: { - response?: Record; - } - ) { - return this.dbWrite.prisma.app_RoutingForms_FormResponse.update({ - where: { - id: responseId, - formId: routingFormId, - form: { - team: { - parentId: orgId, - }, - }, - }, - data: { - ...data, - }, - }); - } -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/outputs/create-routing-form-response.output.ts b/apps/api/v2/src/modules/organizations/routing-forms/outputs/create-routing-form-response.output.ts deleted file mode 100644 index 2b7ea1bf790831..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/outputs/create-routing-form-response.output.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsArray, IsBoolean, IsInt, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator"; - -import { - ApiResponseWithoutData, - SlotsOutput_2024_09_04, - RangeSlotsOutput_2024_09_04, -} from "@calcom/platform-types"; - -class Routing { - @ApiProperty({ - type: String, - description: "The ID of the queued form response. Only present if the form response was queued.", - example: "123", - }) - @IsString() - @IsOptional() - @ApiPropertyOptional() - queuedResponseId?: string | null; - - @ApiProperty({ - type: Number, - description: "The ID of the routing form response.", - example: 123, - }) - @IsInt() - @IsOptional() - @ApiPropertyOptional() - responseId?: number | null; - - @ApiProperty({ - type: [Number], - description: "Array of team member IDs that were routed to handle this booking.", - example: [101, 102], - }) - @IsArray() - @IsInt({ each: true }) - teamMemberIds!: number[]; - - @ApiPropertyOptional({ - type: String, - description: "The email of the team member assigned to handle this booking.", - example: "john.doe@example.com", - }) - @IsString() - @IsOptional() - teamMemberEmail?: string; - - @ApiPropertyOptional({ - type: Boolean, - description: "Whether to skip contact owner assignment from CRM integration.", - example: true, - }) - @IsBoolean() - @IsOptional() - skipContactOwner?: boolean; - - @ApiPropertyOptional({ - type: String, - description: "The CRM application slug for integration.", - example: "salesforce", - }) - @IsString() - @IsOptional() - crmAppSlug?: string; - - @ApiPropertyOptional({ - type: String, - description: "The CRM owner record type for contact assignment.", - example: "Account", - }) - @IsString() - @IsOptional() - crmOwnerRecordType?: string; -} - -@ApiExtraModels(SlotsOutput_2024_09_04, RangeSlotsOutput_2024_09_04) -export class CreateRoutingFormResponseOutputData { - @ApiPropertyOptional({ - type: Number, - description: "The ID of the event type that was routed to.", - example: 123, - }) - @IsNumber() - @IsOptional() - eventTypeId?: number; - - @ValidateNested() - @Type(() => Routing) - @ApiPropertyOptional({ - type: Routing, - description: "The routing information that could be passed as is to the booking API.", - example: { - eventTypeId: 123, - routing: { - teamMemberIds: [101, 102], - teamMemberEmail: "john.doe@example.com", - skipContactOwner: true, - }, - }, - }) - routing?: Routing; - - @IsString() - @IsOptional() - @ApiPropertyOptional({ - type: String, - description: "A custom message to be displayed to the user in case of routing to a custom page.", - example: "This is a custom message.", - }) - routingCustomMessage?: string; - - @IsString() - @IsOptional() - @ApiPropertyOptional({ - type: String, - description: "The external redirect URL to be used in case of routing to a non cal.com event type URL.", - example: "https://example.com/", - }) - routingExternalRedirectUrl?: string; - - @ValidateNested() - @ApiProperty({ - oneOf: [ - { $ref: getSchemaPath(SlotsOutput_2024_09_04) }, - { $ref: getSchemaPath(RangeSlotsOutput_2024_09_04) }, - ], - }) - @Type(() => Object) - slots?: SlotsOutput_2024_09_04 | RangeSlotsOutput_2024_09_04; -} - -export class CreateRoutingFormResponseOutput extends ApiResponseWithoutData { - @ValidateNested() - @ApiProperty({ type: CreateRoutingFormResponseOutputData }) - @Type(() => CreateRoutingFormResponseOutputData) - data!: CreateRoutingFormResponseOutputData; -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/outputs/get-routing-form-responses.output.ts b/apps/api/v2/src/modules/organizations/routing-forms/outputs/get-routing-form-responses.output.ts deleted file mode 100644 index c32ebd9d0bb035..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/outputs/get-routing-form-responses.output.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { RoutingFormResponseOutput } from "@calcom/platform-types"; - -export class GetRoutingFormResponsesOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ type: [RoutingFormResponseOutput] }) - @Expose() - @Type(() => RoutingFormResponseOutput) - data!: RoutingFormResponseOutput[]; -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/outputs/get-routing-forms.output.ts b/apps/api/v2/src/modules/organizations/routing-forms/outputs/get-routing-forms.output.ts deleted file mode 100644 index e09947e06c32ff..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/outputs/get-routing-forms.output.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class RoutingFormOutput { - @Expose() - id!: string; - - @ApiProperty({ example: "My Form" }) - @Expose() - name!: string; - - @ApiProperty({ example: "This is the description." }) - @Expose() - description!: string | null; - - @ApiProperty({ example: 0 }) - @Expose() - position!: number; - - @Expose() - routes!: Record | null; - - @ApiProperty({ example: "2024-03-28T10:00:00.000Z" }) - @Expose() - createdAt!: string; - - @ApiProperty({ example: "2024-03-28T10:00:00.000Z" }) - @Expose() - updatedAt!: string; - - @Expose() - fields!: Record | null; - - @ApiProperty({ example: 2313 }) - @Expose() - userId!: number; - - @ApiProperty({ example: 4214321 }) - @Expose() - teamId!: number | null; - - @ApiProperty({ example: false }) - @Expose() - disabled!: boolean; - - @Expose() - settings!: Record | null; -} - -export class GetRoutingFormsOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ type: [RoutingFormOutput] }) - @Expose() - @Type(() => RoutingFormOutput) - data!: RoutingFormOutput[]; -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/outputs/update-routing-form-response.output.ts b/apps/api/v2/src/modules/organizations/routing-forms/outputs/update-routing-form-response.output.ts deleted file mode 100644 index 13fe3b87068243..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/outputs/update-routing-form-response.output.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { RoutingFormResponseOutput } from "@calcom/platform-types"; - -export class UpdateRoutingFormResponseOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ type: RoutingFormResponseOutput }) - @Expose() - @Type(() => RoutingFormResponseOutput) - data!: RoutingFormResponseOutput; -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/services/organizations-routing-forms-responses.service.ts b/apps/api/v2/src/modules/organizations/routing-forms/services/organizations-routing-forms-responses.service.ts deleted file mode 100644 index 2fb399c43aa75b..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/services/organizations-routing-forms-responses.service.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { OrganizationsRoutingFormsRepository } from "@/modules/organizations/routing-forms/organizations-routing-forms.repository"; -import { SharedRoutingFormResponseService } from "@/modules/organizations/routing-forms/services/shared-routing-form-response.service"; -import { OrganizationsTeamsRoutingFormsResponsesOutputService } from "@/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service"; -import { Injectable } from "@nestjs/common"; -import { Request } from "express"; - -import type { CreateRoutingFormResponseInput } from "../inputs/create-routing-form-response.input"; -import type { CreateRoutingFormResponseOutputData } from "../outputs/create-routing-form-response.output"; - -@Injectable() -export class OrganizationsRoutingFormsResponsesService { - constructor( - private readonly organizationsRoutingFormsRepository: OrganizationsRoutingFormsRepository, - private readonly outputService: OrganizationsTeamsRoutingFormsResponsesOutputService, - private readonly sharedRoutingFormResponseService: SharedRoutingFormResponseService - ) {} - - async getOrganizationRoutingFormResponses( - orgId: number, - routingFormId: string, - skip: number, - take: number, - options?: { - sortCreatedAt?: "asc" | "desc"; - sortUpdatedAt?: "asc" | "desc"; - afterCreatedAt?: string; - beforeCreatedAt?: string; - afterUpdatedAt?: string; - beforeUpdatedAt?: string; - routedToBookingUid?: string; - } - ) { - const responses = await this.organizationsRoutingFormsRepository.getOrganizationRoutingFormResponses( - orgId, - routingFormId, - skip, - take, - options - ); - - return this.outputService.getRoutingFormResponses(responses); - } - - async createRoutingFormResponseWithSlots( - routingFormId: string, - query: CreateRoutingFormResponseInput, - request: Request - ): Promise { - return this.sharedRoutingFormResponseService.createRoutingFormResponseWithSlots( - routingFormId, - query, - request - ); - } - - async updateRoutingFormResponse( - orgId: number, - routingFormId: string, - responseId: number, - data: { - response?: Record; - } - ) { - const updatedResponse = await this.organizationsRoutingFormsRepository.updateRoutingFormResponse( - orgId, - routingFormId, - responseId, - data - ); - - return this.outputService.getRoutingFormResponses([updatedResponse])[0]; - } -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/services/organizations-routing-forms.service.ts b/apps/api/v2/src/modules/organizations/routing-forms/services/organizations-routing-forms.service.ts deleted file mode 100644 index 97e178d8a5b272..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/services/organizations-routing-forms.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { OrganizationsRoutingFormsRepository } from "@/modules/organizations/routing-forms/organizations-routing-forms.repository"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrganizationsRoutingFormsService { - constructor(private readonly organizationsRoutingFormsRepository: OrganizationsRoutingFormsRepository) {} - - async getOrganizationRoutingForms( - orgId: number, - skip: number, - take: number, - options?: { - disabled?: boolean; - name?: string; - sortCreatedAt?: "asc" | "desc"; - sortUpdatedAt?: "asc" | "desc"; - afterCreatedAt?: string; - beforeCreatedAt?: string; - afterUpdatedAt?: string; - beforeUpdatedAt?: string; - teamIds?: number[]; - } - ) { - return this.organizationsRoutingFormsRepository.getOrganizationRoutingForms(orgId, skip, take, options); - } -} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/services/shared-routing-form-response.service.ts b/apps/api/v2/src/modules/organizations/routing-forms/services/shared-routing-form-response.service.ts deleted file mode 100644 index 4daab24e29c401..00000000000000 --- a/apps/api/v2/src/modules/organizations/routing-forms/services/shared-routing-form-response.service.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; -import { CreateRoutingFormResponseInput } from "@/modules/organizations/routing-forms/inputs/create-routing-form-response.input"; -import { CreateRoutingFormResponseOutputData } from "@/modules/organizations/routing-forms/outputs/create-routing-form-response.output"; -import { SlotsService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots.service"; -import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; -import { - Injectable, - BadRequestException, - InternalServerErrorException, - NotFoundException, -} from "@nestjs/common"; -import { Request } from "express"; - -import { getRoutedUrl } from "@calcom/platform-libraries"; -import { ById_2024_09_04_type } from "@calcom/platform-types"; - -@Injectable() -export class SharedRoutingFormResponseService { - constructor( - private readonly slotsService: SlotsService_2024_09_04, - private readonly teamsEventTypesRepository: TeamsEventTypesRepository, - private readonly eventTypesRepository: EventTypesRepository_2024_06_14 - ) {} - - async createRoutingFormResponseWithSlots( - routingFormId: string, - query: CreateRoutingFormResponseInput, - request: Request - ): Promise { - const { queueResponse, ...slotsQuery } = query; - const user = request.user as ApiAuthGuardUser; - - this.validateDateRange(slotsQuery.start, slotsQuery.end); - - const { redirectUrl, customMessage } = await this.getRoutingUrl( - request, - routingFormId, - queueResponse ?? false - ); - - // If there is no redirect URL, then we have to show the message as that would be custom page message to be shown as per the route chosen - if (!redirectUrl) { - return { - routingCustomMessage: customMessage, - }; - } - - if (!this.isEventTypeRedirectUrl(redirectUrl)) { - return { - routingExternalRedirectUrl: redirectUrl.toString(), - }; - } - - // Extract event type information from the routed URL - const { eventTypeId, crmParams } = await this.extractEventTypeAndCrmParams(user.id, redirectUrl); - - // Get available slots using the slots service with CRM parameters - const slots = await this.slotsService.getAvailableSlotsWithRouting({ - type: ById_2024_09_04_type, - eventTypeId, - ...slotsQuery, - ...crmParams, - }); - - const teamMemberIds = crmParams.routedTeamMemberIds ?? []; - const teamMemberEmail = crmParams.teamMemberEmail ?? undefined; - const skipContactOwner = crmParams.skipContactOwner ?? undefined; - const queuedResponseId = crmParams.queuedFormResponseId ?? null; - const responseId = crmParams.routingFormResponseId ?? null; - const crmAppSlug = crmParams.crmAppSlug ?? undefined; - const crmOwnerRecordType = crmParams.crmOwnerRecordType ?? undefined; - - if (responseId) { - return { - routing: { - responseId, - teamMemberEmail, - teamMemberIds, - skipContactOwner, - crmAppSlug, - crmOwnerRecordType, - }, - eventTypeId, - slots, - }; - } - - if (!queuedResponseId) { - throw new InternalServerErrorException( - "No routing form response ID or queued form response ID could be found." - ); - } - - return { - routing: { - queuedResponseId, - teamMemberEmail, - teamMemberIds, - skipContactOwner, - crmAppSlug, - crmOwnerRecordType, - }, - eventTypeId, - slots, - }; - } - - private validateDateRange(start: string, end: string) { - const startDate = new Date(start); - const endDate = new Date(end); - - if (endDate < startDate) { - throw new BadRequestException("End date cannot be before start date."); - } - } - - private async getRoutingUrl(request: Request, formId: string, queueResponse: boolean) { - const params = Object.fromEntries(new URLSearchParams(request.body)); - const routedUrlData = await getRoutedUrl( - { - req: request, - query: { ...params, form: formId, ...(queueResponse && { "cal.queueFormResponse": "true" }) }, - }, - true - ); - - if (routedUrlData.notFound) { - throw new NotFoundException("Routing form not found."); - } - - if (routedUrlData?.props?.errorMessage) { - throw new BadRequestException(routedUrlData.props.errorMessage); - } - - const destination = routedUrlData?.redirect?.destination; - - if (!destination) { - if (routedUrlData?.props?.message) { - return { - redirectUrl: null, - customMessage: routedUrlData.props.message, - }; - } - // This should never happen because there is always a fallback route - throw new InternalServerErrorException("No route found."); - } - - return { - redirectUrl: new URL(destination), - customMessage: null, - }; - } - - private async extractEventTypeAndCrmParams(userId: number, routingUrl: URL) { - // Extract team and event type information - // TODO: Route action also has eventTypeId directly now and instead of using this brittle approach for getting event type by slug, we should get by eventTypeId - const { teamId, eventTypeSlug } = this.extractTeamIdAndEventTypeSlugFromRedirectUrl(routingUrl); - const eventType = teamId - ? await this.teamsEventTypesRepository.getEventTypeByTeamIdAndSlug(teamId, eventTypeSlug) - : await this.eventTypesRepository.getUserEventTypeBySlug(userId, eventTypeSlug); - - if (!eventType?.id) { - // This could only happen if the event-type earlier selected as route action was deleted - throw new InternalServerErrorException( - `Chosen event type identified by slug ${eventTypeSlug} not found.` - ); - } - - // Extract CRM parameters from URL - const urlParams = routingUrl.searchParams; - const crmParams = { - teamMemberEmail: urlParams.get("cal.crmContactOwnerEmail") || undefined, - routedTeamMemberIds: urlParams.get("cal.routedTeamMemberIds") - ? urlParams - .get("cal.routedTeamMemberIds")! - .split(",") - .map((id) => parseInt(id)) - : undefined, - routingFormResponseId: urlParams.get("cal.routingFormResponseId") - ? parseInt(urlParams.get("cal.routingFormResponseId")!) - : undefined, - queuedFormResponseId: urlParams.get("cal.queuedFormResponseId") - ? (urlParams.get("cal.queuedFormResponseId") as string) - : undefined, - skipContactOwner: urlParams.get("cal.skipContactOwner") === "true" ? true : false, - crmAppSlug: urlParams.get("cal.crmAppSlug") || undefined, - crmOwnerRecordType: urlParams.get("cal.crmContactOwnerRecordType") || undefined, - }; - - return { - eventTypeId: eventType.id, - crmParams, - }; - } - - private isEventTypeRedirectUrl(routingUrl: URL) { - const routingSearchParams = routingUrl.searchParams; - return routingSearchParams.get("cal.action") === "eventTypeRedirectUrl"; - } - - private extractTeamIdAndEventTypeSlugFromRedirectUrl(routingUrl: URL) { - const eventTypeSlug = this.extractEventTypeFromRoutedUrl(routingUrl); - const teamId = this.extractTeamIdFromRoutedUrl(routingUrl); - - if (!eventTypeSlug) { - throw new InternalServerErrorException("Event type slug not found in the routed URL."); - } - - return { teamId, eventTypeSlug }; - } - - private extractTeamIdFromRoutedUrl(routingUrl: URL) { - const routingSearchParams = routingUrl.searchParams; - const teamId = Number(routingSearchParams.get("cal.teamId")); - if (isNaN(teamId)) { - return null; - } - return teamId; - } - - private extractEventTypeFromRoutedUrl(routingUrl: URL) { - const pathNameParams = routingUrl.pathname.split("/"); - return pathNameParams[pathNameParams.length - 1]; - } -} diff --git a/apps/api/v2/src/modules/organizations/schedules/organizations-schedules.controller.ts b/apps/api/v2/src/modules/organizations/schedules/organizations-schedules.controller.ts deleted file mode 100644 index 07c16c9d0a6e77..00000000000000 --- a/apps/api/v2/src/modules/organizations/schedules/organizations-schedules.controller.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard"; -import { OrganizationsSchedulesService } from "@/modules/organizations/schedules/services/organizations-schedules.service"; -import { - Controller, - UseGuards, - Get, - Post, - Param, - ParseIntPipe, - Body, - Patch, - Delete, - HttpCode, - HttpStatus, - Query, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { - CreateScheduleInput_2024_06_11, - CreateScheduleOutput_2024_06_11, - DeleteScheduleOutput_2024_06_11, - GetScheduleOutput_2024_06_11, - GetSchedulesOutput_2024_06_11, - UpdateScheduleInput_2024_06_11, - UpdateScheduleOutput_2024_06_11, -} from "@calcom/platform-types"; -import { SkipTakePagination } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -@ApiParam({ name: "orgId", type: Number, required: true }) -export class OrganizationsSchedulesController { - constructor( - private schedulesService: SchedulesService_2024_06_11, - private organizationScheduleService: OrganizationsSchedulesService - ) {} - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Get("/schedules") - @DocsTags("Orgs / Schedules") - @ApiOperation({ summary: "Get all schedules" }) - async getOrganizationSchedules( - @Param("orgId", ParseIntPipe) orgId: number, - @Query() queryParams: SkipTakePagination - ): Promise { - const { skip, take } = queryParams; - - const schedules = await this.organizationScheduleService.getOrganizationSchedules(orgId, skip, take); - - return { - status: SUCCESS_STATUS, - data: schedules, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsUserInOrg) - @Post("/users/:userId/schedules") - @DocsTags("Orgs / Users / Schedules") - @ApiOperation({ summary: "Create a schedule" }) - async createUserSchedule( - @Param("userId", ParseIntPipe) userId: number, - @Body() bodySchedule: CreateScheduleInput_2024_06_11 - ): Promise { - const schedule = await this.schedulesService.createUserSchedule(userId, bodySchedule); - - return { - status: SUCCESS_STATUS, - data: schedule, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsUserInOrg) - @Get("/users/:userId/schedules/:scheduleId") - @DocsTags("Orgs / Users / Schedules") - @ApiOperation({ summary: "Get a schedule" }) - async getUserSchedule( - @Param("userId", ParseIntPipe) userId: number, - @Param("scheduleId") scheduleId: number - ): Promise { - const schedule = await this.schedulesService.getUserSchedule(userId, scheduleId); - - return { - status: SUCCESS_STATUS, - data: schedule, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsUserInOrg) - @Get("/users/:userId/schedules") - @DocsTags("Orgs / Users / Schedules") - @ApiOperation({ summary: "Get all schedules" }) - async getUserSchedules( - @Param("userId", ParseIntPipe) userId: number - ): Promise { - const schedules = await this.schedulesService.getUserSchedules(userId); - - return { - status: SUCCESS_STATUS, - data: schedules, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsUserInOrg) - @Patch("/users/:userId/schedules/:scheduleId") - @DocsTags("Orgs / Users / Schedules") - @ApiOperation({ summary: "Update a schedule" }) - async updateUserSchedule( - @Param("userId", ParseIntPipe) userId: number, - @Param("scheduleId", ParseIntPipe) scheduleId: number, - @Body() bodySchedule: UpdateScheduleInput_2024_06_11 - ): Promise { - const updatedSchedule = await this.schedulesService.updateUserSchedule(userId, scheduleId, bodySchedule); - - return { - status: SUCCESS_STATUS, - data: updatedSchedule, - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsUserInOrg) - @Delete("/users/:userId/schedules/:scheduleId") - @HttpCode(HttpStatus.OK) - @DocsTags("Orgs / Users / Schedules") - @ApiOperation({ summary: "Delete a schedule" }) - async deleteUserSchedule( - @Param("userId", ParseIntPipe) userId: number, - @Param("scheduleId", ParseIntPipe) scheduleId: number - ): Promise { - await this.schedulesService.deleteUserSchedule(userId, scheduleId); - - return { - status: SUCCESS_STATUS, - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/schedules/organizations-schedules.e2e-spec.ts b/apps/api/v2/src/modules/organizations/schedules/organizations-schedules.e2e-spec.ts deleted file mode 100644 index 571796719472a4..00000000000000 --- a/apps/api/v2/src/modules/organizations/schedules/organizations-schedules.e2e-spec.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { - CreateScheduleInput_2024_06_11, - CreateScheduleOutput_2024_06_11, - GetScheduleOutput_2024_06_11, - GetSchedulesOutput_2024_06_11, - ScheduleAvailabilityInput_2024_06_11, - ScheduleOutput_2024_06_11, - UpdateScheduleInput_2024_06_11, - UpdateScheduleOutput_2024_06_11, -} from "@calcom/platform-types"; -import type { Membership, Profile, 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 { 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 { 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"; - -describe("Organizations Schedules Endpoints", () => { - describe("User lacks required role", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipFixtures: MembershipRepositoryFixture; - - const userEmail = `organizations-schedules-member-${randomString()}@api.com`; - let user: User; - let org: Team; - let membership: Membership; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipFixtures = new MembershipRepositoryFixture(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-schedules-organization-${randomString()}`, - isOrganization: true, - }); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - organization: { connect: { id: org.id } }, - }); - - membership = await membershipFixtures.addUserToOrg(user, org, "MEMBER", true); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should not be able to create schedule for org user", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users/${user.id}/schedules`) - .send({ - name: "work", - timeZone: "Europe/Rome", - isDefault: true, - }) - .expect(403); - }); - - it("should not be able to get org schedules", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/schedules`).expect(403); - }); - - it("should not be able to get user schedules", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/users/${user.id}/schedules/`) - .expect(403); - }); - - afterAll(async () => { - await membershipFixtures.delete(membership.id); - await userRepositoryFixture.deleteByEmail(user.email); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); - - describe("User has required role", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipFixtures: MembershipRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - - const userEmail = `organizations-schedules-admin-${randomString()}@api.com`; - - let user: User; - let org: Team; - let membership: Membership; - let profile: Profile; - - let createdSchedule: ScheduleOutput_2024_06_11; - - const createScheduleInput: CreateScheduleInput_2024_06_11 = { - name: "work", - timeZone: "Europe/Rome", - isDefault: true, - }; - - const defaultAvailability: ScheduleAvailabilityInput_2024_06_11[] = [ - { - days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], - startTime: "09:00", - endTime: "17:00", - }, - ]; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipFixtures = new MembershipRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-schedules-admin-organization-${randomString()}`, - isOrganization: true, - }); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - organization: { connect: { id: org.id } }, - }); - - profile = await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: userEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - - membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should create schedule for org user", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users/${user.id}/schedules`) - .send(createScheduleInput) - .expect(201) - .then(async (response) => { - const responseBody: CreateScheduleOutput_2024_06_11 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - createdSchedule = response.body.data; - - const expectedSchedule = { - ...createScheduleInput, - availability: defaultAvailability, - overrides: [], - }; - outputScheduleMatchesExpected(createdSchedule, expectedSchedule, 1); - - const scheduleOwner = createdSchedule.ownerId - ? await userRepositoryFixture.get(createdSchedule.ownerId) - : null; - expect(scheduleOwner?.defaultScheduleId).toEqual(createdSchedule.id); - }); - }); - - function outputScheduleMatchesExpected( - outputSchedule: ScheduleOutput_2024_06_11 | null, - expected: CreateScheduleInput_2024_06_11 & { - availability: CreateScheduleInput_2024_06_11["availability"]; - } & { - overrides: CreateScheduleInput_2024_06_11["overrides"]; - }, - expectedAvailabilityLength: number - ) { - expect(outputSchedule).toBeTruthy(); - expect(outputSchedule?.name).toEqual(expected.name); - expect(outputSchedule?.timeZone).toEqual(expected.timeZone); - expect(outputSchedule?.isDefault).toEqual(expected.isDefault); - expect(outputSchedule?.availability.length).toEqual(expectedAvailabilityLength); - - const outputScheduleAvailability = outputSchedule?.availability[0]; - expect(outputScheduleAvailability).toBeDefined(); - expect(outputScheduleAvailability?.days).toEqual(expected.availability?.[0].days); - expect(outputScheduleAvailability?.startTime).toEqual(expected.availability?.[0].startTime); - expect(outputScheduleAvailability?.endTime).toEqual(expected.availability?.[0].endTime); - - expect(JSON.stringify(outputSchedule?.overrides)).toEqual(JSON.stringify(expected.overrides)); - } - - it("should get org schedules", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/schedules`) - .expect(200) - .then(async (response) => { - const responseBody: GetSchedulesOutput_2024_06_11 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const schedules = response.body.data; - expect(schedules.length).toEqual(1); - - const expectedSchedule = { - ...createScheduleInput, - availability: defaultAvailability, - overrides: [], - }; - - outputScheduleMatchesExpected(schedules[0], expectedSchedule, 1); - }); - }); - - it("should get org user schedule", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/users/${user.id}/schedules/${createdSchedule.id}`) - .expect(200) - .then(async (response) => { - const responseBody: GetScheduleOutput_2024_06_11 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const schedule = response.body.data; - - const expectedSchedule = { - ...createScheduleInput, - availability: defaultAvailability, - overrides: [], - }; - - outputScheduleMatchesExpected(schedule, expectedSchedule, 1); - }); - }); - - it("should get user schedules", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/users/${user.id}/schedules/`) - .expect(200) - .then(async (response) => { - const responseBody: GetSchedulesOutput_2024_06_11 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const schedules = response.body.data; - expect(schedules.length).toEqual(1); - - const expectedSchedule = { - ...createScheduleInput, - availability: defaultAvailability, - overrides: [], - }; - - outputScheduleMatchesExpected(schedules[0], expectedSchedule, 1); - }); - }); - - it("should update user schedule name", async () => { - const newScheduleName = "updated-schedule-name"; - - const body: UpdateScheduleInput_2024_06_11 = { - name: newScheduleName, - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/users/${user.id}/schedules/${createdSchedule.id}`) - .send(body) - .expect(200) - .then((response: any) => { - const responseData: UpdateScheduleOutput_2024_06_11 = response.body; - expect(responseData.status).toEqual(SUCCESS_STATUS); - const responseSchedule = responseData.data; - - const expectedSchedule = { ...createdSchedule, name: newScheduleName }; - outputScheduleMatchesExpected(responseSchedule, expectedSchedule, 1); - - createdSchedule = responseSchedule; - }); - }); - - it("should delete user schedule", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/users/${user.id}/schedules/${createdSchedule.id}`) - .expect(200); - }); - - afterAll(async () => { - await profileRepositoryFixture.delete(profile.id); - await membershipFixtures.delete(membership.id); - await userRepositoryFixture.deleteByEmail(user.email); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/schedules/organizations-schedules.repository.ts b/apps/api/v2/src/modules/organizations/schedules/organizations-schedules.repository.ts deleted file mode 100644 index e652a85df79654..00000000000000 --- a/apps/api/v2/src/modules/organizations/schedules/organizations-schedules.repository.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrganizationSchedulesRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async getSchedulesByUserIds(userIds: number[], skip: number, take: number) { - return this.dbRead.prisma.schedule.findMany({ - where: { - userId: { - in: userIds, - }, - }, - include: { - availability: true, - }, - skip, - take, - }); - } -} diff --git a/apps/api/v2/src/modules/organizations/schedules/services/organizations-schedules.service.ts b/apps/api/v2/src/modules/organizations/schedules/services/organizations-schedules.service.ts deleted file mode 100644 index e70ad523eb86af..00000000000000 --- a/apps/api/v2/src/modules/organizations/schedules/services/organizations-schedules.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository"; -import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service"; -import { UsersRepository } from "@/modules/users/users.repository"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrganizationsSchedulesService { - constructor( - private readonly outputSchedulesService: OutputSchedulesService_2024_06_11, - private readonly schedulesRepository: SchedulesRepository_2024_06_11, - private readonly usersRepository: UsersRepository - ) {} - - async getOrganizationSchedules(organizationId: number, skip = 0, take = 250) { - const users = await this.usersRepository.getOrganizationUsers(organizationId); - const usersIds = users.map((user) => user.id); - - const schedules = await this.schedulesRepository.getSchedulesByUserIds(usersIds, skip, take); - - return this.outputSchedulesService.getResponseSchedules(schedules); - } -} diff --git a/apps/api/v2/src/modules/organizations/stripe/organizations-stripe.controller.ts b/apps/api/v2/src/modules/organizations/stripe/organizations-stripe.controller.ts deleted file mode 100644 index 543804dd60d873..00000000000000 --- a/apps/api/v2/src/modules/organizations/stripe/organizations-stripe.controller.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { OrganizationsStripeService } from "@/modules/organizations/stripe/services/organizations-stripe.service"; -import { - StripConnectOutputDto, - StripConnectOutputResponseDto, - StripCredentialsCheckOutputResponseDto, - StripCredentialsSaveOutputResponseDto, -} from "@/modules/stripe/outputs/stripe.output"; -import { StripeService } from "@/modules/stripe/stripe.service"; -import { getOnErrorReturnToValueFromQueryState } from "@/modules/stripe/utils/getReturnToValueFromQueryState"; -import { TokensRepository } from "@/modules/tokens/tokens.repository"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { - Controller, - Get, - Query, - HttpCode, - HttpStatus, - UseGuards, - Param, - Headers, - Req, - ParseIntPipe, - Redirect, - BadRequestException, -} from "@nestjs/common"; -import { ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; -import { Request } from "express"; -import { stringify } from "querystring"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; - -export type OAuthCallbackState = { - accessToken: string; - teamId?: string; - orgId?: string; - fromApp?: boolean; - returnTo?: string; - onErrorReturnTo?: string; -}; - -@Controller({ - path: "/v2/organizations/:orgId/teams/:teamId/stripe", - version: API_VERSIONS_VALUES, -}) -@DocsTags("Orgs / Teams / Stripe") -@ApiParam({ name: "orgId", type: Number, required: true }) -export class OrganizationsStripeController { - constructor( - private readonly organizationsStripeService: OrganizationsStripeService, - private readonly tokensRepository: TokensRepository - ) {} - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Get("/connect") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Get Stripe connect URL for a team" }) - async getTeamStripeConnectUrl( - @Req() req: Request, - @Headers("Authorization") authorization: string, - @GetUser() user: UserWithProfile, - @Param("teamId") teamId: string, - @Param("orgId") orgId: string, - @Query("returnTo") returnTo?: string, - @Query("onErrorReturnTo") onErrorReturnTo?: string - ): Promise { - const origin = req.headers.origin; - const accessToken = authorization.replace("Bearer ", ""); - - const state: OAuthCallbackState = { - onErrorReturnTo: !!onErrorReturnTo ? onErrorReturnTo : origin, - fromApp: false, - returnTo: !!returnTo ? returnTo : origin, - accessToken, - teamId, - orgId, - }; - - const stripeRedirectUrl = await this.organizationsStripeService.getStripeTeamRedirectUrl({ - state, - userEmail: user.email, - userName: user.name, - }); - - return { - status: SUCCESS_STATUS, - data: plainToClass(StripConnectOutputDto, { authUrl: stripeRedirectUrl }, { strategy: "excludeAll" }), - }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Get("/check") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Check team Stripe connection" }) - async checkTeamStripeConnection( - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - return await this.organizationsStripeService.checkIfTeamStripeAccountConnected(teamId); - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) - @Get("/save") - @Redirect(undefined, 301) - @ApiOperation({ summary: "Save Stripe credentials" }) - async save( - @Query("state") state: string, - @Query("code") code: string, - @Query("error") error: string | undefined, - @Query("error_description") error_description: string | undefined, - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - if (!state) { - throw new BadRequestException("Missing `state` query param"); - } - - const decodedCallbackState: OAuthCallbackState = JSON.parse(state); - try { - const userId = await this.tokensRepository.getAccessTokenOwnerId(decodedCallbackState.accessToken); - - // user cancels flow - if (error === "access_denied") { - return { url: getOnErrorReturnToValueFromQueryState(state) }; - } - - if (error) { - throw new BadRequestException(stringify({ error, error_description })); - } - - return await this.organizationsStripeService.saveStripeAccount( - decodedCallbackState, - code, - teamId, - userId - ); - } catch (error) { - if (error instanceof Error) { - console.error(error.message); - } - return { - url: decodedCallbackState.onErrorReturnTo ?? "", - }; - } - } -} diff --git a/apps/api/v2/src/modules/organizations/stripe/organizations-stripe.module.ts b/apps/api/v2/src/modules/organizations/stripe/organizations-stripe.module.ts deleted file mode 100644 index 7aaafc42e742dd..00000000000000 --- a/apps/api/v2/src/modules/organizations/stripe/organizations-stripe.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AppsRepository } from "@/modules/apps/apps.repository"; -import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; -import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsStripeController } from "@/modules/organizations/stripe/organizations-stripe.controller"; -import { OrganizationsStripeService } from "@/modules/organizations/stripe/services/organizations-stripe.service"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisService } from "@/modules/redis/redis.service"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { TokensRepository } from "@/modules/tokens/tokens.repository"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [StripeModule, PrismaModule], - exports: [OrganizationsStripeService], - providers: [ - OrganizationsStripeService, - CredentialsRepository, - AppsRepository, - RedisService, - OrganizationsRepository, - MembershipsRepository, - OrganizationsTeamsRepository, - TokensRepository, - ], - controllers: [OrganizationsStripeController], -}) -export class OrganizationsStripeModule {} diff --git a/apps/api/v2/src/modules/organizations/stripe/services/organizations-stripe.service.ts b/apps/api/v2/src/modules/organizations/stripe/services/organizations-stripe.service.ts deleted file mode 100644 index c416dec0a1c192..00000000000000 --- a/apps/api/v2/src/modules/organizations/stripe/services/organizations-stripe.service.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { AppsRepository } from "@/modules/apps/apps.repository"; -import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; -import { OAuthCallbackState, StripeService } from "@/modules/stripe/stripe.service"; -import { stripeInstance } from "@/modules/stripe/utils/newStripeInstance"; -import { StripeData } from "@/modules/stripe/utils/stripeDataSchemas"; -import { Logger, UnauthorizedException } from "@nestjs/common"; -import { Injectable } from "@nestjs/common"; - -import { ApiResponseWithoutData } from "@calcom/platform-types"; -import type { Prisma } from "@calcom/prisma/client"; - -@Injectable() -export class OrganizationsStripeService { - private logger = new Logger("OrganizationsStripeService"); - - constructor( - private readonly stripeService: StripeService, - private readonly credentialRepository: CredentialsRepository, - private readonly appsRepository: AppsRepository - ) {} - - async getStripeTeamRedirectUrl({ - state, - userEmail, - userName, - }: { - state: OAuthCallbackState; - userEmail?: string; - userName?: string | null; - }): Promise { - return await this.stripeService.getStripeRedirectUrl(state, userEmail, userName); - } - - async saveStripeAccount( - state: OAuthCallbackState, - code: string, - teamId: number, - userId?: number - ): Promise<{ url: string }> { - if (!userId) { - throw new UnauthorizedException("Invalid Access token."); - } - - const response = await stripeInstance.oauth.token({ - grant_type: "authorization_code", - code: code?.toString(), - }); - - const data: StripeData = { ...response, default_currency: "" }; - if (response["stripe_user_id"]) { - const account = await stripeInstance.accounts.retrieve(response["stripe_user_id"]); - data["default_currency"] = account.default_currency; - } - - const existingCredentials = await this.credentialRepository.findAllCredentialsByTypeAndTeamId( - "stripe_payment", - teamId - ); - - const credentialIdsToDelete = existingCredentials.map((item) => item.id); - if (credentialIdsToDelete.length > 0) { - await this.appsRepository.deleteTeamAppCredentials(credentialIdsToDelete, teamId); - } - - await this.appsRepository.createTeamAppCredential( - "stripe_payment", - data as unknown as Prisma.InputJsonObject, - teamId, - "stripe" - ); - - return { url: state.returnTo ?? "" }; - } - - async checkIfTeamStripeAccountConnected(teamId: number): Promise { - const stripeCredentials = await this.credentialRepository.findCredentialByTypeAndTeamId( - "stripe_payment", - teamId - ); - - return await this.stripeService.validateStripeCredentials(stripeCredentials); - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/bookings/inputs/get-organizations-teams-bookings.input.ts b/apps/api/v2/src/modules/organizations/teams/bookings/inputs/get-organizations-teams-bookings.input.ts deleted file mode 100644 index b344a255fae347..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/bookings/inputs/get-organizations-teams-bookings.input.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Transform, Type } from "class-transformer"; -import { - ArrayMinSize, - ArrayNotEmpty, - IsArray, - IsEnum, - IsInt, - IsISO8601, - IsNumber, - IsOptional, - IsString, - Max, - Min, -} from "class-validator"; - -enum Status { - upcoming = "upcoming", - recurring = "recurring", - past = "past", - cancelled = "cancelled", - unconfirmed = "unconfirmed", -} -type StatusType = keyof typeof Status; - -enum SortOrder { - asc = "asc", - desc = "desc", -} -type SortOrderType = keyof typeof SortOrder; - -export class GetOrganizationsTeamsBookingsInput_2024_08_13 { - // note(Lauris): filters - @IsOptional() - @Transform(({ value }) => { - if (typeof value === "string") { - return value.split(",").map((status: string) => status.trim()); - } - return value; - }) - @ArrayNotEmpty({ message: "status cannot be empty." }) - @IsEnum(Status, { - each: true, - message: "Invalid status. Allowed are upcoming, recurring, past, cancelled, unconfirmed", - }) - @ApiProperty({ - required: false, - description: - "Filter bookings by status. If you want to filter by multiple statuses, separate them with a comma.", - example: "?status=upcoming,past", - enum: Status, - isArray: true, - }) - status?: StatusType[]; - - @IsString() - @IsOptional() - @ApiProperty({ - type: String, - required: false, - description: "Filter bookings by the attendee's email address.", - example: "example@domain.com", - }) - attendeeEmail?: string; - - @IsString() - @IsOptional() - @ApiProperty({ - type: String, - required: false, - description: "Filter bookings by the attendee's name.", - example: "John Doe", - }) - attendeeName?: string; - - @IsString() - @IsOptional() - @ApiProperty({ - type: String, - required: false, - description: "Filter bookings by the booking Uid.", - example: "2NtaeaVcKfpmSZ4CthFdfk", - }) - bookingUid?: string; - - @IsOptional() - @Transform(({ value }) => { - if (typeof value === "string") { - return value.split(",").map((eventTypeId: string) => parseInt(eventTypeId)); - } - return value; - }) - @IsArray() - @IsNumber({}, { each: true }) - @ArrayMinSize(1, { message: "eventTypeIds must contain at least 1 event type id" }) - @ApiProperty({ - type: String, - required: false, - description: - "Filter bookings by event type ids belonging to the team. Event type ids must be separated by a comma.", - example: "?eventTypeIds=100,200", - }) - eventTypeIds?: number[]; - - @IsInt() - @IsOptional() - @Type(() => Number) - @ApiProperty({ - type: String, - required: false, - description: "Filter bookings by event type id belonging to the team.", - example: "?eventTypeId=100", - }) - eventTypeId?: number; - - @IsOptional() - @IsISO8601({ strict: true }, { message: "fromDate must be a valid ISO 8601 date." }) - @ApiProperty({ - type: String, - required: false, - description: "Filter bookings with start after this date string.", - example: "?afterStart=2025-03-07T10:00:00.000Z", - }) - afterStart?: string; - - @IsOptional() - @IsISO8601({ strict: true }, { message: "toDate must be a valid ISO 8601 date." }) - @ApiProperty({ - type: String, - required: false, - description: "Filter bookings with end before this date string.", - example: "?beforeEnd=2025-03-07T11:00:00.000Z", - }) - beforeEnd?: string; - - // note(Lauris): sort - @IsOptional() - @IsEnum(SortOrder, { - message: 'SortStart must be either "asc" or "desc".', - }) - @ApiProperty({ - required: false, - description: "Sort results by their start time in ascending or descending order.", - example: "?sortStart=asc OR ?sortStart=desc", - enum: SortOrder, - }) - sortStart?: SortOrderType; - - @IsOptional() - @IsEnum(SortOrder, { - message: 'SortEnd must be either "asc" or "desc".', - }) - @ApiProperty({ - required: false, - description: "Sort results by their end time in ascending or descending order.", - example: "?sortEnd=asc OR ?sortEnd=desc", - enum: SortOrder, - }) - sortEnd?: SortOrderType; - - @IsOptional() - @IsEnum(SortOrder, { - message: 'SortCreated must be either "asc" or "desc".', - }) - @ApiProperty({ - required: false, - description: - "Sort results by their creation time (when booking was made) in ascending or descending order.", - example: "?sortCreated=asc OR ?sortCreated=desc", - enum: SortOrder, - }) - sortCreated?: SortOrderType; - - // note(Lauris): pagination - @ApiProperty({ required: false, description: "The number of items to return", example: 10 }) - @Transform(({ value }: { value: string }) => value && parseInt(value)) - @IsNumber() - @Min(1) - @Max(250) - @IsOptional() - take?: number; - - @ApiProperty({ required: false, description: "The number of items to skip", example: 0 }) - @Transform(({ value }: { value: string }) => value && parseInt(value)) - @IsNumber() - @Min(0) - @IsOptional() - skip?: number; -} diff --git a/apps/api/v2/src/modules/organizations/teams/bookings/organizations-teams-bookings.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/bookings/organizations-teams-bookings.controller.e2e-spec.ts deleted file mode 100644 index 3d9b3a85391d11..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/bookings/organizations-teams-bookings.controller.e2e-spec.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { - CAL_API_VERSION_HEADER, - SUCCESS_STATUS, - VERSION_2024_08_13, - X_CAL_CLIENT_ID, - X_CAL_SECRET_KEY, -} 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, -} 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 { 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 { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { 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 { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; -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 { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.module"; - -describe("Organizations TeamsBookings Endpoints 2024-08-13", () => { - describe("Organization Team bookings", () => { - let app: INestApplication; - let organization: Team; - let team1: Team; - - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let oAuthClient: PlatformOAuthClient; - let teamRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let hostsRepositoryFixture: HostsRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - - const teamUserEmail = "orgUser1team1@api.com"; - const teamUserEmail2 = "orgUser2team1@api.com"; - let teamUser: User; - let teamUser2: User; - - let team1EventTypeId: number; - let bookingUid: string; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - teamUserEmail, - Test.createTestingModule({ - imports: [AppModule, OrganizationsTeamsBookingsModule], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - - organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" }); - oAuthClient = await createOAuthClient(organization.id); - - team1 = await teamRepositoryFixture.create({ - name: "team 1", - isOrganization: false, - parent: { connect: { id: organization.id } }, - createdByOAuthClient: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - teamUser = await userRepositoryFixture.create({ - email: teamUserEmail, - locale: "it", - name: "orgUser1team1", - platformOAuthClients: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - teamUser2 = await userRepositoryFixture.create({ - email: teamUserEmail2, - locale: "es", - name: "orgUser2team1", - platformOAuthClients: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: "working time", - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(teamUser.id, userSchedule); - await schedulesService.createUserSchedule(teamUser2.id, userSchedule); - - await profileRepositoryFixture.create({ - uid: `usr-${teamUser.id}`, - username: teamUserEmail, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: teamUser.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teamUser2.id}`, - username: teamUserEmail2, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: teamUser2.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: teamUser.id } }, - team: { connect: { id: team1.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: teamUser.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team1.id }, - }, - title: "Collective Event Type", - slug: "collective-event-type", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - team1EventTypeId = team1EventType.id; - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: teamUser.id, - }, - }, - eventType: { - connect: { - id: team1EventType.id, - }, - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - describe("create team bookings", () => { - it("should create a team 1 booking", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), - eventTypeId: team1EventTypeId, - attendee: { - name: "alice", - email: "alice@gmail.com", - timeZone: "Europe/Madrid", - language: "es", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - bookingUid = data.uid; - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(team1EventTypeId); - expect(data.attendees.length).toEqual(1); - 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, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - }); - - describe("get team bookings", () => { - it("should get bookings by teamId", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/teams/${team1.id}/bookings`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].eventTypeId).toEqual(team1EventTypeId); - }); - }); - - it("should get bookings by teamId and eventTypeId", async () => { - return request(app.getHttpServer()) - .get( - `/v2/organizations/${organization.id}/teams/${team1.id}/bookings?eventTypeId=${team1EventTypeId}` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].eventTypeId).toEqual(team1EventTypeId); - }); - }); - - it("should get bookings by teamId and bookingUid", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/teams/${team1.id}/bookings?bookingUid=${bookingUid}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set(X_CAL_CLIENT_ID, oAuthClient.id) - .set(X_CAL_SECRET_KEY, oAuthClient.secret) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].uid).toEqual(bookingUid); - }); - }); - - it("should not get bookings by teamId and non existing eventTypeId", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/teams/${team1.id}/bookings?eventTypeId=90909`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(400); - }); - - it("should not get bookings by non existing teamId", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/teams/90909/bookings`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(404); - }); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: ["http://localhost:5555"], - permissions: 32, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 { - return !Array.isArray(data) && typeof data === "object" && data !== null && "id" in data; - } - - afterAll(async () => { - await oauthClientRepositoryFixture.delete(oAuthClient.id); - await teamRepositoryFixture.delete(organization.id); - await userRepositoryFixture.deleteByEmail(teamUser.email); - await userRepositoryFixture.deleteByEmail(teamUserEmail2); - await bookingsRepositoryFixture.deleteAllBookings(teamUser.id, teamUser.email); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/bookings/organizations-teams-bookings.controller.ts b/apps/api/v2/src/modules/organizations/teams/bookings/organizations-teams-bookings.controller.ts deleted file mode 100644 index c4e4eda89d5e0a..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/bookings/organizations-teams-bookings.controller.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard"; -import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input"; -import { BookingReferencesOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/booking-references.output"; -import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service"; -import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, - OPTIONAL_API_KEY_HEADER, - API_KEY_OR_ACCESS_TOKEN_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { GetOrganizationsTeamsBookingsInput_2024_08_13 } from "@/modules/organizations/teams/bookings/inputs/get-organizations-teams-bookings.input"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { Controller, UseGuards, Get, Param, ParseIntPipe, Query, HttpStatus, HttpCode } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS, BOOKING_READ } from "@calcom/platform-constants"; -import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId/teams/:teamId/bookings", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Teams / Bookings") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -@ApiParam({ name: "orgId", type: Number, required: true }) -@ApiParam({ name: "teamId", type: Number, required: true }) -export class OrganizationsTeamsBookingsController { - constructor( - private readonly bookingsService: BookingsService_2024_08_13, - private readonly bookingReferencesService: BookingReferencesService_2024_08_13 - ) {} - - @Get("/") - @ApiOperation({ summary: "Get organization team bookings" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @HttpCode(HttpStatus.OK) - async getAllOrgTeamBookings( - @Query() queryParams: GetOrganizationsTeamsBookingsInput_2024_08_13, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("orgId", ParseIntPipe) orgId: number, - @GetUser() user: UserWithProfile - ): Promise { - const { bookings, pagination } = await this.bookingsService.getBookings( - { ...queryParams, teamId }, - { email: user.email, id: user.id, orgId } - ); - - return { - status: SUCCESS_STATUS, - data: bookings, - pagination, - }; - } - - @Get("/:bookingUid/references") - @PlatformPlan("SCALE") - @Roles("TEAM_ADMIN") - @Permissions([BOOKING_READ]) - @UseGuards( - ApiAuthGuard, - BookingUidGuard, - IsOrgGuard, - RolesGuard, - IsTeamInOrg, - PlatformPlanGuard, - IsAdminAPIEnabledGuard - ) - @ApiOperation({ - summary: "Get booking references", - }) - @HttpCode(HttpStatus.OK) - async getBookingReferences( - @Param("bookingUid") bookingUid: string, - @Query() filter: BookingReferencesFilterInput_2024_08_13 - ): Promise { - const bookingReferences = await this.bookingReferencesService.getOrgBookingReferences(bookingUid, filter); - - return { - status: SUCCESS_STATUS, - data: bookingReferences, - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/bookings/organizations-teams-bookings.module.ts b/apps/api/v2/src/modules/organizations/teams/bookings/organizations-teams-bookings.module.ts deleted file mode 100644 index c2b026df51c499..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/bookings/organizations-teams-bookings.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository"; -import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module"; -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service"; -import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service"; -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsTeamsBookingsController } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.controller"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [BookingsModule_2024_08_13, PrismaModule, StripeModule, RedisModule, MembershipsModule], - providers: [ - OrganizationsRepository, - OrganizationsTeamsRepository, - BookingReferencesService_2024_08_13, - BookingReferencesRepository_2024_08_13, - BookingsRepository_2024_08_13, - OutputBookingReferencesService_2024_08_13, - ], - controllers: [OrganizationsTeamsBookingsController], -}) -export class OrganizationsTeamsBookingsModule {} diff --git a/apps/api/v2/src/modules/organizations/teams/index/inputs/create-organization-team.input.ts b/apps/api/v2/src/modules/organizations/teams/index/inputs/create-organization-team.input.ts deleted file mode 100644 index 6837f3b0a7029f..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/index/inputs/create-organization-team.input.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { CreateTeamInput } from "@/modules/teams/teams/inputs/create-team.input"; - -export class CreateOrgTeamDto extends CreateTeamInput {} diff --git a/apps/api/v2/src/modules/organizations/teams/index/inputs/update-organization-team.input.ts b/apps/api/v2/src/modules/organizations/teams/index/inputs/update-organization-team.input.ts deleted file mode 100644 index 87d9402cbfdd6f..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/index/inputs/update-organization-team.input.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { UpdateTeamDto } from "@/modules/teams/teams/inputs/update-team.input"; - -export class UpdateOrgTeamDto extends UpdateTeamDto {} diff --git a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.e2e-spec.ts deleted file mode 100644 index 7030658c350652..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.e2e-spec.ts +++ /dev/null @@ -1,696 +0,0 @@ -import { SUCCESS_STATUS, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants"; -import type { ApiSuccessResponse } 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 { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; -import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; -import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; -import { randomString } from "test/utils/randomString"; -import { withApiAuth } from "test/utils/withApiAuth"; -import { mockThrottlerGuard } from "test/utils/withNoThrottler"; -import { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input"; -import { OrgMeTeamOutputDto } from "@/modules/organizations/teams/index/outputs/organization-team.output"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Team Endpoints", () => { - describe("User Authentication - User is Org Admin", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let org: Team; - let team: Team; - let team2: Team; - let teamCreatedViaApi: Team; - let teamCreatedViaApi2: Team; - let teamCreatedViaApi3: Team; - - const userEmail = `organizations-teams-admin-${randomString()}@api.com`; - let user: User; - - beforeAll(async () => { - mockThrottlerGuard(); - - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-teams-organization-${randomString()}`, - isOrganization: true, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - team = await teamsRepositoryFixture.create({ - name: `organizations-teams-team1-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - team2 = await teamsRepositoryFixture.create({ - name: `organizations-teams-team2-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(teamsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should get all the teams of the org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data[0].id).toEqual(team.id); - expect(responseBody.data[1].id).toEqual(team2.id); - }); - }); - - it("should get all the teams of the org paginated", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams?skip=1&take=1`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - console.log("WOOOW", responseBody.data); - expect([team2.id, team.id]).toContain(responseBody.data[0].id); - expect([team2.id]); - }); - }); - - it("should fail if org does not exist", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/120494059/teams`).expect(403); - }); - - it("should get the team of the org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(team.id); - expect(responseBody.data.parentId).toEqual(team.parentId); - }); - }); - - it("should create the team of the org", async () => { - const teamName = `organizations-teams-api-team1-${randomString()}`; - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams`) - .send({ - name: teamName, - slug: "team-created-via-api", - bio: "This is our test team created via API", - } satisfies CreateOrgTeamDto) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - teamCreatedViaApi = responseBody.data; - expect(teamCreatedViaApi.name).toEqual(teamName); - expect(teamCreatedViaApi.slug).toEqual("team-created-via-api"); - expect(teamCreatedViaApi.bio).toEqual("This is our test team created via API"); - expect(teamCreatedViaApi.parentId).toEqual(org.id); - const membership = await membershipsRepositoryFixture.getUserMembershipByTeamId( - user.id, - teamCreatedViaApi.id - ); - expect(membership?.role ?? "").toEqual("OWNER"); - expect(membership?.accepted).toEqual(true); - }); - }); - - it("should get all the teams of the authenticated org member", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/me`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.data.find((t) => t.id === teamCreatedViaApi.id)).toBeDefined(); - expect(responseBody.data.some((t) => t.accepted)).toBeTruthy(); - expect(responseBody.data.find((t) => t.id === teamCreatedViaApi.id)?.role).toBe("OWNER"); - }); - }); - - it("should update the team of the org", async () => { - const updatedTeamName = `organizations-teams-api-team1-${randomString()}-updated`; - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${teamCreatedViaApi.id}`) - .send({ - name: updatedTeamName, - weekStart: "Monday", - logoUrl: "https://i.cal.com/api/avatar/b0b58752-68ad-4c0d-8024-4fa382a77752.png", - bannerUrl: "https://i.cal.com/api/avatar/949be534-7a88-4185-967c-c020b0c0bef3.png", - } satisfies CreateOrgTeamDto) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - teamCreatedViaApi = responseBody.data; - expect(teamCreatedViaApi.name).toEqual(updatedTeamName); - expect(teamCreatedViaApi.weekStart).toEqual("Monday"); - expect(teamCreatedViaApi.logoUrl).toEqual( - "https://i.cal.com/api/avatar/b0b58752-68ad-4c0d-8024-4fa382a77752.png" - ); - expect(teamCreatedViaApi.bannerUrl).toEqual( - "https://i.cal.com/api/avatar/949be534-7a88-4185-967c-c020b0c0bef3.png" - ); - expect(teamCreatedViaApi.parentId).toEqual(org.id); - }); - }); - - it("should delete the team of the org we created via api", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${teamCreatedViaApi.id}`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(teamCreatedViaApi.id); - expect(responseBody.data.parentId).toEqual(teamCreatedViaApi.parentId); - }); - }); - - it("should fail to get the team of the org we just deleted", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${teamCreatedViaApi.id}`) - .expect(404); - }); - - it("should fail if the team does not exist", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams/123132145`).expect(404); - }); - - it("should create the team of the org without auto-accepting creator", async () => { - const teamName = `organizations-teams-api-team2-${randomString()}`; - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams`) - .send({ - name: teamName, - autoAcceptCreator: false, - } satisfies CreateOrgTeamDto) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - teamCreatedViaApi2 = responseBody.data; - expect(teamCreatedViaApi2.name).toEqual(teamName); - expect(teamCreatedViaApi2.parentId).toEqual(org.id); - const membership = await membershipsRepositoryFixture.getUserMembershipByTeamId( - user.id, - teamCreatedViaApi2.id - ); - expect(membership?.role ?? "").toEqual("OWNER"); - expect(membership?.accepted).toEqual(false); - }); - }); - - it("should create the team of the org with automatically set slug", async () => { - const teamName = `Organizations Teams Automatic Slug`; - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams`) - .send({ - name: teamName, - bio: "This is our test team created via API", - } satisfies CreateOrgTeamDto) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - teamCreatedViaApi3 = responseBody.data; - expect(teamCreatedViaApi3.name).toEqual(teamName); - expect(teamCreatedViaApi3.slug).toEqual("organizations-teams-automatic-slug"); - expect(teamCreatedViaApi3.bio).toEqual("This is our test team created via API"); - expect(teamCreatedViaApi3.parentId).toEqual(org.id); - const membership = await membershipsRepositoryFixture.getUserMembershipByTeamId( - user.id, - teamCreatedViaApi3.id - ); - expect(membership?.role ?? "").toEqual("OWNER"); - expect(membership?.accepted).toEqual(true); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(user.email); - await teamsRepositoryFixture.delete(team.id); - await teamsRepositoryFixture.delete(team2.id); - await teamsRepositoryFixture.delete(teamCreatedViaApi2.id); - await teamsRepositoryFixture.delete(teamCreatedViaApi3.id); - await teamsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); -}); - -describe("Organizations Team Endpoints", () => { - describe("User Authentication - User is Org Member", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let org: Team; - let team: Team; - let team2: Team; - - const userEmail = `organizations-teams-member-${randomString()}@api.com`; - let user: User; - - beforeAll(async () => { - mockThrottlerGuard(); - - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-teams-organization-${randomString()}`, - isOrganization: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - team = await teamsRepositoryFixture.create({ - name: `organizations-teams-team1-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - team2 = await teamsRepositoryFixture.create({ - name: `organizations-teams-team2-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(teamsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should deny get all the teams of the org", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams`).expect(403); - }); - - it("should deny get all the teams of the org paginated", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams?skip=1&take=1`).expect(403); - }); - - it("should deny get the team of the org", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams/${team.id}`).expect(403); - }); - - it("should deny create the team of the org", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams`) - .send({ - name: `organizations-teams-api-team1-${randomString()}`, - } satisfies CreateOrgTeamDto) - .expect(403); - }); - - it("should deny update the team of the org", async () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}`) - .send({ - name: `organizations-teams-api-team1-${randomString()}-updated`, - } satisfies CreateOrgTeamDto) - .expect(403); - }); - - it("should deny delete the team of the org we created via api", async () => { - return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/teams/${team2.id}`).expect(403); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(user.email); - await teamsRepositoryFixture.delete(team.id); - await teamsRepositoryFixture.delete(team2.id); - await teamsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); -}); - -describe("Organizations Team Endpoints", () => { - describe("User Authentication - User is Team Owner", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let org: Team; - let team: Team; - let team2: Team; - - const userEmail = `organizations-teams-owner-${randomString()}@api.com`; - let user: User; - - beforeAll(async () => { - mockThrottlerGuard(); - - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-teams-organization-${randomString()}`, - isOrganization: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - team = await teamsRepositoryFixture.create({ - name: `organizations-teams-team1-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - team2 = await teamsRepositoryFixture.create({ - name: `organizations-teams-team2-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: user.id } }, - team: { connect: { id: team.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: user.id } }, - team: { connect: { id: team2.id } }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should deny get all the teams of the org", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams`).expect(403); - }); - - it("should deny get all the teams of the org paginated", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams?skip=1&take=1`).expect(403); - }); - - it("should get the team of the org for which the user is team owner", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams/${team.id}`).expect(200); - }); - - it("should deny create the team of the org", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams`) - .send({ - name: `organizations-teams-api-team1-${randomString()}`, - } satisfies CreateOrgTeamDto) - .expect(403); - }); - - it("should deny update the team of the org", async () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}`) - .send({ - name: `organizations-teams-api-team1-${randomString()}-updated`, - } satisfies CreateOrgTeamDto) - .expect(403); - }); - - it("should deny delete the team of the org we created via api", async () => { - return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/teams/${team2.id}`).expect(403); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(user.email); - await teamsRepositoryFixture.delete(team.id); - await teamsRepositoryFixture.delete(team2.id); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); -}); - -describe("Organizations Team Endpoints", () => { - describe("Platform teams", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let orgRepositoryFixture: OrganizationRepositoryFixture; - - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - - let oAuthClient1: PlatformOAuthClient; - let oAuthClient2: PlatformOAuthClient; - let org: Team; - let team1: Team; - let team2: Team; - - const userEmail = `organizations-teams-platform-owner-${randomString()}@api.com`; - let user: User; - - beforeAll(async () => { - mockThrottlerGuard(); - - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - orgRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - org = await orgRepositoryFixture.create({ - name: `organizations-teams-platform-organization-${randomString()}`, - isOrganization: true, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - oAuthClient1 = await createOAuthClient(org.id); - oAuthClient2 = await createOAuthClient(org.id); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: ["redirect-uri"], - permissions: 32, - }; - const secret = "secret"; - - return await oauthClientRepositoryFixture.create(organizationId, data, secret); - } - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(teamsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should create first oAuth client team", async () => { - const teamName = `organizations-teams-platform-api-team1-${randomString()}`; - const teamMetadata = { - key: `value ${randomString()}`, - }; - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams`) - .set(X_CAL_CLIENT_ID, oAuthClient1.id) - .send({ - name: teamName, - metadata: teamMetadata, - } satisfies CreateOrgTeamDto) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - team1 = responseBody.data; - expect(team1.name).toEqual(teamName); - expect(team1.metadata).toEqual(teamMetadata); - expect(team1.parentId).toEqual(org.id); - - const membership = await membershipsRepositoryFixture.getUserMembershipByTeamId(user.id, team1.id); - - expect(membership?.role ?? "").toEqual("OWNER"); - expect(membership?.accepted).toEqual(true); - }); - }); - - it("should create second oAuth client team", async () => { - const teamName = `organizations-teams-platform-api-team2-${randomString()}`; - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams`) - .set(X_CAL_CLIENT_ID, oAuthClient2.id) - .set(X_CAL_SECRET_KEY, oAuthClient2.secret) - .send({ - name: teamName, - } satisfies CreateOrgTeamDto) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - team2 = responseBody.data; - expect(team2.name).toEqual(teamName); - expect(team2.parentId).toEqual(org.id); - - const membership = await membershipsRepositoryFixture.getUserMembershipByTeamId(user.id, team2.id); - - expect(membership?.role ?? "").toEqual("OWNER"); - expect(membership?.accepted).toEqual(true); - }); - }); - - it("should get all the platform teams correctly tied to OAuth clients", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams`) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data[0].id).toEqual(team1.id); - expect(responseBody.data[1].id).toEqual(team2.id); - - const oAuthClientTeams = await teamsRepositoryFixture.getPlatformOrgTeams(org.id, oAuthClient1.id); - expect(oAuthClientTeams.length).toEqual(1); - const oAuthClientTeam = oAuthClientTeams[0]; - expect(oAuthClientTeam.id).toEqual(team1.id); - expect(oAuthClientTeam.name).toEqual(team1.name); - - const oAuthClient2Teams = await teamsRepositoryFixture.getPlatformOrgTeams(org.id, oAuthClient2.id); - expect(oAuthClient2Teams.length).toEqual(1); - const oAuthClientTeam2 = oAuthClient2Teams[0]; - expect(oAuthClientTeam2.id).toEqual(team2.id); - expect(oAuthClientTeam2.name).toEqual(team2.name); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(user.email); - await teamsRepositoryFixture.delete(team1.id); - await teamsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.ts b/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.ts deleted file mode 100644 index 8fa6df83489ff7..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { SUCCESS_STATUS, X_CAL_CLIENT_ID } from "@calcom/platform-constants"; -import { OrgTeamOutputDto, SkipTakePagination } from "@calcom/platform-types"; -import type { Team } from "@calcom/prisma/client"; -import { - Body, - Controller, - Delete, - Get, - Param, - ParseIntPipe, - Patch, - Post, - Query, - Req, - UseGuards, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; -import { Request } from "express"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { Throttle } from "@/lib/endpoint-throttler-decorator"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { GetTeam } from "@/modules/auth/decorators/get-team/get-team.decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service"; -import { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input"; -import { UpdateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/update-organization-team.input"; -import { - OrgMeTeamOutputDto, - OrgMeTeamsOutputResponseDto, - OrgTeamOutputResponseDto, - OrgTeamsOutputResponseDto, -} from "@/modules/organizations/teams/index/outputs/organization-team.output"; -import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service"; -import { UserWithProfile } from "@/modules/users/users.repository"; - -@Controller({ - path: "/v2/organizations/:orgId/teams", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Teams") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -@ApiParam({ name: "orgId", type: Number, required: true }) -export class OrganizationsTeamsController { - constructor( - private organizationsTeamsService: OrganizationsTeamsService, - private organizationsMembershipService: OrganizationsMembershipService - ) {} - - @Get() - @ApiOperation({ summary: "Get all teams" }) - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - async getAllTeams( - @Param("orgId", ParseIntPipe) orgId: number, - @Query() queryParams: SkipTakePagination - ): Promise { - const { skip, take } = queryParams; - const teams = await this.organizationsTeamsService.getPaginatedOrgTeams(orgId, skip ?? 0, take ?? 250); - return { - status: SUCCESS_STATUS, - data: teams.map((team) => plainToClass(OrgTeamOutputDto, team, { strategy: "excludeAll" })), - }; - } - - @Get("/me") - @ApiOperation({ summary: "Get teams membership for user" }) - @Roles("ORG_MEMBER") - @PlatformPlan("ESSENTIALS") - async getMyTeams( - @Param("orgId", ParseIntPipe) orgId: number, - @Query() queryParams: SkipTakePagination, - @GetUser() user: UserWithProfile - ): Promise { - const { skip, take } = queryParams; - const isOrgAdminOrOwner = await this.organizationsMembershipService.isOrgAdminOrOwner(orgId, user.id); - const teams = isOrgAdminOrOwner - ? await this.organizationsTeamsService.getPaginatedOrgTeamsWithMembers(orgId, skip ?? 0, take ?? 250) - : await this.organizationsTeamsService.getPaginatedOrgUserTeams(orgId, user.id, skip ?? 0, take ?? 250); - - return { - status: SUCCESS_STATUS, - data: teams.map((team) => { - const me = team.members.find((member) => member.userId === user.id); - return plainToClass( - OrgMeTeamOutputDto, - me ? { ...team, role: me.role, accepted: me.accepted } : team, - { strategy: "excludeAll" } - ); - }), - }; - } - - @UseGuards(IsTeamInOrg) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @Get("/:teamId") - @ApiOperation({ summary: "Get a team" }) - @ApiParam({ name: "teamId", type: Number, required: true }) - async getTeam(@GetTeam() team: Team): Promise { - return { - status: SUCCESS_STATUS, - data: plainToClass(OrgTeamOutputDto, team, { strategy: "excludeAll" }), - }; - } - - @UseGuards(IsTeamInOrg) - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Delete("/:teamId") - @Throttle({ limit: 1, ttl: 1000, blockDuration: 1000, name: "org_teams_delete" }) - @ApiOperation({ summary: "Delete a team" }) - async deleteTeam( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - const team = await this.organizationsTeamsService.deleteOrgTeam(orgId, teamId); - return { - status: SUCCESS_STATUS, - data: plainToClass(OrgTeamOutputDto, team, { strategy: "excludeAll" }), - }; - } - - @UseGuards(IsTeamInOrg) - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Patch("/:teamId") - @ApiOperation({ summary: "Update a team" }) - async updateTeam( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Body() body: UpdateOrgTeamDto - ): Promise { - const team = await this.organizationsTeamsService.updateOrgTeam(orgId, teamId, body); - return { - status: SUCCESS_STATUS, - data: plainToClass(OrgTeamOutputDto, team, { strategy: "excludeAll" }), - }; - } - - @Post() - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @ApiOperation({ summary: "Create a team" }) - async createTeam( - @Param("orgId", ParseIntPipe) orgId: number, - @Body() body: CreateOrgTeamDto, - @GetUser() user: UserWithProfile, - @Req() req: Request - ): Promise { - const oAuthClientId = req.headers[X_CAL_CLIENT_ID] as string | undefined; - const team = oAuthClientId - ? await this.organizationsTeamsService.createPlatformOrgTeam(orgId, oAuthClientId, body, user) - : await this.organizationsTeamsService.createOrgTeam(orgId, body, user); - - return { - status: SUCCESS_STATUS, - data: plainToClass(OrgTeamOutputDto, team, { strategy: "excludeAll" }), - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.repository.ts b/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.repository.ts deleted file mode 100644 index 27f905f3a86824..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.repository.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input"; -import { UpdateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/update-organization-team.input"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrganizationsTeamsRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async findOrgTeam(organizationId: number, teamId: number) { - return this.dbRead.prisma.team.findUnique({ - where: { - id: teamId, - isOrganization: false, - parentId: organizationId, - }, - }); - } - - async findOrgTeamBySlug(organizationId: number, teamSlug: string) { - return this.dbRead.prisma.team.findUnique({ - where: { - slug_parentId: { - slug: teamSlug, - parentId: organizationId, - }, - isOrganization: false, - }, - }); - } - - async findOrgTeams(organizationId: number) { - return this.dbRead.prisma.team.findMany({ - where: { - parentId: organizationId, - }, - }); - } - - async deleteOrgTeam(organizationId: number, teamId: number) { - return this.dbWrite.prisma.team.delete({ - where: { - id: teamId, - isOrganization: false, - parentId: organizationId, - }, - }); - } - - async createOrgTeam(organizationId: number, data: CreateOrgTeamDto) { - return this.dbWrite.prisma.team.create({ - data: { ...data, parentId: organizationId }, - }); - } - - async createPlatformOrgTeam(organizationId: number, oAuthClientId: string, data: CreateOrgTeamDto) { - return this.dbWrite.prisma.team.create({ - data: { - ...data, - parentId: organizationId, - createdByOAuthClientId: oAuthClientId, - }, - }); - } - - async getPlatformOrgTeams(organizationId: number, oAuthClientId: string) { - return this.dbRead.prisma.team.findMany({ - where: { - parentId: organizationId, - createdByOAuthClientId: oAuthClientId, - }, - }); - } - - async updateOrgTeam(organizationId: number, teamId: number, data: UpdateOrgTeamDto) { - return this.dbWrite.prisma.team.update({ - data: { ...data }, - where: { id: teamId, parentId: organizationId, isOrganization: false }, - }); - } - - async findOrgTeamsPaginated(organizationId: number, skip: number, take: number) { - return this.dbRead.prisma.team.findMany({ - where: { - parentId: organizationId, - }, - skip, - take, - }); - } - - async findOrgUserTeamsPaginated(organizationId: number, userId: number, skip: number, take: number) { - return this.dbRead.prisma.team.findMany({ - where: { - parentId: organizationId, - members: { - some: { - userId, - }, - }, - }, - include: { - members: { select: { accepted: true, userId: true, role: true } }, - }, - skip, - take, - }); - } - - async findOrgTeamsPaginatedWithMembers(organizationId: number, skip: number, take: number) { - return this.dbRead.prisma.team.findMany({ - where: { - parentId: organizationId, - }, - include: { - members: { select: { accepted: true, userId: true, role: true } }, - }, - skip, - take, - }); - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/index/outputs/organization-team.output.ts b/apps/api/v2/src/modules/organizations/teams/index/outputs/organization-team.output.ts deleted file mode 100644 index 5b8b9b8a580f35..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/index/outputs/organization-team.output.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum, IsString, ValidateNested } from "class-validator"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import { MembershipRole } from "@calcom/platform-libraries"; -import { OrgTeamOutputDto } from "@calcom/platform-types"; - -export class OrgMeTeamOutputDto extends OrgTeamOutputDto { - @IsString() - @Expose() - readonly accepted!: boolean; - - @ApiProperty({ - example: MembershipRole.MEMBER, - enum: [MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER], - }) - @IsEnum([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER]) - @Expose() - readonly role!: MembershipRole; -} - -export class OrgTeamsOutputResponseDto { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @Expose() - @ValidateNested() - @Type(() => OrgTeamOutputDto) - data!: OrgTeamOutputDto[]; -} - -export class OrgMeTeamsOutputResponseDto { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @Expose() - @ValidateNested() - @Type(() => OrgTeamOutputDto) - data!: OrgTeamOutputDto[]; -} - -export class OrgTeamOutputResponseDto { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @Expose() - @ValidateNested() - @Type(() => OrgTeamOutputDto) - data!: OrgTeamOutputDto; -} diff --git a/apps/api/v2/src/modules/organizations/teams/index/services/organizations-teams.service.ts b/apps/api/v2/src/modules/organizations/teams/index/services/organizations-teams.service.ts deleted file mode 100644 index 5a5ea2e5a2d6f0..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/index/services/organizations-teams.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input"; -import { UpdateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/update-organization-team.input"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable } from "@nestjs/common"; - -import { slugify } from "@calcom/platform-libraries"; - -@Injectable() -export class OrganizationsTeamsService { - constructor( - private readonly organizationsTeamRepository: OrganizationsTeamsRepository, - private readonly membershipsRepository: MembershipsRepository - ) {} - - async getPaginatedOrgUserTeams(organizationId: number, userId: number, skip = 0, take = 250) { - const teams = await this.organizationsTeamRepository.findOrgUserTeamsPaginated( - organizationId, - userId, - skip, - take - ); - return teams; - } - - async getPaginatedOrgTeamsWithMembers(organizationId: number, skip = 0, take = 250) { - const teams = await this.organizationsTeamRepository.findOrgTeamsPaginatedWithMembers( - organizationId, - skip, - take - ); - return teams; - } - - async getPaginatedOrgTeams(organizationId: number, skip = 0, take = 250) { - const teams = await this.organizationsTeamRepository.findOrgTeamsPaginated(organizationId, skip, take); - return teams; - } - - async deleteOrgTeam(organizationId: number, teamId: number) { - const team = await this.organizationsTeamRepository.deleteOrgTeam(organizationId, teamId); - return team; - } - - async updateOrgTeam(organizationId: number, teamId: number, data: UpdateOrgTeamDto) { - const team = await this.organizationsTeamRepository.updateOrgTeam(organizationId, teamId, data); - return team; - } - - async createOrgTeam(organizationId: number, data: CreateOrgTeamDto, user: UserWithProfile) { - const { autoAcceptCreator, ...rest } = data; - - if (!rest.slug) { - rest.slug = slugify(rest.name); - } - - const team = await this.organizationsTeamRepository.createOrgTeam(organizationId, rest); - - if (user.role !== "ADMIN") { - await this.membershipsRepository.createMembership(team.id, user.id, "OWNER", !!autoAcceptCreator); - } - return team; - } - - async createPlatformOrgTeam( - organizationId: number, - oAuthClientId: string, - data: CreateOrgTeamDto, - user: UserWithProfile - ) { - const { autoAcceptCreator, ...rest } = data; - - if (!rest.slug) { - rest.slug = slugify(rest.name); - } - - const team = await this.organizationsTeamRepository.createPlatformOrgTeam( - organizationId, - oAuthClientId, - rest - ); - - if (user.role !== "ADMIN") { - await this.membershipsRepository.createMembership(team.id, user.id, "OWNER", !!autoAcceptCreator); - } - return team; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/invite/organizations-teams-invite.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/invite/organizations-teams-invite.controller.e2e-spec.ts deleted file mode 100644 index baeee396f1fa47..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/invite/organizations-teams-invite.controller.e2e-spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; -import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; -import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; -import { randomString } from "test/utils/randomString"; -import { withApiAuth } from "test/utils/withApiAuth"; -import { 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"; - -describe("Organizations Teams Invite Endpoints", () => { - describe("User Authentication - User is Org Team Admin", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let org: Team; - let orgTeam: Team; - let nonOrgTeam: Team; - - const userEmail = `organizations-teams-invite-admin-${randomString()}@api.com`; - - let user: User; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-teams-invite-organization-${randomString()}`, - isOrganization: true, - }); - - orgTeam = await teamsRepositoryFixture.create({ - name: `organizations-teams-invite-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - nonOrgTeam = await teamsRepositoryFixture.create({ - name: `organizations-teams-invite-non-org-team-${randomString()}`, - isOrganization: false, - }); - - // Admin of the org team - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: orgTeam.id } }, - }); - - // Also a member of the organization - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - it("should create a team invite", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/invite`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - expect(response.body.data.token.length).toBeGreaterThan(0); - expect(response.body.data.inviteLink).toEqual(expect.any(String)); - expect(response.body.data.inviteLink).toContain(response.body.data.token); - }); - }); - - it("should create a new invite on each request", async () => { - const first = await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/invite`) - .expect(200); - const firstToken = first.body.data.token as string; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/invite`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - expect(response.body.data.token).not.toEqual(firstToken); - expect(response.body.data.inviteLink).toEqual(expect.any(String)); - expect(response.body.data.inviteLink).toContain(response.body.data.token); - }); - }); - - it("should fail for team not in organization", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${nonOrgTeam.id}/invite`) - .expect(404); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(user.email); - await organizationsRepositoryFixture.delete(org.id); - await teamsRepositoryFixture.delete(nonOrgTeam.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/invite/organizations-teams-invite.controller.ts b/apps/api/v2/src/modules/organizations/teams/invite/organizations-teams-invite.controller.ts deleted file mode 100644 index 271ba8066c364d..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/invite/organizations-teams-invite.controller.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { Controller, UseGuards, Post, Param, ParseIntPipe, HttpCode, HttpStatus } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { TeamService } from "@calcom/platform-libraries"; - -import { CreateInviteOutputDto } from "./outputs/invite.output"; - -@Controller({ - path: "/v2/organizations/:orgId/teams/:teamId", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Teams / Invite") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -export class OrganizationsTeamsInviteController { - @Post("/invite") - @Roles("TEAM_ADMIN") - @ApiOperation({ summary: "Create team invite link" }) - @HttpCode(HttpStatus.OK) - async createInvite( - @Param("orgId", ParseIntPipe) _orgId: number, - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - const result = await TeamService.createInvite(teamId); - return { status: SUCCESS_STATUS, data: result }; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/invite/outputs/invite.output.ts b/apps/api/v2/src/modules/organizations/teams/invite/outputs/invite.output.ts deleted file mode 100644 index 059ca461e10882..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/invite/outputs/invite.output.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; - -export class InviteDataDto { - @ApiProperty({ - description: - "Unique invitation token for this team. Share this token with prospective members to allow them to join the team/organization.", - example: "f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2", - }) - token!: string; - - @ApiProperty({ - description: - "Complete invitation URL that can be shared with prospective members. Opens the signup page with the token and redirects to getting started after signup.", - example: - "http://app.cal.com/signup?token=f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2&callbackUrl=/getting-started", - }) - inviteLink!: string; -} - -export class CreateInviteOutputDto { - @ApiProperty({ example: "success" }) - status!: string; - - @ApiProperty({ type: InviteDataDto }) - data!: InviteDataDto; -} diff --git a/apps/api/v2/src/modules/organizations/teams/memberships/e2e/organizations-teams-memberships-guard.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/memberships/e2e/organizations-teams-memberships-guard.controller.e2e-spec.ts deleted file mode 100644 index c1c5c49c15b5fe..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/memberships/e2e/organizations-teams-memberships-guard.controller.e2e-spec.ts +++ /dev/null @@ -1,468 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { ApiSuccessResponse } from "@calcom/platform-types"; -import { 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 { 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"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { EmailService } from "@/modules/email/email.service"; -import { - CreateManagedUserData, - CreateManagedUserOutput, -} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output"; -import { - PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR, - REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR, -} from "@/modules/organizations/memberships/services/organizations-membership.service"; -import { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input"; -import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { - PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR, - PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR, - REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR, -} from "@/modules/teams/memberships/services/teams-memberships.service"; -import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input"; -import { CreateUserInput } from "@/modules/users/inputs/create-user.input"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Teams Memberships Endpoints", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let profilesRepositoryFixture: ProfileRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - - let org: Team; - let orgOwner: User; - let orgUser: User; - let orgTeam: Team; - let orgApiKey: string; - - let platformOrg: Team; - let platformOrgOwner: User; - let platformOrgUser: CreateManagedUserData; - let platformOrgTeam: Team; - let platformOAuthClient: PlatformOAuthClient; - let secondPlatformOAuthClient: PlatformOAuthClient; - let secondPlatformOrgUser: CreateManagedUserData; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule, TeamsMembershipsModule], - }).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - - const orgName = `organizations-teams-memberships-organization-${randomString()}`; - org = await organizationsRepositoryFixture.create({ - name: orgName, - slug: orgName, - isOrganization: true, - }); - - const platformOrgName = `organizations-teams-memberships-platform-organization-${randomString()}`; - platformOrg = await organizationsRepositoryFixture.create({ - isPlatform: true, - name: platformOrgName, - slug: platformOrgName, - isOrganization: true, - platformBilling: { - create: { - customerId: "cus_999", - plan: "ESSENTIALS", - subscriptionId: "sub_999", - }, - }, - }); - platformOAuthClient = await createOAuthClient(platformOrg.id); - secondPlatformOAuthClient = await createOAuthClient(platformOrg.id); - - orgOwner = await userRepositoryFixture.create({ - email: `organizations-teams-memberships-org-owner-${randomString()}@api.com`, - username: `organizations-teams-memberships-org-owner-${randomString()}`, - }); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - const { keyString } = await apiKeysRepositoryFixture.createApiKey(orgOwner.id, null); - orgApiKey = `cal_test_${keyString}`; - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: orgOwner.id } }, - team: { connect: { id: org.id } }, - accepted: true, - }); - - platformOrgOwner = await userRepositoryFixture.create({ - email: `platform-org-owner-${randomString()}@api.com`, - username: `platform-org-owner-${randomString()}`, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: platformOrgOwner.id } }, - team: { connect: { id: platformOrg.id } }, - accepted: true, - }); - - await profilesRepositoryFixture.create({ - uid: "asd1qwwqeqw-asddsadasd", - username: `platform-org-owner-${randomString()}`, - organization: { connect: { id: platformOrg.id } }, - user: { - connect: { id: platformOrgOwner.id }, - }, - }); - - await profilesRepositoryFixture.create({ - uid: "asd1qwwqeqw-asddsadasd-2", - username: `org-owner-${randomString()}`, - organization: { connect: { id: org.id } }, - user: { - connect: { id: orgOwner.id }, - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: ["http://localhost:4321"], - permissions: 1023, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - describe("should create org users", () => { - it("should create a new org user", async () => { - const newOrgUser: CreateUserInput = { - email: `organization-user-${randomString()}@api.com`, - }; - - jest - .spyOn(EmailService.prototype, "sendSignupToOrganizationEmail") - .mockImplementation(() => Promise.resolve()); - - const { body } = await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users`) - .send(newOrgUser) - .set("Content-Type", "application/json") - .set("Authorization", `Bearer ${orgApiKey}`) - .set("Accept", "application/json"); - - const userData = body.data; - expect(body.status).toBe(SUCCESS_STATUS); - expect(userData.email).toBe(newOrgUser.email); - orgUser = userData; - }); - - it(`should create a new platform org manager user using first oAuth client`, async () => { - const managedUserEmail = `platform-organization-manager-user-${randomString()}@api.com`; - const requestBody: CreateManagedUserInput = { - email: managedUserEmail, - timeZone: "Europe/Berlin", - weekStart: "Monday", - timeFormat: 24, - name: "Alice Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - bio: "I am a bio", - metadata: { - key: "value", - }, - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${platformOAuthClient.id}/users`) - .set("x-cal-secret-key", platformOAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone); - expect(responseBody.data.user.name).toEqual(requestBody.name); - await userConnectedToOAuth(platformOAuthClient.id, responseBody.data.user.email, 1); - platformOrgUser = responseBody.data; - }); - - it(`should create a new platform org manager user using second oAuth client`, async () => { - const managedUserEmail = `platform-organization-manager-user-${randomString()}@api.com`; - const requestBody: CreateManagedUserInput = { - email: managedUserEmail, - timeZone: "Europe/Berlin", - weekStart: "Monday", - timeFormat: 24, - name: "Alice Smith", - avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", - bio: "I am a bio", - metadata: { - key: "value", - }, - }; - - const response = await request(app.getHttpServer()) - .post(`/api/v2/oauth-clients/${secondPlatformOAuthClient.id}/users`) - .set("x-cal-secret-key", secondPlatformOAuthClient.secret) - .send(requestBody) - .expect(201); - - const responseBody: CreateManagedUserOutput = response.body; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone); - expect(responseBody.data.user.name).toEqual(requestBody.name); - await userConnectedToOAuth(secondPlatformOAuthClient.id, responseBody.data.user.email, 1); - secondPlatformOrgUser = responseBody.data; - }); - }); - - describe("should create org teams", () => { - it("should create the team for org", async () => { - const teamName = `organization team ${randomString()}`; - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams`) - .set("Authorization", `Bearer ${orgApiKey}`) - .send({ - name: teamName, - } satisfies CreateOrgTeamDto) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - orgTeam = responseBody.data; - expect(orgTeam.name).toEqual(teamName); - expect(orgTeam.parentId).toEqual(org.id); - }); - }); - - it("should create the team for platform org", async () => { - const teamName = `platform organization team ${randomString()}`; - return request(app.getHttpServer()) - .post(`/v2/organizations/${platformOrg.id}/teams`) - .set("x-cal-client-id", platformOAuthClient.id) - .set("x-cal-secret-key", platformOAuthClient.secret) - .send({ - name: teamName, - } satisfies CreateOrgTeamDto) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - platformOrgTeam = responseBody.data; - expect(platformOrgTeam.name).toEqual(teamName); - expect(platformOrgTeam.parentId).toEqual(platformOrg.id); - }); - }); - }); - - describe("organization memberships", () => { - describe("negative tests", () => { - it("should not add managed user to organization", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/memberships`) - .set("Authorization", `Bearer ${orgApiKey}`) - .send({ - userId: platformOrgUser.user.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(400); - expect(response.body.error.message).toEqual(PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR); - }); - - it("should not add managed user to organization team", async () => { - await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`) - .set("Authorization", `Bearer ${orgApiKey}`) - .send({ - userId: platformOrgUser.user.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(422); - }); - - it("should not add managed user to organization team", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/teams/${orgTeam.id}/memberships`) - .set("Authorization", `Bearer ${orgApiKey}`) - .send({ - userId: platformOrgUser.user.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(400); - expect(response.body.error.message).toEqual(PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR); - }); - }); - - describe("positive tests", () => { - it("should add user to organization", async () => { - await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/memberships`) - .set("Authorization", `Bearer ${orgApiKey}`) - .send({ - userId: orgUser.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(201); - }); - - it("should add user to organization team", async () => { - await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`) - .set("Authorization", `Bearer ${orgApiKey}`) - .send({ - userId: orgUser.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(201); - }); - }); - }); - - describe("platform organization memberships", () => { - describe("negative tests", () => { - it("should not add user to platform organization", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${platformOrg.id}/memberships`) - .set("x-cal-client-id", platformOAuthClient.id) - .set("x-cal-secret-key", platformOAuthClient.secret) - .send({ - userId: orgUser.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(400); - expect(response.body.error.message).toEqual(REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR); - }); - - it("should not add user to platform organization team", async () => { - await request(app.getHttpServer()) - .post(`/v2/organizations/${platformOrg.id}/teams/${platformOrgTeam.id}/memberships`) - .set("x-cal-client-id", platformOAuthClient.id) - .set("x-cal-secret-key", platformOAuthClient.secret) - .send({ - userId: orgUser.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(422); - }); - it("should not add user to platform organization team", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/teams/${platformOrgTeam.id}/memberships`) - .set("x-cal-client-id", platformOAuthClient.id) - .set("x-cal-secret-key", platformOAuthClient.secret) - .send({ - userId: orgUser.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(400); - expect(response.body.error.message).toEqual(REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR); - }); - - it("should not add user to platform organization team because user is created using different oAuth client", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${platformOrg.id}/teams/${platformOrgTeam.id}/memberships`) - .set("x-cal-client-id", platformOAuthClient.id) - .set("x-cal-secret-key", platformOAuthClient.secret) - .send({ - userId: secondPlatformOrgUser.user.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(400); - expect(response.body.error.message).toEqual( - PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR - ); - }); - }); - - describe("positive tests", () => { - it("should add user to organization", async () => { - await request(app.getHttpServer()) - .post(`/v2/organizations/${platformOrg.id}/memberships`) - .set("x-cal-client-id", platformOAuthClient.id) - .set("x-cal-secret-key", platformOAuthClient.secret) - .send({ - userId: platformOrgUser.user.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(201); - }); - - it("should add user to organization team", async () => { - await request(app.getHttpServer()) - .post(`/v2/organizations/${platformOrg.id}/teams/${platformOrgTeam.id}/memberships`) - .set("x-cal-client-id", platformOAuthClient.id) - .set("x-cal-secret-key", platformOAuthClient.secret) - .send({ - userId: platformOrgUser.user.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(201); - }); - }); - }); - - async function userConnectedToOAuth(oAuthClientId: string, userEmail: string, usersCount: number) { - const oAuthUsers = await oauthClientRepositoryFixture.getUsers(oAuthClientId); - const newOAuthUser = oAuthUsers?.find((user) => user.email === userEmail); - - expect(oAuthUsers?.length).toEqual(usersCount); - expect(newOAuthUser?.email).toEqual(userEmail); - } - - afterAll(async () => { - await organizationsRepositoryFixture.delete(org.id); - await teamsRepositoryFixture.delete(orgTeam.id); - await organizationsRepositoryFixture.delete(platformOrg.id); - await teamsRepositoryFixture.delete(platformOrgTeam.id); - await userRepositoryFixture.deleteByEmail(platformOrgUser.user.email); - await userRepositoryFixture.deleteByEmail(orgUser.email); - await userRepositoryFixture.deleteByEmail(orgOwner.email); - await userRepositoryFixture.deleteByEmail(platformOrgOwner.email); - await userRepositoryFixture.deleteByEmail(secondPlatformOrgUser.user.email); - await app.close(); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/memberships/e2e/organizations-teams-memberships.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/memberships/e2e/organizations-teams-memberships.controller.e2e-spec.ts deleted file mode 100644 index 6b5daca93d197a..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/memberships/e2e/organizations-teams-memberships.controller.e2e-spec.ts +++ /dev/null @@ -1,632 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { ApiSuccessResponse } from "@calcom/platform-types"; -import type { EventType, Membership, 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 { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { withApiAuth } from "test/utils/withApiAuth"; -import { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input"; -import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/update-organization-team-membership.input"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Teams Memberships Endpoints", () => { - describe("User Authentication - User is Org Admin", () => { - let app: INestApplication; - - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let org: Team; - let orgTeam: Team; - let nonOrgTeam: Team; - let teamEventType: EventType; - let managedEventType: EventType; - let membership: Membership; - let membership2: Membership; - let membershipCreatedViaApi: TeamMembershipOutput; - - const userEmail = `organizations-teams-memberships-admin-${randomString()}@api.com`; - const userEmail2 = `organizations-teams-memberships-member-${randomString()}@api.com`; - const nonOrgUserEmail = `organizations-teams-memberships-non-org-${randomString()}@api.com`; - const invitedUserEmail = `organizations-teams-memberships-invited-${randomString()}@api.com`; - - let user: User; - let user2: User; - let nonOrgUser: User; - - let userToInviteViaApi: User; - - const metadata = { - some: "key", - }; - const bio = "This is a bio"; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - bio, - metadata, - }); - user2 = await userRepositoryFixture.create({ - email: userEmail2, - username: userEmail2, - bio, - metadata, - }); - - nonOrgUser = await userRepositoryFixture.create({ - email: nonOrgUserEmail, - username: nonOrgUserEmail, - }); - - userToInviteViaApi = await userRepositoryFixture.create({ - email: invitedUserEmail, - username: invitedUserEmail, - bio, - metadata, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-teams-memberships-organization-${randomString()}`, - isOrganization: true, - }); - - orgTeam = await teamsRepositoryFixture.create({ - name: `organizations-teams-memberships-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - teamEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: orgTeam.id }, - }, - title: "Collective Event Type", - slug: "collective-event-type", - length: 30, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - managedEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "MANAGED", - team: { - connect: { id: orgTeam.id }, - }, - title: "Managed Event Type", - slug: "managed-event-type", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - nonOrgTeam = await teamsRepositoryFixture.create({ - name: `organizations-teams-memberships-non-org-team-${randomString()}`, - isOrganization: false, - }); - - membership = await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: orgTeam.id } }, - }); - - membership2 = await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: user2.id } }, - team: { connect: { id: orgTeam.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: user2.id } }, - team: { connect: { id: org.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: userToInviteViaApi.id } }, - team: { connect: { id: org.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: nonOrgUser.id } }, - team: { connect: { id: nonOrgTeam.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: userEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user2.id}`, - username: userEmail2, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user2.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${userToInviteViaApi.id}`, - username: invitedUserEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: userToInviteViaApi.id, - }, - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should get all the memberships of the org's team", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data[0].id).toEqual(membership.id); - expect(responseBody.data[0].userId).toEqual(user.id); - expect(responseBody.data[0].role).toEqual("ADMIN"); - expect(responseBody.data[0].user.bio).toEqual(bio); - expect(responseBody.data[0].user.metadata).toEqual(metadata); - expect(responseBody.data[0].user.email).toEqual(user.email); - expect(responseBody.data[0].user.username).toEqual(user.username); - expect(responseBody.data[1].id).toEqual(membership2.id); - expect(responseBody.data[1].userId).toEqual(user2.id); - expect(responseBody.data[1].role).toEqual("MEMBER"); - expect(responseBody.data[1].user.bio).toEqual(user2.bio); - expect(responseBody.data[1].user.metadata).toEqual(user2.metadata); - expect(responseBody.data[1].user.email).toEqual(user2.email); - expect(responseBody.data[1].user.username).toEqual(user2.username); - expect(responseBody.data.length).toEqual(2); - expect(responseBody.data[0].teamId).toEqual(orgTeam.id); - expect(responseBody.data[1].teamId).toEqual(orgTeam.id); - }); - }); - - it("should fail to get all the memberships of team which is not in the org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${nonOrgTeam.id}/memberships`) - .expect(404); - }); - - it("should get all the memberships of the org's team paginated", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships?skip=1&take=1`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data[0].id).toEqual(membership2.id); - expect(responseBody.data[0].userId).toEqual(user2.id); - expect(responseBody.data[0].role).toEqual("MEMBER"); - expect(responseBody.data[0].user.bio).toEqual(bio); - expect(responseBody.data[0].user.metadata).toEqual(metadata); - expect(responseBody.data[0].user.email).toEqual(user2.email); - expect(responseBody.data[0].user.username).toEqual(user2.username); - expect(responseBody.data.length).toEqual(1); - }); - }); - - it("should fail if org does not exist", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/120494059/teams/${orgTeam.id}/memberships`) - .expect(403); - }); - - it("should get the membership of the org's team", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships/${membership.id}`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(membership.id); - expect(responseBody.data.userId).toEqual(user.id); - expect(responseBody.data.user.email).toEqual(user.email); - expect(responseBody.data.user.username).toEqual(user.username); - expect(responseBody.data.user.bio).toEqual(bio); - expect(responseBody.data.user.metadata).toEqual(metadata); - expect(responseBody.data.role).toEqual("ADMIN"); - expect(responseBody.data.teamId).toEqual(orgTeam.id); - }); - }); - - it("should fail to get the membership of a team not in the org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${nonOrgTeam.id}/memberships/${membership.id}`) - .expect(404); - }); - - it("should fail to create the membership of a team not in the org", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${nonOrgTeam.id}/memberships`) - .send({ - userId: userToInviteViaApi.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(404); - }); - - it("should have created the membership of the org's team and assigned team wide events", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`) - .send({ - userId: userToInviteViaApi.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(201) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - membershipCreatedViaApi = responseBody.data; - expect(membershipCreatedViaApi.teamId).toEqual(orgTeam.id); - expect(membershipCreatedViaApi.role).toEqual("MEMBER"); - expect(membershipCreatedViaApi.userId).toEqual(userToInviteViaApi.id); - expect(membershipCreatedViaApi.user.email).toEqual(userToInviteViaApi.email); - expect(membershipCreatedViaApi.user.username).toEqual(userToInviteViaApi.username); - expect(membershipCreatedViaApi.user.bio).toEqual(userToInviteViaApi.bio); - expect(membershipCreatedViaApi.user.metadata).toEqual(userToInviteViaApi.metadata); - userHasCorrectEventTypes(membershipCreatedViaApi.userId); - }); - }); - - async function userHasCorrectEventTypes(userId: number) { - const managedEventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(userId); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(orgTeam.id); - expect(managedEventTypes?.length).toEqual(1); - expect(teamEventTypes?.length).toEqual(2); - const collectiveEvenType = teamEventTypes?.find((eventType) => eventType.slug === teamEventType.slug); - expect(collectiveEvenType).toBeTruthy(); - const userHost = collectiveEvenType?.hosts.find((host) => host.userId === userId); - expect(userHost).toBeTruthy(); - expect(managedEventTypes?.find((eventType) => eventType.slug === managedEventType.slug)).toBeTruthy(); - } - - it("should fail to create the membership of the org's team for a non org user", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`) - .send({ - userId: nonOrgUser.id, - accepted: true, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(422); - }); - - it("should update the membership of the org's team", async () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships/${membershipCreatedViaApi.id}`) - .send({ - role: "OWNER", - } satisfies UpdateOrgTeamMembershipDto) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - membershipCreatedViaApi = responseBody.data; - expect(membershipCreatedViaApi.role).toEqual("OWNER"); - expect(membershipCreatedViaApi.user.email).toEqual(userToInviteViaApi.email); - expect(membershipCreatedViaApi.user.username).toEqual(userToInviteViaApi.username); - expect(membershipCreatedViaApi.user.bio).toEqual(userToInviteViaApi.bio); - expect(membershipCreatedViaApi.user.metadata).toEqual(userToInviteViaApi.metadata); - }); - }); - - it("should delete the membership of the org's team we created via api", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships/${membershipCreatedViaApi.id}`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(membershipCreatedViaApi.id); - }); - }); - - it("should fail to get the membership of the org's team we just deleted", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships/${membershipCreatedViaApi.id}`) - .expect(404); - }); - - it("should fail if the membership does not exist", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships/123132145`) - .expect(404); - }); - - // Auto-accept tests - describe("auto-accept based on email domain", () => { - let orgWithAutoAccept: Team; - let subteamWithAutoAccept: Team; - let userWithMatchingEmail: User; - let userWithUppercaseEmail: User; - let userWithMatchingEmailForOverride: User; - let userWithNonMatchingEmail: User; - - beforeAll(async () => { - // Create org with auto-accept settings - orgWithAutoAccept = await organizationsRepositoryFixture.create({ - name: `auto-accept-org-${randomString()}`, - isOrganization: true, - }); - - // Update organizationSettings with orgAutoAcceptEmail - await organizationsRepositoryFixture.updateSettings(orgWithAutoAccept.id, { - orgAutoAcceptEmail: "acme.com", - isOrganizationVerified: true, - isOrganizationConfigured: true, - }); - - // Create subteam - subteamWithAutoAccept = await teamsRepositoryFixture.create({ - name: `auto-accept-subteam-${randomString()}`, - isOrganization: false, - parent: { connect: { id: orgWithAutoAccept.id } }, - }); - - // Create event type with assignAllTeamMembers - await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { connect: { id: subteamWithAutoAccept.id } }, - title: "Auto Accept Event Type", - slug: "auto-accept-event-type", - length: 30, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - // Create users - userWithMatchingEmail = await userRepositoryFixture.create({ - email: `alice@acme.com`, - username: `alice-${randomString()}`, - }); - - userWithUppercaseEmail = await userRepositoryFixture.create({ - email: `bob@ACME.COM`, - username: `bob-${randomString()}`, - }); - - userWithMatchingEmailForOverride = await userRepositoryFixture.create({ - email: `david@acme.com`, - username: `david-${randomString()}`, - }); - - userWithNonMatchingEmail = await userRepositoryFixture.create({ - email: `charlie@external.com`, - username: `charlie-${randomString()}`, - }); - - // Add users to org - await membershipsRepositoryFixture.create({ - role: "MEMBER", - accepted: true, - user: { connect: { id: userWithMatchingEmail.id } }, - team: { connect: { id: orgWithAutoAccept.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - accepted: true, - user: { connect: { id: userWithUppercaseEmail.id } }, - team: { connect: { id: orgWithAutoAccept.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - accepted: true, - user: { connect: { id: userWithMatchingEmailForOverride.id } }, - team: { connect: { id: orgWithAutoAccept.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - accepted: true, - user: { connect: { id: userWithNonMatchingEmail.id } }, - team: { connect: { id: orgWithAutoAccept.id } }, - }); - - // Create profiles for users - await profileRepositoryFixture.create({ - uid: `usr-${userWithMatchingEmail.id}`, - username: userWithMatchingEmail.username || `user-${userWithMatchingEmail.id}`, - organization: { connect: { id: orgWithAutoAccept.id } }, - user: { connect: { id: userWithMatchingEmail.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${userWithUppercaseEmail.id}`, - username: userWithUppercaseEmail.username || `user-${userWithUppercaseEmail.id}`, - organization: { connect: { id: orgWithAutoAccept.id } }, - user: { connect: { id: userWithUppercaseEmail.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${userWithMatchingEmailForOverride.id}`, - username: - userWithMatchingEmailForOverride.username || `user-${userWithMatchingEmailForOverride.id}`, - organization: { connect: { id: orgWithAutoAccept.id } }, - user: { connect: { id: userWithMatchingEmailForOverride.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${userWithNonMatchingEmail.id}`, - username: userWithNonMatchingEmail.username || `user-${userWithNonMatchingEmail.id}`, - organization: { connect: { id: orgWithAutoAccept.id } }, - user: { connect: { id: userWithNonMatchingEmail.id } }, - }); - - // Make user an admin of the org for API access - await membershipsRepositoryFixture.create({ - role: "ADMIN", - accepted: true, - user: { connect: { id: user.id } }, - team: { connect: { id: orgWithAutoAccept.id } }, - }); - }); - - it("should auto-accept when email matches orgAutoAcceptEmail", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${orgWithAutoAccept.id}/teams/${subteamWithAutoAccept.id}/memberships`) - .send({ - userId: userWithMatchingEmail.id, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(201); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.data.accepted).toBe(true); - - // Verify EventTypes assignment - const eventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(subteamWithAutoAccept.id); - const eventTypeWithAssignAll = eventTypes.find((et) => et.assignAllTeamMembers); - expect(eventTypeWithAssignAll).toBeTruthy(); - const userIsHost = eventTypeWithAssignAll?.hosts.some((h) => h.userId === userWithMatchingEmail.id); - expect(userIsHost).toBe(true); - }); - - it("should handle case-insensitive email domain matching", async () => { - // User with email="bob@ACME.COM" should match orgAutoAcceptEmail="acme.com" - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${orgWithAutoAccept.id}/teams/${subteamWithAutoAccept.id}/memberships`) - .send({ - userId: userWithUppercaseEmail.id, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(201); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.data.accepted).toBe(true); - }); - - it("should ALWAYS auto-accept when email matches, even if accepted:false", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${orgWithAutoAccept.id}/teams/${subteamWithAutoAccept.id}/memberships`) - .send({ - userId: userWithMatchingEmailForOverride.id, - role: "MEMBER", - accepted: false, - } satisfies CreateOrgTeamMembershipDto) - .expect(201); - - const responseBody: ApiSuccessResponse = response.body; - // Should override to true because email matches - expect(responseBody.data.accepted).toBe(true); - }); - - it("should NOT auto-accept when email does not match orgAutoAcceptEmail", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/organizations/${orgWithAutoAccept.id}/teams/${subteamWithAutoAccept.id}/memberships`) - .send({ - userId: userWithNonMatchingEmail.id, - role: "MEMBER", - } satisfies CreateOrgTeamMembershipDto) - .expect(201); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.data.accepted).toBe(false); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(userWithMatchingEmail.email); - await userRepositoryFixture.deleteByEmail(userWithUppercaseEmail.email); - await userRepositoryFixture.deleteByEmail(userWithMatchingEmailForOverride.email); - await userRepositoryFixture.deleteByEmail(userWithNonMatchingEmail.email); - await organizationsRepositoryFixture.delete(orgWithAutoAccept.id); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(user.email); - await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email); - await userRepositoryFixture.deleteByEmail(nonOrgUser.email); - await userRepositoryFixture.deleteByEmail(user2.email); - await organizationsRepositoryFixture.delete(org.id); - await teamsRepositoryFixture.delete(nonOrgTeam.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input.ts b/apps/api/v2/src/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input.ts deleted file mode 100644 index 33852e89e7f12a..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsBoolean, IsOptional, IsEnum, IsInt } from "class-validator"; - -import { MembershipRole } from "@calcom/platform-libraries"; - -export class CreateOrgTeamMembershipDto { - @IsInt() - @ApiProperty() - readonly userId!: number; - - @IsOptional() - @ApiPropertyOptional({ type: Boolean, default: false }) - @IsBoolean() - readonly accepted?: boolean = false; - - @IsEnum(MembershipRole) - @ApiProperty({ enum: ["MEMBER", "OWNER", "ADMIN"] }) - readonly role: MembershipRole = MembershipRole.MEMBER; - - @IsOptional() - @ApiPropertyOptional({ type: Boolean, default: false }) - @IsBoolean() - readonly disableImpersonation?: boolean = false; -} diff --git a/apps/api/v2/src/modules/organizations/teams/memberships/inputs/update-organization-team-membership.input.ts b/apps/api/v2/src/modules/organizations/teams/memberships/inputs/update-organization-team-membership.input.ts deleted file mode 100644 index da1f11a14eabeb..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/memberships/inputs/update-organization-team-membership.input.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsBoolean, IsOptional, IsEnum } from "class-validator"; - -import { MembershipRole } from "@calcom/platform-libraries"; - -export class UpdateOrgTeamMembershipDto { - @IsOptional() - @IsBoolean() - @ApiPropertyOptional() - readonly accepted?: boolean; - - @IsOptional() - @IsEnum(MembershipRole) - @ApiPropertyOptional({ enum: ["MEMBER", "OWNER", "ADMIN"] }) - readonly role?: MembershipRole; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional() - readonly disableImpersonation?: boolean; -} diff --git a/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.controller.ts b/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.controller.ts deleted file mode 100644 index 72747f4a90c86e..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.controller.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { OrganizationMembershipService } from "@/lib/services/organization-membership.service"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input"; -import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/update-organization-team-membership.input"; -import { - OrgTeamMembershipsOutputResponseDto, - OrgTeamMembershipOutputResponseDto, -} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output"; -import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service"; -import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; -import { - Controller, - UseGuards, - Get, - Param, - ParseIntPipe, - Query, - Delete, - Patch, - Post, - Body, - HttpStatus, - HttpCode, - UnprocessableEntityException, - Logger, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries/event-types"; -import { SkipTakePagination } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId/teams/:teamId/memberships", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Teams / Memberships") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -export class OrganizationsTeamsMembershipsController { - private logger = new Logger("OrganizationsTeamsMembershipsController"); - - constructor( - private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService, - private readonly organizationsRepository: OrganizationsRepository, - private readonly orgMembershipService: OrganizationMembershipService - ) { } - - @Get("/") - @ApiOperation({ summary: "Get all memberships" }) - @UseGuards() - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @HttpCode(HttpStatus.OK) - async getAllOrgTeamMemberships( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Query() queryParams: SkipTakePagination - ): Promise { - const { skip, take } = queryParams; - const orgTeamMemberships = await this.organizationsTeamsMembershipsService.getPaginatedOrgTeamMemberships( - orgId, - teamId, - skip ?? 0, - take ?? 250 - ); - return { - status: SUCCESS_STATUS, - data: orgTeamMemberships.map((membership) => - plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }) - ), - }; - } - - @Get("/:membershipId") - @ApiOperation({ summary: "Get a membership" }) - @UseGuards() - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @HttpCode(HttpStatus.OK) - async getOrgTeamMembership( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("membershipId", ParseIntPipe) membershipId: number - ): Promise { - const orgTeamMembership = await this.organizationsTeamsMembershipsService.getOrgTeamMembership( - orgId, - teamId, - membershipId - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamMembershipOutput, orgTeamMembership, { strategy: "excludeAll" }), - }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @Delete("/:membershipId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Delete a membership" }) - async deleteOrgTeamMembership( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("membershipId", ParseIntPipe) membershipId: number - ): Promise { - const membership = await this.organizationsTeamsMembershipsService.deleteOrgTeamMembership( - orgId, - teamId, - membershipId - ); - - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }), - }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @Patch("/:membershipId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Update a membership" }) - async updateOrgTeamMembership( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("membershipId", ParseIntPipe) membershipId: number, - @Body() data: UpdateOrgTeamMembershipDto - ): Promise { - const currentMembership = await this.organizationsTeamsMembershipsService.getOrgTeamMembership( - orgId, - teamId, - membershipId - ); - const updatedMembership = await this.organizationsTeamsMembershipsService.updateOrgTeamMembership( - orgId, - teamId, - membershipId, - data - ); - - if (!currentMembership.accepted && updatedMembership.accepted) { - try { - await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId); - } catch (err) { - this.logger.error("Could not update new team member eventTypes", err); - } - } - - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamMembershipOutput, updatedMembership, { strategy: "excludeAll" }), - }; - } - - - // TODO: Refactor to use inviteMembersWithNoInviterPermissionCheck when it is moved to a Service - // See: packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @Post("/") - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: "Create a membership" }) - async createOrgTeamMembership( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Body() data: CreateOrgTeamMembershipDto - ): Promise { - const user = await this.organizationsRepository.findOrgUser(Number(orgId), Number(data.userId)); - - if (!user) { - throw new UnprocessableEntityException("User is not part of the Organization"); - } - - const shouldAutoAccept = await this.orgMembershipService.shouldAutoAccept({ - organizationId: orgId, - userEmail: user.email, - }); - - // ALWAYS override when email matches - prevents pending memberships - // Remember organizations expect added team member to automatically start receiving bookings for the team event - const acceptedStatus = shouldAutoAccept ? true : (data.accepted ?? false); - - const membershipData = { ...data, accepted: acceptedStatus }; - const membership = await this.organizationsTeamsMembershipsService.createOrgTeamMembership( - teamId, - membershipData - ); - - if (membership.accepted) { - try { - await updateNewTeamMemberEventTypes(user.id, teamId); - } catch (err) { - this.logger.error("Could not update new team member eventTypes", err); - } - } - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }), - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.repository.ts b/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.repository.ts deleted file mode 100644 index 0019c9994696c2..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.repository.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input"; -import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/update-organization-team-membership.input"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { MembershipUserSelect } from "@/modules/teams/memberships/teams-memberships.repository"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrganizationsTeamsMembershipsRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async findOrgTeamMembershipsPaginated(organizationId: number, teamId: number, skip: number, take: number) { - return await this.dbRead.prisma.membership.findMany({ - where: { - teamId: teamId, - team: { - parentId: organizationId, - }, - }, - include: { user: { select: MembershipUserSelect } }, - skip, - take, - }); - } - - async findOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) { - return this.dbRead.prisma.membership.findUnique({ - where: { - id: membershipId, - teamId: teamId, - team: { - parentId: organizationId, - }, - }, - include: { user: { select: MembershipUserSelect } }, - }); - } - async deleteOrgTeamMembershipById(organizationId: number, teamId: number, membershipId: number) { - return this.dbWrite.prisma.membership.delete({ - where: { - id: membershipId, - teamId: teamId, - team: { - parentId: organizationId, - }, - }, - include: { user: { select: MembershipUserSelect } }, - }); - } - - async updateOrgTeamMembershipById( - organizationId: number, - teamId: number, - membershipId: number, - data: UpdateOrgTeamMembershipDto - ) { - return this.dbWrite.prisma.membership.update({ - data: { ...data }, - where: { - id: membershipId, - teamId: teamId, - team: { - parentId: organizationId, - }, - }, - include: { user: { select: MembershipUserSelect } }, - }); - } - - async createOrgTeamMembership(teamId: number, data: CreateOrgTeamMembershipDto) { - return this.dbWrite.prisma.membership.create({ - data: { - createdAt: new Date(), - ...data, - teamId: teamId, - }, - include: { user: { select: MembershipUserSelect } }, - }); - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output.ts b/apps/api/v2/src/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output.ts deleted file mode 100644 index a151a4358d47a2..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsArray, IsEnum, ValidateNested } from "class-validator"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; - -export class OrgTeamMembershipsOutputResponseDto { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @Expose() - @ValidateNested() - @Type(() => TeamMembershipOutput) - @IsArray() - @ApiProperty({ type: [TeamMembershipOutput] }) - data!: TeamMembershipOutput[]; -} - -export class OrgTeamMembershipOutputResponseDto { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @Expose() - @ValidateNested() - @Type(() => TeamMembershipOutput) - @ApiProperty({ type: TeamMembershipOutput }) - data!: TeamMembershipOutput; -} diff --git a/apps/api/v2/src/modules/organizations/teams/memberships/services/organizations-teams-memberships.service.ts b/apps/api/v2/src/modules/organizations/teams/memberships/services/organizations-teams-memberships.service.ts deleted file mode 100644 index 8a4a474e4f153d..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/memberships/services/organizations-teams-memberships.service.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input"; -import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/update-organization-team-membership.input"; -import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository"; -import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service"; -import { Injectable, NotFoundException } from "@nestjs/common"; - -import { TeamService } from "@calcom/platform-libraries"; - -@Injectable() -export class OrganizationsTeamsMembershipsService { - constructor( - private readonly organizationsTeamsMembershipsRepository: OrganizationsTeamsMembershipsRepository, - private readonly teamsMembershipsService: TeamsMembershipsService - ) {} - - async createOrgTeamMembership(teamId: number, data: CreateOrgTeamMembershipDto) { - await this.teamsMembershipsService.canUserBeAddedToTeam(data.userId, teamId); - const teamMembership = await this.organizationsTeamsMembershipsRepository.createOrgTeamMembership( - teamId, - data - ); - return teamMembership; - } - - async getPaginatedOrgTeamMemberships(organizationId: number, teamId: number, skip = 0, take = 250) { - const teamMemberships = - await this.organizationsTeamsMembershipsRepository.findOrgTeamMembershipsPaginated( - organizationId, - teamId, - skip, - take - ); - return teamMemberships; - } - - async getOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) { - const teamMemberships = await this.organizationsTeamsMembershipsRepository.findOrgTeamMembership( - organizationId, - teamId, - membershipId - ); - - if (!teamMemberships) { - throw new NotFoundException("Organization's Team membership not found"); - } - - return teamMemberships; - } - - async updateOrgTeamMembership( - organizationId: number, - teamId: number, - membershipId: number, - data: UpdateOrgTeamMembershipDto - ) { - const teamMembership = await this.organizationsTeamsMembershipsRepository.updateOrgTeamMembershipById( - organizationId, - teamId, - membershipId, - data - ); - return teamMembership; - } - - async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) { - // First get the membership to get the userId - const teamMembership = await this.organizationsTeamsMembershipsRepository.findOrgTeamMembership( - organizationId, - teamId, - membershipId - ); - - if (!teamMembership) { - throw new NotFoundException( - `Membership with id ${membershipId} not found in team ${teamId} of organization ${organizationId}` - ); - } - - await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false }); - - return teamMembership; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/inputs/base-team-role.input.ts b/apps/api/v2/src/modules/organizations/teams/roles/inputs/base-team-role.input.ts deleted file mode 100644 index f0fad18af63175..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/inputs/base-team-role.input.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsArray, IsOptional, IsString, Validate } from "class-validator"; - -import type { PermissionString } from "@calcom/platform-libraries/pbac"; -import { getAllPermissionStringsForScope, Scope } from "@calcom/platform-libraries/pbac"; - -import { TeamPermissionStringValidator } from "../permissions/inputs/validators/team-permission-string.validator"; - -export const teamPermissionEnum = [...getAllPermissionStringsForScope(Scope.Team)] as const; - -export class BaseTeamRoleInput { - @ApiPropertyOptional({ description: "Color for the role (hex code)" }) - @IsString() - @IsOptional() - color?: string; - - @ApiPropertyOptional({ description: "Description of the role" }) - @IsString() - @IsOptional() - description?: string; - - @ApiPropertyOptional({ - description: - "Permissions for this role (format: resource.action). On update, this field replaces the entire permission set for the role (full replace). Use granular permission endpoints for one-by-one changes.", - enum: teamPermissionEnum, - isArray: true, - example: ["eventType.read", "eventType.create", "booking.read"], - }) - @IsArray() - @IsString({ each: true }) - @Validate(TeamPermissionStringValidator, { each: true }) - @IsOptional() - permissions?: PermissionString[]; -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/inputs/create-team-role.input.ts b/apps/api/v2/src/modules/organizations/teams/roles/inputs/create-team-role.input.ts deleted file mode 100644 index 35a1e56c665748..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/inputs/create-team-role.input.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsString, MinLength } from "class-validator"; - -import { BaseTeamRoleInput } from "./base-team-role.input"; - -export class CreateTeamRoleInput extends BaseTeamRoleInput { - @ApiProperty({ description: "Name of the role", minLength: 1 }) - @IsString() - @MinLength(1) - name!: string; -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/inputs/update-team-role.input.ts b/apps/api/v2/src/modules/organizations/teams/roles/inputs/update-team-role.input.ts deleted file mode 100644 index 7df0cf86d5c9f3..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/inputs/update-team-role.input.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsString, IsOptional, MinLength } from "class-validator"; - -import { BaseTeamRoleInput } from "./base-team-role.input"; - -export class UpdateTeamRoleInput extends BaseTeamRoleInput { - @ApiPropertyOptional({ description: "Name of the role", minLength: 1 }) - @IsString() - @MinLength(1) - @IsOptional() - name?: string; -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/organizations-teams-roles.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/roles/organizations-teams-roles.controller.e2e-spec.ts deleted file mode 100644 index 573f4aa97396fc..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/organizations-teams-roles.controller.e2e-spec.ts +++ /dev/null @@ -1,466 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { RoleService } from "@calcom/platform-libraries/pbac"; -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 { FeaturesRepositoryFixture } from "test/fixtures/repository/features.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; -import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; -import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; -import { randomString } from "test/utils/randomString"; -import { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { CreateTeamRoleInput } from "@/modules/organizations/teams/roles/inputs/create-team-role.input"; -import { UpdateTeamRoleInput } from "@/modules/organizations/teams/roles/inputs/update-team-role.input"; -import { CreateTeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/create-team-role.output"; -import { DeleteTeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/delete-team-role.output"; -import { GetAllTeamRolesOutput } from "@/modules/organizations/teams/roles/outputs/get-all-team-roles.output"; -import { GetTeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/get-team-role.output"; -import { UpdateTeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/update-team-role.output"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Roles Endpoints", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let membershipRepositoryFixture: MembershipRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let featuresRepositoryFixture: FeaturesRepositoryFixture; - let roleService: RoleService; - - // Test users - let legacyOrgAdminUser: User; - let legacyOrgMemberUser: User; - let pbacOrgUserWithRolePermission: User; - let pbacOrgUserWithoutRolePermission: User; - let nonOrgUser: User; - - // API Keys - let legacyOrgAdminApiKey: string; - let legacyOrgMemberApiKey: string; - let pbacOrgUserWithRolePermissionApiKey: string; - let pbacOrgUserWithoutRolePermissionApiKey: string; - let nonOrgUserApiKey: string; - - // Organization and team - let organization: Team; - let team: Team; - let pbacEnabledOrganization: Team; - let pbacEnabledTeam: Team; - - const legacyOrgAdminEmail = `legacy-org-admin-${randomString()}@api.com`; - const legacyOrgMemberEmail = `legacy-org-member-${randomString()}@api.com`; - const pbacOrgUserWithRolePermissionEmail = `pbac-org-user-with-role-permission-${randomString()}@api.com`; - const pbacOrgUserWithoutRolePermissionEmail = `pbac-org-user-without-role-permission-${randomString()}@api.com`; - const nonOrgUserEmail = `non-org-user-${randomString()}@api.com`; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - featuresRepositoryFixture = new FeaturesRepositoryFixture(moduleRef); - roleService = new RoleService(); - - // Create test users - legacyOrgAdminUser = await userRepositoryFixture.create({ - email: legacyOrgAdminEmail, - username: legacyOrgAdminEmail, - }); - - legacyOrgMemberUser = await userRepositoryFixture.create({ - email: legacyOrgMemberEmail, - username: legacyOrgMemberEmail, - }); - - pbacOrgUserWithRolePermission = await userRepositoryFixture.create({ - email: pbacOrgUserWithRolePermissionEmail, - username: pbacOrgUserWithRolePermissionEmail, - }); - - pbacOrgUserWithoutRolePermission = await userRepositoryFixture.create({ - email: pbacOrgUserWithoutRolePermissionEmail, - username: pbacOrgUserWithoutRolePermissionEmail, - }); - - nonOrgUser = await userRepositoryFixture.create({ - email: nonOrgUserEmail, - username: nonOrgUserEmail, - }); - - // Create organizations and teams - organization = await organizationsRepositoryFixture.create({ - name: `org-roles-test-${randomString()}`, - isOrganization: true, - }); - - team = await teamRepositoryFixture.create({ - name: `team-roles-test-${randomString()}`, - isOrganization: false, - parent: { connect: { id: organization.id } }, - }); - - pbacEnabledOrganization = await organizationsRepositoryFixture.create({ - name: `pbac-org-roles-test-${randomString()}`, - isOrganization: true, - }); - - pbacEnabledTeam = await teamRepositoryFixture.create({ - name: `pbac-team-roles-test-${randomString()}`, - isOrganization: false, - parent: { connect: { id: pbacEnabledOrganization.id } }, - }); - - await featuresRepositoryFixture.create({ slug: "pbac", enabled: true }); - await featuresRepositoryFixture.setTeamFeatureState({ - teamId: pbacEnabledTeam.id, - featureId: "pbac", - state: "enabled", - }); - - // Create memberships - await membershipRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: legacyOrgAdminUser.id } }, - team: { connect: { id: organization.id } }, - }); - - await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: legacyOrgMemberUser.id } }, - team: { connect: { id: organization.id } }, - }); - - const pbacOrgUserWithRolePermissionMembership = await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: pbacOrgUserWithRolePermission.id } }, - team: { connect: { id: pbacEnabledOrganization.id } }, - }); - - const pbacOrgUserWithoutRolePermissionMembership = await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: pbacOrgUserWithoutRolePermission.id } }, - team: { connect: { id: pbacEnabledOrganization.id } }, - }); - - const roleWithPermission = await roleService.createRole({ - name: "Role Manager", - teamId: pbacEnabledOrganization.id, - permissions: ["role.create", "role.read", "role.update", "role.delete"], - type: "CUSTOM", - }); - - const roleWithoutPermission = await roleService.createRole({ - name: "Basic Role", - teamId: pbacEnabledOrganization.id, - permissions: ["booking.read"], - type: "CUSTOM", - }); - - await roleService.assignRoleToMember(roleWithPermission.id, pbacOrgUserWithRolePermissionMembership.id); - await roleService.assignRoleToMember( - roleWithoutPermission.id, - pbacOrgUserWithoutRolePermissionMembership.id - ); - - const { keyString: legacyOrgAdminKeyString } = await apiKeysRepositoryFixture.createApiKey( - legacyOrgAdminUser.id, - null - ); - legacyOrgAdminApiKey = `cal_test_${legacyOrgAdminKeyString}`; - - const { keyString: legacyOrgMemberKeyString } = await apiKeysRepositoryFixture.createApiKey( - legacyOrgMemberUser.id, - null - ); - legacyOrgMemberApiKey = `cal_test_${legacyOrgMemberKeyString}`; - - const { keyString: pbacOrgUserWithRolePermissionKeyString } = await apiKeysRepositoryFixture.createApiKey( - pbacOrgUserWithRolePermission.id, - null - ); - pbacOrgUserWithRolePermissionApiKey = `cal_test_${pbacOrgUserWithRolePermissionKeyString}`; - - const { keyString: pbacOrgUserWithoutRolePermissionKeyString } = - await apiKeysRepositoryFixture.createApiKey(pbacOrgUserWithoutRolePermission.id, null); - pbacOrgUserWithoutRolePermissionApiKey = `cal_test_${pbacOrgUserWithoutRolePermissionKeyString}`; - - const { keyString: nonOrgUserKeyString } = await apiKeysRepositoryFixture.createApiKey( - nonOrgUser.id, - null - ); - nonOrgUserApiKey = `cal_test_${nonOrgUserKeyString}`; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - describe("Role Creation Authorization", () => { - describe("Positive Tests", () => { - it("should allow role creation when organization has PBAC enabled and user has a create permission", async () => { - const createRoleInput: CreateTeamRoleInput = { - name: "Test Role PBAC", - permissions: ["booking.read", "eventType.create"], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(createRoleInput) - .expect(201) - .then((response) => { - const responseBody: CreateTeamRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.name).toEqual(createRoleInput.name); - expect(responseBody.data.permissions).toEqual(createRoleInput.permissions); - expect(responseBody.data.teamId).toEqual(pbacEnabledTeam.id); - }); - }); - - it("should allow role creation when organization does not have PBAC enabled and user is org admin", async () => { - const createRoleInput: CreateTeamRoleInput = { - name: "Test Role Legacy Admin", - permissions: ["booking.read"], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/teams/${team.id}/roles`) - .set("Authorization", `Bearer ${legacyOrgAdminApiKey}`) - .send(createRoleInput) - .expect(201) - .then((response) => { - const responseBody: CreateTeamRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.name).toEqual(createRoleInput.name); - expect(responseBody.data.teamId).toEqual(team.id); - }); - }); - }); - - describe("Negative Tests", () => { - it("should not allow role creation when organization has PBAC enabled but user has no role assigned", async () => { - const createRoleInput: CreateTeamRoleInput = { - name: "Test Role No Role", - permissions: ["booking.read"], - }; - - const userWithNoRole = await userRepositoryFixture.create({ - email: `no-role-user-${randomString()}@api.com`, - username: `no-role-user-${randomString()}@api.com`, - }); - - await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: userWithNoRole.id } }, - team: { connect: { id: pbacEnabledOrganization.id } }, - }); - - const { keyString: noRoleKeyString } = await apiKeysRepositoryFixture.createApiKey( - userWithNoRole.id, - null - ); - const noRoleApiKey = `cal_test_${noRoleKeyString}`; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles`) - .set("Authorization", `Bearer ${noRoleApiKey}`) - .send(createRoleInput) - .expect(403); - }); - - it("should not allow role creation when organization has PBAC enabled but user role lacks required permission", async () => { - const createRoleInput: CreateTeamRoleInput = { - name: "Test Role No Permission", - permissions: ["booking.read"], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithoutRolePermissionApiKey}`) - .send(createRoleInput) - .expect(403); - }); - - it("should not allow role creation when organization does not have PBAC enabled and user has no membership", async () => { - const createRoleInput: CreateTeamRoleInput = { - name: "Test Role No Membership", - permissions: ["booking.read"], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/teams/${team.id}/roles`) - .set("Authorization", `Bearer ${nonOrgUserApiKey}`) - .send(createRoleInput) - .expect(403); - }); - - it("should not allow role creation when organization does not have PBAC enabled and user has member membership (not admin)", async () => { - const createRoleInput: CreateTeamRoleInput = { - name: "Test Role Member Only", - permissions: ["booking.read"], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${organization.id}/teams/${team.id}/roles`) - .set("Authorization", `Bearer ${legacyOrgMemberApiKey}`) - .send(createRoleInput) - .expect(403); - }); - }); - }); - - describe("CRUD Role Endpoints", () => { - let createdRoleId: string; - - it("should create a role", async () => { - const createRoleInput: CreateTeamRoleInput = { - name: "CRUD Test Role", - permissions: ["booking.read", "eventType.create"], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(createRoleInput) - .expect(201) - .then((response) => { - const responseBody: CreateTeamRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.name).toEqual(createRoleInput.name); - expect(responseBody.data.permissions).toEqual(createRoleInput.permissions); - expect(responseBody.data.teamId).toEqual(pbacEnabledTeam.id); - createdRoleId = responseBody.data.id; - }); - }); - - it("should update role permissions and name", async () => { - const updateRoleInput: UpdateTeamRoleInput = { - name: "CRUD Test Role Updated", - permissions: ["booking.read", "eventType.read"], - }; - - return request(app.getHttpServer()) - .patch( - `/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles/${createdRoleId}` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(updateRoleInput) - .expect(200) - .then((response) => { - const responseBody: UpdateTeamRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(createdRoleId); - expect(responseBody.data.name).toEqual(updateRoleInput.name); - expect(responseBody.data.permissions).toEqual(updateRoleInput.permissions); - expect(responseBody.data.teamId).toEqual(pbacEnabledTeam.id); - }); - }); - - it("should update only name and keep permissions unchanged", async () => { - const updateNameOnly: UpdateTeamRoleInput = { - name: "CRUD Test Role Renamed Only", - }; - const expectedPermissions = ["booking.read", "eventType.read"]; // from previous update - - return request(app.getHttpServer()) - .patch( - `/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles/${createdRoleId}` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(updateNameOnly) - .expect(200) - .then((response) => { - const responseBody: UpdateTeamRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(createdRoleId); - expect(responseBody.data.name).toEqual(updateNameOnly.name); - expect(responseBody.data.permissions).toEqual(expectedPermissions); - expect(responseBody.data.teamId).toEqual(pbacEnabledTeam.id); - }); - }); - - it("should fetch the role", async () => { - return request(app.getHttpServer()) - .get( - `/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles/${createdRoleId}` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(200) - .then((response) => { - const responseBody: GetTeamRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(createdRoleId); - expect(responseBody.data.teamId).toEqual(pbacEnabledTeam.id); - }); - }); - - it("should fetch all roles", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(200) - .then((response) => { - const responseBody: GetAllTeamRolesOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(Array.isArray(responseBody.data)).toBe(true); - expect(responseBody.data.find((r) => r.id === createdRoleId)).toBeDefined(); - const created = responseBody.data.find((r) => r.id === createdRoleId); - expect(created?.teamId).toEqual(pbacEnabledTeam.id); - }); - }); - - it("should delete the role", async () => { - return request(app.getHttpServer()) - .delete( - `/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles/${createdRoleId}` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(200) - .then((response) => { - const responseBody: DeleteTeamRoleOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(createdRoleId); - expect(responseBody.data.teamId).toEqual(pbacEnabledTeam.id); - }); - }); - }); - - afterAll(async () => { - try { - await featuresRepositoryFixture.deleteTeamFeature(pbacEnabledTeam.id, "pbac"); - - await teamRepositoryFixture.delete(team.id); - await teamRepositoryFixture.delete(pbacEnabledTeam.id); - - await organizationsRepositoryFixture.delete(organization.id); - await organizationsRepositoryFixture.delete(pbacEnabledOrganization.id); - - await userRepositoryFixture.deleteByEmail(legacyOrgAdminUser.email); - await userRepositoryFixture.deleteByEmail(legacyOrgMemberUser.email); - await userRepositoryFixture.deleteByEmail(pbacOrgUserWithRolePermission.email); - await userRepositoryFixture.deleteByEmail(pbacOrgUserWithoutRolePermission.email); - await userRepositoryFixture.deleteByEmail(nonOrgUser.email); - } catch (error) { - console.error("Cleanup error:", error); - } finally { - await app.close(); - } - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/roles/organizations-teams-roles.controller.ts b/apps/api/v2/src/modules/organizations/teams/roles/organizations-teams-roles.controller.ts deleted file mode 100644 index 8b150a9103762b..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/organizations-teams-roles.controller.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Pbac } from "@/modules/auth/decorators/pbac/pbac.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { PbacGuard } from "@/modules/auth/guards/pbac/pbac.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { CreateTeamRoleInput } from "@/modules/organizations/teams/roles/inputs/create-team-role.input"; -import { UpdateTeamRoleInput } from "@/modules/organizations/teams/roles/inputs/update-team-role.input"; -import { CreateTeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/create-team-role.output"; -import { DeleteTeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/delete-team-role.output"; -import { GetAllTeamRolesOutput } from "@/modules/organizations/teams/roles/outputs/get-all-team-roles.output"; -import { GetTeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/get-team-role.output"; -import { UpdateTeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/update-team-role.output"; -import { TeamRolesOutputService } from "@/modules/organizations/teams/roles/services/team-roles-output.service"; -import { RolesService } from "@/modules/roles/services/roles.service"; -import { - Controller, - UseGuards, - Get, - Param, - ParseIntPipe, - Query, - Delete, - Patch, - Post, - Body, - HttpCode, - HttpStatus, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { SkipTakePagination } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId/teams/:teamId/roles", - version: API_VERSIONS_VALUES, -}) -@UseGuards( - ApiAuthGuard, - IsOrgGuard, - PbacGuard, - RolesGuard, - IsTeamInOrg, - PlatformPlanGuard, - IsAdminAPIEnabledGuard -) -@DocsTags("Orgs / Teams / Roles") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -export class OrganizationsTeamsRolesController { - constructor( - private readonly rolesService: RolesService, - private readonly rolesOutputService: TeamRolesOutputService - ) {} - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.create"]) - @Post("/") - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: "Create a new organization team role" }) - async createRole( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Body() body: CreateTeamRoleInput - ): Promise { - const role = await this.rolesService.createRole(teamId, body); - return { - status: SUCCESS_STATUS, - data: this.rolesOutputService.getTeamRoleOutput(role), - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.read"]) - @Get("/:roleId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Get a specific organization team role" }) - async getRole( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("roleId") roleId: string - ): Promise { - const role = await this.rolesService.getRole(teamId, roleId); - return { - status: SUCCESS_STATUS, - data: this.rolesOutputService.getTeamRoleOutput(role), - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.read"]) - @Get("/") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Get all organization team roles" }) - async getAllRoles( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Query() queryParams: SkipTakePagination - ): Promise { - const { skip, take } = queryParams; - const roles = await this.rolesService.getTeamRoles(teamId, skip ?? 0, take ?? 250); - return { - status: SUCCESS_STATUS, - data: this.rolesOutputService.getTeamRolesOutput(roles), - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.update"]) - @Patch("/:roleId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Update an organization team role" }) - async updateRole( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("roleId") roleId: string, - @Body() body: UpdateTeamRoleInput - ): Promise { - const role = await this.rolesService.updateRole(teamId, roleId, body); - return { - status: SUCCESS_STATUS, - data: this.rolesOutputService.getTeamRoleOutput(role), - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.delete"]) - @Delete("/:roleId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Delete an organization team role" }) - async deleteRole( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("roleId") roleId: string - ): Promise { - const role = await this.rolesService.deleteRole(teamId, roleId); - return { - status: SUCCESS_STATUS, - data: this.rolesOutputService.getTeamRoleOutput(role), - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/organizations-teams-roles.module.ts b/apps/api/v2/src/modules/organizations/teams/roles/organizations-teams-roles.module.ts deleted file mode 100644 index 7321539306ad9b..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/organizations-teams-roles.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { OrganizationsTeamsRolesPermissionsController } from "@/modules/organizations/teams/roles/permissions/organizations-teams-roles-permissions.controller"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { RolesModule } from "@/modules/roles/roles.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { Module } from "@nestjs/common"; - -import { OrganizationsTeamsRolesController } from "./organizations-teams-roles.controller"; - -@Module({ - imports: [StripeModule, PrismaModule, RedisModule, MembershipsModule, RolesModule], - providers: [OrganizationsRepository, OrganizationsTeamsRepository], - controllers: [OrganizationsTeamsRolesController, OrganizationsTeamsRolesPermissionsController], -}) -export class OrganizationsTeamsRolesModule {} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/outputs/create-team-role.output.ts b/apps/api/v2/src/modules/organizations/teams/roles/outputs/create-team-role.output.ts deleted file mode 100644 index 77fa4d5f8674ad..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/outputs/create-team-role.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/team-role.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class CreateTeamRoleOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: TeamRoleOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => TeamRoleOutput) - data!: TeamRoleOutput; -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/outputs/delete-team-role.output.ts b/apps/api/v2/src/modules/organizations/teams/roles/outputs/delete-team-role.output.ts deleted file mode 100644 index fae7ca6d23c4ab..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/outputs/delete-team-role.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/team-role.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class DeleteTeamRoleOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: TeamRoleOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => TeamRoleOutput) - data!: TeamRoleOutput; -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/outputs/get-all-team-roles.output.ts b/apps/api/v2/src/modules/organizations/teams/roles/outputs/get-all-team-roles.output.ts deleted file mode 100644 index 404088d5c91ee3..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/outputs/get-all-team-roles.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/team-role.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsArray, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class GetAllTeamRolesOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: [TeamRoleOutput], - }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => TeamRoleOutput) - data!: TeamRoleOutput[]; -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/outputs/get-team-role.output.ts b/apps/api/v2/src/modules/organizations/teams/roles/outputs/get-team-role.output.ts deleted file mode 100644 index 8a0b52835b81bb..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/outputs/get-team-role.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/team-role.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class GetTeamRoleOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: TeamRoleOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => TeamRoleOutput) - data!: TeamRoleOutput; -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/outputs/team-role.output.ts b/apps/api/v2/src/modules/organizations/teams/roles/outputs/team-role.output.ts deleted file mode 100644 index 98c56d2d3c73a6..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/outputs/team-role.output.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { Expose } from "class-transformer"; -import { IsString, IsOptional, IsArray, IsEnum, IsDateString, IsNumber } from "class-validator"; - -import { teamPermissionEnum } from "../inputs/base-team-role.input"; - -enum RoleTypeEnum { - SYSTEM = "SYSTEM", - CUSTOM = "CUSTOM", -} - -type RoleType = keyof typeof RoleTypeEnum; - -export class TeamRoleOutput { - @ApiProperty({ description: "Unique identifier for the role" }) - @IsString() - @Expose() - id!: string; - - @ApiProperty({ description: "Name of the role" }) - @IsString() - @Expose() - name!: string; - - @ApiPropertyOptional({ description: "Color for the role (hex code)" }) - @IsString() - @IsOptional() - @Expose() - color?: string | null; - - @ApiPropertyOptional({ description: "Description of the role" }) - @IsString() - @IsOptional() - @Expose() - description?: string | null; - - @ApiPropertyOptional({ description: "Team ID this role belongs to" }) - @IsNumber() - @IsOptional() - @Expose() - teamId?: number | null; - - @ApiProperty({ - description: "Type of role", - enum: RoleTypeEnum, - }) - @IsEnum(RoleTypeEnum) - @Expose() - type!: RoleType; - - @ApiProperty({ - description: "Permissions assigned to this role in 'resource.action' format.", - enum: teamPermissionEnum, - isArray: true, - example: ["booking.read", "eventType.create"], - }) - @IsArray() - @IsString({ each: true }) - @Expose() - permissions!: string[]; - - @ApiProperty({ description: "When the role was created" }) - @IsDateString() - @Expose() - createdAt!: string; - - @ApiProperty({ description: "When the role was last updated" }) - @IsDateString() - @Expose() - updatedAt!: string; -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/outputs/update-team-role.output.ts b/apps/api/v2/src/modules/organizations/teams/roles/outputs/update-team-role.output.ts deleted file mode 100644 index bea1e365d28910..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/outputs/update-team-role.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/team-role.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class UpdateTeamRoleOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: TeamRoleOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => TeamRoleOutput) - data!: TeamRoleOutput; -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/permissions/inputs/create-team-role-permissions.input.ts b/apps/api/v2/src/modules/organizations/teams/roles/permissions/inputs/create-team-role-permissions.input.ts deleted file mode 100644 index 0ae5a2acee022f..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/permissions/inputs/create-team-role-permissions.input.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, IsString, Validate } from "class-validator"; - -import type { PermissionString } from "@calcom/platform-libraries/pbac"; - -import { teamPermissionEnum } from "../../inputs/base-team-role.input"; -import { TeamPermissionStringValidator } from "./validators/team-permission-string.validator"; - -export class CreateTeamRolePermissionsInput { - @ApiProperty({ - description: "Permissions to add (format: resource.action)", - enum: teamPermissionEnum, - isArray: true, - example: ["eventType.read", "booking.read"], - }) - @IsArray() - @IsString({ each: true }) - @Validate(TeamPermissionStringValidator, { each: true }) - permissions!: PermissionString[]; -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/permissions/inputs/delete-team-role-permissions.query.ts b/apps/api/v2/src/modules/organizations/teams/roles/permissions/inputs/delete-team-role-permissions.query.ts deleted file mode 100644 index 12c0af3673695f..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/permissions/inputs/delete-team-role-permissions.query.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { Transform } from "class-transformer"; -import { ArrayNotEmpty, IsArray, IsOptional, IsString, Validate } from "class-validator"; - -import type { PermissionString } from "@calcom/platform-libraries/pbac"; - -import { teamPermissionEnum } from "../../inputs/base-team-role.input"; -import { TeamPermissionStringValidator } from "./validators/team-permission-string.validator"; - -export class DeleteTeamRolePermissionsQuery { - @ApiPropertyOptional({ - description: - "Permissions to remove (format: resource.action). Supports comma-separated values as well as repeated query params.", - example: "?permissions=eventType.read,booking.read", - enum: teamPermissionEnum, - isArray: true, - }) - @IsOptional() - @Transform(({ value }) => { - if (typeof value === "string") { - return value - .split(",") - .map((p: string) => p.trim()) - .filter((p: string) => p.length > 0); - } - return value; - }) - @IsArray() - @ArrayNotEmpty({ message: "permissions cannot be empty." }) - @IsString({ each: true }) - @Validate(TeamPermissionStringValidator, { each: true }) - permissions?: PermissionString[]; -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/permissions/inputs/validators/team-permission-string.validator.spec.ts b/apps/api/v2/src/modules/organizations/teams/roles/permissions/inputs/validators/team-permission-string.validator.spec.ts deleted file mode 100644 index d603355df11060..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/permissions/inputs/validators/team-permission-string.validator.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { BadRequestException } from "@nestjs/common"; - -import { TeamPermissionStringValidator } from "./team-permission-string.validator"; - -describe("PermissionStringValidator", () => { - let validator: TeamPermissionStringValidator; - - beforeEach(() => { - validator = new TeamPermissionStringValidator(); - }); - - describe("validate", () => { - it("should return true for valid permission strings", () => { - const validPermissions = [ - "eventType.read", - "eventType.create", - "eventType.update", - "eventType.delete", - "booking.read", - "booking.update", - "role.create", - "role.read", - "role.update", - "role.delete", - "team.read", - "team.invite", - ]; - - validPermissions.forEach((permission) => { - expect(validator.validate(permission)).toBe(true); - }); - }); - - it("should throw for wildcard permission strings", () => { - const wildcardPermissions = ["*", "*.read", "eventType.*"]; - - wildcardPermissions.forEach((permission) => { - expect(() => validator.validate(permission)).toThrow(BadRequestException); - }); - }); - - it("should throw for invalid permission strings", () => { - const invalidPermissions = [ - "invalid", // no dot - "invalid.", // no action - ".invalid", // no resource - "invalid.action", // invalid resource - "eventType.invalid", // invalid action - "event-type.read", // wrong format (should be eventType) - "", // empty string - "eventType..read", // double dot - "eventType.read.extra", // too many parts - ]; - - invalidPermissions.forEach((permission) => { - expect(() => validator.validate(permission)).toThrow(BadRequestException); - }); - }); - - it("should throw for organization-scoped permission strings", () => { - // These are explicitly marked with scope: [Scope.Organization] in permission-registry - const orgScoped = [ - "organization.create", - "organization.read", - "organization.listMembers", - "organization.listMembersPrivate", - "organization.invite", - "organization.remove", - "organization.manageBilling", - "organization.changeMemberRole", - "organization.impersonate", - "organization.update", - ]; - - orgScoped.forEach((permission) => { - expect(() => validator.validate(permission)).toThrow(BadRequestException); - }); - }); - }); - - describe("defaultMessage", () => { - it("should return appropriate error message", () => { - const message = validator.defaultMessage(); - expect(message).toContain("Permission must be a valid permission string"); - expect(message).toContain("resource.action"); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/roles/permissions/inputs/validators/team-permission-string.validator.ts b/apps/api/v2/src/modules/organizations/teams/roles/permissions/inputs/validators/team-permission-string.validator.ts deleted file mode 100644 index 674a6235455204..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/permissions/inputs/validators/team-permission-string.validator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BadRequestException } from "@nestjs/common"; -import type { ValidatorConstraintInterface } from "class-validator"; -import { ValidatorConstraint } from "class-validator"; - -import { isValidPermissionStringForScope } from "@calcom/platform-libraries/pbac"; -import { Scope } from "@calcom/platform-libraries/pbac"; - -@ValidatorConstraint({ name: "teamPermissionStringValidator", async: false }) -export class TeamPermissionStringValidator implements ValidatorConstraintInterface { - validate(permission: string) { - const isValid = isValidPermissionStringForScope(permission, Scope.Team); - if (!isValid) { - throw new BadRequestException( - `Permission '${permission}' must be a valid permission string in format 'resource.action' (e.g., 'eventType.read', 'booking.create')` - ); - } - return true; - } - - defaultMessage() { - return "Permission must be a valid permission string in format 'resource.action' (e.g., 'eventType.read', 'booking.create')"; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/permissions/organizations-teams-roles-permissions.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/roles/permissions/organizations-teams-roles-permissions.controller.e2e-spec.ts deleted file mode 100644 index 7a7e6e120863b9..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/permissions/organizations-teams-roles-permissions.controller.e2e-spec.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { PermissionString } from "@calcom/platform-libraries/pbac"; -import { RoleService } from "@calcom/platform-libraries/pbac"; -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 { FeaturesRepositoryFixture } from "test/fixtures/repository/features.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; -import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; -import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; -import { randomString } from "test/utils/randomString"; -import { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { CreateTeamRoleInput } from "@/modules/organizations/teams/roles/inputs/create-team-role.input"; -import type { CreateTeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/create-team-role.output"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Teams Roles Permissions Endpoints", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let membershipRepositoryFixture: MembershipRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let featuresRepositoryFixture: FeaturesRepositoryFixture; - let roleService: RoleService; - - let pbacOrgUserWithRolePermission: User; - let pbacOrgUserWithRolePermissionApiKey: string; - - let pbacEnabledOrganization: Team; - let pbacEnabledTeam: Team; - - const pbacUserEmail = `pbac-org-user-with-permissions-${randomString()}@api.com`; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - featuresRepositoryFixture = new FeaturesRepositoryFixture(moduleRef); - roleService = new RoleService(); - - // Create PBAC org/team - pbacEnabledOrganization = await organizationsRepositoryFixture.create({ - name: `pbac-org-role-perm-test-${randomString()}`, - isOrganization: true, - }); - - pbacEnabledTeam = await teamRepositoryFixture.create({ - name: `pbac-team-role-perm-test-${randomString()}`, - isOrganization: false, - parent: { connect: { id: pbacEnabledOrganization.id } }, - }); - - await featuresRepositoryFixture.create({ slug: "pbac", enabled: true }); - await featuresRepositoryFixture.setTeamFeatureState({ - teamId: pbacEnabledTeam.id, - featureId: "pbac", - state: "enabled", - }); - - // Create user + membership in org - pbacOrgUserWithRolePermission = await userRepositoryFixture.create({ - email: pbacUserEmail, - username: pbacUserEmail, - }); - - const membership = await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: pbacOrgUserWithRolePermission.id } }, - team: { connect: { id: pbacEnabledOrganization.id } }, - }); - - // Create a role that allows role.read and role.update, assign to user - const managerRole = await roleService.createRole({ - name: `role-manager-${randomString()}`, - teamId: pbacEnabledOrganization.id, - permissions: ["role.create", "role.read", "role.update"], - type: "CUSTOM", - }); - await roleService.assignRoleToMember(managerRole.id, membership.id); - - // API key for user - const { keyString } = await apiKeysRepositoryFixture.createApiKey(pbacOrgUserWithRolePermission.id, null); - pbacOrgUserWithRolePermissionApiKey = `cal_test_${keyString}`; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - it("lists permissions for a role (GET /)", async () => { - const initialPermissions = ["booking.read"] as const; - const baseRoleInput: CreateTeamRoleInput = { - name: `perm-target-role-list-${randomString()}`, - permissions: [...initialPermissions], - }; - - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const responseBody: CreateTeamRoleOutput = createRes.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.permissions).toEqual(baseRoleInput.permissions); - const roleId = responseBody.data.id; - - const listRes = await request(app.getHttpServer()) - .get( - `/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles/${roleId}/permissions` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(200); - expect(listRes.body.status).toEqual(SUCCESS_STATUS); - expect(listRes.body.data).toEqual(initialPermissions); - }); - - it("adds permissions (POST /)", async () => { - const initialPermissions = ["booking.read"] as const; - const toAdd = ["eventType.create", "eventType.read"] as const; - const expected = [...initialPermissions, ...toAdd] as string[]; - - const baseRoleInput: CreateTeamRoleInput = { - name: `perm-target-role-add-${randomString()}`, - permissions: [...initialPermissions], - }; - - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const responseBody: CreateTeamRoleOutput = createRes.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.permissions).toEqual(baseRoleInput.permissions); - const roleId = responseBody.data.id; - - const addRes = await request(app.getHttpServer()) - .post( - `/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles/${roleId}/permissions` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send({ permissions: toAdd }) - .expect(200); - expect(addRes.body.status).toEqual(SUCCESS_STATUS); - expect(addRes.body.data).toEqual(expected); - }); - - it("bulk removes permissions via query (DELETE /)", async () => { - // Seed role - const initialPermissions: PermissionString[] = ["booking.read", "eventType.create", "eventType.read"]; - const toRemove: PermissionString[] = ["eventType.create", "eventType.read"]; - const expected: PermissionString[] = ["booking.read"]; - - const baseRoleInput: CreateTeamRoleInput = { - name: `perm-target-role-delmany-${randomString()}`, - permissions: initialPermissions, - }; - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const responseBody: CreateTeamRoleOutput = createRes.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.permissions).toEqual(baseRoleInput.permissions); - const roleId = responseBody.data.id; - - await request(app.getHttpServer()) - .delete( - `/v2/organizations/${pbacEnabledOrganization.id}/teams/${ - pbacEnabledTeam.id - }/roles/${roleId}/permissions?permissions=${toRemove.join(",")}` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(204); - - const listRes = await request(app.getHttpServer()) - .get( - `/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles/${roleId}/permissions` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(200); - expect(listRes.body.data).toEqual(expected); - }); - - it("replaces all permissions (PUT /)", async () => { - const initialPermissions = ["booking.read"] as const; - const replacement = ["booking.read", "eventType.update"] as const; - - const baseRoleInput: CreateTeamRoleInput = { - name: `perm-target-role-put-${randomString()}`, - permissions: [...initialPermissions], - }; - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const responseBody: CreateTeamRoleOutput = createRes.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.permissions).toEqual(baseRoleInput.permissions); - const roleId = responseBody.data.id; - - const putRes = await request(app.getHttpServer()) - .put( - `/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles/${roleId}/permissions` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send({ permissions: replacement }) - .expect(200); - - expect(putRes.body.status).toEqual(SUCCESS_STATUS); - expect(putRes.body.data).toEqual(replacement); - }); - - it("removes a single permission (DELETE /:permission)", async () => { - const initialPermissions = ["booking.read", "eventType.update"] as const; - const toRemove = "eventType.update" as const; - const expected = ["booking.read"] as const; - - const baseRoleInput: CreateTeamRoleInput = { - name: `perm-target-role-delone-${randomString()}`, - permissions: [...initialPermissions], - }; - const createRes = await request(app.getHttpServer()) - .post(`/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles`) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .send(baseRoleInput) - .expect(201); - const responseBody: CreateTeamRoleOutput = createRes.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.permissions).toEqual(baseRoleInput.permissions); - const roleId = responseBody.data.id; - - await request(app.getHttpServer()) - .delete( - `/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles/${roleId}/permissions/${toRemove}` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(204); - - const listRes = await request(app.getHttpServer()) - .get( - `/v2/organizations/${pbacEnabledOrganization.id}/teams/${pbacEnabledTeam.id}/roles/${roleId}/permissions` - ) - .set("Authorization", `Bearer ${pbacOrgUserWithRolePermissionApiKey}`) - .expect(200); - expect(listRes.body.data).toEqual(expected); - }); - - afterAll(async () => { - try { - // Clean up feature flag from team - await featuresRepositoryFixture.deleteTeamFeature(pbacEnabledTeam.id, "pbac"); - - // Clean up teams and orgs - await teamRepositoryFixture.delete(pbacEnabledTeam.id); - await organizationsRepositoryFixture.delete(pbacEnabledOrganization.id); - - // Clean up user - await userRepositoryFixture.deleteByEmail(pbacOrgUserWithRolePermission.email); - } catch (err) { - console.log(err); - } finally { - await app.close(); - } - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/roles/permissions/organizations-teams-roles-permissions.controller.ts b/apps/api/v2/src/modules/organizations/teams/roles/permissions/organizations-teams-roles-permissions.controller.ts deleted file mode 100644 index f2fc201d9b2e62..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/permissions/organizations-teams-roles-permissions.controller.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Pbac } from "@/modules/auth/decorators/pbac/pbac.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { PbacGuard } from "@/modules/auth/guards/pbac/pbac.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { CreateTeamRolePermissionsInput } from "@/modules/organizations/teams/roles/permissions/inputs/create-team-role-permissions.input"; -import { DeleteTeamRolePermissionsQuery } from "@/modules/organizations/teams/roles/permissions/inputs/delete-team-role-permissions.query"; -import { GetTeamRolePermissionsOutput } from "@/modules/organizations/teams/roles/permissions/outputs/get-team-role-permissions.output"; -import { RolesPermissionsService } from "@/modules/roles/permissions/services/roles-permissions.service"; -import { - Body, - Controller, - Delete, - Get, - HttpCode, - HttpStatus, - Param, - ParseIntPipe, - Put, - Query, - Post, - UseGuards, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { PermissionString } from "@calcom/platform-libraries/pbac"; - -@Controller({ - path: "/v2/organizations/:orgId/teams/:teamId/roles/:roleId/permissions", - version: API_VERSIONS_VALUES, -}) -@UseGuards( - ApiAuthGuard, - IsOrgGuard, - PbacGuard, - RolesGuard, - IsTeamInOrg, - PlatformPlanGuard, - IsAdminAPIEnabledGuard -) -@DocsTags("Orgs / Teams / Roles / Permissions") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -export class OrganizationsTeamsRolesPermissionsController { - constructor(private readonly rolePermissionsService: RolesPermissionsService) {} - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.update"]) - @Post("/") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Add permissions to an organization team role (single or batch)" }) - async addPermissions( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("roleId") roleId: string, - @Body() body: CreateTeamRolePermissionsInput - ): Promise { - const permissions = await this.rolePermissionsService.addRolePermissions( - teamId, - roleId, - body.permissions - ); - return { status: SUCCESS_STATUS, data: permissions }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.read"]) - @Get("/") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "List permissions for an organization team role" }) - async listPermissions( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("roleId") roleId: string - ): Promise { - const permissions = await this.rolePermissionsService.getRolePermissions(teamId, roleId); - return { status: SUCCESS_STATUS, data: permissions }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.update"]) - @Put("/") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Replace all permissions for an organization team role" }) - async setPermissions( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("roleId") roleId: string, - @Body() body: CreateTeamRolePermissionsInput - ): Promise { - const permissions = await this.rolePermissionsService.setRolePermissions( - teamId, - roleId, - body.permissions || [] - ); - return { status: SUCCESS_STATUS, data: permissions }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.update"]) - @Delete("/:permission") - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: "Remove a permission from an organization team role" }) - async removePermission( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("roleId") roleId: string, - @Param("permission") permission: PermissionString - ): Promise { - await this.rolePermissionsService.removeRolePermission(teamId, roleId, permission); - } - - @Roles("ORG_ADMIN") - @PlatformPlan("SCALE") - @Pbac(["role.update"]) - @Delete("/") - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: "Remove multiple permissions from an organization team role" }) - async removePermissions( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("roleId") roleId: string, - @Query() query: DeleteTeamRolePermissionsQuery - ): Promise { - await this.rolePermissionsService.removeRolePermissions(teamId, roleId, query.permissions || []); - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/permissions/outputs/get-team-role-permissions.output.ts b/apps/api/v2/src/modules/organizations/teams/roles/permissions/outputs/get-team-role-permissions.output.ts deleted file mode 100644 index 0d9297a52ebb70..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/permissions/outputs/get-team-role-permissions.output.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, IsString } from "class-validator"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; - -export class GetTeamRolePermissionsOutput { - @ApiProperty({ example: SUCCESS_STATUS }) - status!: typeof SUCCESS_STATUS; - - @ApiProperty({ type: [String] }) - @IsArray() - @IsString({ each: true }) - data!: string[]; -} diff --git a/apps/api/v2/src/modules/organizations/teams/roles/services/team-roles-output.service.ts b/apps/api/v2/src/modules/organizations/teams/roles/services/team-roles-output.service.ts deleted file mode 100644 index 60bbfb889dd96b..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/roles/services/team-roles-output.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { TeamRoleOutput } from "@/modules/organizations/teams/roles/outputs/team-role.output"; -import { Injectable } from "@nestjs/common"; - -import type { Role } from "@calcom/platform-libraries/pbac"; - -@Injectable() -export class TeamRolesOutputService { - getTeamRoleOutput(role: Role): TeamRoleOutput { - return { - id: role.id, - name: role.name, - color: role.color || null, - description: role.description || null, - teamId: role.teamId || null, - type: role.type, - permissions: role.permissions.map((permission) => `${permission.resource}.${permission.action}`), - createdAt: role.createdAt.toISOString(), - updatedAt: role.updatedAt.toISOString(), - }; - } - - getTeamRolesOutput(roles: Role[]): TeamRoleOutput[] { - return roles.map((role) => this.getTeamRoleOutput(role)); - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.e2e-spec.ts deleted file mode 100644 index c350c3bb4805d5..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.e2e-spec.ts +++ /dev/null @@ -1,769 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; -import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; -import { RoutingFormsRepositoryFixture } from "test/fixtures/repository/routing-forms.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 { GetRoutingFormResponsesOutput } from "@/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Teams Routing Forms Responses", () => { - let app: INestApplication; - let prismaWriteService: PrismaWriteService; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - - let teamsRepositoryFixture: TeamRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let routingFormsRepositoryFixture: RoutingFormsRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let org: Team; - let orgTeam: Team; - let routingFormResponseId: number; - const authEmail = `organizations-teams-routing-forms-responses-user-${randomString()}@api.com`; - let user: User; - let apiKeyString: string; - - let routingFormId: string; - let routingEventType: { - id: number; - slug: string | null; - teamId: number | null; - userId: number | null; - title: string; - }; - const routingFormResponses = [ - { - formFillerId: `${randomString()}`, - response: { - "participant-field": { - label: "participant", - value: "mamut", - }, - }, - createdAt: new Date("2025-02-17T09:03:18.121Z"), - }, - ]; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); - - prismaWriteService = moduleRef.get(PrismaWriteService); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - routingFormsRepositoryFixture = new RoutingFormsRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-teams-routing-forms-responses-organization-${randomString()}`, - isOrganization: true, - }); - - user = await userRepositoryFixture.create({ - email: authEmail, - username: authEmail, - }); - - const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); - apiKeyString = keyString; - - orgTeam = await teamsRepositoryFixture.create({ - name: `organizations-teams-routing-forms-responses-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: orgTeam.id } }, - }); - - // Create an event type for routing form to route to - routingEventType = await prismaWriteService.prisma.eventType.create({ - data: { - title: "Test Event Type", - slug: "test-event-type", - length: 30, - userId: user.id, - teamId: orgTeam.id, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: authEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - - const routingForm = await routingFormsRepositoryFixture.create({ - name: "Test Routing Form", - description: null, - position: 0, - disabled: false, - fields: [ - { - id: "participant-field", - type: "text", - label: "participant", - required: true, - identifier: "participant", - }, - { - id: "question2-field", - type: "text", - label: "question2", - required: false, - identifier: "question2", - }, - ], - routes: [ - { - id: "route-1", - queryValue: { - id: "route-1", - type: "group", - children1: { - "rule-1": { - type: "rule", - properties: { - field: "question1", - operator: "equal", - value: ["answer1"], - valueSrc: ["value"], - valueType: ["text"], - }, - }, - }, - }, - action: { - type: "eventTypeRedirectUrl", - eventTypeId: routingEventType.id, - value: `team/${orgTeam.slug}/${routingEventType.slug}`, - }, - isFallback: false, - }, - { - id: "fallback-route", - queryValue: { - id: "fallback-route", - type: "group", - children1: {}, - }, - action: { type: "customPageMessage", value: "Thank you for your response" }, - isFallback: true, - }, - ], - user: { - connect: { - id: user.id, - }, - }, - team: { - connect: { - id: orgTeam.id, - }, - }, - responses: { - create: routingFormResponses, - }, - }); - routingFormId = routingForm.id; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should not get routing form responses for non existing org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/99999/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses`) - .expect(401); - }); - - it("should not get routing form responses for non existing team", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/99999/routing-forms/${routingFormId}/responses`) - .expect(401); - }); - - it("should not get routing form responses for non existing routing form", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/99999/responses`) - .expect(401); - }); - - it("should get routing form responses", async () => { - return request(app.getHttpServer()) - .get( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses?skip=0&take=1` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - const responseBody: GetRoutingFormResponsesOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseData = responseBody.data; - expect(responseData).toBeDefined(); - expect(responseData.length).toEqual(1); - expect(responseData[0].response).toEqual(routingFormResponses[0].response); - expect(responseData[0].formFillerId).toEqual(routingFormResponses[0].formFillerId); - expect(responseData[0].createdAt).toEqual(routingFormResponses[0].createdAt.toISOString()); - routingFormResponseId = responseData[0].id; - }); - }); - - describe(`POST /v2/organizations/:orgId/teams/:teamId/routing-forms/:routingFormId/responses`, () => { - describe("permissions", () => { - it("should return 403 when organization does not exist", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/99999/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - question1: "answer1", - }) - .expect(403); - }); - - it("should return 404 when team does not exist", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/99999/routing-forms/${routingFormId}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - question1: "answer1", - }) - .expect(404) - .then((response) => { - expect(response.body.error.message).toContain(`IsTeamInOrg - Team (99999) not found.`); - }); - }); - - it("should return 403 when routing form does not exist in the team", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/non-existent-id/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - question1: "answer1", - }) - .expect(403) - .then((response) => { - expect(response.body.error.message).toContain( - `IsRoutingFormInTeam - team with id=(${orgTeam.id}) does not own routing form with id=(non-existent-id).` - ); - }); - }); - - it("should return 403 when routing form belongs to different team", async () => { - // Create a second team within the same organization - const otherTeam = await teamsRepositoryFixture.create({ - name: `other-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - // Create a routing form that belongs to the other team - const otherTeamRoutingForm = await routingFormsRepositoryFixture.create({ - name: "Other Team's Routing Form", - description: "Test Description", - position: 0, - disabled: false, - fields: [ - { - type: "text", - label: "Question 1", - required: true, - }, - ], - routes: [ - { - action: { type: "customPageMessage", value: "Thank you for your response" }, - }, - ], - user: { - connect: { - id: user.id, - }, - }, - team: { - connect: { - id: otherTeam.id, - }, - }, - }); - - // Try to access the routing form that belongs to the other team - const response = await request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${otherTeamRoutingForm.id}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - question1: "answer1", - }); - - expect(response.status).toBe(403); - expect(response.body.error.message).toContain( - `IsRoutingFormInTeam - team with id=(${orgTeam.id}) does not own routing form with id=(${otherTeamRoutingForm.id}).` - ); - - // Clean up - await routingFormsRepositoryFixture.delete(otherTeamRoutingForm.id); - await teamsRepositoryFixture.delete(otherTeam.id); - }); - - it("should return 401 when authentication token is missing", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses?start=2050-09-05&end=2050-09-06` - ) - .send({ - question1: "answer1", - }) - .expect(401); - }); - }); - - it("should return 400 when required form fields are missing", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - // Missing required participant field - }) - .expect(400); - }); - - it("should return 400 when required slot query parameters are missing", async () => { - // Missing start parameter - await request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses?end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - participant: "test-participant", - }) - .expect(400); - - // Missing end parameter - await request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses?start=2050-09-05` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - participant: "test-participant", - }) - .expect(400); - }); - - it("should return 400 when date parameters have invalid format", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses?start=invalid-date&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - participant: "test-participant", - }) - .expect(400); - }); - - it("should return 400 when end date is before start date", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses?start=2050-09-10&end=2050-09-05` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - participant: "test-participant", - }) - .expect(400); - }); - - it("should handle queued response creation", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses?start=2050-09-05&end=2050-09-06&queueResponse=true` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - participant: "test-participant", - }) - .expect(201) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - console.log("responseBody", responseBody.data); - expect(data.routing?.queuedResponseId).toBeDefined(); - }); - }); - - it("should create response and return available slots when routing to event type", async () => { - // Create a routing form with event type routing - const eventTypeRoutingForm = await prismaWriteService.prisma.app_RoutingForms_Form.create({ - data: { - name: "Test Event Type Routing Form", - description: "Test Description", - disabled: false, - routes: [ - { - id: "route-1", - queryValue: { - id: "route-1", - type: "group", - children1: { - "rule-1": { - type: "rule", - properties: { - field: "question1", - operator: "equal", - value: ["answer1"], - valueSrc: ["value"], - valueType: ["text"], - }, - }, - }, - }, - action: { - type: "eventTypeRedirectUrl", - eventTypeId: routingEventType.id, - value: `team/${orgTeam.slug}/${routingEventType.slug}`, - }, - isFallback: false, - }, - { - id: "fallback-route", - action: { type: "customPageMessage", value: "Fallback Message" }, - isFallback: true, - queryValue: { id: "fallback-route", type: "group" }, - }, - ], - fields: [ - { - id: "question1", - type: "text", - label: "Question 1", - required: true, - identifier: "question1", - }, - { - id: "question2", - type: "text", - label: "Question 2", - required: false, - identifier: "question2", - }, - ], - settings: { - emailOwnerOnSubmission: false, - }, - teamId: orgTeam.id, - userId: user.id, - }, - }); - - const response = await request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${eventTypeRoutingForm.id}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - question1: "answer1", // This matches the route condition - question2: "answer2", - }) - .expect(201); - - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data).toBeDefined(); - expect(data.routing?.responseId).toBeDefined(); - expect(typeof data.routing?.responseId).toBe("number"); - expect(data.eventTypeId).toEqual(routingEventType.id); - expect(data.slots).toBeDefined(); - expect(typeof data.slots).toBe("object"); - expect(data.routing?.teamMemberIds).toBeDefined(); - - // Clean up - await prismaWriteService.prisma.app_RoutingForms_Form.delete({ - where: { id: eventTypeRoutingForm.id }, - }); - }); - - it("should return 500 when event type is not found", async () => { - // Create a routing form with an invalid eventTypeId - const routingFormWithInvalidEventType = await prismaWriteService.prisma.app_RoutingForms_Form.create({ - data: { - name: "Test Routing Form with Invalid Event Type", - description: "Test Description", - disabled: false, - routes: [ - { - id: "route-1", - queryValue: { - id: "route-1", - type: "group", - children1: { - "rule-1": { - type: "rule", - properties: { - field: "question1", - operator: "equal", - value: ["answer1"], - valueSrc: ["value"], - valueType: ["text"], - }, - }, - }, - }, - action: { - type: "eventTypeRedirectUrl", - eventTypeId: 99999, // Invalid event type ID - value: `team/${orgTeam.slug}/non-existent-event-type`, - }, - isFallback: false, - }, - { - id: "fallback-route", - action: { type: "customPageMessage", value: "Fallback Message" }, - isFallback: true, - queryValue: { id: "fallback-route", type: "group" }, - }, - ], - fields: [ - { - id: "question1", - type: "text", - label: "Question 1", - required: true, - identifier: "question1", - }, - ], - settings: { - emailOwnerOnSubmission: false, - }, - teamId: orgTeam.id, - userId: user.id, - }, - }); - - // Try to create a response for the form with invalid event type - const response = await request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormWithInvalidEventType.id}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - question1: "answer1", - }); - - expect(response.status).toBe(500); - - // Clean up the form - await prismaWriteService.prisma.app_RoutingForms_Form.delete({ - where: { id: routingFormWithInvalidEventType.id }, - }); - }); - - it("should return external redirect URL when routing to external URL", async () => { - // Create a routing form with external redirect action - const externalRoutingForm = await prismaWriteService.prisma.app_RoutingForms_Form.create({ - data: { - name: "Test External Routing Form", - description: "Test Description for External Redirect", - disabled: false, - routes: [ - { - id: "external-route-1", - queryValue: { - id: "external-route-1", - type: "group", - children1: { - "rule-1": { - type: "rule", - properties: { - field: "question1", - operator: "equal", - value: ["external"], - valueSrc: ["value"], - valueType: ["text"], - }, - }, - }, - }, - action: { - type: "externalRedirectUrl", - value: "https://example.com/external-booking", - }, - isFallback: false, - }, - { - id: "fallback-route", - action: { type: "customPageMessage", value: "Fallback Message" }, - isFallback: true, - queryValue: { id: "fallback-route", type: "group" }, - }, - ], - fields: [ - { - id: "question1", - type: "text", - label: "Question 1", - required: true, - identifier: "question1", - }, - ], - settings: { - emailOwnerOnSubmission: false, - }, - teamId: orgTeam.id, - userId: user.id, - }, - }); - - const response = await request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${externalRoutingForm.id}/responses?start=2050-09-05&end=2050-09-06` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - question1: "external", // This matches the route condition for external redirect - }) - .expect(201); - - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data).toBeDefined(); - expect(data.routingExternalRedirectUrl).toBeDefined(); - expect(data.routingExternalRedirectUrl).toContain("https://example.com/external-booking"); - expect(data.routingExternalRedirectUrl).toContain("cal.action=externalRedirectUrl"); - - // Verify that it doesn't contain event type routing data - expect(data.eventTypeId).toBeUndefined(); - expect(data.slots).toBeUndefined(); - expect(data.routing).toBeUndefined(); - expect(data.routingCustomMessage).toBeUndefined(); - - // Clean up the external routing form - await prismaWriteService.prisma.app_RoutingForms_Form.delete({ - where: { id: externalRoutingForm.id }, - }); - }); - }); - - describe(`PATCH /v2/organizations/:orgId/routing-forms/:routingFormId/responses/:responseId`, () => { - it("should not update routing form response for non existing org", async () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/99999/routing-forms/${routingFormId}/responses/${routingFormResponseId}`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ response: JSON.stringify({ question1: "updated_answer1" }) }) - .expect(403); - }); - - it("should not update routing form response for non existing form", async () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/routing-forms/non-existent-id/responses/${routingFormResponseId}`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ response: JSON.stringify({ question1: "updated_answer1" }) }) - .expect(404); - }); - - it("should not update routing form response for non existing response", async () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/routing-forms/${routingFormId}/responses/99999`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ response: JSON.stringify({ question1: "updated_answer1" }) }) - .expect(404); - }); - - it("should not update routing form response without authentication", async () => { - return request(app.getHttpServer()) - .patch( - `/v2/organizations/${org.id}/routing-forms/${routingFormId}/responses/${routingFormResponseId}` - ) - .send({ response: JSON.stringify({ question1: "updated_answer1" }) }) - .expect(401); - }); - - it("should update routing form response", async () => { - const updatedResponse = { question1: "updated_answer1", question2: "updated_answer2" }; - return request(app.getHttpServer()) - .patch( - `/v2/organizations/${org.id}/routing-forms/${routingFormId}/responses/${routingFormResponseId}` - ) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ response: updatedResponse }) - .expect(200) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data).toBeDefined(); - expect(data.id).toEqual(routingFormResponseId); - expect(data.formId).toEqual(routingFormId); - expect(data.response).toEqual(updatedResponse); - }); - }); - }); - - afterAll(async () => { - if (routingFormResponseId) { - await routingFormsRepositoryFixture.deleteResponse(routingFormResponseId); - } - await routingFormsRepositoryFixture.delete(routingFormId); - await prismaWriteService.prisma.eventType.delete({ - where: { id: routingEventType.id }, - }); - await userRepositoryFixture.deleteByEmail(user.email); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts deleted file mode 100644 index c96c085c031cf5..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsRoutingFormInTeam } from "@/modules/auth/guards/routing-forms/is-routing-form-in-team.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { CreateRoutingFormResponseInput } from "@/modules/organizations/routing-forms/inputs/create-routing-form-response.input"; -import { GetRoutingFormResponsesParams } from "@/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input"; -import { UpdateRoutingFormResponseInput } from "@/modules/organizations/routing-forms/inputs/update-routing-form-response.input"; -import { CreateRoutingFormResponseOutput } from "@/modules/organizations/routing-forms/outputs/create-routing-form-response.output"; -import { UpdateRoutingFormResponseOutput } from "@/modules/organizations/routing-forms/outputs/update-routing-form-response.output"; -import { GetRoutingFormResponsesOutput } from "@/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output"; -import { OrganizationsTeamsRoutingFormsResponsesService } from "@/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses.service"; -import { - Controller, - Get, - Post, - Patch, - Param, - ParseIntPipe, - Query, - UseGuards, - Body, - Req, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger"; -import { Request } from "express"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; - -@Controller({ - path: "/v2/organizations/:orgId/teams/:teamId/routing-forms/:routingFormId/responses", - version: API_VERSIONS_VALUES, -}) -@ApiTags("Orgs / Teams / Routing forms / Responses") -@UseGuards( - ApiAuthGuard, - IsOrgGuard, - IsTeamInOrg, - IsRoutingFormInTeam, - PlatformPlanGuard, - RolesGuard, - IsAdminAPIEnabledGuard -) -@ApiHeader(API_KEY_HEADER) -@ApiParam({ name: "orgId", type: Number, required: true }) -@ApiParam({ name: "teamId", type: Number, required: true }) -@ApiParam({ name: "routingFormId", type: String, required: true }) -export class OrganizationsTeamsRoutingFormsResponsesController { - constructor( - private readonly organizationsTeamsRoutingFormsResponsesService: OrganizationsTeamsRoutingFormsResponsesService - ) {} - - @Get("/") - @ApiOperation({ summary: "Get organization team routing form responses" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - async getRoutingFormResponses( - @Param("routingFormId") routingFormId: string, - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Query() queryParams: GetRoutingFormResponsesParams - ): Promise { - const { skip, take, ...filters } = queryParams; - - const routingFormResponses = - await this.organizationsTeamsRoutingFormsResponsesService.getTeamRoutingFormResponses( - teamId, - routingFormId, - skip ?? 0, - take ?? 250, - { ...(filters ?? {}) } - ); - - return { - status: SUCCESS_STATUS, - data: routingFormResponses, - }; - } - - @Post("/") - @ApiOperation({ summary: "Create routing form response and get available slots" }) - @Roles("TEAM_MEMBER") - @PlatformPlan("ESSENTIALS") - async createRoutingFormResponse( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Param("routingFormId") routingFormId: string, - @Query() query: CreateRoutingFormResponseInput, - @Req() request: Request - ): Promise { - const result = - await this.organizationsTeamsRoutingFormsResponsesService.createRoutingFormResponseWithSlots( - routingFormId, - query, - request - ); - - return { - status: SUCCESS_STATUS, - data: result, - }; - } - - @Patch("/:responseId") - @ApiOperation({ summary: "Update routing form response" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - async updateRoutingFormResponse( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("routingFormId") routingFormId: string, - @Param("responseId", ParseIntPipe) responseId: number, - @Body() updateRoutingFormResponseInput: UpdateRoutingFormResponseInput - ): Promise { - const updatedResponse = - await this.organizationsTeamsRoutingFormsResponsesService.updateTeamRoutingFormResponse( - teamId, - routingFormId, - responseId, - updateRoutingFormResponseInput - ); - - return { - status: SUCCESS_STATUS, - data: updatedResponse, - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms.controller.e2e-spec.ts deleted file mode 100644 index b8f25708421a2b..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms.controller.e2e-spec.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { App_RoutingForms_Form, 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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { GetRoutingFormsOutput } from "@/modules/organizations/routing-forms/outputs/get-routing-forms.output"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("OrganizationsRoutingFormsResponsesController", () => { - let app: INestApplication; - let prismaWriteService: PrismaWriteService; - let org: Team; - let team: Team; - let apiKeyString: string; - let routingForm: App_RoutingForms_Form; - - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - - let user: User; - const userEmail = `OrganizationsRoutingFormsResponsesController-key-bookings-2024-08-13-user-${randomString()}@api.com`; - let profileRepositoryFixture: ProfileRepositoryFixture; - - let membershipsRepositoryFixture: MembershipRepositoryFixture; - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); - - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - - prismaWriteService = moduleRef.get(PrismaWriteService); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - org = await organizationsRepositoryFixture.create({ - name: `OrganizationsRoutingFormsResponsesController-teams-memberships-organization-${randomString()}`, - isOrganization: true, - }); - - team = await teamRepositoryFixture.create({ - name: "OrganizationsRoutingFormsResponsesController orgs booking 1", - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - user = await userRepositoryFixture.create({ - email: userEmail, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: userEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - const now = new Date(); - now.setDate(now.getDate() + 1); - const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); - apiKeyString = `${keyString}`; - - routingForm = await prismaWriteService.prisma.app_RoutingForms_Form.create({ - data: { - name: "Test Routing Form", - description: "Test Description", - disabled: false, - routes: JSON.stringify([]), - fields: JSON.stringify([]), - settings: JSON.stringify({}), - teamId: team.id, - userId: user.id, - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - afterAll(async () => { - await prismaWriteService.prisma.app_RoutingForms_Form.deleteMany({ - where: { - teamId: team.id, - }, - }); - await prismaWriteService.prisma.apiKey.deleteMany({ - where: { - teamId: org.id, - }, - }); - await prismaWriteService.prisma.team.delete({ - where: { - id: team.id, - }, - }); - await prismaWriteService.prisma.team.delete({ - where: { - id: org.id, - }, - }); - await app.close(); - }); - - describe(`GET /v2/organizations/:orgId/teams/:teamId/routing-forms`, () => { - it("should not get team routing forms for non existing org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/99999/teams/${team.id}/routing-forms`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(403); - }); - - it("should not get team routing forms for non existing team", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/99999/routing-forms`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(404); - }); - - it("should not get team routing forms without authentication", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/routing-forms`) - .expect(401); - }); - - it("should get team routing forms", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/routing-forms`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - const responseBody: GetRoutingFormsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const routingForms = responseBody.data; - expect(routingForms).toBeDefined(); - expect(routingForms.length).toBeGreaterThan(0); - expect(routingForms[0].id).toEqual(routingForm.id); - expect(routingForms[0].name).toEqual(routingForm.name); - expect(routingForms[0].description).toEqual(routingForm.description); - expect(routingForms[0].disabled).toEqual(routingForm.disabled); - }); - }); - - it("should filter team routing forms by name", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/routing-forms?name=Team`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - const responseBody: GetRoutingFormsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const routingForms = responseBody.data; - expect(routingForms).toBeDefined(); - expect(routingForms.length).toBeGreaterThan(0); - expect(routingForms[0].name).toContain("Test Routing Form"); - }); - }); - - it("should filter team routing forms by disabled status", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/routing-forms?disabled=false`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - const responseBody: GetRoutingFormsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const routingForms = responseBody.data; - expect(routingForms).toBeDefined(); - expect(routingForms.length).toBeGreaterThan(0); - expect(routingForms[0].disabled).toEqual(false); - }); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms.controller.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms.controller.ts deleted file mode 100644 index 8d97528e870bc5..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms.controller.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { GetRoutingFormResponsesParams } from "@/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input"; -import { - GetRoutingFormsOutput, - RoutingFormOutput, -} from "@/modules/organizations/routing-forms/outputs/get-routing-forms.output"; -import { OrganizationsTeamsRoutingFormsService } from "@/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms.service"; -import { Controller, Get, Param, Query, UseGuards, ParseIntPipe } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; - -@Controller({ - path: "/v2/organizations/:orgId/teams/:teamId/routing-forms", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@ApiTags("Orgs / Teams / Routing forms") -@ApiHeader(API_KEY_HEADER) -export class OrganizationsTeamsRoutingFormsController { - constructor( - private readonly organizationsTeamsRoutingFormsService: OrganizationsTeamsRoutingFormsService - ) {} - - @Get() - @ApiOperation({ summary: "Get team routing forms" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - async getTeamRoutingForms( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Query() queryParams: GetRoutingFormResponsesParams - ): Promise { - const { skip, take, ...filters } = queryParams; - - const routingForms = await this.organizationsTeamsRoutingFormsService.getTeamRoutingForms( - teamId, - skip ?? 0, - take ?? 250, - { ...(filters ?? {}) } - ); - - return { - status: SUCCESS_STATUS, - data: routingForms.map((form) => plainToClass(RoutingFormOutput, form)), - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms.module.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms.module.ts deleted file mode 100644 index c1527ee6cd9e33..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms.module.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsRoutingFormsRepository } from "@/modules/organizations/routing-forms/organizations-routing-forms.repository"; -import { OrganizationsRoutingFormsResponsesService } from "@/modules/organizations/routing-forms/services/organizations-routing-forms-responses.service"; -import { SharedRoutingFormResponseService } from "@/modules/organizations/routing-forms/services/shared-routing-form-response.service"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { RoutingFormsModule } from "@/modules/routing-forms/routing-forms.module"; -import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; -import { Module } from "@nestjs/common"; - -import { OrganizationsTeamsRoutingFormsResponsesController } from "./controllers/organizations-teams-routing-forms-responses.controller"; -import { OrganizationsTeamsRoutingFormsController } from "./controllers/organizations-teams-routing-forms.controller"; -import { OrganizationsTeamsRoutingFormsResponsesRepository } from "./repositories/organizations-teams-routing-forms-responses.repository"; -import { OrganizationsTeamsRoutingFormsRepository } from "./repositories/organizations-teams-routing-forms.repository"; -import { OrganizationsTeamsRoutingFormsResponsesOutputService } from "./services/organizations-teams-routing-forms-responses-output.service"; -import { OrganizationsTeamsRoutingFormsResponsesService } from "./services/organizations-teams-routing-forms-responses.service"; -import { OrganizationsTeamsRoutingFormsService } from "./services/organizations-teams-routing-forms.service"; - -@Module({ - imports: [ - PrismaModule, - StripeModule, - RedisModule, - RoutingFormsModule, - SlotsModule_2024_09_04, - TeamsEventTypesModule, - ], - providers: [ - OrganizationsTeamsRoutingFormsService, - OrganizationsTeamsRoutingFormsResponsesService, - SharedRoutingFormResponseService, - OrganizationsTeamsRoutingFormsResponsesOutputService, - OrganizationsTeamsRoutingFormsResponsesRepository, - OrganizationsTeamsRoutingFormsRepository, - OrganizationsRepository, - OrganizationsTeamsRepository, - MembershipsRepository, - OrganizationsRoutingFormsResponsesService, - OrganizationsRoutingFormsRepository, - EventTypesRepository_2024_06_14, - ], - controllers: [OrganizationsTeamsRoutingFormsResponsesController, OrganizationsTeamsRoutingFormsController], -}) -export class OrganizationsTeamsRoutingFormsModule {} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output.ts deleted file mode 100644 index c32ebd9d0bb035..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { RoutingFormResponseOutput } from "@calcom/platform-types"; - -export class GetRoutingFormResponsesOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ type: [RoutingFormResponseOutput] }) - @Expose() - @Type(() => RoutingFormResponseOutput) - data!: RoutingFormResponseOutput[]; -} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/repositories/organizations-teams-routing-forms-responses.repository.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/repositories/organizations-teams-routing-forms-responses.repository.ts deleted file mode 100644 index ac288fa9032d64..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/repositories/organizations-teams-routing-forms-responses.repository.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrganizationsTeamsRoutingFormsResponsesRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async getTeamRoutingFormResponses( - teamId: number, - routingFormId: string, - skip: number, - take: number, - options?: { - sortCreatedAt?: "asc" | "desc"; - afterCreatedAt?: string; - beforeCreatedAt?: string; - routedToBookingUid?: string; - } - ) { - const { sortCreatedAt, afterCreatedAt, beforeCreatedAt, routedToBookingUid } = options || {}; - - return this.dbRead.prisma.app_RoutingForms_FormResponse.findMany({ - where: { - formId: routingFormId, - form: { - teamId, - }, - ...(afterCreatedAt && { createdAt: { gte: afterCreatedAt } }), - ...(beforeCreatedAt && { createdAt: { lte: beforeCreatedAt } }), - ...(routedToBookingUid && { routedToBookingUid }), - }, - orderBy: [...(sortCreatedAt ? [{ createdAt: sortCreatedAt }] : [])], - skip, - take, - }); - } - - async updateTeamRoutingFormResponse( - teamId: number, - routingFormId: string, - responseId: number, - data: { - response?: Record; - } - ) { - return this.dbWrite.prisma.app_RoutingForms_FormResponse.update({ - where: { - id: responseId, - formId: routingFormId, - form: { - teamId, - }, - }, - data: { - ...data, - }, - }); - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/repositories/organizations-teams-routing-forms.repository.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/repositories/organizations-teams-routing-forms.repository.ts deleted file mode 100644 index 1386173222534a..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/repositories/organizations-teams-routing-forms.repository.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrganizationsTeamsRoutingFormsRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async getTeamRoutingForms( - teamId: number, - skip: number, - take: number, - options?: { - disabled?: boolean; - name?: string; - sortCreatedAt?: "asc" | "desc"; - sortUpdatedAt?: "asc" | "desc"; - afterCreatedAt?: string; - beforeCreatedAt?: string; - afterUpdatedAt?: string; - beforeUpdatedAt?: string; - } - ) { - const { - disabled, - name, - sortCreatedAt, - sortUpdatedAt, - afterCreatedAt, - beforeCreatedAt, - afterUpdatedAt, - beforeUpdatedAt, - } = options || {}; - - return this.dbRead.prisma.app_RoutingForms_Form.findMany({ - where: { - teamId, - ...(disabled !== undefined && { disabled }), - ...(name && { name: { contains: name, mode: "insensitive" } }), - ...(afterCreatedAt && { createdAt: { gte: afterCreatedAt } }), - ...(beforeCreatedAt && { createdAt: { lte: beforeCreatedAt } }), - ...(afterUpdatedAt && { updatedAt: { gte: afterUpdatedAt } }), - ...(beforeUpdatedAt && { updatedAt: { lte: beforeUpdatedAt } }), - }, - orderBy: [ - ...(sortCreatedAt ? [{ createdAt: sortCreatedAt }] : []), - ...(sortUpdatedAt ? [{ updatedAt: sortUpdatedAt }] : []), - ], - skip, - take, - }); - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service.ts deleted file mode 100644 index ac1f87cd648c23..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { plainToClass } from "class-transformer"; - -import { RoutingFormResponseOutput, RoutingFormResponseResponseOutput } from "@calcom/platform-types"; -import type { App_RoutingForms_FormResponse } from "@calcom/prisma/client"; - -@Injectable() -export class OrganizationsTeamsRoutingFormsResponsesOutputService { - getRoutingFormResponses( - dbRoutingFormResponses: App_RoutingForms_FormResponse[] - ): RoutingFormResponseOutput[] { - return dbRoutingFormResponses.map((response) => { - const parsed = plainToClass(RoutingFormResponseOutput, response, { strategy: "excludeAll" }); - - // note(Lauris): I don't know why plainToClass(RoutingFormResponseOutput) - // erases nested "response" object so parsing and attaching it manually - const parsedResponse: Record = {}; - const responseData = response.response || {}; - for (const [key, value] of Object.entries(responseData)) { - parsedResponse[key] = plainToClass(RoutingFormResponseResponseOutput, value, { - strategy: "excludeAll", - }); - } - - return { - ...parsed, - response: parsedResponse, - }; - }); - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses.service.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses.service.ts deleted file mode 100644 index 2d9644ca348a1a..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses.service.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { CreateRoutingFormResponseInput } from "@/modules/organizations/routing-forms/inputs/create-routing-form-response.input"; -import { CreateRoutingFormResponseOutputData } from "@/modules/organizations/routing-forms/outputs/create-routing-form-response.output"; -import { SharedRoutingFormResponseService } from "@/modules/organizations/routing-forms/services/shared-routing-form-response.service"; -import { OrganizationsTeamsRoutingFormsResponsesOutputService } from "@/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service"; -import { Injectable } from "@nestjs/common"; -import { Request } from "express"; - -import { OrganizationsTeamsRoutingFormsResponsesRepository } from "../repositories/organizations-teams-routing-forms-responses.repository"; - -@Injectable() -export class OrganizationsTeamsRoutingFormsResponsesService { - constructor( - private readonly routingFormsRepository: OrganizationsTeamsRoutingFormsResponsesRepository, - private readonly routingFormsResponsesOutputService: OrganizationsTeamsRoutingFormsResponsesOutputService, - private readonly sharedRoutingFormResponseService: SharedRoutingFormResponseService - ) {} - - async getTeamRoutingFormResponses( - teamId: number, - routingFormId: string, - skip: number, - take: number, - options?: { - sortCreatedAt?: "asc" | "desc"; - sortUpdatedAt?: "asc" | "desc"; - afterCreatedAt?: string; - beforeCreatedAt?: string; - afterUpdatedAt?: string; - beforeUpdatedAt?: string; - routedToBookingUid?: string; - } - ) { - const responses = await this.routingFormsRepository.getTeamRoutingFormResponses( - teamId, - routingFormId, - skip, - take, - options - ); - - return this.routingFormsResponsesOutputService.getRoutingFormResponses(responses); - } - - async createRoutingFormResponseWithSlots( - routingFormId: string, - query: CreateRoutingFormResponseInput, - request: Request - ): Promise { - return this.sharedRoutingFormResponseService.createRoutingFormResponseWithSlots( - routingFormId, - query, - request - ); - } - - async updateTeamRoutingFormResponse( - teamId: number, - routingFormId: string, - responseId: number, - data: { - response?: Record; - } - ) { - const updatedResponse = await this.routingFormsRepository.updateTeamRoutingFormResponse( - teamId, - routingFormId, - responseId, - data - ); - - return this.routingFormsResponsesOutputService.getRoutingFormResponses([updatedResponse])[0]; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms.service.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms.service.ts deleted file mode 100644 index 920ec28946a86e..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Injectable } from "@nestjs/common"; - -import { OrganizationsTeamsRoutingFormsRepository } from "../repositories/organizations-teams-routing-forms.repository"; - -@Injectable() -export class OrganizationsTeamsRoutingFormsService { - constructor(private readonly routingFormsRepository: OrganizationsTeamsRoutingFormsRepository) {} - - async getTeamRoutingForms( - teamId: number, - skip: number, - take: number, - options?: { - disabled?: boolean; - name?: string; - sortCreatedAt?: "asc" | "desc"; - sortUpdatedAt?: "asc" | "desc"; - afterCreatedAt?: string; - beforeCreatedAt?: string; - afterUpdatedAt?: string; - beforeUpdatedAt?: string; - } - ) { - return this.routingFormsRepository.getTeamRoutingForms(teamId, skip, take, options); - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/schedules/inputs/teams-schedules.input.ts b/apps/api/v2/src/modules/organizations/teams/schedules/inputs/teams-schedules.input.ts deleted file mode 100644 index 9f5fde010ae7e3..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/schedules/inputs/teams-schedules.input.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { Transform } from "class-transformer"; -import { IsNumber, IsOptional } from "class-validator"; - -import { SkipTakePagination } from "@calcom/platform-types"; - -export class GetTeamSchedulesQuery extends SkipTakePagination { - @ApiPropertyOptional({ - description: "Filter schedules by event type ID", - example: 1, - }) - @Transform(({ value }: { value: string }) => value && parseInt(value)) - @IsNumber() - @IsOptional() - eventTypeId?: number; -} - -export class GetUserSchedulesQuery { - @ApiPropertyOptional({ - description: "Filter schedules by event type ID", - example: 1, - }) - @Transform(({ value }: { value: string }) => value && parseInt(value)) - @IsNumber() - @IsOptional() - eventTypeId?: number; -} diff --git a/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.controller.ts b/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.controller.ts deleted file mode 100644 index 83db1cf15a78ac..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.controller.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard"; -import { - GetTeamSchedulesQuery, - GetUserSchedulesQuery, -} from "@/modules/organizations/teams/schedules/inputs/teams-schedules.input"; -import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service"; -import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId/teams/:teamId", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, IsTeamInOrg, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -export class OrganizationsTeamsSchedulesController { - constructor( - private schedulesService: SchedulesService_2024_06_11, - - private teamsSchedulesService: TeamsSchedulesService - ) {} - - @UseGuards(IsTeamInOrg) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @Get("/schedules") - @DocsTags("Orgs / Teams / Schedules") - @ApiOperation({ summary: "Get all team member schedules" }) - async getTeamSchedules( - // note(Lauris): putting orgId so swagger is generated correctly - @Param("orgId", ParseIntPipe) _orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Query() queryParams: GetTeamSchedulesQuery - ): Promise { - const { skip, take, eventTypeId } = queryParams; - - const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take, eventTypeId); - - return { - status: SUCCESS_STATUS, - data: schedules, - }; - } - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsUserInOrgTeam) - @Get("/users/:userId/schedules") - @DocsTags("Orgs / Teams / Users / Schedules") - @ApiOperation({ - summary: "Get schedules of a team member", - }) - async getUserSchedules( - // note(Lauris): putting orgId and teamId so swagger is generated correctly - @Param("orgId", ParseIntPipe) _orgId: number, - @Param("teamId", ParseIntPipe) _teamId: number, - @Param("userId", ParseIntPipe) userId: number, - @Query() queryParams: GetUserSchedulesQuery - ): Promise { - const { eventTypeId } = queryParams; - const schedules = await this.schedulesService.getUserSchedules(userId, eventTypeId); - - return { - status: SUCCESS_STATUS, - data: schedules, - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.e2e-spec.ts deleted file mode 100644 index 121716afeac319..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.e2e-spec.ts +++ /dev/null @@ -1,512 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { - ApiSuccessResponse, - GetSchedulesOutput_2024_06_11, - ScheduleOutput_2024_06_11, -} from "@calcom/platform-types"; -import type { EventType, Schedule, 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 { 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 { 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 { 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 { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Teams Schedules Endpoints", () => { - describe("User Authentication - User is Org Admin", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let scheduleRepositoryFixture: SchedulesRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let hostsRepositoryFixture: HostsRepositoryFixture; - let prismaWriteService: PrismaWriteService; - - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let org: Team; - let orgTeam: Team; - let nonOrgTeam: Team; - - let userSchedule: Schedule; - let user2Schedule: Schedule; - let userEventTypeSchedule: Schedule; - let teamEventTypeSchedule: Schedule; - - let userEventType: EventType; - let teamEventType: EventType; - - const userEmail = `organizations-teams-schedules-admin-${randomString()}@api.com`; - const userEmail2 = `organizations-teams-schedules-member-${randomString()}@api.com`; - const nonOrgUserEmail = `organizations-teams-schedules-non-org-${randomString()}@api.com`; - const invitedUserEmail = `organizations-teams-schedules-invited-${randomString()}@api.com`; - - let user: User; - let user2: User; - let nonOrgUser: User; - - let userToInviteViaApi: User; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); - prismaWriteService = moduleRef.get(PrismaWriteService); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - user2 = await userRepositoryFixture.create({ - email: userEmail2, - username: userEmail2, - }); - - userSchedule = await scheduleRepositoryFixture.create({ - user: { - connect: { - id: user.id, - }, - }, - name: `organizations-teams-schedules-user-schedule-${randomString()}`, - timeZone: "America/New_York", - }); - - user2Schedule = await scheduleRepositoryFixture.create({ - user: { - connect: { - id: user2.id, - }, - }, - name: `organizations-teams-schedules-user2-schedule-${randomString()}`, - timeZone: "America/New_York", - }); - - userEventTypeSchedule = await scheduleRepositoryFixture.create({ - user: { - connect: { - id: user2.id, - }, - }, - name: `organizations-teams-schedules-user-event-type-schedule-${randomString()}`, - timeZone: "America/New_York", - }); - - teamEventTypeSchedule = await scheduleRepositoryFixture.create({ - user: { - connect: { - id: user2.id, - }, - }, - name: `organizations-teams-schedules-team-event-type-schedule-${randomString()}`, - timeZone: "America/New_York", - }); - - nonOrgUser = await userRepositoryFixture.create({ - email: nonOrgUserEmail, - username: nonOrgUserEmail, - }); - - userToInviteViaApi = await userRepositoryFixture.create({ - email: invitedUserEmail, - username: invitedUserEmail, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-teams-schedules-organization-${randomString()}`, - isOrganization: true, - }); - - orgTeam = await teamsRepositoryFixture.create({ - name: `organizations-teams-schedules-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - nonOrgTeam = await teamsRepositoryFixture.create({ - name: `organizations-teams-schedules-non-org-team-${randomString()}`, - isOrganization: false, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: orgTeam.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: user2.id } }, - team: { connect: { id: orgTeam.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: user2.id } }, - team: { connect: { id: org.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: userToInviteViaApi.id } }, - team: { connect: { id: org.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: nonOrgUser.id } }, - team: { connect: { id: nonOrgTeam.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: userEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user2.id}`, - username: userEmail2, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user2.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${userToInviteViaApi.id}`, - username: invitedUserEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: userToInviteViaApi.id, - }, - }, - }); - - userEventType = await eventTypesRepositoryFixture.create( - { - title: `User Event Type ${randomString()}`, - slug: `user-event-type-${randomString()}`, - length: 30, - schedule: { - connect: { - id: userEventTypeSchedule.id, - }, - }, - }, - user2.id - ); - - teamEventType = await eventTypesRepositoryFixture.createTeamEventType({ - title: `Team Event Type ${randomString()}`, - slug: `team-event-type-${randomString()}`, - length: 30, - team: { - connect: { - id: orgTeam.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - user: { - connect: { - id: user2.id, - }, - }, - eventType: { - connect: { - id: teamEventType.id, - }, - }, - schedule: { - connect: { - id: teamEventTypeSchedule.id, - }, - }, - isFixed: false, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should get all the schedule of the org's team's member", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.find((d) => d.id === user2Schedule.id)?.name).toEqual(user2Schedule.name); - }); - }); - - it("should get all the schedules of members in a team", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules`) - .expect(200) - .then((response) => { - const responseBody: GetSchedulesOutput_2024_06_11 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(Array.isArray(responseBody.data)).toBe(true); - - expect(responseBody.data.length).toBeGreaterThanOrEqual(2); - - const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id); - const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id); - - expect(userOneSchedule).toBeDefined(); - expect(userTwoSchedule).toBeDefined(); - - expect(userOneSchedule?.id).toEqual(userSchedule.id); - expect(userOneSchedule?.name).toEqual(userSchedule.name); - expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone); - - expect(userTwoSchedule?.id).toEqual(user2Schedule.id); - expect(userTwoSchedule?.name).toEqual(user2Schedule.name); - expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone); - }); - }); - - describe("eventTypeId query parameter", () => { - describe("GET /users/:userId/schedules", () => { - it("should filter user schedules by user-owned event type", async () => { - return request(app.getHttpServer()) - .get( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules?eventTypeId=${userEventType.id}` - ) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(Array.isArray(responseBody.data)).toBe(true); - expect(responseBody.data.length).toEqual(1); - expect(responseBody.data[0].id).toEqual(userEventTypeSchedule.id); - expect(responseBody.data[0].name).toEqual(userEventTypeSchedule.name); - }); - }); - - it("should filter user schedules by team event type where user is a host", async () => { - return request(app.getHttpServer()) - .get( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules?eventTypeId=${teamEventType.id}` - ) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(Array.isArray(responseBody.data)).toBe(true); - expect(responseBody.data.length).toEqual(1); - expect(responseBody.data[0].id).toEqual(teamEventTypeSchedule.id); - expect(responseBody.data[0].name).toEqual(teamEventTypeSchedule.name); - }); - }); - - it("should return 404 when event type does not exist", async () => { - return request(app.getHttpServer()) - .get( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules?eventTypeId=999999` - ) - .expect(404); - }); - - it("should return 404 when user is not associated with event type", async () => { - const otherUserEventType = await eventTypesRepositoryFixture.create( - { - title: `Other User Event Type ${randomString()}`, - slug: `other-user-event-type-${randomString()}`, - length: 30, - }, - user.id - ); - - try { - await request(app.getHttpServer()) - .get( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules?eventTypeId=${otherUserEventType.id}` - ) - .expect(404); - } finally { - await eventTypesRepositoryFixture.delete(otherUserEventType.id); - } - }); - - it("should return empty array when event type has no schedule and user has no default schedule", async () => { - const eventTypeWithoutSchedule = await eventTypesRepositoryFixture.create( - { - title: `Event Type Without Schedule ${randomString()}`, - slug: `event-type-no-schedule-${randomString()}`, - length: 30, - }, - user2.id - ); - - const user2Original = await prismaWriteService.prisma.user.findUnique({ - where: { id: user2.id }, - select: { defaultScheduleId: true }, - }); - const originalDefaultScheduleId = user2Original?.defaultScheduleId ?? null; - - await prismaWriteService.prisma.user.update({ - where: { id: user2.id }, - data: { defaultScheduleId: null }, - }); - - try { - const response = await request(app.getHttpServer()) - .get( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules?eventTypeId=${eventTypeWithoutSchedule.id}` - ) - .expect(200); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(Array.isArray(responseBody.data)).toBe(true); - expect(responseBody.data.length).toEqual(0); - } finally { - await prismaWriteService.prisma.user.update({ - where: { id: user2.id }, - data: { defaultScheduleId: originalDefaultScheduleId }, - }); - await eventTypesRepositoryFixture.delete(eventTypeWithoutSchedule.id); - } - }); - }); - - describe("GET /schedules (team schedules)", () => { - it("should filter team schedules by event type", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules?eventTypeId=${teamEventType.id}`) - .expect(200) - .then((response) => { - const responseBody: GetSchedulesOutput_2024_06_11 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(Array.isArray(responseBody.data)).toBe(true); - - expect(responseBody.data.length).toBeGreaterThanOrEqual(1); - const user2ScheduleFound = responseBody.data.find( - (schedule) => schedule.id === teamEventTypeSchedule.id - ); - expect(user2ScheduleFound).toBeDefined(); - expect(user2ScheduleFound?.id).toEqual(teamEventTypeSchedule.id); - }); - }); - - it("should return 404 when team event type does not exist", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules?eventTypeId=999999`) - .expect(404); - }); - - it("should return empty array when event type has no hosts with schedules", async () => { - const teamEventTypeNoHosts = await eventTypesRepositoryFixture.createTeamEventType({ - title: `Team Event Type No Hosts ${randomString()}`, - slug: `team-event-type-no-hosts-${randomString()}`, - length: 30, - team: { - connect: { - id: orgTeam.id, - }, - }, - }); - - try { - const response = await request(app.getHttpServer()) - .get( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules?eventTypeId=${teamEventTypeNoHosts.id}` - ) - .expect(200); - - const responseBody: GetSchedulesOutput_2024_06_11 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(Array.isArray(responseBody.data)).toBe(true); - expect(responseBody.data.length).toEqual(0); - } finally { - await eventTypesRepositoryFixture.delete(teamEventTypeNoHosts.id); - } - }); - }); - }); - - afterAll(async () => { - if (userEventType) { - await eventTypesRepositoryFixture.delete(userEventType.id); - } - if (teamEventType) { - await eventTypesRepositoryFixture.delete(teamEventType.id); - } - await userRepositoryFixture.deleteByEmail(user.email); - await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email); - await userRepositoryFixture.deleteByEmail(nonOrgUser.email); - await userRepositoryFixture.deleteByEmail(user2.email); - await organizationsRepositoryFixture.delete(org.id); - await teamsRepositoryFixture.delete(nonOrgTeam.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller.e2e-spec.ts deleted file mode 100644 index 030d976550c869..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller.e2e-spec.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { AttendeeVerifyEmail } from "@calcom/platform-libraries/emails"; -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 { TOTP as TOTPtoMock } from "@otplib/core"; -import { totp } from "otplib"; -import request from "supertest"; -import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { VerifiedResourcesRepositoryFixtures } from "test/fixtures/repository/verified-resources.repository.fixture"; -import { randomString } from "test/utils/randomString"; -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 { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input"; -import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input"; -import { - TeamVerifiedEmailOutput, - TeamVerifiedEmailsOutput, -} from "@/modules/verified-resources/outputs/verified-email.output"; -import { - TeamVerifiedPhoneOutput, - TeamVerifiedPhonesOutput, -} from "@/modules/verified-resources/outputs/verified-phone.output"; - -jest.spyOn(totp, "generate").mockImplementation(function () { - return "1234"; -}); - -jest.spyOn(TOTPtoMock.prototype, "check").mockImplementation(function () { - return true; -}); - -jest - .spyOn(AttendeeVerifyEmail.prototype as any, "getNodeMailerPayload") - .mockImplementation(async function () { - return { - to: `testnotrealemail@notreal.com`, - from: `testnotrealemail@notreal.com`, - subject: "No Subject", - html: "Mocked Email Content", - text: "body", - }; - }); - -describe("Organizations Teams Verified Resources", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let verifiedResourcesRepositoryFixtures: VerifiedResourcesRepositoryFixtures; - let teamsRepositoryFixture: TeamRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let org: Team; - let orgTeam: Team; - const emailToVerify = `org-team-e2e-verified-resources-${randomString()}@example.com`; - const phoneToVerify = "+37255556666"; - const authEmail = `organizations-verified-resources-responses-user-${randomString()}@api.com`; - let user: User; - let apiKeyString: string; - let verifiedEmailId: number; - let verifiedPhoneId: number; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); - - verifiedResourcesRepositoryFixtures = new VerifiedResourcesRepositoryFixtures(moduleRef); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-verified-resources-responses-organization-${randomString()}`, - isOrganization: true, - }); - - user = await userRepositoryFixture.create({ - email: authEmail, - username: authEmail, - }); - - const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); - apiKeyString = keyString; - - orgTeam = await teamsRepositoryFixture.create({ - name: `organizations-verified-resources-responses-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: orgTeam.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: authEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - - verifiedPhoneId = ( - await verifiedResourcesRepositoryFixtures.createPhone({ - phoneNumber: phoneToVerify, - user: { connect: { id: user.id } }, - team: { - connect: { - id: orgTeam.id, - }, - }, - }) - ).id; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should trigger email verification code", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails/verification-code/request` - ) - .send({ email: emailToVerify } satisfies RequestEmailVerificationInput) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200); - }); - - it("should verify email", async () => { - return request(app.getHttpServer()) - .post( - `/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails/verification-code/verify` - ) - .send({ email: emailToVerify, code: "1234" } satisfies VerifyEmailInput) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((res) => { - const response = res.body as TeamVerifiedEmailOutput; - verifiedEmailId = response.data.id; - expect(response.data.email).toEqual(emailToVerify); - expect(response.data.teamId).toEqual(orgTeam.id); - }); - }); - - it("should fetch verified email by id", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails/${verifiedEmailId}`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((res) => { - const response = res.body as TeamVerifiedEmailOutput; - expect(response.data.email).toEqual(emailToVerify); - expect(response.data.teamId).toEqual(orgTeam.id); - }); - }); - - it("should fetch verified number by id", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/phones/${verifiedPhoneId}`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((res) => { - const response = res.body as TeamVerifiedPhoneOutput; - expect(response.data.phoneNumber).toEqual(phoneToVerify); - expect(response.data.teamId).toEqual(orgTeam.id); - }); - }); - - it("should fetch verified emails", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((res) => { - const response = res.body as TeamVerifiedEmailsOutput; - expect(response.data.find((verifiedEmail) => verifiedEmail.email === emailToVerify)).toBeDefined(); - expect(response.data.find((verifiedEmail) => verifiedEmail.teamId === orgTeam.id)).toBeDefined(); - }); - }); - - it("should fetch verified phones", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/phones`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((res) => { - const response = res.body as TeamVerifiedPhonesOutput; - expect( - response.data.find((verifiedPhone) => verifiedPhone.phoneNumber === phoneToVerify) - ).toBeDefined(); - expect(response.data.find((verifiedPhone) => verifiedPhone.teamId === orgTeam.id)).toBeDefined(); - }); - }); - - afterAll(async () => { - await verifiedResourcesRepositoryFixtures.deleteEmailById(verifiedEmailId); - await verifiedResourcesRepositoryFixtures.deletePhoneById(verifiedPhoneId); - await userRepositoryFixture.deleteByEmail(user.email); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller.ts b/apps/api/v2/src/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller.ts deleted file mode 100644 index 2a507ea0adacd4..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; -import { Throttle } from "@/lib/endpoint-throttler-decorator"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input"; -import { RequestPhoneVerificationInput } from "@/modules/verified-resources/inputs/request-phone-verification.input"; -import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input"; -import { VerifyPhoneInput } from "@/modules/verified-resources/inputs/verify-phone.input"; -import { RequestEmailVerificationOutput } from "@/modules/verified-resources/outputs/request-email-verification-output"; -import { RequestPhoneVerificationOutput } from "@/modules/verified-resources/outputs/request-phone-verification-output"; -import { - TeamVerifiedEmailOutput, - TeamVerifiedEmailOutputData, - TeamVerifiedEmailsOutput, -} from "@/modules/verified-resources/outputs/verified-email.output"; -import { - TeamVerifiedPhoneOutput, - TeamVerifiedPhoneOutputData, - TeamVerifiedPhonesOutput, -} from "@/modules/verified-resources/outputs/verified-phone.output"; -import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service"; -import { - Body, - Controller, - Get, - HttpCode, - HttpStatus, - Param, - ParseIntPipe, - Post, - Query, - UseGuards, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import { SkipTakePagination } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId/teams/:teamId/verified-resources", -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@ApiTags("Organization Team Verified Resources") -@ApiParam({ name: "orgId", type: Number, required: true }) -@ApiParam({ name: "teamId", type: Number, required: true }) -export class OrgTeamsVerifiedResourcesController { - constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {} - @ApiOperation({ - summary: "Request email verification code", - description: `Sends a verification code to the email`, - }) - @Roles("TEAM_ADMIN") - @Throttle({ - limit: 3, - ttl: 60000, - blockDuration: 60000, - name: "org_teams_verified_resources_emails_requests", - }) - @PlatformPlan("ESSENTIALS") - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Post("/emails/verification-code/request") - @HttpCode(HttpStatus.OK) - async requestEmailVerificationCode( - @Body() body: RequestEmailVerificationInput, - @GetUser("username") username: string, - @GetUser("locale") locale: string - ): Promise { - const verificationCodeRequest = await this.verifiedResourcesService.requestEmailVerificationCode( - { username, locale }, - body.email - ); - - return { - status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS, - }; - } - - @ApiOperation({ - summary: "Request phone number verification code", - description: `Sends a verification code to the phone number`, - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Roles("TEAM_ADMIN") - @Throttle({ - limit: 3, - ttl: 60000, - blockDuration: 60000, - name: "org_teams_verified_resources_phones_requests", - }) - @Post("/phones/verification-code/request") - @HttpCode(HttpStatus.OK) - async requestPhoneVerificationCode( - @Body() body: RequestPhoneVerificationInput - ): Promise { - const verificationCodeRequest = await this.verifiedResourcesService.requestPhoneVerificationCode( - body.phone - ); - - return { - status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS, - }; - } - - @ApiOperation({ - summary: "Verify an email for an org team", - description: `Use code to verify an email`, - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @Post("/emails/verification-code/verify") - @HttpCode(HttpStatus.OK) - async verifyEmail( - @Body() body: VerifyEmailInput, - @GetUser("id") userId: number, - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - const verifiedEmail = await this.verifiedResourcesService.verifyEmail( - userId, - body.email, - body.code, - teamId - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail), - }; - } - - @ApiOperation({ - summary: "Verify a phone number for an org team", - description: `Use code to verify a phone number`, - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @Post("/phones/verification-code/verify") - @Throttle({ - limit: 3, - ttl: 60000, - blockDuration: 60000, - name: "org_teams_verified_resources_phones_verify", - }) - @HttpCode(HttpStatus.OK) - async verifyPhoneNumber( - @Body() body: VerifyPhoneInput, - @GetUser("id") userId: number, - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - const verifiedPhone = await this.verifiedResourcesService.verifyPhone( - userId, - body.phone, - body.code, - teamId - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhone), - }; - } - - @ApiOperation({ - summary: "Get list of verified emails of an org team", - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @Get("/emails") - @HttpCode(HttpStatus.OK) - async getVerifiedEmails( - @Param("teamId", ParseIntPipe) teamId: number, - @Query() pagination: SkipTakePagination - ): Promise { - const verifiedEmails = await this.verifiedResourcesService.getTeamVerifiedEmails( - teamId, - pagination?.skip ?? 0, - pagination?.take ?? 250 - ); - return { - status: SUCCESS_STATUS, - data: verifiedEmails.map((verifiedEmail) => plainToClass(TeamVerifiedEmailOutputData, verifiedEmail)), - }; - } - - @ApiOperation({ - summary: "Get list of verified phone numbers of an org team", - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @PlatformPlan("ESSENTIALS") - @Get("/phones") - @Roles("TEAM_ADMIN") - @HttpCode(HttpStatus.OK) - async getVerifiedPhoneNumbers( - @Param("teamId", ParseIntPipe) teamId: number, - @Query() pagination: SkipTakePagination - ): Promise { - const verifiedPhoneNumbers = await this.verifiedResourcesService.getTeamVerifiedPhoneNumbers( - teamId, - pagination?.skip ?? 0, - pagination?.take ?? 250 - ); - return { - status: SUCCESS_STATUS, - data: verifiedPhoneNumbers.map((verifiedPhoneNumber) => - plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber) - ), - }; - } - - @ApiOperation({ - summary: "Get verified email of an org team by id", - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @Get("/emails/:id") - @HttpCode(HttpStatus.OK) - async getVerifiedEmailById( - @Param("id") id: number, - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - const verifiedEmail = await this.verifiedResourcesService.getTeamVerifiedEmailById(teamId, id); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail), - }; - } - - @ApiOperation({ - summary: "Get verified phone number of an org team by id", - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @Get("/phones/:id") - @HttpCode(HttpStatus.OK) - async getVerifiedPhoneById( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("id") id: number - ): Promise { - const verifiedPhoneNumber = await this.verifiedResourcesService.getTeamVerifiedPhoneNumberById( - teamId, - id - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber), - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.e2e-spec.ts deleted file mode 100644 index 2b17a6fefa8961..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.e2e-spec.ts +++ /dev/null @@ -1,1005 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { VerifiedResourcesRepositoryFixtures } from "test/fixtures/repository/verified-resources.repository.fixture"; -import { WorkflowRepositoryFixture } from "test/fixtures/repository/workflow.repository.fixture"; -import { randomString } from "test/utils/randomString"; -// Assuming this is your main app bootstrapper -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 { - CreateEventTypeWorkflowDto, - WorkflowActivationDto, -} from "@/modules/workflows/inputs/create-event-type-workflow.input"; -import { - CreateFormWorkflowDto, - WorkflowFormActivationDto, -} from "@/modules/workflows/inputs/create-form-workflow"; -import { - ATTENDEE, - EMAIL, - PHONE_NUMBER, - REMINDER, - UpdateEmailAddressWorkflowStepDto, - UpdatePhoneWhatsAppNumberWorkflowStepDto, - WorkflowEmailAddressStepDto, - WorkflowEmailAttendeeStepDto, -} from "@/modules/workflows/inputs/workflow-step.input"; -import { - AFTER_EVENT, - BEFORE_EVENT, - DAY, - FORM_SUBMITTED, - FORM_SUBMITTED_NO_EVENT, - OnAfterEventTriggerDto, - OnBeforeEventTriggerDto, - OnFormSubmittedNoEventTriggerDto, - OnFormSubmittedTriggerDto, -} from "@/modules/workflows/inputs/workflow-trigger.input"; -import { - GetEventTypeWorkflowOutput, - GetEventTypeWorkflowsOutput, -} from "@/modules/workflows/outputs/event-type-workflow.output"; -// Adjust path if needed -import { - GetRoutingFormWorkflowOutput, - GetRoutingFormWorkflowsOutput, -} from "@/modules/workflows/outputs/routing-form-workflow.output"; - -describe("OrganizationsTeamsWorkflowsController (E2E)", () => { - let app: INestApplication; - let verifiedResourcesRepositoryFixtures: VerifiedResourcesRepositoryFixtures; - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let workflowsRepositoryFixture: WorkflowRepositoryFixture; - let basePath = ""; - let org: Team; - let orgTeam: Team; - let createdWorkflowId: number; - let createdFormWorkflowId: number; - const authEmail = `org-teams-workflows-user-${randomString()}@example.com`; - let user: User; - let apiKeyString: string; - let verifiedPhoneId: number; - let verifiedPhoneId2: number; - let verifiedEmailId: number; - let verifiedEmailId2: number; - let createdWorkflow: GetEventTypeWorkflowOutput["data"]; - let createdFormWorkflow: GetRoutingFormWorkflowOutput["data"]; - - const emailToVerify = `org-teams-workflows-team-${randomString()}@example.com`; - const phoneToVerify = `+37255556666`; - - let sampleCreateEventTypeWorkflowDto = { - name: `E2E Test Workflow ${randomString()}`, - activation: { - isActiveOnAllEventTypes: true, - activeOnEventTypeIds: [], - }, - trigger: { - type: BEFORE_EVENT, - offset: { - value: 1, - unit: DAY, - }, - }, - steps: [], - } as unknown as CreateEventTypeWorkflowDto; - - let sampleCreateWorkflowRoutingFormDto: CreateFormWorkflowDto = { - name: `E2E Test Workflow ${randomString()}`, - activation: { - activeOnRoutingFormIds: [], - isActiveOnAllRoutingForms: true, - }, - trigger: { - type: FORM_SUBMITTED, - }, - steps: [], - }; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); - - // Instantiate Fixtures - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - workflowsRepositoryFixture = new WorkflowRepositoryFixture(moduleRef); - verifiedResourcesRepositoryFixtures = new VerifiedResourcesRepositoryFixtures(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `org-teams-workflows-org-${randomString()}`, - slug: `org-teams-workflows-org-${randomString()}`, - isOrganization: true, - platformBilling: { - create: { - customerId: "cus_999", - plan: "SCALE", - subscriptionId: "sub_999", - }, - }, - }); - - user = await userRepositoryFixture.create({ - email: authEmail, - username: authEmail.split("@")[0], - }); - - const apiKey = await apiKeysRepositoryFixture.createApiKey(user.id, null); - apiKeyString = apiKey.keyString; - - orgTeam = await teamsRepositoryFixture.create({ - name: `org-teams-workflows-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - accepted: true, - team: { connect: { id: org.id } }, - }); - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - accepted: true, - team: { connect: { id: orgTeam.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: authEmail.split("@")[0], - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - - const verifiedPhone = await verifiedResourcesRepositoryFixtures.createPhone({ - user: { connect: { id: user.id } }, - phoneNumber: "+37255555555", - team: { connect: { id: orgTeam.id } }, - }); - const verifiedPhone2 = await verifiedResourcesRepositoryFixtures.createPhone({ - user: { connect: { id: user.id } }, - phoneNumber: phoneToVerify, - team: { connect: { id: orgTeam.id } }, - }); - const verifiedEmail = await verifiedResourcesRepositoryFixtures.createEmail({ - user: { connect: { id: user.id } }, - email: authEmail, - team: { connect: { id: orgTeam.id } }, - }); - - const verifiedEmail2 = await verifiedResourcesRepositoryFixtures.createEmail({ - user: { connect: { id: user.id } }, - email: emailToVerify, - team: { connect: { id: orgTeam.id } }, - }); - verifiedEmailId = verifiedEmail.id; - verifiedEmailId2 = verifiedEmail2.id; - - verifiedPhoneId = verifiedPhone.id; - verifiedPhoneId2 = verifiedPhone2.id; - - sampleCreateEventTypeWorkflowDto = { - name: `E2E Test Workflow ${randomString()}`, - activation: { - isActiveOnAllEventTypes: true, - activeOnEventTypeIds: [], - }, - trigger: { - type: BEFORE_EVENT, - offset: { - value: 1, - unit: DAY, - }, - }, - steps: [ - { - stepNumber: 1, - action: "email_attendee", - recipient: ATTENDEE, - template: REMINDER, - sender: "CalcomE2EStep1", - includeCalendarEvent: true, - message: { - subject: "Upcoming: {EVENT_NAME}", - html: "

Reminder for your event {EVENT_NAME}.

", - }, - }, - { - stepNumber: 2, - action: "sms_number", - recipient: PHONE_NUMBER, - template: REMINDER, - verifiedPhoneId: verifiedPhoneId, - sender: "CalcomE2EStep2", - message: { - subject: "Upcoming: {EVENT_NAME}", - text: "Reminder for your event {EVENT_NAME}.", - }, - }, - { - stepNumber: 3, - action: "email_address", - recipient: EMAIL, - template: REMINDER, - verifiedEmailId: verifiedEmailId, - sender: "CalcomE2EStep3", - includeCalendarEvent: true, - message: { - subject: "Upcoming: {EVENT_NAME}", - html: "

Reminder for your event {EVENT_NAME}.

", - }, - }, - { - stepNumber: 4, - action: "sms_attendee", - recipient: PHONE_NUMBER, - template: REMINDER, - phoneRequired: true, - sender: "CalcomE2EStep4", - message: { - subject: "Upcoming: {EVENT_NAME}", - text: "Reminder for your event {EVENT_NAME}.", - }, - }, - ], - }; - - sampleCreateWorkflowRoutingFormDto = { - name: `E2E Test Form Workflow ${randomString()}`, - activation: { - isActiveOnAllRoutingForms: true, - activeOnRoutingFormIds: [], - }, - trigger: { - type: FORM_SUBMITTED, - }, - steps: [ - { - stepNumber: 1, - action: "email_attendee", - recipient: ATTENDEE, - template: REMINDER, - sender: "CalcomE2EStep1", - includeCalendarEvent: true, - message: { - subject: "Upcoming: {EVENT_NAME}", - html: "

Reminder for your event {EVENT_NAME}.

", - }, - }, - ], - }; - basePath = `/v2/organizations/${org.id}/teams/${orgTeam.id}/workflows`; - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - afterAll(async () => { - if (createdWorkflowId) { - try { - await workflowsRepositoryFixture.delete(createdWorkflowId); - } catch { - /* empty */ - } - } - - await userRepositoryFixture.deleteByEmail(user.email); - await teamsRepositoryFixture.delete(orgTeam.id); - await organizationsRepositoryFixture.delete(org.id); - - await app.close(); - }); - - describe(`POST ${basePath}`, () => { - it("should create a new workflow", async () => { - return request(app.getHttpServer()) - .post(basePath) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send(sampleCreateEventTypeWorkflowDto) - .expect(201) - .then((response) => { - const responseBody: GetEventTypeWorkflowOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.activation).toBeDefined(); - - expect(responseBody.data.name).toEqual(sampleCreateEventTypeWorkflowDto.name); - if (responseBody.data.activation instanceof WorkflowActivationDto) { - expect(responseBody.data.activation.isActiveOnAllEventTypes).toEqual( - sampleCreateEventTypeWorkflowDto.activation.isActiveOnAllEventTypes - ); - } - if ( - responseBody.data.activation instanceof WorkflowFormActivationDto && - sampleCreateEventTypeWorkflowDto.activation instanceof WorkflowFormActivationDto - ) { - expect(responseBody.data.activation.isActiveOnAllRoutingForms).toEqual( - sampleCreateEventTypeWorkflowDto.activation.isActiveOnAllRoutingForms - ); - } - - expect(responseBody.data.trigger.type).toEqual(sampleCreateEventTypeWorkflowDto.trigger.type); - expect(responseBody.data.steps).toHaveLength(sampleCreateEventTypeWorkflowDto.steps.length); - expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.id).toBeDefined(); - expect(responseBody.data.steps.find((step) => step.stepNumber === 2)?.id).toBeDefined(); - expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.id).toBeDefined(); - expect(responseBody.data.steps.find((step) => step.stepNumber === 4)?.id).toBeDefined(); - - expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.sender).toEqual( - "CalcomE2EStep1" - ); - expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.includeCalendarEvent).toEqual( - true - ); - expect(responseBody.data.steps.find((step) => step.stepNumber === 2)?.sender).toEqual( - "CalcomE2EStep2" - ); - expect(responseBody.data.steps.find((step) => step.stepNumber === 2)?.phone).toEqual( - "+37255555555" - ); - expect(responseBody.data.steps.find((step) => step.stepNumber === 4)?.phoneRequired).toEqual(true); - - expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail); - const trigger = sampleCreateEventTypeWorkflowDto.trigger as OnBeforeEventTriggerDto; - expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value); - expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit); - - createdWorkflowId = responseBody.data.id; - createdWorkflow = responseBody.data; - expect(responseBody.data.type).toEqual("event-type"); - }); - }); - - it("should not create a new routing form workflow with trigger not FORM_SUBMITTED", async () => { - const invalidWorkflow = structuredClone( - sampleCreateWorkflowRoutingFormDto - ) as unknown as CreateEventTypeWorkflowDto; - invalidWorkflow.trigger.type = AFTER_EVENT; - return request(app.getHttpServer()) - .post(`${basePath}/routing-form`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send(invalidWorkflow) - .expect(400); - }); - - it("should not create a new routing form workflow with not allowed actions", async () => { - // force impossible step to test validation, should fail with 400 - const invalidWorkflow = structuredClone( - sampleCreateWorkflowRoutingFormDto - ) as unknown as CreateEventTypeWorkflowDto; - invalidWorkflow.steps = [ - { - stepNumber: 1, - action: "cal_ai_phone_call", - recipient: PHONE_NUMBER, - template: REMINDER, - verifiedPhoneId: verifiedPhoneId, - sender: "CalcomE2EStep2", - message: { - subject: "Upcoming: {EVENT_NAME}", - text: "Reminder for your event {EVENT_NAME}.", - }, - } as unknown as WorkflowEmailAddressStepDto, - ]; - return request(app.getHttpServer()) - .post(`${basePath}/routing-form`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send(invalidWorkflow) - .expect(400); - }); - - it("should create a new routing form workflow with allowed actions", async () => { - const validWorkflow = structuredClone( - sampleCreateWorkflowRoutingFormDto - ) as unknown as CreateEventTypeWorkflowDto; - validWorkflow.steps = [ - { - stepNumber: 1, - action: "email_attendee", - recipient: ATTENDEE, - template: REMINDER, - sender: "CalcomE2EStep1", - includeCalendarEvent: true, - message: { - subject: "Upcoming: {EVENT_NAME}", - html: "

Reminder for your event {EVENT_NAME}.

", - }, - }, - { - stepNumber: 2, - action: "sms_attendee", - recipient: EMAIL, - template: REMINDER, - phoneRequired: false, - sender: "updatedSender", - message: { - subject: "Update Upcoming: {EVENT_NAME}", - text: "Update Reminder for your event {EVENT_NAME}.

", - }, - }, - ]; - return request(app.getHttpServer()) - .post(`${basePath}/routing-form`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send(validWorkflow) - .expect(201) - .then((response) => { - const responseBody: GetRoutingFormWorkflowOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.name).toEqual(sampleCreateWorkflowRoutingFormDto.name); - expect(responseBody.data.type).toEqual("routing-form"); - - if (responseBody.data.activation instanceof WorkflowFormActivationDto) { - expect(responseBody.data.activation.isActiveOnAllRoutingForms).toEqual( - sampleCreateWorkflowRoutingFormDto.activation.isActiveOnAllRoutingForms - ); - } - - expect(responseBody.data.trigger.type).toEqual(sampleCreateWorkflowRoutingFormDto.trigger.type); - expect(responseBody.data.steps).toHaveLength(validWorkflow.steps.length); - expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.id).toBeDefined(); - expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.sender).toEqual( - "CalcomE2EStep1" - ); - - expect(responseBody.data.steps.find((step) => step.stepNumber === 2)?.id).toBeDefined(); - expect(responseBody.data.steps.find((step) => step.action === "sms_attendee")).toBeDefined(); - - const trigger = sampleCreateWorkflowRoutingFormDto.trigger as OnFormSubmittedTriggerDto; - expect(responseBody.data.trigger?.type).toEqual(trigger.type); - - createdFormWorkflowId = responseBody.data.id; - createdFormWorkflow = responseBody.data; - }); - }); - - it("should create a new routing form workflow with allowed actions and offset trigger", async () => { - const validWorkflow = structuredClone( - sampleCreateWorkflowRoutingFormDto - ) as unknown as CreateFormWorkflowDto; - validWorkflow.steps = [ - { - stepNumber: 1, - action: "email_attendee", - recipient: ATTENDEE, - template: REMINDER, - sender: "CalcomE2EStep1", - includeCalendarEvent: true, - message: { - subject: "Upcoming: {EVENT_NAME}", - html: "

Reminder for your event {EVENT_NAME}.

", - }, - }, - ]; - - validWorkflow.trigger = { - type: FORM_SUBMITTED_NO_EVENT, - offset: { - value: 1, - unit: DAY, - }, - }; - return request(app.getHttpServer()) - .post(`${basePath}/routing-form`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send(validWorkflow) - .expect(201) - .then((response) => { - const responseBody: GetRoutingFormWorkflowOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.name).toEqual(sampleCreateWorkflowRoutingFormDto.name); - expect(responseBody.data.type).toEqual("routing-form"); - - if (responseBody.data.activation instanceof WorkflowFormActivationDto) { - expect(responseBody.data.activation.isActiveOnAllRoutingForms).toEqual( - sampleCreateWorkflowRoutingFormDto.activation.isActiveOnAllRoutingForms - ); - } - - expect(responseBody.data.trigger.type).toEqual(validWorkflow.trigger.type); - expect(responseBody.data.steps).toHaveLength(sampleCreateWorkflowRoutingFormDto.steps.length); - expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.id).toBeDefined(); - expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.sender).toEqual( - "CalcomE2EStep1" - ); - - const trigger = validWorkflow.trigger as OnFormSubmittedNoEventTriggerDto; - expect(responseBody.data.trigger?.type).toEqual(trigger.type); - expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit); - expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value); - }); - }); - - it("should create a new workflow", async () => { - return request(app.getHttpServer()) - .post(basePath) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ ...sampleCreateEventTypeWorkflowDto, type: undefined }) - .expect(201) - .then((response) => { - const responseBody: GetEventTypeWorkflowOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.name).toEqual(sampleCreateEventTypeWorkflowDto.name); - if (responseBody.data.activation instanceof WorkflowActivationDto) { - expect(responseBody.data.activation.isActiveOnAllEventTypes).toEqual( - sampleCreateEventTypeWorkflowDto.activation.isActiveOnAllEventTypes - ); - } - - expect(responseBody.data.trigger.type).toEqual(sampleCreateEventTypeWorkflowDto.trigger.type); - expect(responseBody.data.steps).toHaveLength(sampleCreateEventTypeWorkflowDto.steps.length); - expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.id).toBeDefined(); - expect(responseBody.data.steps.find((step) => step.stepNumber === 2)?.id).toBeDefined(); - expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.id).toBeDefined(); - expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.sender).toEqual( - "CalcomE2EStep1" - ); - expect(responseBody.data.steps.find((step) => step.stepNumber === 2)?.sender).toEqual( - "CalcomE2EStep2" - ); - expect(responseBody.data.steps.find((step) => step.stepNumber === 2)?.phone).toEqual( - "+37255555555" - ); - expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail); - const trigger = sampleCreateEventTypeWorkflowDto.trigger as OnBeforeEventTriggerDto; - expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value); - expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit); - - createdWorkflowId = responseBody.data.id; - createdWorkflow = responseBody.data; - }); - }); - - it("should not create a new workflow", async () => { - return request(app.getHttpServer()) - .post(basePath) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - ...sampleCreateEventTypeWorkflowDto, - trigger: { ...sampleCreateEventTypeWorkflowDto, type: "formSubmitted" }, - }) - .expect(400); - }); - - it("should return 401 if not authenticated", async () => { - return request(app.getHttpServer()).post(basePath).send(sampleCreateEventTypeWorkflowDto).expect(401); - }); - - it("should return 400 for invalid data (e.g. missing name)", async () => { - const invalidDto = { ...sampleCreateEventTypeWorkflowDto, name: undefined }; - return request(app.getHttpServer()) - .post(basePath) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send(invalidDto) - .expect(400); - }); - }); - - describe(`GET ${basePath}`, () => { - it("should get a list of event-type workflows for the team", async () => { - return request(app.getHttpServer()) - .get(`${basePath}?skip=0&take=10`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - const responseBody: GetEventTypeWorkflowsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeInstanceOf(Array); - expect(responseBody.data.length).toBeGreaterThanOrEqual(1); - expect(responseBody.data.some((wf) => wf.id === createdWorkflowId)).toBe(true); - expect(responseBody.data.every((wf) => wf.type === "event-type")).toBe(true); - }); - }); - - it("should get a list of routing-form workflows for the team", async () => { - return request(app.getHttpServer()) - .get(`${basePath}/routing-form?skip=0&take=10`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - const responseBody: GetRoutingFormWorkflowsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeInstanceOf(Array); - expect(responseBody.data.length).toBeGreaterThanOrEqual(1); - expect(responseBody.data.some((wf) => wf.id === createdFormWorkflowId)).toBe(true); - expect(responseBody.data.every((wf) => wf.type === "routing-form")).toBe(true); - }); - }); - - it("should return 401 if not authenticated", async () => { - return request(app.getHttpServer()).get(basePath).expect(401); - }); - }); - - describe(`GET ${basePath}/:workflowId`, () => { - it("should get a specific workflow by ID", async () => { - expect(createdWorkflowId).toBeDefined(); - return request(app.getHttpServer()) - .get(`${basePath}/${createdWorkflowId}`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - const responseBody: GetEventTypeWorkflowOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.id).toEqual(createdWorkflowId); - expect(responseBody.data.type).toEqual("event-type"); - }); - }); - - it("should get a specific routing-form workflow by ID", async () => { - expect(createdWorkflowId).toBeDefined(); - return request(app.getHttpServer()) - .get(`${basePath}/${createdFormWorkflowId}/routing-form`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - const responseBody: GetEventTypeWorkflowOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.id).toEqual(createdFormWorkflowId); - expect(responseBody.data.type).toEqual("routing-form"); - }); - }); - - it("should return 404 for a non-existent workflow ID", async () => { - return request(app.getHttpServer()) - .get(`${basePath}/999999`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(404); - }); - - it("should return 401 if not authenticated", async () => { - expect(createdWorkflowId).toBeDefined(); - return request(app.getHttpServer()).get(`${basePath}/${createdWorkflowId}`).expect(401); - }); - }); - - describe(`PATCH ${basePath}/:workflowId`, () => { - const updatedName = `Updated Workflow Name ${randomString()}`; - - it("should update an existing workflow, update the first and second step and discard other steps", async () => { - const step2 = createdWorkflow.steps.find((step) => step.stepNumber === 2); - expect(step2).toBeDefined(); - const step3 = createdWorkflow.steps.find((step) => step.stepNumber === 3); - expect(step3).toBeDefined(); - const partialUpdateDto: Partial = { - name: updatedName, - trigger: { - type: "afterEvent", - offset: { - unit: "minute", - value: 10, - }, - }, - steps: - step3 && step2 - ? [ - { - stepNumber: 1, - id: step3.id, - action: "email_address", - recipient: EMAIL, - template: REMINDER, - verifiedEmailId: verifiedEmailId2, - sender: "updatedSender", - includeCalendarEvent: false, - message: { - subject: "Update Upcoming: {EVENT_NAME}", - html: "

Update Reminder for your event {EVENT_NAME}.

", - }, - } as UpdateEmailAddressWorkflowStepDto, - { - stepNumber: 2, - id: step2.id, - action: "whatsapp_number", - recipient: PHONE_NUMBER, - template: REMINDER, - verifiedPhoneId: verifiedPhoneId2, - sender: "updatedSender", - message: { - subject: "Update Upcoming: {EVENT_NAME}", - text: "Update Reminder for your event {EVENT_NAME}.", - }, - } as UpdatePhoneWhatsAppNumberWorkflowStepDto, - ] - : [], - }; - expect(createdWorkflowId).toBeDefined(); - expect(createdWorkflow).toBeDefined(); - return request(app.getHttpServer()) - .patch(`${basePath}/${createdWorkflowId}`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send(partialUpdateDto) - .expect(200) - .then((response) => { - const responseBody: GetEventTypeWorkflowOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.id).toEqual(createdWorkflowId); - expect(responseBody.data.name).toEqual(updatedName); - if (step3) { - const newStep3 = responseBody.data.steps.find((step) => step.id === step3.id); - expect(newStep3).toBeDefined(); - if (newStep3) { - expect(newStep3.sender).toEqual("updatedSender"); - expect(newStep3.email).toEqual(emailToVerify); - expect(newStep3.includeCalendarEvent).toEqual(false); - } - } - if (step2) { - const newStep2 = responseBody.data.steps.find((step) => step.id === step2.id); - expect(newStep2).toBeDefined(); - if (newStep2) { - expect(responseBody.data.steps[1].sender).toEqual("updatedSender"); - expect(responseBody.data.steps[1].phone).toEqual(phoneToVerify); - } - } - - // we updated 2 steps, third one should have been discarded - expect(responseBody.data.steps[2]?.id).toBeUndefined(); - - const trigger = partialUpdateDto.trigger as OnAfterEventTriggerDto; - expect(responseBody.data.trigger?.type).toEqual(trigger.type); - expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value); - expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit); - }); - }); - - it("should not update an existing event-type workflow, trying to use form workflow trigger", async () => { - const step2 = createdWorkflow.steps.find((step) => step.stepNumber === 2); - expect(step2).toBeDefined(); - const step3 = createdWorkflow.steps.find((step) => step.stepNumber === 3); - expect(step3).toBeDefined(); - const partialUpdateDto = { - name: updatedName, - trigger: { - type: "formSubmitted", - offset: { - unit: "minute", - value: 10, - }, - }, - }; - - expect(createdWorkflowId).toBeDefined(); - expect(createdWorkflow).toBeDefined(); - return request(app.getHttpServer()) - .patch(`${basePath}/${createdWorkflowId}`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send(partialUpdateDto) - .expect(400); - }); - - it("should not update an existing event-type workflow, trying to use routing-form workflow endpoint", async () => { - const partialUpdateDto = { - name: updatedName, - }; - - expect(createdWorkflowId).toBeDefined(); - expect(createdWorkflow).toBeDefined(); - return request(app.getHttpServer()) - .patch(`${basePath}/${createdWorkflowId}/routing-form`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send(partialUpdateDto) - .expect(404); - }); - - it("should update an existing routing form workflow, update the first step and discard any other steps", async () => { - const step1 = createdFormWorkflow.steps.find((step) => step.stepNumber === 1); - expect(step1).toBeDefined(); - - const partialUpdateDto: Partial = { - name: updatedName, - trigger: { - type: "formSubmitted", - }, - steps: step1 - ? [ - { - stepNumber: 1, - id: step1.id, - action: "email_address", - recipient: EMAIL, - template: REMINDER, - verifiedEmailId: verifiedEmailId2, - sender: "updatedSender", - includeCalendarEvent: true, - message: { - subject: "Update Upcoming: {EVENT_NAME}", - html: "

Update Reminder for your event {EVENT_NAME}.

", - }, - } as UpdateEmailAddressWorkflowStepDto, - ] - : [], - }; - expect(createdFormWorkflowId).toBeDefined(); - expect(createdFormWorkflow).toBeDefined(); - return request(app.getHttpServer()) - .patch(`${basePath}/${createdFormWorkflowId}/routing-form`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send(partialUpdateDto) - .expect(200) - .then((response) => { - const responseBody: GetRoutingFormWorkflowOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.id).toEqual(createdFormWorkflowId); - expect(responseBody.data.name).toEqual(updatedName); - expect(responseBody.data.activation).toBeDefined(); - if (step1) { - const newStep1 = responseBody.data.steps.find((step) => step.id === step1.id); - expect(newStep1).toBeDefined(); - if (newStep1) { - expect(newStep1.sender).toEqual("updatedSender"); - expect(newStep1.email).toEqual(emailToVerify); - expect(newStep1.includeCalendarEvent).toEqual(true); - } - } - - // we updated 1 steps, no more steps should be defined - expect(responseBody.data.steps[1]?.id).toBeUndefined(); - const trigger = partialUpdateDto.trigger as OnFormSubmittedTriggerDto; - expect(responseBody.data.trigger?.type).toEqual(trigger.type); - expect(responseBody.data.type).toEqual("routing-form"); - }); - }); - - it("should return 404 for updating a non-existent workflow ID", async () => { - const partialUpdateDto: Partial = { - name: updatedName, - steps: [{ ...createdWorkflow.steps[0], sender: "updatedSender" } as WorkflowEmailAttendeeStepDto], - }; - return request(app.getHttpServer()) - .patch(`${basePath}/999999`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send(partialUpdateDto) - .expect(404); - }); - - it("should return 401 if not authenticated", async () => { - const partialUpdateDto: Partial = { - name: updatedName, - steps: [{ ...createdWorkflow.steps[0], sender: "updatedSender" } as WorkflowEmailAttendeeStepDto], - }; - expect(createdWorkflowId).toBeDefined(); - return request(app.getHttpServer()) - .patch(`${basePath}/${createdWorkflowId}`) - .send(partialUpdateDto) - .expect(401); - }); - - it("should preserve time and timeUnit when not provided in partial update", async () => { - const workflowWithOffset = await request(app.getHttpServer()) - .post(basePath) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ - name: `Workflow With Offset ${randomString()}`, - activation: { - isActiveOnAllEventTypes: true, - activeOnEventTypeIds: [], - }, - trigger: { - type: BEFORE_EVENT, - offset: { - value: 2, - unit: DAY, - }, - }, - steps: [ - { - stepNumber: 1, - action: "email_attendee", - recipient: ATTENDEE, - template: REMINDER, - sender: "CalcomE2ETest", - includeCalendarEvent: true, - message: { - subject: "Upcoming: {EVENT_NAME}", - html: "

Reminder for your event {EVENT_NAME}.

", - }, - }, - ], - }) - .expect(201); - - const workflowId = workflowWithOffset.body.data.id; - expect(workflowWithOffset.body.data.trigger?.offset?.value).toEqual(2); - expect(workflowWithOffset.body.data.trigger?.offset?.unit).toEqual(DAY); - - const partialUpdateDto = { - name: `Updated Workflow Name ${randomString()}`, - }; - - const updatedWorkflow = await request(app.getHttpServer()) - .patch(`${basePath}/${workflowId}`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send(partialUpdateDto) - .expect(200); - - expect(updatedWorkflow.body.data.trigger?.offset?.value).toEqual(2); - expect(updatedWorkflow.body.data.trigger?.offset?.unit).toEqual(DAY); - - await workflowsRepositoryFixture.delete(workflowId); - }); - }); - - describe(`DELETE ${basePath}/:workflowId`, () => { - let workflowToDeleteId: number; - - beforeEach(async () => { - const res = await request(app.getHttpServer()) - .post(basePath) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .send({ ...sampleCreateEventTypeWorkflowDto, name: `Workflow To Delete ${randomString()}` }); - workflowToDeleteId = res.body.data.id; - }); - - it("should delete an existing event-type workflow", async () => { - return request(app.getHttpServer()) - .delete(`${basePath}/${workflowToDeleteId}`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - }); - }); - - it("should delete an existing routing-form workflow", async () => { - return request(app.getHttpServer()) - .delete(`${basePath}/${createdFormWorkflowId}/routing-form`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - }); - }); - - it("should return 404 when trying to delete a non-existent workflow ID", async () => { - return request(app.getHttpServer()) - .delete(`${basePath}/999999`) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .expect(404); - }); - - it("should return 401 if not authenticated", async () => { - return request(app.getHttpServer()).delete(`${basePath}/${workflowToDeleteId}`).expect(401); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.ts b/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.ts deleted file mode 100644 index e2cb8489178870..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; -import { IsEventTypeWorkflowInTeam } from "@/modules/auth/guards/workflows/is-event-type-workflow-in-team"; -import { IsRoutingFormWorkflowInTeam } from "@/modules/auth/guards/workflows/is-routing-form-workflow-in-team"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { CreateEventTypeWorkflowDto } from "@/modules/workflows/inputs/create-event-type-workflow.input"; -import { CreateFormWorkflowDto } from "@/modules/workflows/inputs/create-form-workflow"; -import { UpdateEventTypeWorkflowDto } from "@/modules/workflows/inputs/update-event-type-workflow.input"; -import { UpdateFormWorkflowDto } from "@/modules/workflows/inputs/update-form-workflow.input"; -import { - GetEventTypeWorkflowsOutput, - GetEventTypeWorkflowOutput, -} from "@/modules/workflows/outputs/event-type-workflow.output"; -import { - GetRoutingFormWorkflowOutput, - GetRoutingFormWorkflowsOutput, -} from "@/modules/workflows/outputs/routing-form-workflow.output"; -import { TeamEventTypeWorkflowsService } from "@/modules/workflows/services/team-event-type-workflows.service"; -import { TeamRoutingFormWorkflowsService } from "@/modules/workflows/services/team-routing-form-workflows.service"; -import { - Controller, - Get, - Patch, - Post, - Param, - ParseIntPipe, - Query, - UseGuards, - Body, - Delete, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { SkipTakePagination } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId/teams/:teamId/workflows", - version: API_VERSIONS_VALUES, -}) -@ApiTags("Orgs / Teams / Workflows") -@UseGuards(ApiAuthGuard, IsOrgGuard, IsTeamInOrg, PlatformPlanGuard, RolesGuard, IsAdminAPIEnabledGuard) -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -@ApiParam({ name: "orgId", type: Number, required: true }) -@ApiParam({ name: "teamId", type: Number, required: true }) -export class OrganizationTeamWorkflowsController { - constructor( - private readonly eventTypeWorkflowsService: TeamEventTypeWorkflowsService, - private readonly routingFormWorkflowsService: TeamRoutingFormWorkflowsService - ) {} - - @Get("/") - @ApiOperation({ summary: "Get organization team workflows" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("SCALE") - async getWorkflows( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Query() queryParams: SkipTakePagination - ): Promise { - const { skip, take } = queryParams; - - const workflows = await this.eventTypeWorkflowsService.getEventTypeTeamWorkflows(teamId, skip, take); - - return { data: workflows, status: SUCCESS_STATUS }; - } - - @Get("/routing-form") - @ApiOperation({ summary: "Get organization team workflows" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("SCALE") - async getRoutingFormWorkflows( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("teamId", ParseIntPipe) teamId: number, - @Query() queryParams: SkipTakePagination - ): Promise { - const { skip, take } = queryParams; - - const workflows = await this.routingFormWorkflowsService.getRoutingFormTeamWorkflows(teamId, skip, take); - - return { data: workflows, status: SUCCESS_STATUS }; - } - - @Get("/:workflowId") - @UseGuards(IsEventTypeWorkflowInTeam) - @ApiOperation({ summary: "Get organization team workflow" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("SCALE") - async getWorkflowById( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("workflowId", ParseIntPipe) workflowId: number - ): Promise { - const workflow = await this.eventTypeWorkflowsService.getEventTypeTeamWorkflowById(teamId, workflowId); - - return { data: workflow, status: SUCCESS_STATUS }; - } - - @Get("/:workflowId/routing-form") - @UseGuards(IsRoutingFormWorkflowInTeam) - @ApiOperation({ summary: "Get organization team workflow" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("SCALE") - async getRoutingFormWorkflowById( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("workflowId", ParseIntPipe) workflowId: number - ): Promise { - const workflow = await this.routingFormWorkflowsService.getRoutingFormTeamWorkflowById( - teamId, - workflowId - ); - - return { data: workflow, status: SUCCESS_STATUS }; - } - - @Post("/") - @ApiOperation({ summary: "Create organization team workflow for event-types" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("SCALE") - async createEventTypeWorkflow( - @GetUser() user: UserWithProfile, - @Param("teamId", ParseIntPipe) teamId: number, - @Body() data: CreateEventTypeWorkflowDto - ): Promise { - const workflow = await this.eventTypeWorkflowsService.createEventTypeTeamWorkflow(user, teamId, data); - return { data: workflow, status: SUCCESS_STATUS }; - } - - @Post("/routing-form") - @ApiOperation({ summary: "Create organization team workflow for routing-forms" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("SCALE") - async createFormWorkflow( - @GetUser() user: UserWithProfile, - @Param("teamId", ParseIntPipe) teamId: number, - @Body() data: CreateFormWorkflowDto - ): Promise { - const workflow = await this.routingFormWorkflowsService.createFormTeamWorkflow(user, teamId, data); - return { data: workflow, status: SUCCESS_STATUS }; - } - - @Patch("/:workflowId") - @UseGuards(IsEventTypeWorkflowInTeam) - @ApiOperation({ summary: "Update organization team workflow" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("SCALE") - async updateWorkflow( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("workflowId", ParseIntPipe) workflowId: number, - @GetUser() user: UserWithProfile, - @Body() data: UpdateEventTypeWorkflowDto - ): Promise { - const workflow = await this.eventTypeWorkflowsService.updateEventTypeTeamWorkflow( - user, - teamId, - workflowId, - data - ); - return { data: workflow, status: SUCCESS_STATUS }; - } - - @Patch("/:workflowId/routing-form") - @UseGuards(IsRoutingFormWorkflowInTeam) - @ApiOperation({ summary: "Update organization routing form team workflow" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("SCALE") - async updateRoutingFormWorkflow( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("workflowId", ParseIntPipe) workflowId: number, - @GetUser() user: UserWithProfile, - @Body() data: UpdateFormWorkflowDto - ): Promise { - const workflow = await this.routingFormWorkflowsService.updateFormTeamWorkflow( - user, - teamId, - workflowId, - data - ); - return { data: workflow, status: SUCCESS_STATUS }; - } - - @Delete("/:workflowId") - @UseGuards(IsEventTypeWorkflowInTeam) - @ApiOperation({ summary: "Delete organization team workflow" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("SCALE") - async deleteWorkflow( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("workflowId") workflowId: number - ): Promise<{ status: typeof SUCCESS_STATUS }> { - await this.eventTypeWorkflowsService.deleteTeamEventTypeWorkflow(teamId, workflowId); - return { status: SUCCESS_STATUS }; - } - - @Delete("/:workflowId/routing-form") - @UseGuards(IsRoutingFormWorkflowInTeam) - @ApiOperation({ summary: "Delete organization team routing-form workflow" }) - @Roles("TEAM_ADMIN") - @PlatformPlan("SCALE") - async deleteRoutingFormWorkflow( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("workflowId") workflowId: number - ): Promise<{ status: typeof SUCCESS_STATUS }> { - await this.routingFormWorkflowsService.deleteTeamRoutingFormWorkflow(teamId, workflowId); - return { status: SUCCESS_STATUS }; - } -} diff --git a/apps/api/v2/src/modules/organizations/users/bookings/controllers/organizations-users-bookings-controller.ts b/apps/api/v2/src/modules/organizations/users/bookings/controllers/organizations-users-bookings-controller.ts deleted file mode 100644 index 9dc3dd25b63268..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/bookings/controllers/organizations-users-bookings-controller.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, - OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard"; -import { OrganizationUsersBookingsService } from "@/modules/organizations/users/bookings/services/organization-users-bookings.service"; -import { Controller, UseGuards, Get, Query, ParseIntPipe, Param } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { GetBookingsInput_2024_08_13, GetBookingsOutput_2024_08_13 } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId/users/:userId/bookings", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsUserInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Users / Bookings") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER) -export class OrganizationsUsersBookingsController { - constructor(private readonly organizationUsersBookingsService: OrganizationUsersBookingsService) {} - - @Get("/") - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @ApiOperation({ summary: "Get all bookings for an organization user" }) - async getOrganizationUserBookings( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("userId", ParseIntPipe) userId: number, - @Query() query: GetBookingsInput_2024_08_13 - ): Promise { - const { bookings, pagination } = await this.organizationUsersBookingsService.getOrganizationUserBookings( - orgId, - userId, - query - ); - - return { - status: SUCCESS_STATUS, - data: bookings, - pagination, - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/users/bookings/controllers/organizations-users-bookings.e2e-spec.ts b/apps/api/v2/src/modules/organizations/users/bookings/controllers/organizations-users-bookings.e2e-spec.ts deleted file mode 100644 index 8ceff75f7f396a..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/bookings/controllers/organizations-users-bookings.e2e-spec.ts +++ /dev/null @@ -1,427 +0,0 @@ -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, - PaginationMetaDto, - 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 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"; -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 { 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 { 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 { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; -import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; -import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.module"; - -describe("Organizations UsersBookings Endpoints 2024-08-13", () => { - describe("Organization User bookings", () => { - let app: INestApplication; - let organization: Team; - let team1: Team; - - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let oAuthClient: PlatformOAuthClient; - let teamRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let hostsRepositoryFixture: HostsRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - - const teamUserEmail = `organizations-users-team-user-${randomString()}@api.com`; - let teamUser: User; - - let team1EventTypeId: number; - - let personalEventTypeId: number; - const personalEventTypeSlug = `organizations-users-bookings-personal-event-type-${randomString()}`; - - let createdPersonalBooking: BookingOutput_2024_08_13; - let createdPersonalBookingUsingUsername: BookingOutput_2024_08_13; - let createdTeamBooking: BookingOutput_2024_08_13; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - teamUserEmail, - Test.createTestingModule({ - imports: [AppModule, OrganizationsTeamsBookingsModule], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - - organization = await organizationsRepositoryFixture.create({ name: "organizations user bookings" }); - oAuthClient = await createOAuthClient(organization.id); - - team1 = await teamRepositoryFixture.create({ - name: "team 1", - isOrganization: false, - parent: { connect: { id: organization.id } }, - createdByOAuthClient: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - teamUser = await userRepositoryFixture.create({ - email: teamUserEmail, - username: teamUserEmail, - locale: "it", - name: "orgUser1team1", - platformOAuthClients: { - connect: { - id: oAuthClient.id, - }, - }, - }); - - const personalEvent = await eventTypesRepositoryFixture.create( - { - title: `user-bookings-2024-08-13-event-type-${randomString()}`, - slug: personalEventTypeSlug, - length: 15, - }, - teamUser.id - ); - personalEventTypeId = personalEvent.id; - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: "working time", - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(teamUser.id, userSchedule); - - await profileRepositoryFixture.create({ - uid: `usr-${teamUser.id}`, - username: teamUserEmail, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: teamUser.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: teamUser.id } }, - team: { connect: { id: team1.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: teamUser.id } }, - team: { connect: { id: organization.id } }, - accepted: true, - }); - - const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team1.id }, - }, - title: "Collective Event Type", - slug: "collective-event-type", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - team1EventTypeId = team1EventType.id; - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: teamUser.id, - }, - }, - eventType: { - connect: { - id: team1EventType.id, - }, - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - describe("create bookings", () => { - it("should create a personal booking", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), - eventTypeId: personalEventTypeId, - attendee: { - name: "Mr Proper", - email: "mr_proper@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - location: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.hosts[0].username).toEqual(teamUser.username); - expect(data.hosts[0].email).toEqual(teamUser.email); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 13, 15, 0)).toISOString()); - expect(data.duration).toEqual(15); - expect(data.eventTypeId).toEqual(personalEventTypeId); - 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, - }); - expect(data.location).toEqual(body.location); - expect(data.meetingUrl).toEqual(body.location); - expect(data.absentHost).toEqual(false); - expect(data.bookingFieldsResponses.email).toEqual(body.attendee.email); - createdPersonalBooking = data; - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - - it("should create a personal booking using username and event slug and organization slug", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(), - eventTypeSlug: personalEventTypeSlug, - username: teamUser.username!, - organizationSlug: organization.slug!, - attendee: { - name: "Mr Proper", - email: "mr_proper@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - location: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.hosts[0].username).toEqual(teamUser.username); - expect(data.hosts[0].email).toEqual(teamUser.email); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 15, 0)).toISOString()); - expect(data.duration).toEqual(15); - expect(data.eventTypeId).toEqual(personalEventTypeId); - 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, - }); - expect(data.location).toEqual(body.location); - expect(data.meetingUrl).toEqual(body.location); - expect(data.absentHost).toEqual(false); - expect(data.bookingFieldsResponses.email).toEqual(body.attendee.email); - createdPersonalBookingUsingUsername = data; - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - - it("should create a team booking", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(), - eventTypeId: team1EventTypeId, - attendee: { - name: "alice", - email: "alice@gmail.com", - timeZone: "Europe/Madrid", - language: "es", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 16, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(team1EventTypeId); - expect(data.attendees.length).toEqual(1); - 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, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - createdTeamBooking = data; - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibly recurring bookings" - ); - } - }); - }); - }); - - describe("get bookings", () => { - it("should get individual and team organization user bookings", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${organization.id}/users/${teamUser.id}/bookings`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.pagination).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - const pagination: PaginationMetaDto = responseBody.pagination; - expect(data.length).toEqual(3); - expect(data.find((booking) => booking.id === createdPersonalBooking.id)).toBeDefined(); - expect(data.find((booking) => booking.id === createdTeamBooking.id)).toBeDefined(); - expect( - data.find((booking) => booking.id === createdPersonalBookingUsingUsername.id) - ).toBeDefined(); - expect(pagination.totalItems).toEqual(3); - expect(pagination.remainingItems).toEqual(0); - expect(pagination.hasNextPage).toEqual(false); - expect(pagination.hasPreviousPage).toEqual(false); - expect(pagination.itemsPerPage).toEqual(100); - expect(pagination.totalPages).toEqual(1); - expect(pagination.currentPage).toEqual(1); - }); - }); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: ["http://localhost:5555"], - permissions: 32, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { - return !Array.isArray(data) && typeof data === "object" && data && "id" in data; - } - - afterAll(async () => { - await oauthClientRepositoryFixture.delete(oAuthClient.id); - await teamRepositoryFixture.delete(organization.id); - await userRepositoryFixture.deleteByEmail(teamUser.email); - await bookingsRepositoryFixture.deleteAllBookings(teamUser.id, teamUser.email); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/users/bookings/organizations-users-bookings.module.ts b/apps/api/v2/src/modules/organizations/users/bookings/organizations-users-bookings.module.ts deleted file mode 100644 index 5d231f015b7a3e..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/bookings/organizations-users-bookings.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module"; -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsUsersBookingsController } from "@/modules/organizations/users/bookings/controllers/organizations-users-bookings-controller"; -import { OrganizationUsersBookingsService } from "@/modules/organizations/users/bookings/services/organization-users-bookings.service"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { UsersModule } from "@/modules/users/users.module"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [ - BookingsModule_2024_08_13, - UsersModule, - PrismaModule, - StripeModule, - RedisModule, - MembershipsModule, - ], - providers: [OrganizationUsersBookingsService, OrganizationsRepository], - controllers: [OrganizationsUsersBookingsController], -}) -export class OrganizationsUsersBookingsModule {} diff --git a/apps/api/v2/src/modules/organizations/users/bookings/services/organization-users-bookings.service.ts b/apps/api/v2/src/modules/organizations/users/bookings/services/organization-users-bookings.service.ts deleted file mode 100644 index 2acc56bf11361c..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/bookings/services/organization-users-bookings.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; -import { UsersRepository } from "@/modules/users/users.repository"; -import { Injectable, NotFoundException } from "@nestjs/common"; - -import { GetBookingsInput_2024_08_13 } from "@calcom/platform-types"; - -@Injectable() -export class OrganizationUsersBookingsService { - constructor( - private readonly bookingsService: BookingsService_2024_08_13, - private readonly usersRepository: UsersRepository - ) {} - - async getOrganizationUserBookings(orgId: number, userId: number, queryParams: GetBookingsInput_2024_08_13) { - const user = await this.usersRepository.findById(userId); - if (!user) { - throw new NotFoundException(`getOrganizationUserBookings - User ${userId} not found`); - } - return await this.bookingsService.getBookings(queryParams, { orgId, id: user.id, email: user.email }); - } -} diff --git a/apps/api/v2/src/modules/organizations/users/index/controllers/organizations-users.controller.ts b/apps/api/v2/src/modules/organizations/users/index/controllers/organizations-users.controller.ts deleted file mode 100644 index 8574d7faf94c4e..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/index/controllers/organizations-users.controller.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { GetOrg } from "@/modules/auth/decorators/get-org/get-org.decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard"; -import { CreateOrganizationUserInput } from "@/modules/organizations/users/index/inputs/create-organization-user.input"; -import { GetOrganizationsUsersInput } from "@/modules/organizations/users/index/inputs/get-organization-users.input"; -import { UpdateOrganizationUserInput } from "@/modules/organizations/users/index/inputs/update-organization-user.input"; -import { - GetOrganizationUsersResponseDTO, - GetOrgUsersWithProfileOutput, -} from "@/modules/organizations/users/index/outputs/get-organization-users.output"; -import { GetOrganizationUserOutput } from "@/modules/organizations/users/index/outputs/get-organization-users.output"; -import { OrganizationsUsersService } from "@/modules/organizations/users/index/services/organizations-users-service"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { - Controller, - UseGuards, - Get, - Post, - Patch, - Delete, - Param, - ParseIntPipe, - Body, - UseInterceptors, - Query, -} from "@nestjs/common"; -import { ClassSerializerInterceptor } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger"; -import { plainToInstance } from "class-transformer"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { Team } from "@calcom/prisma/client"; - -@Controller({ - path: "/v2/organizations/:orgId/users", - version: API_VERSIONS_VALUES, -}) -@UseInterceptors(ClassSerializerInterceptor) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@UseGuards(IsOrgGuard) -@DocsTags("Orgs / Users") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -@ApiParam({ name: "orgId", type: Number, required: true }) -export class OrganizationsUsersController { - constructor(private readonly organizationsUsersService: OrganizationsUsersService) {} - - @Get() - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @ApiOperation({ summary: "Get all users" }) - async getOrganizationsUsers( - @Param("orgId", ParseIntPipe) orgId: number, - @Query() query: GetOrganizationsUsersInput - ): Promise { - const { emails, assignedOptionIds, attributeQueryOperator, teamIds } = query ?? {}; - const users = await this.organizationsUsersService.getUsers( - orgId, - emails, - { assignedOptionIds, attributeQueryOperator, teamIds }, - query.skip ?? 0, - query.take ?? 250 - ); - - return { - status: SUCCESS_STATUS, - data: users.map((user) => - plainToInstance( - GetOrgUsersWithProfileOutput, - { ...user, profile: user?.profiles?.[0] ?? {} }, - { strategy: "excludeAll" } - ) - ), - }; - } - - @Post() - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @ApiOperation({ summary: "Create a user" }) - async createOrganizationUser( - @GetOrg() org: Team, - @Body() input: CreateOrganizationUserInput, - @GetUser() inviter: UserWithProfile - ): Promise { - const user = await this.organizationsUsersService.createUser( - org, - input, - inviter.name ?? inviter.username ?? inviter.email - ); - return { - status: SUCCESS_STATUS, - data: plainToInstance( - GetOrgUsersWithProfileOutput, - { ...user, profile: user?.profiles?.[0] ?? {} }, - { strategy: "excludeAll" } - ), - }; - } - - @Patch("/:userId") - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsUserInOrg) - @ApiOperation({ summary: "Update a user" }) - async updateOrganizationUser( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("userId", ParseIntPipe) userId: number, - @Body() input: UpdateOrganizationUserInput - ): Promise { - const user = await this.organizationsUsersService.updateUser(orgId, userId, input); - return { - status: SUCCESS_STATUS, - data: plainToInstance( - GetOrgUsersWithProfileOutput, - { ...user, profile: user?.profiles?.[0] ?? {} }, - { strategy: "excludeAll" } - ), - }; - } - - @Delete("/:userId") - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsUserInOrg) - @ApiOperation({ summary: "Delete a user" }) - async deleteOrganizationUser( - @Param("orgId", ParseIntPipe) orgId: number, - @Param("userId", ParseIntPipe) userId: number - ): Promise { - const user = await this.organizationsUsersService.deleteUser(orgId, userId); - return { - status: SUCCESS_STATUS, - data: plainToInstance( - GetOrgUsersWithProfileOutput, - { ...user, profile: user?.profiles?.[0] ?? {} }, - { strategy: "excludeAll" } - ), - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/users/index/controllers/organizations-users.e2e-spec.ts b/apps/api/v2/src/modules/organizations/users/index/controllers/organizations-users.e2e-spec.ts deleted file mode 100644 index b0b0a1cd7b4953..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/index/controllers/organizations-users.e2e-spec.ts +++ /dev/null @@ -1,889 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { AttributeOption, 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 { AttributeRepositoryFixture } from "test/fixtures/repository/attributes.repository.fixture"; -import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { withApiAuth } from "test/utils/withApiAuth"; -import { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { EmailService } from "@/modules/email/email.service"; -import { GetOrganizationsUsersInput } from "@/modules/organizations/users/index/inputs/get-organization-users.input"; -import { GetOrgUsersWithProfileOutput } from "@/modules/organizations/users/index/outputs/get-organization-users.output"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { CreateUserInput } from "@/modules/users/inputs/create-user.input"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Users Endpoints", () => { - const bio = "I am a bio"; - const metadata = { foo: "bar" }; - - describe("Member role", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipFixtures: MembershipRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - - const userEmail = `organizations-users-member-${randomString()}@api.com`; - let user: User; - let org: Team; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipFixtures = new MembershipRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-users-organization-${randomString()}`, - isOrganization: true, - }); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - organization: { connect: { id: org.id } }, - bio, - metadata, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: userEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - - await membershipFixtures.addUserToOrg(user, org, "MEMBER", true); - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should not be able to find org users", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/users`).expect(403); - }); - - it("should not be able to create a new org user", async () => { - return request(app.getHttpServer()).post(`/v2/organizations/${org.id}/users`).expect(403); - }); - - it("should not be able to update an org user", async () => { - return request(app.getHttpServer()).patch(`/v2/organizations/${org.id}/users/${user.id}`).expect(403); - }); - - it("should not be able to delete an org user", async () => { - return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/users/${user.id}`).expect(403); - }); - - afterAll(async () => { - // await membershipFixtures.delete(membership.id); - await Promise.all([userRepositoryFixture.deleteByEmail(user.email)]); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); - describe("Admin role", () => { - let app: INestApplication; - let profileRepositoryFixture: ProfileRepositoryFixture; - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipFixtures: MembershipRepositoryFixture; - - const userEmail = `organizations-users-admin-${randomString()}@api.com`; - const nonMemberEmail = `organizations-users-non-member-${randomString()}@api.com`; - let user: User; - let org: Team; - let createdUser: User; - - const orgMembersData = [ - { - email: `organizations-users-member1-${randomString()}@api.com`, - username: `organizations-users-member1-${randomString()}@api.com`, - }, - { - email: `organizations-users-member2-${randomString()}@api.com`, - username: `organizations-users-member2-${randomString()}@api.com`, - }, - { - email: `organizations-users-member3-${randomString()}@api.com`, - username: `organizations-users-member3-${randomString()}@api.com`, - }, - ]; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - membershipFixtures = new MembershipRepositoryFixture(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-users-admin-organization-${randomString()}`, - isOrganization: true, - }); - - await userRepositoryFixture.create({ - email: nonMemberEmail, - username: `non-member-${randomString()}`, - }); - - const orgMembers = await Promise.all( - orgMembersData.map((member) => - userRepositoryFixture.create({ - email: member.email, - username: member.username, - organization: { connect: { id: org.id } }, - bio, - metadata, - }) - ) - ); - // create profiles of orgMember like they would be when being invied to the org - await Promise.all( - orgMembers.map((member) => - profileRepositoryFixture.create({ - uid: `usr-${member.id}`, - username: member.username ?? `usr-${member.id}`, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: member.id, - }, - }, - }) - ) - ); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - organization: { connect: { id: org.id } }, - bio, - metadata, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: userEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - - await membershipFixtures.addUserToOrg(user, org, "ADMIN", true); - await Promise.all( - orgMembers.map((member) => membershipFixtures.addUserToOrg(member, org, "MEMBER", true)) - ); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should get all org users", async () => { - const { body } = await request(app.getHttpServer()).get(`/v2/organizations/${org.id}/users`); - - const userData = body.data as GetOrgUsersWithProfileOutput[]; - - expect(body.status).toBe(SUCCESS_STATUS); - expect(userData.length).toBe(4); - // Find and verify each member's data - const member0 = userData.find((u) => u.profile.username === orgMembersData[0].username); - const member1 = userData.find((u) => u.profile.username === orgMembersData[1].username); - const member2 = userData.find((u) => u.profile.username === orgMembersData[2].username); - - // Verify member 0 - expect(member0).toBeDefined(); - expect(member0?.email).toBe(orgMembersData[0].email); - expect(member0?.profile.username).toBe(orgMembersData[0].username); - expect(member0?.bio).toBe(bio); - expect(member0?.metadata).toEqual(metadata); - - // Verify member 1 - expect(member1).toBeDefined(); - expect(member1?.email).toBe(orgMembersData[1].email); - expect(member1?.profile.username).toBe(orgMembersData[1].username); - expect(member1?.bio).toBe(bio); - expect(member1?.metadata).toEqual(metadata); - - // Verify member 2 - expect(member2).toBeDefined(); - expect(member2?.email).toBe(orgMembersData[2].email); - expect(member2?.profile.username).toBe(orgMembersData[2].username); - expect(member2?.bio).toBe(bio); - expect(member2?.metadata).toEqual(metadata); - - expect(userData.filter((user: { email: string }) => user.email === nonMemberEmail).length).toBe(0); - }); - - it("should only get users with the specified email", async () => { - const { body } = await request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/users`) - .query({ - emails: userEmail, - }) - .set("Content-Type", "application/json") - .set("Accept", "application/json"); - - const userData = body.data as GetOrgUsersWithProfileOutput[]; - - expect(body.status).toBe(SUCCESS_STATUS); - expect(userData.length).toBe(1); - - const foundUser = userData.find((u) => u.email === userEmail); - expect(foundUser).toBeDefined(); - expect(foundUser?.email).toBe(userEmail); - expect(foundUser?.profile.username).toBe(user.username); - expect(foundUser?.bio).toBe(bio); - expect(foundUser?.metadata).toEqual(metadata); - }); - - it("should get users within the specified emails array", async () => { - const orgMemberEmail = orgMembersData[0].email; - - const { body } = await request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/users`) - .query({ - emails: [userEmail, orgMemberEmail], - }) - .set("Content-Type", "application/json") - .set("Accept", "application/json"); - - const userData = body.data; - - expect(body.status).toBe(SUCCESS_STATUS); - expect(userData.length).toBe(2); - - const adminUser = userData.find((u: GetOrgUsersWithProfileOutput) => u.email === userEmail); - const orgMember = userData.find((u: GetOrgUsersWithProfileOutput) => u.email === orgMemberEmail); - - expect(adminUser).toBeDefined(); - expect(adminUser?.email).toBe(userEmail); - expect(adminUser?.profile.username).toBe(user.username); - expect(adminUser?.bio).toBe(bio); - expect(adminUser?.metadata).toEqual(metadata); - - expect(orgMember).toBeDefined(); - expect(orgMember?.email).toBe(orgMemberEmail); - expect(orgMember?.profile.username).toBe(orgMembersData[0].username); - expect(orgMember?.bio).toBe(bio); - expect(orgMember?.metadata).toEqual(metadata); - }); - - it("should update an org user", async () => { - const { body } = await request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/users/${user.id}`) - .send({ - theme: "light", - }) - .set("Content-Type", "application/json") - .set("Accept", "application/json"); - - const userData = body.data as User; - expect(body.status).toBe(SUCCESS_STATUS); - expect(userData.theme).toBe("light"); - }); - - it("should create a new org user", async () => { - const newOrgUser: CreateUserInput = { - email: `organizations-users-new-member-${randomString()}@api.com`, - bio, - metadata, - timeZone: "Europe/Rome", - }; - - const emailSpy = jest - .spyOn(EmailService.prototype, "sendSignupToOrganizationEmail") - .mockImplementation(() => Promise.resolve()); - const { body } = await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users`) - .send(newOrgUser) - .set("Content-Type", "application/json") - .set("Accept", "application/json"); - - const userData = body.data; - expect(body.status).toBe(SUCCESS_STATUS); - expect(userData.email).toBe(newOrgUser.email); - expect(userData.bio).toBe(newOrgUser.bio); - expect(userData.metadata).toEqual(newOrgUser.metadata); - expect(userData.timeZone).toBe(newOrgUser.timeZone); - expect(emailSpy).toHaveBeenCalledWith({ - usernameOrEmail: newOrgUser.email, - orgName: org.name, - orgId: org.id, - inviterName: userEmail, - locale: null, - }); - createdUser = userData; - }); - - it("creates a new org user with username and avatarUrl", async () => { - const shortRandom = randomString().substring(0, 8); - const testUsername = `user${shortRandom}`; - const testEmail = `org-user-${shortRandom}@api.com`; - const githubAvatarUrl = "https://avatars.githubusercontent.com/u/583231?v=4"; - - const newOrgUserWithUsernameAndAvatar: CreateUserInput = { - email: testEmail, - username: testUsername, - avatarUrl: githubAvatarUrl, - bio, - metadata, - timeZone: "America/Sao_Paulo", - timeFormat: 24, - locale: "pt", - }; - - const emailSpy = jest - .spyOn(EmailService.prototype, "sendSignupToOrganizationEmail") - .mockImplementation(() => Promise.resolve()); - - const { body } = await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users`) - .send(newOrgUserWithUsernameAndAvatar) - .set("Content-Type", "application/json") - .set("Accept", "application/json"); - - const userData = body.data; - - // Verify response status - expect(body.status).toBe(SUCCESS_STATUS); - - // Verify all user fields are correctly set - expect(userData.email).toBe(testEmail); - expect(userData.username).toBe(testUsername); - expect(userData.avatarUrl).toBe(githubAvatarUrl); - expect(userData.bio).toBe(bio); - expect(userData.metadata).toEqual(metadata); - expect(userData.timeZone).toBe("America/Sao_Paulo"); - expect(userData.timeFormat).toBe(24); - expect(userData.locale).toBe("pt"); - - // Verify email was sent with correct parameters (using email, not username) - expect(emailSpy).toHaveBeenCalledWith({ - usernameOrEmail: testEmail, - orgName: org.name, - orgId: org.id, - inviterName: userEmail, - locale: "pt", - }); - - // Clean up the created user - await userRepositoryFixture.deleteByEmail(testEmail); - }); - - it("creates a new org user with avatarUrl as base64 image", async () => { - const shortRandom = randomString().substring(0, 8); - const testEmail = `org-b64-${shortRandom}@api.com`; - const base64Avatar = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="; - - const newOrgUserWithBase64Avatar: CreateUserInput = { - email: testEmail, - avatarUrl: base64Avatar, - bio, - metadata, - }; - - const emailSpy = jest - .spyOn(EmailService.prototype, "sendSignupToOrganizationEmail") - .mockImplementation(() => Promise.resolve()); - - const { body } = await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users`) - .send(newOrgUserWithBase64Avatar) - .set("Content-Type", "application/json") - .set("Accept", "application/json"); - - const userData = body.data; - - // Verify response status - expect(body.status).toBe(SUCCESS_STATUS); - - // Verify base64 avatar is accepted and stored - expect(userData.email).toBe(testEmail); - expect(userData.avatarUrl).toBe(base64Avatar); - expect(userData.bio).toBe(bio); - expect(userData.metadata).toEqual(metadata); - - // Verify email was sent - expect(emailSpy).toHaveBeenCalledWith({ - usernameOrEmail: testEmail, - orgName: org.name, - orgId: org.id, - inviterName: userEmail, - locale: null, - }); - - // Clean up the created user - await userRepositoryFixture.deleteByEmail(testEmail); - }); - - it("should delete an org user", async () => { - const { body } = await request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/users/${createdUser.id}`) - .set("Content-Type", "application/json") - .set("Accept", "application/json"); - - const userData = body.data as User; - expect(body.status).toBe(SUCCESS_STATUS); - expect(userData.id).toBe(createdUser.id); - }); - - afterAll(async () => { - // await membershipFixtures.delete(membership.id); - await Promise.all([ - userRepositoryFixture.deleteByEmail(user.email), - userRepositoryFixture.deleteByEmail(nonMemberEmail), - ...orgMembersData.map((member) => userRepositoryFixture.deleteByEmail(member.email)), - ]); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); - - describe("Member event-types", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let membershipFixtures: MembershipRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - - const authEmail = `organizations-users-auth-${randomString()}@api.com`; - let user: User; - let org: Team; - let team: Team; - let createdUser: User; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - authEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - membershipFixtures = new MembershipRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-users-organization-${randomString()}`, - isOrganization: true, - }); - - team = await teamsRepositoryFixture.create({ - name: `organizations-users-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - user = await userRepositoryFixture.create({ - email: authEmail, - username: authEmail, - organization: { connect: { id: org.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: authEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - - await membershipFixtures.addUserToOrg(user, org, "ADMIN", true); - - await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team.id }, - }, - title: "Collective Event Type", - slug: "collective-event-type", - length: 30, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "MANAGED", - team: { - connect: { id: team.id }, - }, - title: "Managed Event Type", - slug: "managed-event-type", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should create a new org user with team event-types", async () => { - const newOrgUser = { - email: `organizations-users-new-member-${randomString()}@api.com`, - organizationRole: "MEMBER", - autoAccept: true, - }; - - const { body } = await request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users`) - .send({ - email: newOrgUser.email, - }) - .set("Content-Type", "application/json") - .set("Accept", "application/json"); - - const userData = body.data; - expect(body.status).toBe(SUCCESS_STATUS); - createdUser = userData; - teamHasCorrectEventTypes(team.id); - }); - - async function teamHasCorrectEventTypes(teamId: number) { - const eventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(teamId); - expect(eventTypes?.length).toEqual(2); - } - - afterAll(async () => { - // await membershipFixtures.delete(membership.id); - await userRepositoryFixture.deleteByEmail(user.email); - await userRepositoryFixture.deleteByEmail(createdUser.email); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); - - describe("Org Members with assigned attributes", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let membershipFixtures: MembershipRepositoryFixture; - let teamsRepositoryFixtures: TeamRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let attributeRepositoryFixture: AttributeRepositoryFixture; - - const authEmail = `organizations-users-auth-${randomString()}@api.com`; - const user2Email = `organizations-users2-auth-${randomString()}@api.com`; - - let user: User; - let user2: User; - - let org: Team; - let team: Team; - let assignedOption1: AttributeOption; - let assignedOption2: AttributeOption; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - authEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixtures = new TeamRepositoryFixture(moduleRef); - - membershipFixtures = new MembershipRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-users-organization-${randomString()}`, - isOrganization: true, - }); - - team = await teamsRepositoryFixtures.create({ name: "org team", parent: { connect: { id: org.id } } }); - - user = await userRepositoryFixture.create({ - email: authEmail, - username: authEmail, - organization: { connect: { id: org.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user.id}`, - username: authEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user.id, - }, - }, - }); - - const membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true); - - await membershipFixtures.create({ - role: "MEMBER", - accepted: true, - team: { connect: { id: team.id } }, - user: { connect: { id: user.id } }, - }); - - user2 = await userRepositoryFixture.create({ - email: user2Email, - username: user2Email, - organization: { connect: { id: org.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${user2.id}`, - username: user2Email, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: user2.id, - }, - }, - }); - - const membership2 = await membershipFixtures.addUserToOrg(user2, org, "ADMIN", true); - - attributeRepositoryFixture = new AttributeRepositoryFixture(moduleRef); - const attribute = await attributeRepositoryFixture.create({ - name: "Test Attribute", - team: { connect: { id: org.id } }, - type: "TEXT", - slug: `test-attribute-${randomString()}`, - }); - - const attribute2 = await attributeRepositoryFixture.create({ - name: "Test Attribute 2", - team: { connect: { id: org.id } }, - type: "TEXT", - slug: `test-attribute-2-${randomString()}`, - }); - - assignedOption1 = await attributeRepositoryFixture.createOption({ - slug: "option1", - value: "option1", - attribute: { connect: { id: attribute.id } }, - assignedUsers: { - create: { - memberId: membership.id, - }, - }, - }); - - assignedOption2 = await attributeRepositoryFixture.createOption({ - slug: "optionA", - value: "optionA", - attribute: { connect: { id: attribute2.id } }, - assignedUsers: { createMany: { data: [{ memberId: membership.id }, { memberId: membership2.id }] } }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should get users with all specified assigned attribute options", async () => { - const { body } = await request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/users`) - .query({ - assignedOptionIds: [assignedOption1.id], - attributeQueryOperator: "AND", - } as GetOrganizationsUsersInput) - .set("Content-Type", "application/json") - .set("Accept", "application/json"); - - const userData = body.data; - - expect(body.status).toBe(SUCCESS_STATUS); - expect(userData.length).toBe(1); - - const userWithAssignedOptions = userData.find( - (u: GetOrgUsersWithProfileOutput) => u.email === user.email - ); - expect(userWithAssignedOptions).toBeDefined(); - expect(userWithAssignedOptions?.email).toBe(user.email); - expect(userWithAssignedOptions?.profile.username).toBe(user.username); - }); - - it("should get users with at least one of the specified assigned attribute options", async () => { - const { body } = await request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/users`) - .query({ - assignedOptionIds: [assignedOption1.id, assignedOption2.id], - attributeQueryOperator: "OR", - } as GetOrganizationsUsersInput) - .set("Content-Type", "application/json") - .set("Accept", "application/json"); - - const userData = body.data; - - expect(body.status).toBe(SUCCESS_STATUS); - expect(userData.length).toBe(2); - - const userWithAssignedOptions = userData.find( - (u: GetOrgUsersWithProfileOutput) => u.email === user.email - ); - expect(userWithAssignedOptions).toBeDefined(); - expect(userWithAssignedOptions?.email).toBe(user.email); - expect(userWithAssignedOptions?.profile.username).toBe(user.username); - - const userWithAssignedOptions2 = userData.find( - (u: GetOrgUsersWithProfileOutput) => u.email === user2.email - ); - expect(userWithAssignedOptions2).toBeDefined(); - expect(userWithAssignedOptions2?.email).toBe(user2.email); - expect(userWithAssignedOptions2?.profile.username).toBe(user2.username); - }); - - it("should get users with at least one of the specified assigned attribute options filtered by teams", async () => { - const { body } = await request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/users`) - .query({ - assignedOptionIds: [assignedOption1.id, assignedOption2.id], - attributeQueryOperator: "OR", - teamIds: [team.id], - } as GetOrganizationsUsersInput) - .set("Content-Type", "application/json") - .set("Accept", "application/json"); - - const userData = body.data; - - expect(body.status).toBe(SUCCESS_STATUS); - expect(userData.length).toBe(1); - - const userWithAssignedOptions = userData.find( - (u: GetOrgUsersWithProfileOutput) => u.email === user.email - ); - expect(userWithAssignedOptions).toBeDefined(); - expect(userWithAssignedOptions?.email).toBe(user.email); - expect(userWithAssignedOptions?.profile.username).toBe(user.username); - }); - - it("should get users with none of the specified assigned attribute options", async () => { - const { body } = await request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/users`) - .query({ - assignedOptionIds: [assignedOption1.id, assignedOption2.id], - attributeQueryOperator: "NONE", - } as GetOrganizationsUsersInput) - .set("Content-Type", "application/json") - .set("Accept", "application/json"); - - const userData = body.data; - - expect(body.status).toBe(SUCCESS_STATUS); - expect(userData.length).toBe(0); - }); - - afterAll(async () => { - // await membershipFixtures.delete(membership.id); - await userRepositoryFixture.deleteByEmail(user.email); - await userRepositoryFixture.deleteByEmail(user2.email); - await organizationsRepositoryFixture.delete(org.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/users/index/inputs/create-organization-user.input.ts b/apps/api/v2/src/modules/organizations/users/index/inputs/create-organization-user.input.ts deleted file mode 100644 index 94b34df576b56d..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/index/inputs/create-organization-user.input.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CreateUserInput } from "@/modules/users/inputs/create-user.input"; -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsString, IsOptional, IsBoolean, IsEnum } from "class-validator"; - -import { MembershipRole } from "@calcom/platform-libraries"; - -export class CreateOrganizationUserInput extends CreateUserInput { - @IsOptional() - @IsString() - @ApiPropertyOptional({ type: String, default: "en" }) - locale = "en"; - - @IsOptional() - @IsEnum(MembershipRole) - @ApiPropertyOptional({ enum: MembershipRole, default: MembershipRole.MEMBER }) - organizationRole: MembershipRole = MembershipRole.MEMBER; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ type: Boolean, default: true }) - autoAccept = true; -} diff --git a/apps/api/v2/src/modules/organizations/users/index/inputs/get-organization-users.input.ts b/apps/api/v2/src/modules/organizations/users/index/inputs/get-organization-users.input.ts deleted file mode 100644 index a999a123a5473a..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/index/inputs/get-organization-users.input.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { GetUsersInput } from "@/modules/users/inputs/get-users.input"; -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { Transform } from "class-transformer"; -import { Expose } from "class-transformer"; -import { IsOptional, IsArray, ArrayMinSize, IsString, IsIn, IsNumber } from "class-validator"; - -export class GetOrganizationsUsersInput extends GetUsersInput { - @Expose() - @IsOptional() - @Transform(({ value }) => { - if (typeof value === "string") { - return value.split(",").map((optId: string) => optId); - } - return value; - }) - @ApiPropertyOptional({ - type: [String], - description: "Filter by assigned attribute option ids. ids must be separated by a comma.", - example: "?assignedOptionIds=aaaaaaaa-bbbb-cccc-dddd-eeeeee1eee,aaaaaaaa-bbbb-cccc-dddd-eeeeee2eee", - }) - @IsArray() - @IsString({ each: true }) - @ArrayMinSize(1, { message: "assignedOptionIds must contain at least 1 attribute option id" }) - assignedOptionIds?: string[]; - - @ApiPropertyOptional({ - type: String, - description: "Query operator used to filter assigned options, AND by default.", - example: "NONE", - }) - @IsOptional() - @IsString() - @IsIn(["OR", "AND", "NONE"]) - attributeQueryOperator: "AND" | "OR" | "NONE" = "AND"; // Default value - - @IsOptional() - @Transform(({ value }) => { - if (typeof value === "string") { - return value.split(",").map((teamId: string) => parseInt(teamId)); - } - return value; - }) - @ApiPropertyOptional({ - type: [Number], - description: "Filter by teamIds. Team ids must be separated by a comma.", - example: "?teamIds=100,200", - }) - @IsArray() - @IsNumber({}, { each: true }) - @ArrayMinSize(1, { message: "teamIds must contain at least 1 team id" }) - teamIds?: number[]; -} diff --git a/apps/api/v2/src/modules/organizations/users/index/inputs/update-organization-user.input.ts b/apps/api/v2/src/modules/organizations/users/index/inputs/update-organization-user.input.ts deleted file mode 100644 index 1cba9543ca7a65..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/index/inputs/update-organization-user.input.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { UpdateUserInput } from "@/modules/users/inputs/update-user.input"; - -export class UpdateOrganizationUserInput extends UpdateUserInput {} diff --git a/apps/api/v2/src/modules/organizations/users/index/organizations-users.repository.ts b/apps/api/v2/src/modules/organizations/users/index/organizations-users.repository.ts deleted file mode 100644 index 388da56e58e565..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/index/organizations-users.repository.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -import type { Prisma, AttributeToUser, Membership, User, Profile } from "@calcom/prisma/client"; - -@Injectable() -export class OrganizationsUsersRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - private filterOnOrgMembership(orgId: number) { - return { - profiles: { - some: { - organizationId: orgId, - }, - }, - }; - } - - async getOrganizationUsersByEmailsAndAttributeFilters( - orgId: number, - filters: { - teamIds?: number[]; - assignedOptionIds: string[]; - attributeQueryOperator?: "AND" | "OR" | "NONE"; - }, - emailArray?: string[], - skip?: number, - take?: number - ) { - const { teamIds, assignedOptionIds, attributeQueryOperator } = filters ?? {}; - const attributeToUsersWithProfile = await this.dbRead.prisma.attributeToUser.findMany({ - include: { - member: { include: { user: { include: { profiles: { where: { organizationId: orgId } } } } } }, - }, - distinct: ["memberId"], - where: { - member: { - teamId: orgId, - ...(teamIds && { user: { teams: { some: { teamId: { in: teamIds } } } } }), - // Filter to only get users which have ALL of the assigned attribute options - ...(attributeQueryOperator === "AND" && { - AND: assignedOptionIds.map((optionId) => ({ - AttributeToUser: { some: { attributeOptionId: optionId } }, - })), - }), - }, - ...(emailArray && emailArray.length ? { email: { in: emailArray } } : {}), - // Filter to get users which have AT LEAST ONE of the assigned attribute options - ...(attributeQueryOperator === "OR" && { - attributeOption: { id: { in: assignedOptionIds } }, - }), - // Filter to get users that have NONE the assigned attribute options - ...(attributeQueryOperator === "NONE" && { - NOT: { - attributeOption: { id: { in: assignedOptionIds } }, - }, - }), - }, - skip, - take, - }); - return attributeToUsersWithProfile.map( - ( - attributeToUser: AttributeToUser & { member: Membership & { user: User & { profiles: Profile[] } } } - ) => attributeToUser.member.user - ); - } - - async getOrganizationUsersByEmails( - orgId: number, - emailArray?: string[], - teamIds?: number[], - skip?: number, - take?: number - ) { - return await this.dbRead.prisma.user.findMany({ - where: { - ...this.filterOnOrgMembership(orgId), - ...(emailArray && emailArray.length ? { email: { in: emailArray } } : {}), - ...(teamIds && { teams: { some: { teamId: { in: teamIds } } } }), - }, - include: { - profiles: { - where: { - organizationId: orgId, - }, - }, - }, - skip, - take, - }); - } - - async getOrganizationUsersByIds(orgId: number, userIds: number[]) { - return await this.dbRead.prisma.user.findMany({ - where: { - profiles: { - some: { - organizationId: orgId, - userId: { in: userIds }, - }, - }, - }, - include: { - profiles: true, - }, - }); - } - - async getOrganizationUserByEmail(orgId: number, email: string) { - return await this.dbRead.prisma.user.findFirst({ - where: { - email, - ...this.filterOnOrgMembership(orgId), - }, - include: { - profiles: { - where: { - organizationId: orgId, - }, - }, - }, - }); - } - - async updateOrganizationUser(orgId: number, userId: number, updateUserBody: Prisma.UserUpdateInput) { - return await this.dbWrite.prisma.user.update({ - where: { - id: userId, - organizationId: orgId, - }, - data: updateUserBody, - include: { - profiles: { - where: { - organizationId: orgId, - }, - }, - }, - }); - } - - async deleteUser(orgId: number, userId: number) { - return await this.dbWrite.prisma.user.delete({ - where: { - id: userId, - OR: [ - { organizationId: orgId }, - { - profiles: { - some: { - organizationId: orgId, - }, - }, - }, - ], - }, - include: { - profiles: { - where: { - organizationId: orgId, - }, - }, - }, - }); - } - - async getOrganizationUserByUsername(orgId: number, username: string) { - const profile = await this.dbRead.prisma.profile.findUnique({ - where: { - username_organizationId: { - organizationId: orgId, - username, - }, - }, - include: { - user: true, - }, - }); - return profile?.user; - } -} diff --git a/apps/api/v2/src/modules/organizations/users/index/outputs/get-organization-users.output.ts b/apps/api/v2/src/modules/organizations/users/index/outputs/get-organization-users.output.ts deleted file mode 100644 index 5bb90c95d4c0fc..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/index/outputs/get-organization-users.output.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { GetUserOutput } from "@/modules/users/outputs/get-users.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum, IsInt, IsString, ValidateNested, IsArray } from "class-validator"; - -import { ERROR_STATUS } from "@calcom/platform-constants"; -import { SUCCESS_STATUS } from "@calcom/platform-constants"; - -export class ProfileOutput { - @IsInt() - @Expose() - @ApiProperty({ type: Number, required: true, description: "The ID of the profile of user", example: 1 }) - id!: number; - - @IsInt() - @Expose() - @ApiProperty({ - type: Number, - required: true, - description: "The ID of the organization of user", - example: 1, - }) - organizationId!: number; - - @IsInt() - @Expose() - @ApiProperty({ type: Number, required: true, description: "The IDof the user", example: 1 }) - userId!: number; - - @IsString() - @Expose() - @ApiProperty({ - type: String, - nullable: true, - required: false, - description: "The username of the user within the organization context", - example: "john_doe", - }) - username!: string; -} -export class GetOrgUsersWithProfileOutput extends GetUserOutput { - @ApiProperty({ - description: "organization user profile, contains user data within the organizaton context", - }) - @Expose() - @ValidateNested() - @IsArray() - @Type(() => ProfileOutput) - profile!: ProfileOutput; -} - -export class GetOrganizationUsersResponseDTO { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - data!: GetOrgUsersWithProfileOutput[]; -} - -export class GetOrganizationUserOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - data!: GetOrgUsersWithProfileOutput; -} diff --git a/apps/api/v2/src/modules/organizations/users/index/services/organizations-users-service.ts b/apps/api/v2/src/modules/organizations/users/index/services/organizations-users-service.ts deleted file mode 100644 index 01bccf1f216529..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/index/services/organizations-users-service.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { EmailService } from "@/modules/email/email.service"; -import { CreateOrganizationUserInput } from "@/modules/organizations/users/index/inputs/create-organization-user.input"; -import { UpdateOrganizationUserInput } from "@/modules/organizations/users/index/inputs/update-organization-user.input"; -import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository"; -import { CreateUserInput } from "@/modules/users/inputs/create-user.input"; -import { Injectable, ConflictException, ForbiddenException } from "@nestjs/common"; -import { plainToInstance } from "class-transformer"; - -import { createNewUsersConnectToOrgIfExists, CreationSource } from "@calcom/platform-libraries"; -import type { Team } from "@calcom/prisma/client"; - -@Injectable() -export class OrganizationsUsersService { - constructor( - private readonly organizationsUsersRepository: OrganizationsUsersRepository, - private readonly emailService: EmailService - ) {} - - async getUsers( - orgId: number, - emailInput?: string[], - filters?: { - teamIds?: number[]; - assignedOptionIds?: string[]; - attributeQueryOperator?: "AND" | "OR" | "NONE"; - }, - skip?: number, - take?: number - ) { - const emailArray = !emailInput ? [] : emailInput; - - if (filters?.assignedOptionIds && filters?.assignedOptionIds?.length) { - return await this.organizationsUsersRepository.getOrganizationUsersByEmailsAndAttributeFilters( - orgId, - { - assignedOptionIds: filters.assignedOptionIds, - attributeQueryOperator: filters?.attributeQueryOperator ?? "AND", - teamIds: filters?.teamIds, - }, - emailArray, - skip, - take - ); - } - - const users = await this.organizationsUsersRepository.getOrganizationUsersByEmails( - orgId, - emailArray, - filters?.teamIds, - skip, - take - ); - - return users; - } - - async createUser(org: Team, userCreateBody: CreateOrganizationUserInput, inviterName: string) { - // Check if email exists in the system - const userEmailCheck = await this.organizationsUsersRepository.getOrganizationUserByEmail( - org.id, - userCreateBody.email - ); - - if (userEmailCheck) throw new ConflictException("A user already exists with that email"); - - // Check if username is already in use in the org - if (userCreateBody.username) { - await this.checkForUsernameConflicts(org.id, userCreateBody.username); - } - - // Create new org user - const createdUserCall = await createNewUsersConnectToOrgIfExists({ - invitations: [ - { - usernameOrEmail: userCreateBody.email, - role: userCreateBody.organizationRole, - }, - ], - teamId: org.id, - creationSource: CreationSource.API_V2, - isOrg: true, - parentId: null, - autoAcceptEmailDomain: "not-required-for-this-endpoint", - orgConnectInfoByUsernameOrEmail: { - [userCreateBody.email]: { - orgId: org.id, - autoAccept: userCreateBody.autoAccept, - }, - }, - language: "en", - }); - - const createdUser = createdUserCall[0]; - - // Update user fields that weren't included in createNewUsersConnectToOrgIfExists - const updateUserBody = plainToInstance(CreateUserInput, userCreateBody, { strategy: "excludeAll" }); - - // Update new user with other userCreateBody params - const user = await this.organizationsUsersRepository.updateOrganizationUser( - org.id, - createdUser.id, - updateUserBody - ); - - // Need to send email to new user to create password - await this.emailService.sendSignupToOrganizationEmail({ - usernameOrEmail: userCreateBody.email, - orgName: org.name, - orgId: org.id, - locale: user?.locale, - inviterName, - }); - - return user; - } - - async updateUser(orgId: number, userId: number, userUpdateBody: UpdateOrganizationUserInput) { - if (userUpdateBody.username) { - await this.checkForUsernameConflicts(orgId, userUpdateBody.username); - } - - const user = await this.organizationsUsersRepository.updateOrganizationUser( - orgId, - userId, - userUpdateBody - ); - return user; - } - - async deleteUser(orgId: number, userId: number) { - const user = await this.organizationsUsersRepository.deleteUser(orgId, userId); - return user; - } - - async checkForUsernameConflicts(orgId: number, username: string) { - const isUsernameTaken = await this.organizationsUsersRepository.getOrganizationUserByUsername( - orgId, - username - ); - - if (isUsernameTaken) throw new ConflictException("Username is already taken"); - } - - async getUsersByIds(orgId: number, userIds: number[]) { - const orgUsers = await this.organizationsUsersRepository.getOrganizationUsersByIds(orgId, userIds); - - if (!orgUsers?.length) { - throw new ForbiddenException("Provided user ids does not belong to the organization."); - } - - return orgUsers; - } -} diff --git a/apps/api/v2/src/modules/organizations/users/ooo/controllers/organizations-users-ooo.controller.ts b/apps/api/v2/src/modules/organizations/users/ooo/controllers/organizations-users-ooo.controller.ts deleted file mode 100644 index 8a28c65043d03d..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/ooo/controllers/organizations-users-ooo.controller.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard"; -import { IsUserOOO } from "@/modules/ooo/guards/is-user-ooo"; -import { - CreateOutOfOfficeEntryDto, - UpdateOutOfOfficeEntryDto, - GetOutOfOfficeEntryFiltersDTO, - GetOrgUsersOutOfOfficeEntryFiltersDTO, -} from "@/modules/ooo/inputs/ooo.input"; -import { - UserOooOutputDto, - UserOooOutputResponseDto, - UserOoosOutputResponseDto, -} from "@/modules/ooo/outputs/ooo.output"; -import { UserOOOService } from "@/modules/ooo/services/ooo.service"; -import { OrgUsersOOOService } from "@/modules/organizations/users/ooo/services/organization-users-ooo.service"; -import { - Controller, - UseGuards, - Get, - Post, - Patch, - Delete, - Param, - ParseIntPipe, - Body, - UseInterceptors, - Query, -} from "@nestjs/common"; -import { ClassSerializerInterceptor } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger"; -import { plainToInstance } from "class-transformer"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; - -@Controller({ - path: "/v2/organizations/:orgId", - version: API_VERSIONS_VALUES, -}) -@UseInterceptors(ClassSerializerInterceptor) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@UseGuards(IsOrgGuard) -@DocsTags("Orgs / Users / OOO") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -@ApiParam({ name: "orgId", type: Number, required: true }) -export class OrganizationsUsersOOOController { - constructor( - private readonly userOOOService: UserOOOService, - private readonly orgUsersOOOService: OrgUsersOOOService - ) {} - - @Get("/users/:userId/ooo") - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsUserInOrg) - @ApiOperation({ summary: "Get all out-of-office entries for a user" }) - async getOrganizationUserOOO( - @Param("userId", ParseIntPipe) userId: number, - @Query() query: GetOutOfOfficeEntryFiltersDTO - ): Promise { - const { skip, take, ...rest } = query ?? { skip: 0, take: 250 }; - const ooos = await this.userOOOService.getUserOOOPaginated(userId, skip ?? 0, take ?? 250, rest); - - return { - status: SUCCESS_STATUS, - data: ooos.map((ooo) => plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" })), - }; - } - - @Post("/users/:userId/ooo") - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsUserInOrg) - @ApiOperation({ summary: "Create an out-of-office entry for a user" }) - async createOrganizationUserOOO( - @Param("userId", ParseIntPipe) userId: number, - @Body() input: CreateOutOfOfficeEntryDto - ): Promise { - const ooo = await this.userOOOService.createUserOOO(userId, input); - return { - status: SUCCESS_STATUS, - data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }), - }; - } - - @Patch("/users/:userId/ooo/:oooId") - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsUserInOrg, IsUserOOO) - @ApiOperation({ summary: "Update an out-of-office entry for a user" }) - async updateOrganizationUserOOO( - @Param("userId", ParseIntPipe) userId: number, - @Param("oooId", ParseIntPipe) oooId: number, - - @Body() input: UpdateOutOfOfficeEntryDto - ): Promise { - const ooo = await this.userOOOService.updateUserOOO(userId, oooId, input); - return { - status: SUCCESS_STATUS, - data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }), - }; - } - - @Delete("/users/:userId/ooo/:oooId") - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsUserInOrg, IsUserOOO) - @ApiOperation({ summary: "Delete an out-of-office entry for a user" }) - @ApiParam({ name: "userId", type: Number, required: true }) - async deleteOrganizationUserOOO( - @Param("oooId", ParseIntPipe) oooId: number - ): Promise { - const ooo = await this.userOOOService.deleteUserOOO(oooId); - return { - status: SUCCESS_STATUS, - data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }), - }; - } - - @Get("/ooo") - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @ApiOperation({ summary: "Get all out-of-office entries for organization users" }) - async getOrganizationUsersOOO( - @Param("orgId", ParseIntPipe) orgId: number, - @Query() query: GetOrgUsersOutOfOfficeEntryFiltersDTO - ): Promise { - const { skip, take, email, ...rest } = query ?? { skip: 0, take: 250 }; - const ooos = await this.orgUsersOOOService.getOrgUsersOOOPaginated(orgId, skip ?? 0, take ?? 250, rest, { - email, - }); - - return { - status: SUCCESS_STATUS, - data: ooos.map((ooo) => plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" })), - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/users/ooo/controllers/organizations-users-ooo.e2e-spec.ts b/apps/api/v2/src/modules/organizations/users/ooo/controllers/organizations-users-ooo.e2e-spec.ts deleted file mode 100644 index 95f5d9ea0626e1..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/ooo/controllers/organizations-users-ooo.e2e-spec.ts +++ /dev/null @@ -1,444 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { withApiAuth } from "test/utils/withApiAuth"; -import { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { UserOooOutputDto } from "@/modules/ooo/outputs/ooo.output"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations User OOO Endpoints", () => { - describe("User Authentication - User is Org Admin", () => { - let app: INestApplication; - let oooCreatedViaApiId: number; - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - - let org: Team; - let team: Team; - let falseTestOrg: Team; - let falseTestTeam: Team; - - const userEmail = `organizations-users-ooo-admin-${randomString()}@api.com`; - let userAdmin: User; - - const teammate1Email = `organizations-users-ooo-member1-${randomString()}@api.com`; - const teammate2Email = `organizations-users-ooo-member2-${randomString()}@api.com`; - const falseTestUserEmail = `organizations-users-ooo-false-user-${randomString()}@api.com`; - let teammate1: User; - let teammate2: User; - let falseTestUser: User; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - - userAdmin = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - role: "ADMIN", - }); - - teammate1 = await userRepositoryFixture.create({ - email: teammate1Email, - username: teammate1Email, - }); - - teammate2 = await userRepositoryFixture.create({ - email: teammate2Email, - username: teammate2Email, - }); - - falseTestUser = await userRepositoryFixture.create({ - email: falseTestUserEmail, - username: falseTestUserEmail, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-users-ooo-organization-${randomString()}`, - isOrganization: true, - }); - - falseTestOrg = await organizationsRepositoryFixture.create({ - name: `organizations-users-ooo-false-org-${randomString()}`, - isOrganization: true, - }); - - team = await teamsRepositoryFixture.create({ - name: `organizations-users-ooo-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - falseTestTeam = await teamsRepositoryFixture.create({ - name: `organizations-users-ooo-false-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: falseTestOrg.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${userAdmin.id}`, - username: userEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: userAdmin.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teammate1.id}`, - username: teammate1Email, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: teammate1.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: userAdmin.id } }, - team: { connect: { id: org.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammate1.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammate2.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: falseTestUser.id } }, - team: { connect: { id: falseTestTeam.id } }, - accepted: true, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(userAdmin).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should create a ooo entry without redirect", async () => { - const body = { - start: "2025-05-01T01:00:00.000Z", - end: "2025-05-10T13:59:59.999Z", - notes: "ooo numero uno", - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`) - .send(body) - .expect(201) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data as UserOooOutputDto; - expect(data.reason).toEqual("unspecified"); - expect(data.userId).toEqual(teammate1.id); - expect(data.start).toEqual("2025-05-01T00:00:00.000Z"); - expect(data.end).toEqual("2025-05-10T23:59:59.999Z"); - oooCreatedViaApiId = data.id; - }); - }); - - it("should create a ooo entry with redirect", async () => { - const body = { - start: "2025-08-01T01:00:00.000Z", - end: "2025-10-10T13:59:59.999Z", - notes: "ooo numero dos with redirect", - toUserId: teammate2.id, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`) - .send(body) - .expect(201) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data as UserOooOutputDto; - expect(data.reason).toEqual("unspecified"); - expect(data.userId).toEqual(teammate1.id); - expect(data.start).toEqual("2025-08-01T00:00:00.000Z"); - expect(data.end).toEqual("2025-10-10T23:59:59.999Z"); - expect(data.toUserId).toEqual(teammate2.id); - }); - }); - - it("should fail to create a ooo entry with start after end", async () => { - const body = { - start: "2025-07-01T00:00:00.000Z", - end: "2025-05-10T23:59:59.999Z", - notes: "ooo numero uno duplicate", - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`) - .send(body) - .expect(400); - }); - - it("should fail to create a duplicate ooo entry", async () => { - const body = { - start: "2025-05-01T00:00:00.000Z", - end: "2025-05-10T23:59:59.999Z", - notes: "ooo numero uno duplicate", - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`) - .send(body) - .expect(409); - }); - - it("should fail to create an ooo entry that redirects to self", async () => { - const body = { - start: "2025-05-02T00:00:00.000Z", - end: "2025-05-03T23:59:59.999Z", - notes: "ooo infinite redirect", - toUserId: teammate1.id, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`) - .send(body) - .expect(400); - }); - - it("should fail to create an ooo entry that redirects to member outside of org", async () => { - const body = { - start: "2025-05-02T00:00:00.000Z", - end: "2025-05-03T23:59:59.999Z", - notes: "ooo invalid redirect", - toUserId: falseTestUser.id, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`) - .send(body) - .expect(400); - }); - - it("should update a ooo entry without redirect", async () => { - const body = { - start: "2025-06-01T01:00:00.000Z", - end: "2025-06-10T13:59:59.999Z", - notes: "ooo numero uno", - reason: "vacation", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`) - .send(body) - .expect(200) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data as UserOooOutputDto; - expect(data.reason).toEqual("vacation"); - expect(data.userId).toEqual(teammate1.id); - expect(data.start).toEqual("2025-06-01T00:00:00.000Z"); - expect(data.end).toEqual("2025-06-10T23:59:59.999Z"); - }); - }); - - it("should fail to update a ooo entry with redirect outside of org", async () => { - const body = { - toUserId: falseTestUser.id, - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`) - .send(body) - .expect(400); - }); - - it("should fail to update a ooo entry with start after end ", async () => { - const body = { - start: "2025-07-01T00:00:00.000Z", - end: "2025-05-10T23:59:59.999Z", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`) - .send(body) - .expect(400); - }); - - it("should fail to update a ooo entry with redirect to self ", async () => { - const body = { - toUserId: teammate1.id, - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`) - .send(body) - .expect(400); - }); - - it("should fail to update a ooo entry with duplicate time", async () => { - const body = { - start: "2025-06-01T01:00:00.000Z", - end: "2025-06-10T13:59:59.999Z", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`) - .send(body) - .expect(409); - }); - - it("should update a ooo entry without redirect", async () => { - const body = { - toUserId: teammate2.id, - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`) - .send(body) - .expect(200) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data as UserOooOutputDto; - expect(data.reason).toEqual("vacation"); - expect(data.toUserId).toEqual(teammate2.id); - expect(data.userId).toEqual(teammate1.id); - expect(data.start).toEqual("2025-06-01T00:00:00.000Z"); - expect(data.end).toEqual("2025-06-10T23:59:59.999Z"); - }); - }); - - it("should get 2 ooo entries", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/ooo?sortEnd=desc&email=${teammate1Email}`) - .expect(200) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data as UserOooOutputDto[]; - expect(data.length).toEqual(2); - const oooUno = data.find((ooo) => ooo.id === oooCreatedViaApiId); - expect(oooUno).toBeDefined(); - if (oooUno) { - expect(oooUno.reason).toEqual("vacation"); - expect(oooUno.toUserId).toEqual(teammate2.id); - expect(oooUno.userId).toEqual(teammate1.id); - expect(oooUno.start).toEqual("2025-06-01T00:00:00.000Z"); - expect(oooUno.end).toEqual("2025-06-10T23:59:59.999Z"); - } - // test sort - expect(data[1].id).toEqual(oooCreatedViaApiId); - }); - }); - - it("should get 2 ooo entries", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`) - .expect(200) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data as UserOooOutputDto[]; - expect(data.length).toEqual(2); - const oooUno = data.find((ooo) => ooo.id === oooCreatedViaApiId); - expect(oooUno).toBeDefined(); - if (oooUno) { - expect(oooUno.reason).toEqual("vacation"); - expect(oooUno.toUserId).toEqual(teammate2.id); - expect(oooUno.userId).toEqual(teammate1.id); - expect(oooUno.start).toEqual("2025-06-01T00:00:00.000Z"); - expect(oooUno.end).toEqual("2025-06-10T23:59:59.999Z"); - } - }); - }); - - it("should delete ooo entry", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`) - .expect(200); - }); - - it("user should have 1 ooo entries", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`) - .expect(200) - .then((response) => { - const responseBody = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data as UserOooOutputDto[]; - expect(data.length).toEqual(1); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(userAdmin.email); - await userRepositoryFixture.deleteByEmail(teammate1.email); - await userRepositoryFixture.deleteByEmail(teammate2.email); - await userRepositoryFixture.deleteByEmail(falseTestUser.email); - await teamsRepositoryFixture.delete(team.id); - await teamsRepositoryFixture.delete(falseTestTeam.id); - await organizationsRepositoryFixture.delete(org.id); - await organizationsRepositoryFixture.delete(falseTestOrg.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/users/ooo/organizations-users-ooo.repository.ts b/apps/api/v2/src/modules/organizations/users/ooo/organizations-users-ooo.repository.ts deleted file mode 100644 index 9c4664caa97d14..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/ooo/organizations-users-ooo.repository.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrgUsersOOORepository { - constructor(private readonly dbRead: PrismaReadService) {} - async getOrgUsersOOOPaginated( - orgId: number, - skip: number, - take: number, - sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" }, - filters?: { email?: string } - ) { - return this.dbRead.prisma.outOfOfficeEntry.findMany({ - where: { - user: { - ...(filters?.email && { email: filters.email }), - profiles: { - some: { - organizationId: orgId, - }, - }, - }, - }, - skip, - take, - include: { reason: true }, - ...(sort?.sortStart && { orderBy: { start: sort.sortStart } }), - ...(sort?.sortEnd && { orderBy: { end: sort.sortEnd } }), - }); - } -} diff --git a/apps/api/v2/src/modules/organizations/users/ooo/services/organization-users-ooo.service.ts b/apps/api/v2/src/modules/organizations/users/ooo/services/organization-users-ooo.service.ts deleted file mode 100644 index 08afa61293799e..00000000000000 --- a/apps/api/v2/src/modules/organizations/users/ooo/services/organization-users-ooo.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository"; -import { UserOOOService } from "@/modules/ooo/services/ooo.service"; -import { OrgUsersOOORepository } from "@/modules/organizations/users/ooo/organizations-users-ooo.repository"; -import { UsersRepository } from "@/modules/users/users.repository"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class OrgUsersOOOService { - constructor( - private readonly oooRepository: UserOOORepository, - private readonly oooUserService: UserOOOService, - private readonly usersRepository: UsersRepository, - private readonly orgUsersOOORepository: OrgUsersOOORepository - ) {} - - async getOrgUsersOOOPaginated( - orgId: number, - skip: number, - take: number, - sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" }, - filters?: { email?: string } - ) { - const ooos = await this.orgUsersOOORepository.getOrgUsersOOOPaginated(orgId, skip, take, sort, filters); - return ooos.map((ooo) => this.oooUserService.formatOooReason(ooo)); - } -} diff --git a/apps/api/v2/src/modules/organizations/webhooks/controllers/organizations-webhooks.controller.ts b/apps/api/v2/src/modules/organizations/webhooks/controllers/organizations-webhooks.controller.ts deleted file mode 100644 index a048b117803f17..00000000000000 --- a/apps/api/v2/src/modules/organizations/webhooks/controllers/organizations-webhooks.controller.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { - OPTIONAL_API_KEY_HEADER, - OPTIONAL_X_CAL_CLIENT_ID_HEADER, - OPTIONAL_X_CAL_SECRET_KEY_HEADER, -} from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; -import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; -import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; -import { IsWebhookInOrg } from "@/modules/auth/guards/organizations/is-webhook-in-org.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { OrganizationsWebhooksService } from "@/modules/organizations/webhooks/services/organizations-webhooks.service"; -import { CreateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input"; -import { UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input"; -import { - TeamWebhookOutputDto as OrgWebhookOutputDto, - TeamWebhookOutputResponseDto as OrgWebhookOutputResponseDto, - TeamWebhooksOutputResponseDto as OrgWebhooksOutputResponseDto, -} from "@/modules/webhooks/outputs/team-webhook.output"; -import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe"; -import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe"; -import { WebhooksService } from "@/modules/webhooks/services/webhooks.service"; -import { - Controller, - UseGuards, - Get, - Param, - ParseIntPipe, - Query, - Delete, - Patch, - Post, - Body, - HttpCode, - HttpStatus, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { SkipTakePagination } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/organizations/:orgId/webhooks", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) -@DocsTags("Orgs / Webhooks") -@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) -@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) -@ApiHeader(OPTIONAL_API_KEY_HEADER) -@ApiParam({ name: "orgId", type: Number, required: true }) -export class OrganizationsWebhooksController { - constructor( - private organizationsWebhooksService: OrganizationsWebhooksService, - private webhooksService: WebhooksService - ) {} - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Get("/") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Get all webhooks" }) - async getAllOrganizationWebhooks( - @Param("orgId", ParseIntPipe) orgId: number, - @Query() queryParams: SkipTakePagination - ): Promise { - const { skip, take } = queryParams; - const webhooks = await this.organizationsWebhooksService.getWebhooksPaginated( - orgId, - skip ?? 0, - take ?? 250 - ); - return { - status: SUCCESS_STATUS, - data: webhooks.map((webhook) => - plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { - strategy: "excludeAll", - }) - ), - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @Post("/") - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: "Create a webhook" }) - async createOrganizationWebhook( - @Param("orgId", ParseIntPipe) orgId: number, - @Body() body: CreateWebhookInputDto - ): Promise { - const webhook = await this.organizationsWebhooksService.createWebhook( - orgId, - new WebhookInputPipe().transform(body) - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { - strategy: "excludeAll", - }), - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsWebhookInOrg) - @Get("/:webhookId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Get a webhook" }) - async getOrganizationWebhook(@Param("webhookId") webhookId: string): Promise { - const webhook = await this.organizationsWebhooksService.getWebhook(webhookId); - return { - status: SUCCESS_STATUS, - data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { - strategy: "excludeAll", - }), - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsWebhookInOrg) - @Delete("/:webhookId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Delete a webhook" }) - async deleteWebhook(@Param("webhookId") webhookId: string): Promise { - const webhook = await this.webhooksService.deleteWebhook(webhookId); - return { - status: SUCCESS_STATUS, - data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { - strategy: "excludeAll", - }), - }; - } - - @Roles("ORG_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(IsWebhookInOrg) - @Patch("/:webhookId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Update a webhook" }) - async updateOrgWebhook( - @Param("webhookId") webhookId: string, - @Body() body: UpdateWebhookInputDto - ): Promise { - const webhook = await this.organizationsWebhooksService.updateWebhook( - webhookId, - new PartialWebhookInputPipe().transform(body) - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { - strategy: "excludeAll", - }), - }; - } -} diff --git a/apps/api/v2/src/modules/organizations/webhooks/controllers/organizations-webhooks.e2e-spec.ts b/apps/api/v2/src/modules/organizations/webhooks/controllers/organizations-webhooks.e2e-spec.ts deleted file mode 100644 index f0ba3dff748d99..00000000000000 --- a/apps/api/v2/src/modules/organizations/webhooks/controllers/organizations-webhooks.e2e-spec.ts +++ /dev/null @@ -1,306 +0,0 @@ -import type { Team, 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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input"; -import { - TeamWebhookOutputResponseDto, - TeamWebhooksOutputResponseDto, -} from "@/modules/webhooks/outputs/team-webhook.output"; - -describe("WebhooksController (e2e)", () => { - let app: INestApplication; - const userEmail = `organizations-webhooks-admin-${randomString()}@api.com`; - let org: Team; - - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let webhookRepositoryFixture: WebhookRepositoryFixture; - let userRepositoryFixture: UserRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let webhook: TeamWebhookOutputResponseDto["data"]; - let otherWebhook: Webhook; - let user: UserWithProfile; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-webhooks-organization-${randomString()}`, - isOrganization: true, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); - - otherWebhook = await webhookRepositoryFixture.create({ - id: "2mdfnn2", - subscriberUrl: "https://example.com", - eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: true, - payloadTemplate: "string", - }); - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - afterAll(async () => { - userRepositoryFixture.deleteByEmail(user.email); - webhookRepositoryFixture.delete(otherWebhook.id); - await app.close(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("/organizations/:orgId/webhooks (POST)", () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/webhooks`) - .send({ - subscriberUrl: "https://example.com", - triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: true, - payloadTemplate: "string", - } satisfies CreateWebhookInputDto) - .expect(201) - .then(async (res) => { - process.stdout.write(JSON.stringify(res.body)); - expect(res.body).toMatchObject({ - status: "success", - data: { - id: expect.any(String), - subscriberUrl: "https://example.com", - triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: true, - payloadTemplate: "string", - teamId: org.id, - }, - } satisfies TeamWebhookOutputResponseDto); - webhook = res.body.data; - }); - }); - - it("/organizations/:orgId/webhooks (POST) should fail to create a webhook that already has same orgId / subcriberUrl combo", () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/webhooks`) - .send({ - subscriberUrl: "https://example.com", - triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: true, - payloadTemplate: "string", - } satisfies CreateWebhookInputDto) - .expect(409); - }); - - it("/organizations/:orgId/webhooks/:webhookId (PATCH)", () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/webhooks/${webhook.id}`) - .send({ - active: false, - } satisfies UpdateWebhookInputDto) - .expect(200) - .then((res) => { - expect(res.body.data.active).toBe(false); - }); - }); - - it("/organizations/:orgId/webhooks/:webhookId (GET)", () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/webhooks/${webhook.id}`) - .expect(200) - .then((res) => { - expect(res.body).toMatchObject({ - status: "success", - data: { - id: expect.any(String), - subscriberUrl: "https://example.com", - triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: false, - payloadTemplate: "string", - teamId: org.id, - }, - } satisfies TeamWebhookOutputResponseDto); - }); - }); - - it("/organizations/:orgId/webhooks/:webhookId (GET) should say forbidden to get a webhook that does not exist", () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/webhooks/90284`).expect(403); - }); - - it("/organizations/:orgId/webhooks/:webhookId (GET) should fail to get a webhook that does not belong to org", () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/webhooks/${otherWebhook.id}`) - .expect(403); - }); - - it("/organizations/:orgId/webhooks (GET)", () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/webhooks`) - .expect(200) - .then((res) => { - const responseBody = res.body as TeamWebhooksOutputResponseDto; - responseBody.data.forEach((webhook) => { - expect(webhook.teamId).toBe(org.id); - }); - }); - }); - - it("/organizations/:orgId/webhooks/:webhookId (DELETE)", () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/webhooks/${webhook.id}`) - .expect(200) - .then((res) => { - expect(res.body).toMatchObject({ - status: "success", - data: { - id: expect.any(String), - subscriberUrl: "https://example.com", - triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: false, - payloadTemplate: "string", - teamId: org.id, - }, - } satisfies TeamWebhookOutputResponseDto); - }); - }); - - it("/organizations/:orgId/webhooks/:webhookId (DELETE) should fail to delete a webhook that does not exist", () => { - return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/webhooks/12993`).expect(403); - }); - - it("/organizations/:orgId/webhooks/:webhookId (DELETE) should fail to delete a webhook that does not belong to org", () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/webhooks/${otherWebhook.id}`) - .expect(403); - }); - - describe("DELEGATION_CREDENTIAL_ERROR webhook trigger", () => { - let delegationWebhook: TeamWebhookOutputResponseDto["data"]; - - it("/organizations/:orgId/webhooks (POST) should create webhook with DELEGATION_CREDENTIAL_ERROR trigger", () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/webhooks`) - .send({ - subscriberUrl: "https://example.com/delegation-errors", - triggers: ["DELEGATION_CREDENTIAL_ERROR"], - active: true, - } satisfies CreateWebhookInputDto) - .expect(201) - .then(async (res) => { - expect(res.body.status).toBe("success"); - expect(res.body.data).toMatchObject({ - id: expect.any(String), - subscriberUrl: "https://example.com/delegation-errors", - triggers: ["DELEGATION_CREDENTIAL_ERROR"], - active: true, - teamId: org.id, - }); - delegationWebhook = res.body.data; - }); - }); - - it("/organizations/:orgId/webhooks/:webhookId (GET) should retrieve webhook with DELEGATION_CREDENTIAL_ERROR trigger", () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/webhooks/${delegationWebhook.id}`) - .expect(200) - .then((res) => { - expect(res.body.status).toBe("success"); - expect(res.body.data).toMatchObject({ - id: delegationWebhook.id, - subscriberUrl: "https://example.com/delegation-errors", - triggers: ["DELEGATION_CREDENTIAL_ERROR"], - active: true, - teamId: org.id, - }); - }); - }); - - it("/organizations/:orgId/webhooks/:webhookId (PATCH) should update webhook with DELEGATION_CREDENTIAL_ERROR trigger", () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/webhooks/${delegationWebhook.id}`) - .send({ - active: false, - subscriberUrl: "https://example.com/delegation-errors-updated", - } satisfies UpdateWebhookInputDto) - .expect(200) - .then((res) => { - expect(res.body.data.active).toBe(false); - expect(res.body.data.subscriberUrl).toBe("https://example.com/delegation-errors-updated"); - expect(res.body.data.triggers).toEqual(["DELEGATION_CREDENTIAL_ERROR"]); - }); - }); - - it("/organizations/:orgId/webhooks (POST) should allow combining DELEGATION_CREDENTIAL_ERROR with other triggers", () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/webhooks`) - .send({ - subscriberUrl: "https://example.com/combined-webhook", - triggers: ["BOOKING_CREATED", "DELEGATION_CREDENTIAL_ERROR"], - active: true, - } satisfies CreateWebhookInputDto) - .expect(201) - .then(async (res) => { - expect(res.body.status).toBe("success"); - expect(res.body.data).toMatchObject({ - id: expect.any(String), - subscriberUrl: "https://example.com/combined-webhook", - triggers: expect.arrayContaining(["BOOKING_CREATED", "DELEGATION_CREDENTIAL_ERROR"]), - active: true, - teamId: org.id, - }); - await request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/webhooks/${res.body.data.id}`) - .expect(200); - }); - }); - - it("/organizations/:orgId/webhooks/:webhookId (DELETE) should delete webhook with DELEGATION_CREDENTIAL_ERROR trigger", () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/webhooks/${delegationWebhook.id}`) - .expect(200) - .then((res) => { - expect(res.body.status).toBe("success"); - expect(res.body.data).toMatchObject({ - id: delegationWebhook.id, - triggers: ["DELEGATION_CREDENTIAL_ERROR"], - teamId: org.id, - }); - }); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/webhooks/organizations-webhooks.repository.ts b/apps/api/v2/src/modules/organizations/webhooks/organizations-webhooks.repository.ts deleted file mode 100644 index ff929b9841e172..00000000000000 --- a/apps/api/v2/src/modules/organizations/webhooks/organizations-webhooks.repository.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; -import { v4 as uuidv4 } from "uuid"; - -import type { Webhook } from "@calcom/prisma/client"; - -type WebhookInputData = Pick< - Webhook, - "payloadTemplate" | "eventTriggers" | "subscriberUrl" | "secret" | "active" ->; -@Injectable() -export class OrganizationsWebhooksRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async findWebhookByUrl(organizationId: number, subscriberUrl: string) { - return this.dbRead.prisma.webhook.findFirst({ - where: { teamId: organizationId, subscriberUrl }, - }); - } - - async findWebhook(organizationId: number, webhookId: string) { - return this.dbRead.prisma.webhook.findUnique({ - where: { - id: webhookId, - teamId: organizationId, - }, - }); - } - - async findWebhooks(organizationId: number) { - return this.dbRead.prisma.webhook.findMany({ - where: { - teamId: organizationId, - }, - }); - } - - async deleteWebhook(organizationId: number, webhookId: string) { - return this.dbRead.prisma.webhook.delete({ - where: { - id: webhookId, - teamId: organizationId, - }, - }); - } - - async createWebhook(organizationId: number, data: WebhookInputData) { - const id = uuidv4(); - return this.dbWrite.prisma.webhook.create({ - data: { ...data, id, teamId: organizationId }, - }); - } - - async updateWebhook(organizationId: number, webhookId: string, data: Partial) { - return this.dbRead.prisma.webhook.update({ - data: { ...data }, - where: { id: webhookId, teamId: organizationId }, - }); - } - - async findWebhooksPaginated(organizationId: number, skip: number, take: number) { - return this.dbRead.prisma.webhook.findMany({ - where: { - teamId: organizationId, - }, - skip, - take, - }); - } -} diff --git a/apps/api/v2/src/modules/organizations/webhooks/services/organizations-webhooks.service.ts b/apps/api/v2/src/modules/organizations/webhooks/services/organizations-webhooks.service.ts deleted file mode 100644 index 22d145ba1629b9..00000000000000 --- a/apps/api/v2/src/modules/organizations/webhooks/services/organizations-webhooks.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { OrganizationsWebhooksRepository } from "@/modules/organizations/webhooks/organizations-webhooks.repository"; -import { UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input"; -import { PipedInputWebhookType } from "@/modules/webhooks/pipes/WebhookInputPipe"; -import { validateWebhookUrl, validateWebhookUrlIfChanged } from "@/modules/webhooks/utils/validate-webhook-url"; -import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository"; -import { ConflictException, Injectable, NotFoundException } from "@nestjs/common"; - -@Injectable() -export class OrganizationsWebhooksService { - constructor( - private readonly organizationsWebhooksRepository: OrganizationsWebhooksRepository, - private readonly webhooksRepository: WebhooksRepository - ) {} - - async createWebhook(orgId: number, body: PipedInputWebhookType) { - validateWebhookUrl(body.subscriberUrl); - - const existingWebhook = await this.organizationsWebhooksRepository.findWebhookByUrl( - orgId, - body.subscriberUrl - ); - if (existingWebhook) { - throw new ConflictException("Webhook with this subscriber url already exists for this user"); - } - - return this.organizationsWebhooksRepository.createWebhook(orgId, { - ...body, - payloadTemplate: body.payloadTemplate ?? null, - secret: body.secret ?? null, - }); - } - - async getWebhooksPaginated(orgId: number, skip: number, take: number) { - return this.organizationsWebhooksRepository.findWebhooksPaginated(orgId, skip, take); - } - - async getWebhook(webhookId: string) { - const webhook = await this.webhooksRepository.getWebhookById(webhookId); - if (!webhook) { - throw new NotFoundException(`Webhook (${webhookId}) not found`); - } - return webhook; - } - - async updateWebhook(webhookId: string, body: UpdateWebhookInputDto) { - const existingSubscriberUrl = await this.webhooksRepository.getWebhookSubscriberUrl(webhookId); - validateWebhookUrlIfChanged(body.subscriberUrl, existingSubscriberUrl); - return this.webhooksRepository.updateWebhook(webhookId, body); - } -} diff --git a/apps/api/v2/src/modules/roles/permissions/services/roles-permissions-cache.service.ts b/apps/api/v2/src/modules/roles/permissions/services/roles-permissions-cache.service.ts deleted file mode 100644 index 6d0211055b9f38..00000000000000 --- a/apps/api/v2/src/modules/roles/permissions/services/roles-permissions-cache.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { RedisService } from "@/modules/redis/redis.service"; -import { Injectable, Logger } from "@nestjs/common"; - -export const REDIS_TEAM_PERMISSIONS_CACHE_PATTERN = (teamId: number) => - `apiv2:user:*:team:${teamId}:requiredPermissions:*:guard:pbac`; - -@Injectable() -export class RolesPermissionsCacheService { - private readonly logger = new Logger(RolesPermissionsCacheService.name); - - constructor(private readonly redisService: RedisService) {} - - async deleteTeamPermissionsCache(teamId: number): Promise { - try { - const pattern = REDIS_TEAM_PERMISSIONS_CACHE_PATTERN(teamId); - const keys = await this.redisService.getKeys(pattern); - - if (keys.length > 0) { - await this.redisService.delMany(keys); - this.logger.log(`Deleted ${keys.length} permission cache keys for team ${teamId}`); - } else { - this.logger.log(`No permission cache keys found for team ${teamId}`); - } - } catch (error) { - this.logger.error(`Failed to delete permission cache for team ${teamId}:`, error); - } - } -} diff --git a/apps/api/v2/src/modules/roles/permissions/services/roles-permissions-output.service.ts b/apps/api/v2/src/modules/roles/permissions/services/roles-permissions-output.service.ts deleted file mode 100644 index ca4d8fd08d3c92..00000000000000 --- a/apps/api/v2/src/modules/roles/permissions/services/roles-permissions-output.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Injectable } from "@nestjs/common"; - -import type { PermissionString, Role } from "@calcom/platform-libraries/pbac"; - -@Injectable() -export class RolesPermissionsOutputService { - getPermissionsFromRole(role: Role): PermissionString[] { - return role.permissions.map( - (permission) => `${permission.resource}.${permission.action}` as PermissionString - ); - } -} diff --git a/apps/api/v2/src/modules/roles/permissions/services/roles-permissions.service.ts b/apps/api/v2/src/modules/roles/permissions/services/roles-permissions.service.ts deleted file mode 100644 index adafce62757e80..00000000000000 --- a/apps/api/v2/src/modules/roles/permissions/services/roles-permissions.service.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { RolesPermissionsCacheService } from "@/modules/roles/permissions/services/roles-permissions-cache.service"; -import { RolesPermissionsOutputService } from "@/modules/roles/permissions/services/roles-permissions-output.service"; -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; - -import type { PermissionString, UpdateRolePermissionsData } from "@calcom/platform-libraries/pbac"; -import { RoleService, isValidPermissionString } from "@calcom/platform-libraries/pbac"; - -@Injectable() -export class RolesPermissionsService { - constructor( - private readonly roleService: RoleService, - private readonly rolesPermissionsOutputService: RolesPermissionsOutputService, - private readonly rolesPermissionsCacheService: RolesPermissionsCacheService - ) {} - - async getRolePermissions(teamId: number, roleId: string) { - const belongsToTeam = await this.roleService.roleBelongsToTeam(roleId, teamId); - if (!belongsToTeam) { - throw new NotFoundException(`Role with id ${roleId} within team id ${teamId} not found`); - } - - const role = await this.roleService.getRole(roleId); - if (!role) { - throw new NotFoundException(`Role with id ${roleId} within team id ${teamId} not found`); - } - - return this.rolesPermissionsOutputService.getPermissionsFromRole(role); - } - - async addRolePermissions(teamId: number, roleId: string, permissionsToAdd: PermissionString[]) { - const belongsToTeam = await this.roleService.roleBelongsToTeam(roleId, teamId); - if (!belongsToTeam) { - throw new NotFoundException(`Role with id ${roleId} within team id ${teamId} not found`); - } - - const current = await this.getRolePermissions(teamId, roleId); - const desired = Array.from(new Set([...(current || []), ...(permissionsToAdd || [])])); - - const updateData: UpdateRolePermissionsData = { - roleId, - permissions: desired, - updates: {}, - }; - - try { - const updatedRole = await this.roleService.update(updateData); - - await this.rolesPermissionsCacheService.deleteTeamPermissionsCache(teamId); - - return this.rolesPermissionsOutputService.getPermissionsFromRole(updatedRole); - } catch (error) { - if (error instanceof Error && error.message.includes("Invalid permissions provided")) { - throw new BadRequestException(error.message); - } - throw error; - } - } - - async removeRolePermission(teamId: number, roleId: string, permissionToRemove: PermissionString) { - if (!isValidPermissionString(permissionToRemove)) { - throw new BadRequestException( - `Permission '${permissionToRemove}' must be a valid permission string in format 'resource.action' (e.g., 'eventType.read', 'booking.create')` - ); - } - const belongsToTeam = await this.roleService.roleBelongsToTeam(roleId, teamId); - if (!belongsToTeam) { - throw new NotFoundException(`Role with id ${roleId} within team id ${teamId} not found`); - } - - const current = await this.getRolePermissions(teamId, roleId); - const desired = (current || []).filter((p) => p !== permissionToRemove); - - const updateData: UpdateRolePermissionsData = { - roleId, - permissions: desired, - updates: {}, - }; - - try { - const updatedRole = await this.roleService.update(updateData); - - await this.rolesPermissionsCacheService.deleteTeamPermissionsCache(teamId); - - return this.rolesPermissionsOutputService.getPermissionsFromRole(updatedRole); - } catch (error) { - if (error instanceof Error) { - if (error.message.includes("Invalid permissions provided")) { - throw new BadRequestException(error.message); - } - if (error.message.includes("Cannot update default roles")) { - throw new BadRequestException(error.message); - } - } - throw error; - } - } - - async removeRolePermissions(teamId: number, roleId: string, permissionsToRemove: PermissionString[]) { - if (permissionsToRemove && permissionsToRemove.length > 0) { - for (const permission of permissionsToRemove) { - if (!isValidPermissionString(permission)) { - throw new BadRequestException( - `Permission '${permission}' must be a valid permission string in format 'resource.action' (e.g., 'eventType.read', 'booking.create')` - ); - } - } - } - const belongsToTeam = await this.roleService.roleBelongsToTeam(roleId, teamId); - if (!belongsToTeam) { - throw new NotFoundException(`Role with id ${roleId} within team id ${teamId} not found`); - } - - const current = await this.getRolePermissions(teamId, roleId); - const toRemove = new Set(permissionsToRemove || []); - const desired = (current || []).filter((p) => !toRemove.has(p)); - - const updateData: UpdateRolePermissionsData = { - roleId, - permissions: desired, - updates: {}, - }; - - try { - const updatedRole = await this.roleService.update(updateData); - - await this.rolesPermissionsCacheService.deleteTeamPermissionsCache(teamId); - - return this.rolesPermissionsOutputService.getPermissionsFromRole(updatedRole); - } catch (error) { - if (error instanceof Error) { - if (error.message.includes("Invalid permissions provided")) { - throw new BadRequestException(error.message); - } - if (error.message.includes("Cannot update default roles")) { - throw new BadRequestException(error.message); - } - } - throw error; - } - } - - async setRolePermissions(teamId: number, roleId: string, permissions: PermissionString[]) { - const belongsToTeam = await this.roleService.roleBelongsToTeam(roleId, teamId); - if (!belongsToTeam) { - throw new NotFoundException(`Role with id ${roleId} within team id ${teamId} not found`); - } - - const updateData: UpdateRolePermissionsData = { - roleId, - permissions, - updates: {}, - }; - - try { - const updatedRole = await this.roleService.update(updateData); - - await this.rolesPermissionsCacheService.deleteTeamPermissionsCache(teamId); - - return this.rolesPermissionsOutputService.getPermissionsFromRole(updatedRole); - } catch (error) { - if (error instanceof Error) { - if (error.message.includes("Invalid permissions provided")) { - throw new BadRequestException(error.message); - } - if (error.message.includes("Cannot update default roles")) { - throw new BadRequestException(error.message); - } - } - throw error; - } - } -} diff --git a/apps/api/v2/src/modules/roles/roles.module.ts b/apps/api/v2/src/modules/roles/roles.module.ts deleted file mode 100644 index a39f66eacf66d2..00000000000000 --- a/apps/api/v2/src/modules/roles/roles.module.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OrganizationsRolesOutputService } from "@/modules/organizations/roles/services/organizations-roles-output.service"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { Module } from "@nestjs/common"; - -import { RoleService } from "@calcom/platform-libraries/pbac"; - -import { TeamRolesOutputService } from "../organizations/teams/roles/services/team-roles-output.service"; -import { RolesPermissionsCacheService } from "./permissions/services/roles-permissions-cache.service"; -import { RolesPermissionsOutputService } from "./permissions/services/roles-permissions-output.service"; -import { RolesPermissionsService } from "./permissions/services/roles-permissions.service"; -import { RolesService } from "./services/roles.service"; - -@Module({ - imports: [StripeModule, PrismaModule, RedisModule, MembershipsModule], - providers: [ - { - provide: RoleService, - useFactory: () => new RoleService(), - }, - RolesService, - TeamRolesOutputService, - OrganizationsRolesOutputService, - RolesPermissionsService, - RolesPermissionsOutputService, - RolesPermissionsCacheService, - ], - exports: [ - RolesService, - TeamRolesOutputService, - OrganizationsRolesOutputService, - RolesPermissionsService, - RolesPermissionsOutputService, - RolesPermissionsCacheService, - ], -}) -export class RolesModule {} diff --git a/apps/api/v2/src/modules/roles/services/roles.service.ts b/apps/api/v2/src/modules/roles/services/roles.service.ts deleted file mode 100644 index fe2534ed0b4bb8..00000000000000 --- a/apps/api/v2/src/modules/roles/services/roles.service.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { CreateTeamRoleInput } from "@/modules/organizations/teams/roles/inputs/create-team-role.input"; -import { UpdateTeamRoleInput } from "@/modules/organizations/teams/roles/inputs/update-team-role.input"; -import { RolesPermissionsCacheService } from "@/modules/roles/permissions/services/roles-permissions-cache.service"; -import { BadRequestException, Injectable, NotFoundException, Logger } from "@nestjs/common"; - -import { RoleService } from "@calcom/platform-libraries/pbac"; -import type { CreateRoleData, UpdateRolePermissionsData } from "@calcom/platform-libraries/pbac"; - -@Injectable() -export class RolesService { - private readonly logger = new Logger(RolesService.name); - constructor( - private readonly rolesService: RoleService, - private readonly rolesPermissionsCacheService: RolesPermissionsCacheService - ) {} - - async createRole(teamId: number, data: CreateTeamRoleInput) { - const createRoleData: CreateRoleData = { - name: data.name, - color: data.color, - description: data.description, - permissions: data.permissions || [], - teamId: teamId, - type: "CUSTOM", - }; - - try { - const role = await this.rolesService.createRole(createRoleData); - - await this.rolesPermissionsCacheService.deleteTeamPermissionsCache(teamId); - - return role; - } catch (error) { - this.logger.error( - `RolesService - createRole failed (teamId=${teamId}, roleName=${data.name}): ${ - error instanceof Error ? `${error.name}: ${error.message}` : String(error) - }`, - error instanceof Error ? error.stack : undefined - ); - if (error instanceof Error) { - if (error.message.includes("already exists")) { - throw new BadRequestException(error.message); - } - // Map permission validation failures from PBAC service - if (error.message.toLowerCase().includes("permission")) { - throw new BadRequestException(error.message); - } - } - throw error; - } - } - - async getRole(teamId: number, roleId: string) { - const role = await this.rolesService.getRole(roleId); - - if (!role || role.teamId !== teamId) { - throw new NotFoundException(`Role with id ${roleId} within team id ${teamId} not found`); - } - - return role; - } - - async getTeamRoles(teamId: number, skip = 0, take = 250) { - const allRoles = await this.rolesService.getTeamRoles(teamId); - - const paginatedRoles = allRoles.slice(skip, skip + take); - - return paginatedRoles; - } - - async updateRole(teamId: number, roleId: string, data: UpdateTeamRoleInput) { - const belongsToTeam = await this.rolesService.roleBelongsToTeam(roleId, teamId); - if (!belongsToTeam) { - throw new NotFoundException(`Role with id ${roleId} within team id ${teamId} not found`); - } - - const updateData: UpdateRolePermissionsData = { - roleId, - permissions: data.permissions, - updates: { - name: data.name, - color: data.color, - }, - }; - - try { - const role = await this.rolesService.update(updateData); - - if (data.permissions) { - await this.rolesPermissionsCacheService.deleteTeamPermissionsCache(teamId); - } - - return role; - } catch (error) { - if (error instanceof Error) { - if (error.message.includes("Role not found")) { - throw new NotFoundException(error.message); - } - if (error.message.includes("Cannot update default roles")) { - throw new BadRequestException(error.message); - } - if ( - error.message.includes("Invalid permissions provided") || - error.message.toLowerCase().includes("permission") - ) { - throw new BadRequestException(error.message); - } - } - throw error; - } - } - - async deleteRole(teamId: number, roleId: string) { - const belongsToTeam = await this.rolesService.roleBelongsToTeam(roleId, teamId); - if (!belongsToTeam) { - throw new NotFoundException(`Role with id ${roleId} within team id ${teamId} not found`); - } - - const role = await this.rolesService.getRole(roleId); - if (!role) { - throw new NotFoundException(`Role with id ${roleId} within team id ${teamId} not found`); - } - - try { - await this.rolesService.deleteRole(roleId); - - await this.rolesPermissionsCacheService.deleteTeamPermissionsCache(teamId); - - return role; - } catch (error) { - if (error instanceof Error) { - if (error.message.includes("Cannot delete default roles")) { - throw new BadRequestException(error.message); - } - - if (error.message.includes("Role not found")) { - throw new NotFoundException(error.message); - } - } - throw error; - } - } -} diff --git a/apps/api/v2/src/modules/router/controllers/router.controller.ts b/apps/api/v2/src/modules/router/controllers/router.controller.ts deleted file mode 100644 index bb0c403f8d4f8f..00000000000000 --- a/apps/api/v2/src/modules/router/controllers/router.controller.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; -import { Controller, Req, NotFoundException, Param, Post, Body } from "@nestjs/common"; -import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger"; -import { Request } from "express"; - -import { - getRoutedUrl, - getTeamMemberEmailForResponseOrContactUsingUrlQuery, -} from "@calcom/platform-libraries"; -import { ApiResponse } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/router", - version: API_VERSIONS_VALUES, -}) -@DocsTags("Router controller") -@DocsExcludeController(true) -export class RouterController { - constructor(private readonly teamsEventTypesRepository: TeamsEventTypesRepository) {} - - @Post("/forms/:formId/submit") - async getRoutingFormResponse( - @Req() request: Request, - @Param("formId") formId: string, - @Body() body?: Record - ): Promise & { redirect: boolean })> { - const params = Object.fromEntries(new URLSearchParams(body ?? {})); - const routedUrlData = await getRoutedUrl({ req: request, query: { ...params, form: formId } }); - if (routedUrlData?.notFound) { - throw new NotFoundException("Route not found. Please check the provided form parameter."); - } - - if (routedUrlData?.redirect?.destination) { - return this.handleRedirect(routedUrlData.redirect.destination); - } - - if (routedUrlData?.props) { - if (routedUrlData.props.errorMessage) { - return { - status: "error", - error: { code: "ROUTING_ERROR", message: routedUrlData.props.errorMessage }, - redirect: false, - }; - } - return { status: "success", data: { message: routedUrlData.props.message ?? "" }, redirect: false }; - } - - return { status: "success", data: { message: "No Route nor custom message found." }, redirect: false }; - } - - private async handleRedirect(destination: string): Promise & { redirect: boolean }> { - const routingUrl = new URL(destination); - const routingSearchParams = routingUrl.searchParams; - if ( - routingSearchParams.get("cal.action") === "eventTypeRedirectUrl" && - routingSearchParams.has("email") && - routingSearchParams.has("cal.teamId") && - !routingSearchParams.has("cal.skipContactOwner") - ) { - return this.handleRedirectWithContactOwner(routingUrl, routingSearchParams); - } - console.log("handleRedirect Regular called", { destination }); - - return { status: "success", data: destination, redirect: true }; - } - - private async handleRedirectWithContactOwner( - routingUrl: URL, - routingSearchParams: URLSearchParams - ): Promise & { redirect: boolean }> { - console.log("handleRedirectWithContactOwner called", { routingUrl, routingSearchParams }); - const pathNameParams = routingUrl.pathname.split("/"); - const eventTypeSlug = pathNameParams[pathNameParams.length - 1]; - const teamId = Number(routingSearchParams.get("cal.teamId")); - const eventTypeData = await this.teamsEventTypesRepository.getTeamEventTypeBySlug( - teamId, - eventTypeSlug, - 3 - ); - - if (!eventTypeData) { - throw new NotFoundException("Event type not found."); - } - - // get the salesforce record owner email for the email given as a form response. - const { - email: teamMemberEmail, - recordType: crmOwnerRecordType, - crmAppSlug, - } = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({ - query: Object.fromEntries(routingSearchParams), - eventData: eventTypeData, - }); - - teamMemberEmail && routingUrl.searchParams.set("cal.teamMemberEmail", teamMemberEmail); - crmOwnerRecordType && routingUrl.searchParams.set("cal.crmOwnerRecordType", crmOwnerRecordType); - crmAppSlug && routingUrl.searchParams.set("cal.crmAppSlug", crmAppSlug); - - return { status: "success", data: routingUrl.toString(), redirect: true }; - } -} diff --git a/apps/api/v2/src/modules/router/router.module.ts b/apps/api/v2/src/modules/router/router.module.ts deleted file mode 100644 index 7bb2859f9bbfeb..00000000000000 --- a/apps/api/v2/src/modules/router/router.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RouterController } from "@/modules/router/controllers/router.controller"; -import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [PrismaModule], - providers: [TeamsEventTypesRepository], - exports: [], - controllers: [RouterController], -}) -export class RouterModule {} diff --git a/apps/api/v2/src/modules/routing-forms/controllers/routing-forms.controller.ts b/apps/api/v2/src/modules/routing-forms/controllers/routing-forms.controller.ts deleted file mode 100644 index 26049b3a3656a8..00000000000000 --- a/apps/api/v2/src/modules/routing-forms/controllers/routing-forms.controller.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { ResponseSlotsOutput } from "@/modules/routing-forms/outputs/response-slots.output"; -import { RoutingFormsService } from "@/modules/routing-forms/services/routing-forms.service"; -import { Controller, HttpCode, HttpStatus, Param, Post, Query, Req } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger"; -import { Request } from "express"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { GetAvailableSlotsInput_2024_09_04 } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/routing-forms/:routingFormId", - version: API_VERSIONS_VALUES, -}) -@ApiTags("Routing forms") -@ApiHeader(API_KEY_HEADER) -export class RoutingFormsController { - constructor(private readonly routingFormsService: RoutingFormsService) {} - - @Post("/calculate-slots") - @ApiOperation({ - summary: "Calculate slots based on routing form response", - description: - "It will not actually save the response just return the routed event type and slots when it can be booked.", - }) - @HttpCode(HttpStatus.OK) - async calculateSlotsBasedOnRoutingFormResponse( - @Req() request: Request, - @Query() query: GetAvailableSlotsInput_2024_09_04, - @Param("routingFormId") routingFormId: string - ): Promise { - const responseSlots = await this.routingFormsService.calculateSlotsBasedOnRoutingFormResponse( - request, - routingFormId, - query - ); - - return { - status: SUCCESS_STATUS, - data: responseSlots, - }; - } -} diff --git a/apps/api/v2/src/modules/routing-forms/controllers/routing-forms.e2e-spec.ts b/apps/api/v2/src/modules/routing-forms/controllers/routing-forms.e2e-spec.ts deleted file mode 100644 index c6b642274be8b7..00000000000000 --- a/apps/api/v2/src/modules/routing-forms/controllers/routing-forms.e2e-spec.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { App_RoutingForms_Form, EventType, 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 { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { RoutingFormsRepositoryFixture } from "test/fixtures/repository/routing-forms.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 { 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 { - ResponseSlotsOutput, - ResponseSlotsOutputData, -} from "@/modules/routing-forms/outputs/response-slots.output"; -import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Routing forms endpoints", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let routingFormsRepositoryFixture: RoutingFormsRepositoryFixture; - - const teammateEmailTwo = `routing-forms-teammate-1-${randomString()}`; - const teammateEmailOne = `routing-forms-teammate-2-${randomString()}`; - - let team: Team; - let teammateOne: User; - let teammateTwo: User; - let collectiveEventTypeFootball: EventType; - let collectiveEventTypeBasketball: EventType; - - let routingForm: App_RoutingForms_Form; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [ - AppModule, - PrismaModule, - UsersModule, - TokensModule, - SchedulesModule_2024_06_11, - SlotsModule_2024_09_04, - ], - }) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - routingFormsRepositoryFixture = new RoutingFormsRepositoryFixture(moduleRef); - - teammateOne = await userRepositoryFixture.create({ - email: teammateEmailOne, - name: teammateEmailOne, - username: teammateEmailOne, - }); - - teammateTwo = await userRepositoryFixture.create({ - email: teammateEmailTwo, - name: teammateEmailTwo, - username: teammateEmailTwo, - }); - - const teamSlug = `routing-forms-team-${randomString()}`; - team = await teamRepositoryFixture.create({ - name: teamSlug, - slug: teamSlug, - isOrganization: false, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammateOne.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammateTwo.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - const collectiveEventTypeFootballSlug = `routing-forms-collective-event-type-football-${randomString()}`; - collectiveEventTypeFootball = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team.id }, - }, - title: "Collective Event Type Football", - slug: collectiveEventTypeFootballSlug, - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - schedule: { - create: { - name: "football", - timeZone: "Europe/London", - user: { connect: { id: teammateOne.id } }, - availability: { - create: { - days: [1], - startTime: new Date("1970-01-01T09:00:00Z"), - endTime: new Date("1970-01-01T11:00:00Z"), - }, - }, - }, - }, - users: { - connect: [{ id: teammateOne.id }, { id: teammateTwo.id }], - }, - hosts: { - create: [ - { - userId: teammateOne.id, - isFixed: true, - }, - { - userId: teammateTwo.id, - isFixed: true, - }, - ], - }, - }); - - const collectiveEventTypeBasketballSlug = `routing-forms-collective-event-type-basketball-${randomString()}`; - collectiveEventTypeBasketball = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team.id }, - }, - title: "Collective Event Type Basketball", - slug: collectiveEventTypeBasketballSlug, - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - schedule: { - create: { - name: "basketball", - timeZone: "Europe/London", - user: { connect: { id: teammateTwo.id } }, - availability: { - create: { - days: [2], - startTime: new Date("1970-01-01T11:00:00Z"), - endTime: new Date("1970-01-01T13:00:00Z"), - }, - }, - }, - }, - users: { - connect: [{ id: teammateOne.id }, { id: teammateTwo.id }], - }, - hosts: { - create: [ - { - userId: teammateOne.id, - isFixed: true, - }, - { - userId: teammateTwo.id, - isFixed: true, - }, - ], - }, - }); - - routingForm = await routingFormsRepositoryFixture.create({ - name: "football or basketball", - description: "Do you want to play football or basketball?", - routes: [ - { - id: "baba89aa-4567-489a-bcde-f1961b1ae10c", - action: { - type: "eventTypeRedirectUrl", - value: `team/${team.slug}/${collectiveEventTypeFootballSlug}`, - eventTypeId: collectiveEventTypeFootball.id, - }, - queryValue: { - id: "a8a8b9aa-0123-4456-b89a-b1961b1ae10c", - type: "group", - children1: { - "99ab8aaa-4567-489a-bcde-f1961b1ae5cd": { - type: "rule", - properties: { - field: "dcd8b978-fa11-47be-801a-5ca2cd4ec16e", - value: ["755c11d1-fa46-4d03-8e15-d2c4ab3407d7"], - operator: "select_equals", - valueSrc: ["value"], - valueType: ["select"], - valueError: [null], - }, - }, - }, - }, - attributesQueryValue: { id: "b8998aab-cdef-4012-b456-71961b1ae10c", type: "group" }, - attributeRoutingConfig: {}, - fallbackAttributesQueryValue: { id: "ab98888a-89ab-4cde-b012-31961b1ae10c", type: "group" }, - }, - { - id: "88a98b98-0123-4456-b89a-b1961b1b1a04", - action: { - type: "eventTypeRedirectUrl", - value: `team/${team.slug}/${collectiveEventTypeBasketballSlug}`, - eventTypeId: collectiveEventTypeBasketball.id, - }, - queryValue: { - id: "bab8b9a8-cdef-4012-b456-71961b1b1a04", - type: "group", - children1: { - "9988a98a-0123-4456-b89a-b1961b1b1f8b": { - type: "rule", - properties: { - field: "dcd8b978-fa11-47be-801a-5ca2cd4ec16e", - value: ["4cbb1537-0ab9-4286-b4bf-08b08ff23372"], - operator: "select_equals", - valueSrc: ["value"], - valueType: ["select"], - valueError: [null], - }, - }, - }, - }, - attributesQueryValue: { id: "99a89a9b-89ab-4cde-b012-31961b1b1a04", type: "group" }, - attributeRoutingConfig: {}, - fallbackAttributesQueryValue: { id: "88989a98-4567-489a-bcde-f1961b1b1a04", type: "group" }, - }, - { - id: "9b9ab9bb-cdef-4012-b456-71961b1a8b78", - action: { - type: "customPageMessage", - value: "Thank you for your interest! We will be in touch soon.", - }, - isFallback: true, - queryValue: { id: "9b9ab9bb-cdef-4012-b456-71961b1a8b78", type: "group" }, - attributesQueryValue: { id: "88b988ab-cdef-4012-b456-71961b1ad81a", type: "group" }, - fallbackAttributesQueryValue: { id: "a8b89a9a-89ab-4cde-b012-31961b1ad81a", type: "group" }, - }, - ], - createdAt: new Date("2025-04-09T15:10:46.651Z"), - updatedAt: new Date("2025-04-09T15:11:37.696Z"), - fields: [ - { - id: "dcd8b978-fa11-47be-801a-5ca2cd4ec16e", - type: "select", - label: "sport", - options: [ - { id: "755c11d1-fa46-4d03-8e15-d2c4ab3407d7", label: "football" }, - { id: "4cbb1537-0ab9-4286-b4bf-08b08ff23372", label: "basketball" }, - ], - required: true, - }, - ], - user: { - connect: { id: teammateOne.id }, - }, - disabled: false, - settings: { emailOwnerOnSubmission: true }, - team: { - connect: { id: team.id }, - }, - position: 0, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - describe("Calculate slots", () => { - const slotsQuery = "start=2060-01-12&end=2060-01-16"; - - it("should return correct slots for option 1", async () => { - const body = { - sport: "football", - }; - - const reserveResponse = await request(app.getHttpServer()) - .post(`/v2/routing-forms/${routingForm.id}/calculate-slots?${slotsQuery}`) - .send(body) - .expect(200); - - const responseBody: ResponseSlotsOutput = reserveResponse.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseBodyData: ResponseSlotsOutputData = responseBody.data; - expect(responseBodyData.eventTypeId).toEqual(collectiveEventTypeFootball.id); - expect(responseBodyData.slots).toEqual({ - "2060-01-12": [ - { - start: "2060-01-12T09:00:00.000Z", - }, - { - start: "2060-01-12T10:00:00.000Z", - }, - ], - }); - }); - - it("should return correct slots for option 2", async () => { - const body = { - sport: "basketball", - }; - - const reserveResponse = await request(app.getHttpServer()) - .post(`/v2/routing-forms/${routingForm.id}/calculate-slots?${slotsQuery}`) - .send(body) - .expect(200); - - const responseBody: ResponseSlotsOutput = reserveResponse.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const responseBodyData: ResponseSlotsOutputData = responseBody.data; - expect(responseBodyData.eventTypeId).toEqual(collectiveEventTypeBasketball.id); - expect(responseBodyData.slots).toEqual({ - "2060-01-13": [ - { - start: "2060-01-13T11:00:00.000Z", - }, - { - start: "2060-01-13T12:00:00.000Z", - }, - ], - }); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(teammateOne.email); - await userRepositoryFixture.deleteByEmail(teammateTwo.email); - await teamRepositoryFixture.delete(team.id); - await app.close(); - }); -}); diff --git a/apps/api/v2/src/modules/routing-forms/outputs/response-slots.output.ts b/apps/api/v2/src/modules/routing-forms/outputs/response-slots.output.ts deleted file mode 100644 index f3a6d4afda3745..00000000000000 --- a/apps/api/v2/src/modules/routing-forms/outputs/response-slots.output.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsNumber, ValidateNested } from "class-validator"; - -import { ApiResponseWithoutData, SlotsOutput_2024_09_04 } from "@calcom/platform-types"; -import { RangeSlotsOutput_2024_09_04 } from "@calcom/platform-types"; - -@ApiExtraModels(SlotsOutput_2024_09_04, RangeSlotsOutput_2024_09_04) -export class ResponseSlotsOutputData { - @IsNumber() - @ApiProperty() - eventTypeId!: number; - - @ValidateNested() - @ApiProperty({ - oneOf: [ - { $ref: getSchemaPath(SlotsOutput_2024_09_04) }, - { $ref: getSchemaPath(RangeSlotsOutput_2024_09_04) }, - ], - }) - @Type(() => Object) - slots!: SlotsOutput_2024_09_04 | RangeSlotsOutput_2024_09_04; -} - -export class ResponseSlotsOutput extends ApiResponseWithoutData { - @ValidateNested() - @ApiProperty({ - type: ResponseSlotsOutputData, - }) - @Type(() => ResponseSlotsOutputData) - data!: ResponseSlotsOutputData; -} diff --git a/apps/api/v2/src/modules/routing-forms/routing-forms.module.ts b/apps/api/v2/src/modules/routing-forms/routing-forms.module.ts deleted file mode 100644 index 198863153d8d66..00000000000000 --- a/apps/api/v2/src/modules/routing-forms/routing-forms.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RoutingFormsController } from "@/modules/routing-forms/controllers/routing-forms.controller"; -import { RoutingFormsRepository } from "@/modules/routing-forms/routing-forms.repository"; -import { RoutingFormsService } from "@/modules/routing-forms/services/routing-forms.service"; -import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [PrismaModule, TeamsEventTypesModule, SlotsModule_2024_09_04, StripeModule], - providers: [RoutingFormsRepository, RoutingFormsService, OrganizationsRepository], - controllers: [RoutingFormsController], - exports: [RoutingFormsRepository], -}) -export class RoutingFormsModule {} diff --git a/apps/api/v2/src/modules/routing-forms/routing-forms.repository.ts b/apps/api/v2/src/modules/routing-forms/routing-forms.repository.ts deleted file mode 100644 index f648acff473594..00000000000000 --- a/apps/api/v2/src/modules/routing-forms/routing-forms.repository.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { Injectable } from "@nestjs/common"; - -@Injectable() -export class RoutingFormsRepository { - constructor(private readonly dbRead: PrismaReadService) {} - - async getTeamRoutingForm(teamId: number, routingFormId: string) { - return this.dbRead.prisma.app_RoutingForms_Form.findFirst({ - where: { - id: routingFormId, - teamId, - }, - }); - } -} diff --git a/apps/api/v2/src/modules/routing-forms/services/routing-forms.service.ts b/apps/api/v2/src/modules/routing-forms/services/routing-forms.service.ts deleted file mode 100644 index 136a05507cacd9..00000000000000 --- a/apps/api/v2/src/modules/routing-forms/services/routing-forms.service.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { SlotsService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots.service"; -import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; -import { Injectable, NotFoundException } from "@nestjs/common"; -import { Request } from "express"; - -import { getRoutedUrl } from "@calcom/platform-libraries"; -import { ById_2024_09_04_type, GetAvailableSlotsInput_2024_09_04 } from "@calcom/platform-types"; - -@Injectable() -export class RoutingFormsService { - constructor( - private readonly teamsEventTypesRepository: TeamsEventTypesRepository, - private readonly slotsService: SlotsService_2024_09_04 - ) {} - - async calculateSlotsBasedOnRoutingFormResponse( - request: Request, - formId: string, - slotsQuery: GetAvailableSlotsInput_2024_09_04 - ) { - const eventTypeId = await this.getRoutedEventTypeId(request, formId); - - if (!eventTypeId) { - throw new NotFoundException("Event type not found."); - } - const slots = await this.slotsService.getAvailableSlots({ - type: ById_2024_09_04_type, - eventTypeId, - ...slotsQuery, - }); - - return { - eventTypeId, - slots, - }; - } - - private async getRoutedEventTypeId(request: Request, formId: string) { - const routingUrl = await this.getRoutingUrl(request, formId); - if (!this.isEventTypeRedirectUrl(routingUrl)) { - throw new NotFoundException("Routed to a non cal.com event type URL."); - } - - const { teamId, eventTypeSlug } = this.extractTeamIdAndEventTypeSlugFromRedirectUrl(routingUrl); - - const eventType = await this.teamsEventTypesRepository.getEventTypeByTeamIdAndSlug(teamId, eventTypeSlug); - return eventType?.id; - } - - private async getRoutingUrl(request: Request, formId: string) { - const params = Object.fromEntries(new URLSearchParams(request.body)); - const routedUrlData = await getRoutedUrl({ - req: request, - query: { ...params, "cal.isBookingDryRun": "true", form: formId }, - }); - - const destination = routedUrlData?.redirect?.destination; - - if (!destination) { - throw new NotFoundException("Route to which the form response should be redirected not found."); - } - - return new URL(destination); - } - - private extractTeamIdAndEventTypeSlugFromRedirectUrl(routingUrl: URL) { - const eventTypeSlug = this.extractEventTypeFromRoutedUrl(routingUrl); - const teamId = this.extractTeamIdFromRoutedUrl(routingUrl); - - if (!teamId) { - throw new NotFoundException("Team ID not found in the routed URL."); - } - - if (!eventTypeSlug) { - throw new NotFoundException("Event type slug not found in the routed URL."); - } - - return { teamId, eventTypeSlug }; - } - - private isEventTypeRedirectUrl(routingUrl: URL) { - const routingSearchParams = routingUrl.searchParams; - return routingSearchParams.get("cal.action") === "eventTypeRedirectUrl"; - } - - private extractTeamIdFromRoutedUrl(routingUrl: URL) { - const routingSearchParams = routingUrl.searchParams; - return Number(routingSearchParams.get("cal.teamId")); - } - - private extractEventTypeFromRoutedUrl(routingUrl: URL) { - const pathNameParams = routingUrl.pathname.split("/"); - return pathNameParams[pathNameParams.length - 1]; - } -} diff --git a/apps/api/v2/src/modules/selected-calendars/controllers/selected-calendars.controller.e2e-spec.ts b/apps/api/v2/src/modules/selected-calendars/controllers/selected-calendars.controller.e2e-spec.ts index 06e56ae1ad5c31..20d6badb1a7508 100644 --- a/apps/api/v2/src/modules/selected-calendars/controllers/selected-calendars.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/selected-calendars/controllers/selected-calendars.controller.e2e-spec.ts @@ -12,7 +12,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { CalendarsServiceMock } from "test/mocks/calendars-service-mock"; import { AppModule } from "@/app.module"; import { bootstrap } from "@/bootstrap"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { CalendarsService } from "@/platform/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"; diff --git a/apps/api/v2/src/modules/selected-calendars/selected-calendars.module.ts b/apps/api/v2/src/modules/selected-calendars/selected-calendars.module.ts index 86451a64964745..c3d7c1c4b8b4c8 100644 --- a/apps/api/v2/src/modules/selected-calendars/selected-calendars.module.ts +++ b/apps/api/v2/src/modules/selected-calendars/selected-calendars.module.ts @@ -1,17 +1,12 @@ import { BullModule } from "@nestjs/bull"; import { Module } from "@nestjs/common"; -import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; -import { CALENDARS_QUEUE } from "@/ee/calendars/processors/calendars.processor"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; +import { CalendarsRepository } from "@/platform/calendars/calendars.repository"; +import { CALENDARS_QUEUE } from "@/platform/calendars/processors/calendars.processor"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; import { CalendarsTaskerModule } from "@/lib/modules/calendars-tasker.module"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; -import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository"; -import { OrganizationsDelegationCredentialService } from "@/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service"; -import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository"; -import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service"; -import { OrganizationsMembershipOutputService } from "@/modules/organizations/memberships/services/organizations-membership-output.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; import { SelectedCalendarsController } from "@/modules/selected-calendars/controllers/selected-calendars.controller"; @@ -40,11 +35,6 @@ import { UsersRepository } from "@/modules/users/users.repository"; UsersRepository, CredentialsRepository, AppsRepository, - OrganizationsDelegationCredentialService, - OrganizationsMembershipService, - OrganizationsMembershipOutputService, - OrganizationsDelegationCredentialRepository, - OrganizationsMembershipRepository, SelectedCalendarsService, ], controllers: [SelectedCalendarsController], diff --git a/apps/api/v2/src/modules/selected-calendars/services/selected-calendars.service.ts b/apps/api/v2/src/modules/selected-calendars/services/selected-calendars.service.ts index 18230a54adab7b..0dd17e552f33ff 100644 --- a/apps/api/v2/src/modules/selected-calendars/services/selected-calendars.service.ts +++ b/apps/api/v2/src/modules/selected-calendars/services/selected-calendars.service.ts @@ -1,7 +1,6 @@ -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository"; -import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; import { SelectedCalendarsInputDto, SelectedCalendarsQueryParamsInputDto, @@ -12,37 +11,17 @@ import { SelectedCalendarsRepository, } from "@/modules/selected-calendars/selected-calendars.repository"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; - -import { SelectedCalendarRepository } from "@calcom/platform-libraries"; - -type SelectedCalendarsInputDelegationCredential = SelectedCalendarsInputDto & { - delegationCredentialId: string; -}; @Injectable() export class SelectedCalendarsService { constructor( private readonly calendarsService: CalendarsService, private readonly calendarsCacheService: CalendarsCacheService, - private readonly selectedCalendarsRepository: SelectedCalendarsRepository, - private readonly organizationsMembershipService: OrganizationsMembershipService, - private readonly organizationsDelegationCredentialRepository: OrganizationsDelegationCredentialRepository + private readonly selectedCalendarsRepository: SelectedCalendarsRepository ) {} async addSelectedCalendar(user: UserWithProfile, input: SelectedCalendarsInputDto) { - if (input.delegationCredentialId) { - const delegationCredentialInput = { - ...input, - delegationCredentialId: input.delegationCredentialId, - }; - return this.addSelectedCalendarDelegationCredential(user, delegationCredentialInput); - } - return this.addSelectedCalendarUser(user, input); - } - - private async addSelectedCalendarUser(user: UserWithProfile, selectedCalendar: SelectedCalendarsInputDto) { - const { integration, externalId, credentialId } = selectedCalendar; + const { integration, externalId, credentialId } = input; await this.calendarsService.checkCalendarCredentials(Number(credentialId), user.id); const userSelectedCalendar = await this.selectedCalendarsRepository.addUserSelectedCalendar( @@ -57,74 +36,19 @@ export class SelectedCalendarsService { return userSelectedCalendar; } - private async addSelectedCalendarDelegationCredential( - user: UserWithProfile, - selectedCalendar: SelectedCalendarsInputDelegationCredential - ) { - const isMemberOfOrganization = await this.isMemberOfDelegationCredentialOrganization( - user.id, - selectedCalendar.delegationCredentialId - ); - if (!isMemberOfOrganization) { - throw new NotFoundException( - "User is not a member of the organization that owns the Delegation credential" - ); - } - - const { integration, externalId, credentialId, delegationCredentialId } = selectedCalendar; - const delegationCredentialSelectedCalendar = await SelectedCalendarRepository.upsert({ - userId: user.id, - integration, - externalId, - credentialId, - delegationCredentialId, - eventTypeId: null, - }); - return delegationCredentialSelectedCalendar; - } - - private async isMemberOfDelegationCredentialOrganization(userId: number, delegationCredentialId: string) { - const delegationCredential = await this.organizationsDelegationCredentialRepository.findById( - delegationCredentialId - ); - if (!delegationCredential) { - throw new NotFoundException("DelegationCredential with provided delegationCredentialId not found"); - } - - const isMemberOfOrganization = await this.organizationsMembershipService.getOrgMembershipByUserId( - delegationCredential.organizationId, - userId - ); - - return isMemberOfOrganization; - } - async deleteSelectedCalendar( selectedCalendar: SelectedCalendarsQueryParamsInputDto, user: UserWithProfile ) { - const { integration, externalId, credentialId, delegationCredentialId } = selectedCalendar; - - if (!delegationCredentialId) { - await this.calendarsService.checkCalendarCredentials(Number(credentialId), user.id); - } else { - const isMemberOfOrganization = await this.isMemberOfDelegationCredentialOrganization( - user.id, - delegationCredentialId - ); - if (!isMemberOfOrganization) { - throw new NotFoundException( - "User is not a member of the organization that owns the Delegation credential" - ); - } - } + const { integration, externalId, credentialId } = selectedCalendar; + await this.calendarsService.checkCalendarCredentials(Number(credentialId), user.id); try { const removedCalendarEntry = await this.selectedCalendarsRepository.removeUserSelectedCalendar( user.id, integration, externalId, - delegationCredentialId + undefined ); await this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(user.id); return removedCalendarEntry; diff --git a/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.e2e-spec.ts b/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.e2e-spec.ts index 236cfc8beb56ca..041f078c3bec0d 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.e2e-spec.ts @@ -14,8 +14,8 @@ import { randomString } from "test/utils/randomString"; 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 { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; +import { SchedulesModule_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/schedules.module"; +import { SchedulesService_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/services/schedules.service"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { SlotsModule_2024_04_15 } from "@/modules/slots/slots-2024-04-15/slots.module"; @@ -687,92 +687,6 @@ describe("Slots 2024-04-15 Endpoints", () => { await bookingsRepositoryFixture.deleteById(booking.id); }); - describe("routingFormResponseId and _isDryRun validation", () => { - const baseUrl = "/api/v2/slots/available"; - const baseParams = { - startTime: "2050-09-05", - endTime: "2050-09-10", - }; - - it("should allow routingFormResponseId=0 in dry-run mode", async () => { - return request(app.getHttpServer()) - .get(baseUrl) - .query({ - ...baseParams, - eventTypeId, - routingFormResponseId: 0, - _isDryRun: true, - }) - .expect(200) - .expect((res) => { - expect(res.body.status).toEqual(SUCCESS_STATUS); - }); - }); - - it("should reject routingFormResponseId=1 in dry-run mode", async () => { - return request(app.getHttpServer()) - .get(baseUrl) - .query({ - ...baseParams, - eventTypeId, - routingFormResponseId: 1, - _isDryRun: true, - }) - .expect(400) - .expect((res) => { - expect(res.body.error.details.errors[0].constraints.routingFormResponseIdValidator).toContain( - "routingFormResponseId must be 0 for dry run" - ); - }); - }); - - it("should allow routingFormResponseId=1 in non-dry-run mode", async () => { - return request(app.getHttpServer()) - .get(baseUrl) - .query({ - ...baseParams, - eventTypeId, - routingFormResponseId: 1, - }) - .expect(200) - .expect((res) => { - expect(res.body.status).toEqual(SUCCESS_STATUS); - }); - }); - - it("should reject routingFormResponseId=0 in non-dry-run mode", async () => { - return request(app.getHttpServer()) - .get(baseUrl) - .query({ - ...baseParams, - eventTypeId, - routingFormResponseId: 0, - }) - .expect(400) - .expect((res) => { - expect(res.body.error.details.errors[0].constraints.routingFormResponseIdValidator).toContain( - "routingFormResponseId must be a positive number" - ); - }); - }); - - it("should reject routingFormResponseId=-1 in non-dry-run mode", async () => { - return request(app.getHttpServer()) - .get(baseUrl) - .query({ - ...baseParams, - eventTypeId, - routingFormResponseId: -1, - }) - .expect(400) - .expect((res) => { - expect(res.body.error.details.errors[0].constraints.routingFormResponseIdValidator).toContain( - "routingFormResponseId must be a positive number" - ); - }); - }); - }); - afterAll(async () => { await userRepositoryFixture.deleteByEmail(user.email); await selectedSlotRepositoryFixture.deleteByUId(reservedSlotUid); diff --git a/apps/api/v2/src/modules/slots/slots-2024-04-15/services/slots-output.service.ts b/apps/api/v2/src/modules/slots/slots-2024-04-15/services/slots-output.service.ts index b77fbf148b1924..fe8ca53c7288e6 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-04-15/services/slots-output.service.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-04-15/services/slots-output.service.ts @@ -1,4 +1,4 @@ -import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository"; +import { EventTypesRepository_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/event-types.repository"; import { Injectable, BadRequestException } from "@nestjs/common"; import { DateTime } from "luxon"; diff --git a/apps/api/v2/src/modules/slots/slots-2024-04-15/services/slots-worker.service.ts b/apps/api/v2/src/modules/slots/slots-2024-04-15/services/slots-worker.service.ts index acbb06f703e986..9ae2bc7df7d476 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-04-15/services/slots-worker.service.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-04-15/services/slots-worker.service.ts @@ -1,10 +1,8 @@ import path from "node:path"; import { Worker } from "node:worker_threads"; - -import type { GetScheduleOptions } from "@calcom/trpc/server/routers/viewer/slots/types"; +import type { GetScheduleOptions } from "@calcom/platform-libraries/slots"; import { Injectable, Logger, OnModuleDestroy } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; - import { TimeSlots } from "./slots-output.service"; /** diff --git a/apps/api/v2/src/modules/slots/slots-2024-04-15/services/slots.service.ts b/apps/api/v2/src/modules/slots/slots-2024-04-15/services/slots.service.ts index d62c12b0e3bb64..c459f909c6348a 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-04-15/services/slots.service.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-04-15/services/slots.service.ts @@ -1,4 +1,4 @@ -import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository"; +import { EventTypesRepository_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/event-types.repository"; import { SlotsRepository_2024_04_15 } from "@/modules/slots/slots-2024-04-15/slots.repository"; import { Injectable, NotFoundException } from "@nestjs/common"; import { v4 as uuid } from "uuid"; diff --git a/apps/api/v2/src/modules/slots/slots-2024-04-15/slots.module.ts b/apps/api/v2/src/modules/slots/slots-2024-04-15/slots.module.ts index 5f628973141bac..25c14cf7fd5eb4 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-04-15/slots.module.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-04-15/slots.module.ts @@ -1,4 +1,4 @@ -import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; +import { EventTypesModule_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/event-types.module"; import { AvailableSlotsModule } from "@/lib/modules/available-slots.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { SlotsController_2024_04_15 } from "@/modules/slots/slots-2024-04-15/controllers/slots.controller"; diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/dynamic-event-type-slots.controller.e2e-spec.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/dynamic-event-type-slots.controller.e2e-spec.ts index b9e5864318b850..9ec332e6c4bed4 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/dynamic-event-type-slots.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/dynamic-event-type-slots.controller.e2e-spec.ts @@ -12,8 +12,8 @@ import { randomString } from "test/utils/randomString"; 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 { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; +import { SchedulesModule_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/schedules.module"; +import { SchedulesService_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/services/schedules.service"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/org-team-event-type-slots.controller.e2e-spec.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/org-team-event-type-slots.controller.e2e-spec.ts deleted file mode 100644 index b71145d4549a5c..00000000000000 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/org-team-event-type-slots.controller.e2e-spec.ts +++ /dev/null @@ -1,504 +0,0 @@ -import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_09_04 } from "@calcom/platform-constants"; -import type { CreateScheduleInput_2024_06_11 } 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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { 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 { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; -import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { expectedSlotsUTC } from "@/modules/slots/slots-2024-09-04/controllers/e2e/expected-slots"; -import { GetSlotsOutput_2024_09_04 } from "@/modules/slots/slots-2024-09-04/outputs/get-slots.output"; -import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Slots 2024-09-04 Endpoints", () => { - describe("Organization team event type slots", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let schedulesService: SchedulesService_2024_06_11; - let teamRepositoryFixture: TeamRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - - const sharedUsername = `slots-2024-09-04-shared-username-${randomString()}`; - const sharedEventTypeSlug = `slots-2024-09-04-shared-event-type-slug-${randomString()}`; - - const orgUserEmailOne = `slots-2024-09-04-org-user-one-${randomString()}@api.com`; - const orgUserEmailTwo = `slots-2024-09-04-org-user-two-${randomString()}@api.com`; - - const nonOrgUserEmailOne = `slots-2024-09-04-non-org-user-one-${randomString()}@api.com`; - - const orgSlug = `slots-2024-09-04-organization-${randomString()}`; - let organization: Team; - const teamSlug = `slots-2024-09-04-organization-team-${randomString()}`; - let team: Team; - let orgUserOne: User; - let orgUserTwo: User; - let collectiveEventTypeId: number; - let collectiveEventTypeSlug: string; - let roundRobinEventTypeId: number; - let collectiveBookingId: number; - let roundRobinBookingId: number; - let fullyBookedRoundRobinBookingIdOne: number; - let fullyBookedRoundRobinBookingIdTwo: number; - - let nonOrgUser: User; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - orgUserEmailOne, - Test.createTestingModule({ - imports: [ - AppModule, - PrismaModule, - UsersModule, - TokensModule, - SchedulesModule_2024_06_11, - SlotsModule_2024_09_04, - ], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_06_11); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - - organization = await organizationsRepositoryFixture.create({ - name: orgSlug, - isOrganization: true, - slug: orgSlug, - timeZone: "Europe/Rome", - }); - - orgUserOne = await userRepositoryFixture.create({ - email: orgUserEmailOne, - name: orgUserEmailOne, - username: orgUserEmailOne, - }); - - await eventTypesRepositoryFixture.create( - { title: "frisbee match orgUserOne", slug: sharedEventTypeSlug, length: 60 }, - orgUserOne.id - ); - - orgUserTwo = await userRepositoryFixture.create({ - email: orgUserEmailTwo, - name: orgUserEmailTwo, - username: orgUserEmailTwo, - }); - - nonOrgUser = await userRepositoryFixture.create({ - email: nonOrgUserEmailOne, - name: nonOrgUserEmailOne, - username: sharedUsername, - }); - - await eventTypesRepositoryFixture.create( - { title: "frisbee match nonOrgUser", slug: sharedEventTypeSlug, length: 60 }, - nonOrgUser.id - ); - - await profileRepositoryFixture.create({ - uid: `usr-${orgUserOne.id}`, - username: sharedUsername, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: orgUserOne.id, - }, - }, - }); - - team = await teamRepositoryFixture.create({ - name: teamSlug, - slug: teamSlug, - isOrganization: false, - parent: { connect: { id: organization.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: orgUserOne.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: orgUserTwo.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - const collectiveEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team.id }, - }, - title: "Collective Event Type", - slug: `slots-2024-09-04-org-collective-event-type-${randomString()}`, - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - users: { - connect: [{ id: orgUserOne.id }, { id: orgUserTwo.id }], - }, - }); - collectiveEventTypeId = collectiveEventType.id; - collectiveEventTypeSlug = collectiveEventType.slug; - - const roundRobinEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: team.id }, - }, - title: "RR Event Type", - slug: `slots-2024-09-04-org-round-robin-event-type-${randomString()}`, - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - users: { - connect: [{ id: orgUserOne.id }, { id: orgUserTwo.id }], - }, - }); - roundRobinEventTypeId = roundRobinEventType.id; - - const orgUsersSchedule: CreateScheduleInput_2024_06_11 = { - name: "working time", - timeZone: "Europe/Rome", - isDefault: true, - }; - - const nonOrgUserSchedule: CreateScheduleInput_2024_06_11 = { - name: "working time", - timeZone: "Europe/Rome", - isDefault: true, - availability: [ - { - days: ["Monday"], - startTime: "09:00", - endTime: "17:00", - }, - ], - }; - // note(Lauris): this creates default schedule monday to friday from 9AM to 5PM in Europe/Rome timezone - await schedulesService.createUserSchedule(orgUserOne.id, orgUsersSchedule); - await schedulesService.createUserSchedule(orgUserTwo.id, orgUsersSchedule); - await schedulesService.createUserSchedule(nonOrgUser.id, nonOrgUserSchedule); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - describe("org and non org user have the same username and event type slug", () => { - it("should get org user event slots in UTC", async () => { - return request(app.getHttpServer()) - .get( - `/v2/slots?organizationSlug=${organization.slug}&eventTypeSlug=${sharedEventTypeSlug}&username=${sharedUsername}&start=2050-09-05&end=2050-09-09` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200) - .then(async (response) => { - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - expect(slots).toEqual(expectedSlotsUTC); - }); - }); - - it("should get non org user event slots in UTC", async () => { - return request(app.getHttpServer()) - .get( - `/v2/slots?&eventTypeSlug=${sharedEventTypeSlug}&username=${sharedUsername}&start=2050-09-05&end=2050-09-09` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200) - .then(async (response) => { - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(1); - expect(slots).toEqual({ "2050-09-05": expectedSlotsUTC["2050-09-05"] }); - }); - }); - }); - - it("should get collective team event slots in UTC", async () => { - return request(app.getHttpServer()) - .get(`/v2/slots?eventTypeId=${collectiveEventTypeId}&start=2050-09-05&end=2050-09-09`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200) - .then(async (response) => { - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - expect(slots).toEqual(expectedSlotsUTC); - }); - }); - - it("should get collective team event slots in UTC using teamSlug, eventTypeSlug and organizationSlug", async () => { - return request(app.getHttpServer()) - .get( - `/v2/slots?organizationSlug=${orgSlug}&teamSlug=${teamSlug}&eventTypeSlug=${collectiveEventTypeSlug}&start=2050-09-05&end=2050-09-09` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200) - .then(async (response) => { - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - expect(slots).toEqual(expectedSlotsUTC); - }); - }); - - it("should not get collective team event slots in UTC using teamSlug, eventTypeSlug if organizationSlug is missing", async () => { - return request(app.getHttpServer()) - .get( - `/v2/slots?teamSlug=${teamSlug}&eventTypeSlug=${collectiveEventTypeSlug}&start=2050-09-05&end=2050-09-09` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(404); - }); - - it("should get round robin team event slots in UTC", async () => { - return request(app.getHttpServer()) - .get(`/v2/slots?eventTypeId=${roundRobinEventTypeId}&start=2050-09-05&end=2050-09-09`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200) - .then(async (response) => { - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - expect(slots).toEqual(expectedSlotsUTC); - }); - }); - - it("should book collective event type and slot should not be available at that time", async () => { - const startTime = "2050-09-05T11:00:00.000Z"; - const booking = await bookingsRepositoryFixture.create({ - uid: `booking-uid-${collectiveEventTypeId}`, - title: "booking title", - startTime, - endTime: "2050-09-05T12:00:00.000Z", - eventType: { - connect: { - id: collectiveEventTypeId, - }, - }, - metadata: {}, - responses: { - name: "tester", - email: "tester@example.com", - guests: [], - }, - user: { - connect: { - id: orgUserOne.id, - }, - }, - }); - collectiveBookingId = booking.id; - - const response = await request(app.getHttpServer()) - .get(`/v2/slots?eventTypeId=${collectiveEventTypeId}&start=2050-09-05&end=2050-09-09`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200); - - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - - const expectedSlotsUTC2050_09_05 = expectedSlotsUTC["2050-09-05"].filter( - (slot) => slot.start !== startTime - ); - expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 }); - bookingsRepositoryFixture.deleteById(booking.id); - }); - - it("should book round robin event type and slot should be available at that time", async () => { - const startTime = "2050-09-05T11:00:00.000Z"; - const booking = await bookingsRepositoryFixture.create({ - uid: `booking-uid-${roundRobinEventTypeId}`, - title: "booking title", - startTime, - endTime: "2050-09-05T12:00:00.000Z", - eventType: { - connect: { - id: roundRobinEventTypeId, - }, - }, - metadata: {}, - responses: { - name: "tester", - email: "tester@example.com", - guests: [], - }, - user: { - connect: { - id: orgUserOne.id, - }, - }, - }); - roundRobinBookingId = booking.id; - - const response = await request(app.getHttpServer()) - .get(`/v2/slots?eventTypeId=${roundRobinEventTypeId}&start=2050-09-05&end=2050-09-09`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200); - - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - - expect(slots).toEqual(expectedSlotsUTC); - bookingsRepositoryFixture.deleteById(booking.id); - }); - - it("should fully book round robin event type and slot should not be available at that time", async () => { - const startTime = "2050-09-05T11:00:00.000Z"; - const bookingOne = await bookingsRepositoryFixture.create({ - uid: `booking-uid-${roundRobinEventTypeId}-1`, - title: "booking title", - startTime, - endTime: "2050-09-05T12:00:00.000Z", - eventType: { - connect: { - id: roundRobinEventTypeId, - }, - }, - metadata: {}, - responses: { - name: "tester", - email: "tester@example.com", - guests: [], - }, - user: { - connect: { - id: orgUserOne.id, - }, - }, - }); - fullyBookedRoundRobinBookingIdOne = bookingOne.id; - - const bookingTwo = await bookingsRepositoryFixture.create({ - uid: `booking-uid-${roundRobinEventTypeId}-2`, - title: "booking title", - startTime, - endTime: "2050-09-05T12:00:00.000Z", - eventType: { - connect: { - id: roundRobinEventTypeId, - }, - }, - metadata: {}, - responses: { - name: "tester", - email: "tester@example.com", - guests: [], - }, - user: { - connect: { - id: orgUserTwo.id, - }, - }, - }); - fullyBookedRoundRobinBookingIdTwo = bookingTwo.id; - - const response = await request(app.getHttpServer()) - .get(`/v2/slots?eventTypeId=${roundRobinEventTypeId}&start=2050-09-05&end=2050-09-09`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200); - - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - - const expectedSlotsUTC2050_09_05 = expectedSlotsUTC["2050-09-05"].filter( - (slot) => slot.start !== startTime - ); - expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 }); - bookingsRepositoryFixture.deleteById(bookingOne.id); - bookingsRepositoryFixture.deleteById(bookingTwo.id); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(orgUserOne.email); - await userRepositoryFixture.deleteByEmail(orgUserTwo.email); - await teamRepositoryFixture.delete(team.id); - await organizationsRepositoryFixture.delete(organization.id); - await bookingsRepositoryFixture.deleteById(collectiveBookingId); - await bookingsRepositoryFixture.deleteById(roundRobinBookingId); - await bookingsRepositoryFixture.deleteById(fullyBookedRoundRobinBookingIdOne); - await bookingsRepositoryFixture.deleteById(fullyBookedRoundRobinBookingIdTwo); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/reschedule-uid-slots.controller.e2e-spec.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/reschedule-uid-slots.controller.e2e-spec.ts index 8628a29123ff3d..16058830f66aa3 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/reschedule-uid-slots.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/reschedule-uid-slots.controller.e2e-spec.ts @@ -11,8 +11,8 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { randomString } from "test/utils/randomString"; import { AppModule } from "@/app.module"; import { bootstrap } from "@/bootstrap"; -import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module"; -import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; +import { SchedulesModule_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/schedules.module"; +import { SchedulesService_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/services/schedules.service"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/team-event-type-slots.controller.e2e-spec.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/team-event-type-slots.controller.e2e-spec.ts deleted file mode 100644 index f0332060957080..00000000000000 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/team-event-type-slots.controller.e2e-spec.ts +++ /dev/null @@ -1,869 +0,0 @@ -import { - CAL_API_VERSION_HEADER, - ERROR_STATUS, - SUCCESS_STATUS, - VERSION_2024_09_04, -} from "@calcom/platform-constants"; -import type { - CreateScheduleInput_2024_06_11, - ReserveSlotOutput_2024_09_04 as ReserveSlotOutputData_2024_09_04, -} 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 { advanceTo, clear } from "jest-date-mock"; -import { DateTime } from "luxon"; -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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { SelectedSlotRepositoryFixture } from "test/fixtures/repository/selected-slot.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 { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module"; -import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; -import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { expectedSlotsUTC } from "@/modules/slots/slots-2024-09-04/controllers/e2e/expected-slots"; -import { GetSlotsOutput_2024_09_04 } from "@/modules/slots/slots-2024-09-04/outputs/get-slots.output"; -import { ReserveSlotOutputResponse_2024_09_04 } from "@/modules/slots/slots-2024-09-04/outputs/reserve-slot.output"; -import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Slots 2024-09-04 Endpoints", () => { - describe("Team event type slots", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let schedulesService: SchedulesService_2024_06_11; - let teamRepositoryFixture: TeamRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let selectedSlotRepositoryFixture: SelectedSlotRepositoryFixture; - - const teammateEmailOne = `slots-2024-09-04-user-1-team-slots-${randomString()}`; - let teammateApiKeyString: string; - const teammateEmailTwo = `slots-2024-09-04-user-2-team-slots-${randomString()}`; - const teammateEmailThree = `slots-2024-09-04-user-3-team-slots-${randomString()}`; - let teammateTwoApiKeyString: string; - - const outsiderEmail = `slots-2024-09-04-unrelated-team-slots-${randomString()}`; - let outsider: User; - let outsiderApiKeyString: string; - - const teamSlug = `slots-2024-09-04-team-${randomString()}`; - let team: Team; - let teammateOne: User; - let teammateTwo: User; - let teammateThree: User; - let collectiveEventTypeId: number; - let collectiveEventTypeSlug: string; - let collectiveEventTypeWithoutHostsId: number; - let roundRobinEventTypeId: number; - let roundRobinEventTypeWithoutFixedHostsId: number; - let roundRobinEventTypeWithFixedAndNonFixedHostsId: number; - let collectiveBookingId: number; - let roundRobinBookingId: number; - let fullyBookedRoundRobinBookingIdOne: number; - let fullyBookedRoundRobinBookingIdTwo: number; - - let reservedSlot: ReserveSlotOutputData_2024_09_04; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [ - AppModule, - PrismaModule, - UsersModule, - TokensModule, - SchedulesModule_2024_06_11, - SlotsModule_2024_09_04, - ], - }) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_06_11); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - selectedSlotRepositoryFixture = new SelectedSlotRepositoryFixture(moduleRef); - - teammateOne = await userRepositoryFixture.create({ - email: teammateEmailOne, - name: teammateEmailOne, - username: teammateEmailOne, - }); - - teammateTwo = await userRepositoryFixture.create({ - email: teammateEmailTwo, - name: teammateEmailTwo, - username: teammateEmailTwo, - }); - - teammateThree = await userRepositoryFixture.create({ - email: teammateEmailThree, - name: teammateEmailThree, - username: teammateEmailThree, - }); - - outsider = await userRepositoryFixture.create({ - email: outsiderEmail, - name: outsiderEmail, - username: outsiderEmail, - }); - - const { keyString } = await apiKeysRepositoryFixture.createApiKey(teammateOne.id, null); - teammateApiKeyString = keyString; - - const { keyString: keyStringForTeammateTwo } = await apiKeysRepositoryFixture.createApiKey( - teammateTwo.id, - null - ); - teammateTwoApiKeyString = keyStringForTeammateTwo; - - const { keyString: unrelatedUserKeyString } = await apiKeysRepositoryFixture.createApiKey( - outsider.id, - null - ); - outsiderApiKeyString = unrelatedUserKeyString; - - team = await teamRepositoryFixture.create({ - name: teamSlug, - slug: teamSlug, - isOrganization: false, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammateOne.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammateTwo.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammateThree.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - const collectiveEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team.id }, - }, - title: "Collective Event Type", - slug: `slots-2024-09-04-collective-event-type-${randomString()}`, - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - users: { - connect: [{ id: teammateOne.id }, { id: teammateTwo.id }], - }, - hosts: { - create: [ - { - userId: teammateOne.id, - isFixed: true, - }, - { - userId: teammateTwo.id, - isFixed: true, - }, - ], - }, - }); - collectiveEventTypeId = collectiveEventType.id; - collectiveEventTypeSlug = collectiveEventType.slug; - - const collectiveEventTypeWithoutHosts = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team.id }, - }, - title: "Collective Event Type Without Hosts", - slug: `slots-2024-09-04-collective-event-type-without-hosts-${randomString()}`, - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - users: { - connect: [{ id: teammateOne.id }, { id: teammateTwo.id }], - }, - }); - collectiveEventTypeWithoutHostsId = collectiveEventTypeWithoutHosts.id; - - const roundRobinEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: team.id }, - }, - title: "RR Event Type", - slug: `slots-2024-09-04-round-robin-event-type-${randomString()}`, - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - users: { - connect: [{ id: teammateOne.id }, { id: teammateTwo.id }], - }, - hosts: { - create: [ - { - userId: teammateOne.id, - isFixed: true, - }, - { - userId: teammateTwo.id, - isFixed: true, - }, - ], - }, - }); - roundRobinEventTypeId = roundRobinEventType.id; - - const roundRobinEventTypeWithoutFixedHosts = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: team.id }, - }, - title: "RR Event Type Without Fixed Hosts", - slug: `slots-2024-09-04-round-robin-event-type-${randomString()}`, - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - users: { - connect: [{ id: teammateOne.id }, { id: teammateTwo.id }, { id: teammateThree.id }], - }, - hosts: { - create: [ - { - userId: teammateOne.id, - isFixed: false, - }, - { - userId: teammateTwo.id, - isFixed: false, - }, - { - userId: teammateThree.id, - isFixed: false, - }, - ], - }, - rrHostSubsetEnabled: true, - }); - - roundRobinEventTypeWithoutFixedHostsId = roundRobinEventTypeWithoutFixedHosts.id; - - const roundRobinEventTypeWithFixedAndNonFixedHosts = - await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: team.id }, - }, - title: "RR Event Type With Fixed and Non-Fixed Hosts", - slug: `slots-2024-09-04-round-robin-event-type-${randomString()}`, - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - users: { - connect: [{ id: teammateOne.id }, { id: teammateTwo.id }, { id: teammateThree.id }], - }, - hosts: { - create: [ - { - userId: teammateOne.id, - isFixed: true, - }, - { - userId: teammateTwo.id, - isFixed: false, - }, - { - userId: teammateThree.id, - isFixed: false, - }, - ], - }, - rrHostSubsetEnabled: true, - }); - - roundRobinEventTypeWithFixedAndNonFixedHostsId = roundRobinEventTypeWithFixedAndNonFixedHosts.id; - - const userSchedule: CreateScheduleInput_2024_06_11 = { - name: "working time", - timeZone: "Europe/Rome", - isDefault: true, - }; - // note(Lauris): this creates default schedule monday to friday from 9AM to 5PM in Europe/Rome timezone - await schedulesService.createUserSchedule(teammateOne.id, userSchedule); - await schedulesService.createUserSchedule(teammateTwo.id, userSchedule); - - await schedulesService.createUserSchedule(teammateThree.id, { - ...userSchedule, - availability: [ - { - days: ["Monday", "Friday"], - startTime: "09:00", - endTime: "17:00", - }, - ], - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should get collective team event slots in UTC", async () => { - return request(app.getHttpServer()) - .get(`/v2/slots?eventTypeId=${collectiveEventTypeId}&start=2050-09-05&end=2050-09-09`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200) - .then(async (response) => { - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - expect(slots).toEqual(expectedSlotsUTC); - }); - }); - - it("should get collective team event slots in UTC using teamSlug and eventTypeSlug", async () => { - return request(app.getHttpServer()) - .get( - `/v2/slots?teamSlug=${teamSlug}&eventTypeSlug=${collectiveEventTypeSlug}&start=2050-09-05&end=2050-09-09` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200) - .then(async (response) => { - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - expect(slots).toEqual(expectedSlotsUTC); - }); - }); - - it("should get round robin team event slots in UTC", async () => { - return request(app.getHttpServer()) - .get(`/v2/slots?eventTypeId=${roundRobinEventTypeId}&start=2050-09-05&end=2050-09-09`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200) - .then(async (response) => { - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - expect(slots).toEqual(expectedSlotsUTC); - }); - }); - - it("should get round robin team event without fixed hosts slots in UTC with subsetIds for teammateThree who has a smaller schedule", async () => { - return request(app.getHttpServer()) - .get( - `/v2/slots?eventTypeId=${roundRobinEventTypeWithoutFixedHostsId}&start=2050-09-05&end=2050-09-09&rrHostSubsetIds[]=${teammateThree.id}` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200) - .then(async (response) => { - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(2); - }); - }); - - it("should get round robin team event without fixed hosts slots in UTC with subsetIds for teammateOne", async () => { - return request(app.getHttpServer()) - .get( - `/v2/slots?eventTypeId=${roundRobinEventTypeWithoutFixedHostsId}&start=2050-09-05&end=2050-09-09&rrHostSubsetIds[]=${teammateOne.id}` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200) - .then(async (response) => { - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - expect(slots).toEqual(expectedSlotsUTC); - }); - }); - - it("should get round robin team event with and without fixed hosts slots in UTC with subsetIds for teammateOne(fixed) and teammateThree(not fixed) ", async () => { - return request(app.getHttpServer()) - .get( - `/v2/slots?eventTypeId=${roundRobinEventTypeWithFixedAndNonFixedHostsId}&start=2050-09-05&end=2050-09-09&rrHostSubsetIds[]=${teammateOne.id}&rrHostSubsetIds[]=${teammateThree.id}` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200) - .then(async (response) => { - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(2); - }); - }); - - it("should get round robin team event with and without fixed hosts slots in UTC with subsetIds for teammateOne(fixed) and teammateTwo(not fixed) ", async () => { - return request(app.getHttpServer()) - .get( - `/v2/slots?eventTypeId=${roundRobinEventTypeWithFixedAndNonFixedHostsId}&start=2050-09-05&end=2050-09-09&rrHostSubsetIds[]=${teammateOne.id}&rrHostSubsetIds[]=${teammateTwo.id}` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200) - .then(async (response) => { - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - expect(slots).toEqual(expectedSlotsUTC); - }); - }); - - it("should not be able reserve a team event type slot with custom duration if no auth is provided", async () => { - await request(app.getHttpServer()) - .post(`/v2/slots/reservations`) - .send({ - eventTypeId: collectiveEventTypeId, - slotStart: "2050-09-05T10:00:00.000Z", - reservationDuration: 10, - }) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(401); - }); - - it("should not be able reserve a slot with custom duration if provided auth user is not part of the team that owns the team event type", async () => { - await request(app.getHttpServer()) - .post(`/v2/slots/reservations`) - .send({ - eventTypeId: collectiveEventTypeId, - slotStart: "2050-09-05T10:00:00.000Z", - reservationDuration: 10, - }) - .set({ Authorization: `Bearer cal_test_${outsiderApiKeyString}` }) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(403); - }); - - it("should not be able reserve a slot for team event type without hosts", async () => { - await request(app.getHttpServer()) - .post(`/v2/slots/reservations`) - .send({ - eventTypeId: collectiveEventTypeWithoutHostsId, - slotStart: "2050-09-05T10:00:00.000Z", - }) - .set({ Authorization: `Bearer cal_test_${teammateApiKeyString}` }) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(400); - }); - - it("should reserve a slot as team member of the team that owns the team event type", async () => { - // note(Lauris): mock current date to test slots release time - const now = "2049-09-05T12:00:00.000Z"; - const newDate = DateTime.fromISO(now, { zone: "UTC" }).toJSDate(); - advanceTo(newDate); - - const slotStartTime = "2050-09-05T10:00:00.000Z"; - const reserveResponse = await request(app.getHttpServer()) - .post(`/v2/slots/reservations`) - .set({ Authorization: `Bearer cal_test_${teammateApiKeyString}` }) - .send({ - eventTypeId: collectiveEventTypeId, - slotStart: slotStartTime, - reservationDuration: 10, - }) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(201); - - const reserveResponseBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponse.body; - expect(reserveResponseBody.status).toEqual(SUCCESS_STATUS); - const responseReservedSlot: ReserveSlotOutputData_2024_09_04 = reserveResponseBody.data; - expect(responseReservedSlot.reservationUid).toBeDefined(); - if (!responseReservedSlot.reservationUid) { - throw new Error("Reserved slot uid is undefined"); - } - reservedSlot = responseReservedSlot; - - const response = await request(app.getHttpServer()) - .get(`/v2/slots?eventTypeId=${collectiveEventTypeId}&start=2050-09-05&end=2050-09-09`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200); - - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - - const expectedSlotsUTC2050_09_05 = expectedSlotsUTC["2050-09-05"].filter( - (slot) => slot.start !== slotStartTime - ); - expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 }); - - const dbSlot = await selectedSlotRepositoryFixture.getByUid(reservedSlot.reservationUid); - expect(dbSlot).toBeDefined(); - if (dbSlot) { - const dbReleaseAt = DateTime.fromJSDate(dbSlot.releaseAt, { zone: "UTC" }).toISO(); - const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 10 }).toISO(); - expect(dbReleaseAt).toEqual(expectedReleaseAt); - } - await selectedSlotRepositoryFixture.deleteByUId(reservedSlot.reservationUid); - clear(); - }); - - it("should book collective event type and slot should not be available at that time", async () => { - const startTime = "2050-09-05T11:00:00.000Z"; - const booking = await bookingsRepositoryFixture.create({ - uid: `booking-uid-${collectiveEventTypeId}`, - title: "booking title", - startTime, - endTime: "2050-09-05T12:00:00.000Z", - eventType: { - connect: { - id: collectiveEventTypeId, - }, - }, - metadata: {}, - responses: { - name: "tester", - email: "tester@example.com", - guests: [], - }, - user: { - connect: { - id: teammateOne.id, - }, - }, - }); - collectiveBookingId = booking.id; - - const response = await request(app.getHttpServer()) - .get(`/v2/slots?eventTypeId=${collectiveEventTypeId}&start=2050-09-05&end=2050-09-09`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200); - - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - - const expectedSlotsUTC2050_09_05 = expectedSlotsUTC["2050-09-05"].filter( - (slot) => slot.start !== startTime - ); - expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 }); - bookingsRepositoryFixture.deleteById(booking.id); - }); - - it("should book round robin event type and slot should be available at that time", async () => { - const startTime = "2050-09-05T11:00:00.000Z"; - const booking = await bookingsRepositoryFixture.create({ - uid: `booking-uid-${roundRobinEventTypeId}`, - title: "booking title", - startTime, - endTime: "2050-09-05T12:00:00.000Z", - eventType: { - connect: { - id: roundRobinEventTypeId, - }, - }, - metadata: {}, - responses: { - name: "tester", - email: "tester@example.com", - guests: [], - }, - user: { - connect: { - id: teammateOne.id, - }, - }, - }); - roundRobinBookingId = booking.id; - - const response = await request(app.getHttpServer()) - .get(`/v2/slots?eventTypeId=${roundRobinEventTypeId}&start=2050-09-05&end=2050-09-09`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200); - - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - - const expectedSlotsUTC2050_09_05 = expectedSlotsUTC["2050-09-05"].filter( - (slot) => slot.start !== startTime - ); - expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 }); - bookingsRepositoryFixture.deleteById(booking.id); - }); - - it("should fully book round robin event type and slot should not be available at that time", async () => { - const startTime = "2050-09-05T11:00:00.000Z"; - const bookingOne = await bookingsRepositoryFixture.create({ - uid: `booking-uid-${roundRobinEventTypeId}-1`, - title: "booking title", - startTime, - endTime: "2050-09-05T12:00:00.000Z", - eventType: { - connect: { - id: roundRobinEventTypeId, - }, - }, - metadata: {}, - responses: { - name: "tester", - email: "tester@example.com", - guests: [], - }, - user: { - connect: { - id: teammateOne.id, - }, - }, - }); - fullyBookedRoundRobinBookingIdOne = bookingOne.id; - - const bookingTwo = await bookingsRepositoryFixture.create({ - uid: `booking-uid-${roundRobinEventTypeId}-2`, - title: "booking title", - startTime, - endTime: "2050-09-05T12:00:00.000Z", - eventType: { - connect: { - id: roundRobinEventTypeId, - }, - }, - metadata: {}, - responses: { - name: "tester", - email: "tester@example.com", - guests: [], - }, - user: { - connect: { - id: teammateTwo.id, - }, - }, - }); - fullyBookedRoundRobinBookingIdTwo = bookingTwo.id; - - const response = await request(app.getHttpServer()) - .get(`/v2/slots?eventTypeId=${roundRobinEventTypeId}&start=2050-09-05&end=2050-09-09`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(200); - - const responseBody: GetSlotsOutput_2024_09_04 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const slots = responseBody.data; - - expect(slots).toBeDefined(); - const days = Object.keys(slots); - expect(days.length).toEqual(5); - - const expectedSlotsUTC2050_09_05 = expectedSlotsUTC["2050-09-05"].filter( - (slot) => slot.start !== startTime - ); - expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 }); - bookingsRepositoryFixture.deleteById(bookingOne.id); - bookingsRepositoryFixture.deleteById(bookingTwo.id); - }); - - it("should reserve all available slots for round robin event type with non-fixed hosts", async () => { - const now = "2049-09-05T12:00:00.000Z"; - const newDate = DateTime.fromISO(now, { zone: "UTC" }).toJSDate(); - advanceTo(newDate); - - const slotStartTime = "2050-09-05T10:00:00.000Z"; - - const reserveResponseOne = await request(app.getHttpServer()) - .post(`/v2/slots/reservations`) - .set({ Authorization: `Bearer cal_test_${teammateApiKeyString}` }) - .send({ - eventTypeId: roundRobinEventTypeWithoutFixedHostsId, - slotStart: slotStartTime, - reservationDuration: 10, - }) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(201); - - const reserveResponseOneBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponseOne.body; - expect(reserveResponseOneBody.status).toEqual(SUCCESS_STATUS); - const responseReservedSlotOne: ReserveSlotOutputData_2024_09_04 = reserveResponseOneBody.data; - expect(responseReservedSlotOne.reservationUid).toBeDefined(); - if (!responseReservedSlotOne.reservationUid) { - throw new Error("Reserved slot one uid is undefined"); - } - - const dbSlotOne = await selectedSlotRepositoryFixture.getByUid(responseReservedSlotOne.reservationUid); - expect(dbSlotOne).toBeDefined(); - if (dbSlotOne) { - const dbReleaseAt = DateTime.fromJSDate(dbSlotOne.releaseAt, { zone: "UTC" }).toISO(); - const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 10 }).toISO(); - expect(dbReleaseAt).toEqual(expectedReleaseAt); - } - - const reserveResponseTwo = await request(app.getHttpServer()) - .post(`/v2/slots/reservations`) - .set({ Authorization: `Bearer cal_test_${teammateTwoApiKeyString}` }) - .send({ - eventTypeId: roundRobinEventTypeWithoutFixedHostsId, - slotStart: slotStartTime, - reservationDuration: 10, - }) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(201); - - const reserveResponseTwoBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponseTwo.body; - expect(reserveResponseTwoBody.status).toEqual(SUCCESS_STATUS); - const responseReservedSlotTwo: ReserveSlotOutputData_2024_09_04 = reserveResponseTwoBody.data; - expect(responseReservedSlotTwo.reservationUid).toBeDefined(); - if (!responseReservedSlotTwo.reservationUid) { - throw new Error("Reserved slot two uid is undefined"); - } - - const dbSlotTwo = await selectedSlotRepositoryFixture.getByUid(responseReservedSlotTwo.reservationUid); - expect(dbSlotTwo).toBeDefined(); - if (dbSlotTwo) { - const dbReleaseAt = DateTime.fromJSDate(dbSlotTwo.releaseAt, { zone: "UTC" }).toISO(); - const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 10 }).toISO(); - expect(dbReleaseAt).toEqual(expectedReleaseAt); - } - - const reserveResponseThree = await request(app.getHttpServer()) - .post(`/v2/slots/reservations`) - .set({ Authorization: `Bearer cal_test_${outsiderApiKeyString}` }) - .send({ - eventTypeId: roundRobinEventTypeWithoutFixedHostsId, - slotStart: slotStartTime, - reservationDuration: 10, - }) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04); - - expect(reserveResponseThree.status).toEqual(403); - expect(reserveResponseThree.body.status).toEqual(ERROR_STATUS); - - await selectedSlotRepositoryFixture.deleteByUId(responseReservedSlotOne.reservationUid); - await selectedSlotRepositoryFixture.deleteByUId(responseReservedSlotTwo.reservationUid); - clear(); - }); - - it("should reserve available slot for round robin event type with fixed and non-fixed hosts and should not be able to reserve another slot", async () => { - const now = "2049-09-05T12:00:00.000Z"; - const newDate = DateTime.fromISO(now, { zone: "UTC" }).toJSDate(); - advanceTo(newDate); - - const slotStartTime = "2050-09-05T10:00:00.000Z"; - - const reserveResponseOne = await request(app.getHttpServer()) - .post(`/v2/slots/reservations`) - .set({ Authorization: `Bearer cal_test_${teammateApiKeyString}` }) - .send({ - eventTypeId: roundRobinEventTypeWithFixedAndNonFixedHostsId, - slotStart: slotStartTime, - reservationDuration: 10, - }) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) - .expect(201); - - const reserveResponseBodyOne: ReserveSlotOutputResponse_2024_09_04 = reserveResponseOne.body; - expect(reserveResponseBodyOne.status).toEqual(SUCCESS_STATUS); - const responseReservedSlotOne: ReserveSlotOutputData_2024_09_04 = reserveResponseBodyOne.data; - expect(responseReservedSlotOne.reservationUid).toBeDefined(); - if (!responseReservedSlotOne.reservationUid) { - throw new Error("Reserved slot uid is undefined"); - } - - const dbSlotOne = await selectedSlotRepositoryFixture.getByUid(responseReservedSlotOne.reservationUid); - expect(dbSlotOne).toBeDefined(); - if (dbSlotOne) { - const dbReleaseAt = DateTime.fromJSDate(dbSlotOne.releaseAt, { zone: "UTC" }).toISO(); - const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 10 }).toISO(); - expect(dbReleaseAt).toEqual(expectedReleaseAt); - } - - const reserveResponseTwo = await request(app.getHttpServer()) - .post(`/v2/slots/reservations`) - .send({ - eventTypeId: roundRobinEventTypeWithFixedAndNonFixedHostsId, - slotStart: slotStartTime, - }) - .set({ Authorization: `Bearer cal_test_${teammateTwoApiKeyString}` }) - .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04); - - expect(reserveResponseTwo.status).toEqual(422); - expect(reserveResponseTwo.body.status).toEqual(ERROR_STATUS); - - await selectedSlotRepositoryFixture.deleteByUId(responseReservedSlotOne.reservationUid); - clear(); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(teammateOne.email); - await userRepositoryFixture.deleteByEmail(teammateTwo.email); - await userRepositoryFixture.deleteByEmail(teammateThree.email); - await userRepositoryFixture.deleteByEmail(outsiderEmail); - await teamRepositoryFixture.delete(team.id); - await bookingsRepositoryFixture.deleteById(collectiveBookingId); - await bookingsRepositoryFixture.deleteById(roundRobinBookingId); - await bookingsRepositoryFixture.deleteById(fullyBookedRoundRobinBookingIdOne); - await bookingsRepositoryFixture.deleteById(fullyBookedRoundRobinBookingIdTwo); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/user-event-type-slots.controller.e2e-spec.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/user-event-type-slots.controller.e2e-spec.ts index 57715e542273d7..a0cc1ccf9ea7c5 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/user-event-type-slots.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/user-event-type-slots.controller.e2e-spec.ts @@ -23,8 +23,8 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { randomString } from "test/utils/randomString"; import { AppModule } from "@/app.module"; import { bootstrap } from "@/bootstrap"; -import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module"; -import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; +import { SchedulesModule_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/schedules.module"; +import { SchedulesService_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/services/schedules.service"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-input.service.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-input.service.ts index e4169f7b3ec15a..d1ecdc676dc7ec 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-input.service.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-input.service.ts @@ -1,23 +1,19 @@ -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository"; -import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; -import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; -import { UsersRepository } from "@/modules/users/users.repository"; -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { DateTime } from "luxon"; - import { dynamicEvent } from "@calcom/platform-libraries"; import { - ByUsernameAndEventTypeSlug_2024_09_04, + ById_2024_09_04_type, ByTeamSlugAndEventTypeSlug_2024_09_04, + ByTeamSlugAndEventTypeSlug_2024_09_04_type, + ByUsernameAndEventTypeSlug_2024_09_04, + ByUsernameAndEventTypeSlug_2024_09_04_type, GetSlotsInput_2024_09_04, GetSlotsInputWithRouting_2024_09_04, - ById_2024_09_04_type, - ByUsernameAndEventTypeSlug_2024_09_04_type, - ByTeamSlugAndEventTypeSlug_2024_09_04_type, } from "@calcom/platform-types"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { DateTime } from "luxon"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; +import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; +import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; +import { UsersRepository } from "@/modules/users/users.repository"; export type InternalGetSlotsQuery = { isTeamEvent: boolean; @@ -37,7 +33,6 @@ export type InternalGetSlotsQueryWithRouting = InternalGetSlotsQuery & { routedTeamMemberIds: number[] | null; skipContactOwner: boolean; teamMemberEmail: string | null; - routingFormResponseId: number | undefined; }; @Injectable() @@ -45,9 +40,6 @@ export class SlotsInputService_2024_09_04 { constructor( private readonly eventTypeRepository: EventTypesRepository_2024_06_14, private readonly usersRepository: UsersRepository, - private readonly organizationsUsersRepository: OrganizationsUsersRepository, - private readonly organizationsTeamsRepository: OrganizationsTeamsRepository, - private readonly organizationsRepository: OrganizationsRepository, private readonly teamsRepository: TeamsRepository, private readonly teamsEventTypesRepository: TeamsEventTypesRepository ) {} @@ -87,8 +79,7 @@ export class SlotsInputService_2024_09_04 { async transformRoutingGetSlotsQuery( query: GetSlotsInputWithRouting_2024_09_04 ): Promise { - const { routedTeamMemberIds, skipContactOwner, teamMemberEmail, routingFormResponseId, ...baseQuery } = - query; + const { routedTeamMemberIds, skipContactOwner, teamMemberEmail, ...baseQuery } = query; const baseTransformation = await this.transformGetSlotsQuery(baseQuery); @@ -97,7 +88,6 @@ export class SlotsInputService_2024_09_04 { routedTeamMemberIds: routedTeamMemberIds || null, skipContactOwner: skipContactOwner || false, teamMemberEmail: teamMemberEmail || null, - routingFormResponseId: routingFormResponseId ?? undefined, }; } @@ -126,36 +116,11 @@ export class SlotsInputService_2024_09_04 { } private async getEventTypeUser(input: ByUsernameAndEventTypeSlug_2024_09_04) { - if (!input.organizationSlug) { - return await this.usersRepository.findByUsername(input.username); - } - - const organization = await this.organizationsRepository.findOrgBySlug(input.organizationSlug); - if (!organization) { - throw new NotFoundException( - `slots-input.service.ts: Organization with slug ${input.organizationSlug} not found` - ); - } - - return await this.organizationsUsersRepository.getOrganizationUserByUsername( - organization.id, - input.username - ); + return await this.usersRepository.findByUsername(input.username); } private async getEventTypeTeam(input: ByTeamSlugAndEventTypeSlug_2024_09_04) { - if (!input.organizationSlug) { - return await this.teamsRepository.findTeamBySlug(input.teamSlug); - } - - const organization = await this.organizationsRepository.findOrgBySlug(input.organizationSlug); - if (!organization) { - throw new NotFoundException( - `slots-input.service.ts: Organization with slug ${input.organizationSlug} not found` - ); - } - - return await this.organizationsTeamsRepository.findOrgTeamBySlug(organization.id, input.teamSlug); + return await this.teamsRepository.findTeamBySlug(input.teamSlug); } private adjustStartTime(startTime: string) { diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-output.service.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-output.service.ts index 70f63787258438..521b5531de4a8b 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-output.service.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-output.service.ts @@ -1,4 +1,4 @@ -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; import { Injectable, BadRequestException } from "@nestjs/common"; import { DateTime } from "luxon"; diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.spec.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.spec.ts index 2ffc759c9b9d53..73e5f98b92f6b0 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.spec.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.spec.ts @@ -1,4 +1,7 @@ -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { BadRequestException, NotFoundException } from "@nestjs/common"; +import { Test, TestingModule } from "@nestjs/testing"; +import { SlotsService_2024_09_04 } from "./slots.service"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; import { AvailableSlotsService } from "@/lib/services/available-slots.service"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { MembershipsService } from "@/modules/memberships/services/memberships.service"; @@ -6,10 +9,6 @@ import { SlotsInputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/s import { SlotsOutputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots-output.service"; import { SlotsRepository_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.repository"; import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; -import { BadRequestException, NotFoundException } from "@nestjs/common"; -import { Test, TestingModule } from "@nestjs/testing"; - -import { SlotsService_2024_09_04 } from "./slots.service"; describe("SlotsService_2024_09_04", () => { let service: SlotsService_2024_09_04; @@ -109,7 +108,6 @@ describe("SlotsService_2024_09_04", () => { routedTeamMemberIds: [456, 789], skipContactOwner: true, teamMemberEmail: "test@example.com", - routingFormResponseId: 999, }, }; @@ -200,7 +198,6 @@ describe("SlotsService_2024_09_04", () => { routedTeamMemberIds: null, skipContactOwner: false, teamMemberEmail: null, - routingFormResponseId: undefined, }; (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); @@ -230,7 +227,6 @@ describe("SlotsService_2024_09_04", () => { routedTeamMemberIds: null, skipContactOwner: false, teamMemberEmail: null, - routingFormResponseId: undefined, }; (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); @@ -253,7 +249,6 @@ describe("SlotsService_2024_09_04", () => { routedTeamMemberIds: null, skipContactOwner: false, teamMemberEmail: null, - routingFormResponseId: undefined, }; (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); @@ -275,7 +270,6 @@ describe("SlotsService_2024_09_04", () => { teamMemberEmail: undefined, routedTeamMemberIds: undefined, skipContactOwner: undefined, - routingFormResponseId: undefined, }; const transformedQuery = { @@ -291,7 +285,6 @@ describe("SlotsService_2024_09_04", () => { teamMemberEmail: null, routedTeamMemberIds: null, skipContactOwner: false, - routingFormResponseId: undefined, }; (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); @@ -304,7 +297,6 @@ describe("SlotsService_2024_09_04", () => { teamMemberEmail: null, routedTeamMemberIds: null, skipContactOwner: false, - routingFormResponseId: undefined, }), ctx: {}, }); @@ -333,7 +325,6 @@ describe("SlotsService_2024_09_04", () => { routedTeamMemberIds: [], skipContactOwner: false, teamMemberEmail: null, - routingFormResponseId: undefined, }; (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); @@ -359,7 +350,6 @@ describe("SlotsService_2024_09_04", () => { routedTeamMemberIds: [1, 2, 3], skipContactOwner: true, teamMemberEmail: "team@example.com", - routingFormResponseId: 999, }; const transformedQuery = { @@ -375,7 +365,6 @@ describe("SlotsService_2024_09_04", () => { routedTeamMemberIds: [1, 2, 3], skipContactOwner: true, teamMemberEmail: "team@example.com", - routingFormResponseId: 999, }; (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); @@ -388,7 +377,6 @@ describe("SlotsService_2024_09_04", () => { routedTeamMemberIds: [1, 2, 3], skipContactOwner: true, teamMemberEmail: "team@example.com", - routingFormResponseId: 999, }), ctx: {}, }); diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts index 4b1dcad021defa..3daedcbf98250d 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts @@ -1,4 +1,4 @@ -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; import { AvailableSlotsService } from "@/lib/services/available-slots.service"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { MembershipsService } from "@/modules/memberships/services/memberships.service"; diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/slots.module.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/slots.module.ts index e57a563bc37903..685f79b28c9f10 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/slots.module.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/slots.module.ts @@ -1,9 +1,6 @@ -import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; +import { EventTypesModule_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.module"; import { AvailableSlotsModule } from "@/lib/modules/available-slots.module"; import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { SlotsController_2024_09_04 } from "@/modules/slots/slots-2024-09-04/controllers/slots.controller"; import { SlotsInputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots-input.service"; @@ -11,8 +8,8 @@ import { SlotsOutputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/ import { SlotsService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots.service"; import { SlotsRepository_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.repository"; import { StripeModule } from "@/modules/stripe/stripe.module"; -import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; -import { TeamsModule } from "@/modules/teams/teams/teams.module"; +import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; +import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; import { UsersRepository } from "@/modules/users/users.repository"; import { Module } from "@nestjs/common"; @@ -21,9 +18,7 @@ import { Module } from "@nestjs/common"; PrismaModule, EventTypesModule_2024_06_14, StripeModule, - TeamsModule, MembershipsModule, - TeamsEventTypesModule, AvailableSlotsModule, ], providers: [ @@ -32,9 +27,8 @@ import { Module } from "@nestjs/common"; UsersRepository, SlotsInputService_2024_09_04, SlotsOutputService_2024_09_04, - OrganizationsUsersRepository, - OrganizationsRepository, - OrganizationsTeamsRepository, + TeamsEventTypesRepository, + TeamsRepository, ], controllers: [SlotsController_2024_09_04], exports: [SlotsService_2024_09_04], diff --git a/apps/api/v2/src/modules/teams/bookings/inputs/get-teams-bookings.input.ts b/apps/api/v2/src/modules/teams/bookings/inputs/get-teams-bookings.input.ts deleted file mode 100644 index fc8ef5b3370499..00000000000000 --- a/apps/api/v2/src/modules/teams/bookings/inputs/get-teams-bookings.input.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { GetOrganizationsTeamsBookingsInput_2024_08_13 } from "@/modules/organizations/teams/bookings/inputs/get-organizations-teams-bookings.input"; - -export class GetTeamsBookingsInput_2024_08_13 extends GetOrganizationsTeamsBookingsInput_2024_08_13 {} diff --git a/apps/api/v2/src/modules/teams/bookings/teams-bookings.controller.e2e-spec.ts b/apps/api/v2/src/modules/teams/bookings/teams-bookings.controller.e2e-spec.ts deleted file mode 100644 index 87ee0439e09c63..00000000000000 --- a/apps/api/v2/src/modules/teams/bookings/teams-bookings.controller.e2e-spec.ts +++ /dev/null @@ -1,581 +0,0 @@ -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, -} from "@calcom/platform-types"; -import type { Team, User } from "@calcom/prisma/client"; -import type { INestApplication } from "@nestjs/common"; -import type { NestExpressApplication } from "@nestjs/platform-express"; -import type { TestingModule } from "@nestjs/testing"; -import { Test } from "@nestjs/testing"; -import request from "supertest"; -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 { TeamRepositoryFixture } from "test/fixtures/repository/team.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 type { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; -import type { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; -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 { TeamsBookingsModule } from "@/modules/teams/bookings/teams-bookings.module"; - -type BookingData = - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13; - -describe("Teams Bookings Endpoints 2024-08-13", () => { - describe("Standalone team bookings", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let hostsRepositoryFixture: HostsRepositoryFixture; - - const teamAdminEmail = "team-admin-bookings@api.com"; - const teamOwnerEmail = "team-owner-bookings@api.com"; - const teamMemberEmail = "team-member-bookings@api.com"; - const nonTeamUserEmail = "non-team-user-bookings@api.com"; - - let teamAdmin: User; - let teamOwner: User; - let teamMember: User; - let nonTeamUser: User; - let standaloneTeam: Team; - - let teamEventTypeId: number; - let teamEventTypeId2: number; - - // Store created booking data for filter tests - let createdBookingUid: string; - const attendeeEmail1 = "attendee@example.com"; - const attendeeName1 = "Test Attendee"; - const attendeeEmail2 = "another@example.com"; - const attendeeName2 = "Another Attendee"; - const bookingStart1 = new Date(Date.UTC(2030, 0, 10, 10, 0, 0)); - const bookingStart2 = new Date(Date.UTC(2030, 0, 11, 14, 0, 0)); - - async function createBookingViaApi( - eventTypeId: number, - start: Date, - attendee: { name: string; email: string } - ): Promise { - const body: CreateBookingInput_2024_08_13 = { - start: start.toISOString(), - eventTypeId, - attendee: { - name: attendee.name, - email: attendee.email, - timeZone: "Europe/London", - language: "en", - }, - meetingUrl: "https://meet.google.com/test-meeting", - }; - - const response = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - - const responseData = responseBody.data; - if (!Array.isArray(responseData) && "id" in responseData && !("bookingId" in responseData)) { - return responseData as BookingOutput_2024_08_13; - } - throw new Error("Invalid response data - expected booking but received unexpected format"); - } - - beforeAll(async () => { - const moduleRef: TestingModule = await withApiAuth( - teamOwnerEmail, - Test.createTestingModule({ - imports: [AppModule, TeamsBookingsModule], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - - // Create a standalone team (not part of any organization) - standaloneTeam = await teamRepositoryFixture.create({ - name: "Standalone Team Bookings", - isOrganization: false, - }); - - // Create users - teamAdmin = await userRepositoryFixture.create({ - email: teamAdminEmail, - locale: "en", - name: "TeamAdminBookings", - }); - - teamOwner = await userRepositoryFixture.create({ - email: teamOwnerEmail, - locale: "en", - name: "TeamOwnerBookings", - }); - - teamMember = await userRepositoryFixture.create({ - email: teamMemberEmail, - locale: "en", - name: "TeamMemberBookings", - }); - - nonTeamUser = await userRepositoryFixture.create({ - email: nonTeamUserEmail, - locale: "en", - name: "NonTeamUserBookings", - }); - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: "working time", - timeZone: "Europe/London", - isDefault: true, - }; - await schedulesService.createUserSchedule(teamAdmin.id, userSchedule); - await schedulesService.createUserSchedule(teamOwner.id, userSchedule); - await schedulesService.createUserSchedule(teamMember.id, userSchedule); - await schedulesService.createUserSchedule(nonTeamUser.id, userSchedule); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: teamAdmin.id } }, - team: { connect: { id: standaloneTeam.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: teamOwner.id } }, - team: { connect: { id: standaloneTeam.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teamMember.id } }, - team: { connect: { id: standaloneTeam.id } }, - accepted: true, - }); - - const teamEventType1 = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: standaloneTeam.id }, - }, - title: "Team Collective Event", - slug: "team-collective-event", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - const teamEventType2 = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "ROUND_ROBIN", - team: { - connect: { id: standaloneTeam.id }, - }, - title: "Team Round Robin Event", - slug: "team-round-robin-event", - length: 30, - assignAllTeamMembers: false, - bookingFields: [], - locations: [], - }); - - teamEventTypeId = teamEventType1.id; - teamEventTypeId2 = teamEventType2.id; - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: teamAdmin.id, - }, - }, - eventType: { - connect: { - id: teamEventType1.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: teamOwner.id, - }, - }, - eventType: { - connect: { - id: teamEventType1.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: false, - user: { - connect: { - id: teamMember.id, - }, - }, - eventType: { - connect: { - id: teamEventType2.id, - }, - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - - // Create bookings for filter tests - const booking1 = await createBookingViaApi(teamEventTypeId, bookingStart1, { - name: attendeeName1, - email: attendeeEmail1, - }); - createdBookingUid = booking1.uid; - - await createBookingViaApi(teamEventTypeId2, bookingStart2, { - name: attendeeName2, - email: attendeeEmail2, - }); - }); - - describe("filters", () => { - it("should filter by single status", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?status=upcoming`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - // Verify all bookings are upcoming (in the future) - data.forEach((booking) => { - if ("start" in booking) { - expect(new Date(booking.start).getTime()).toBeGreaterThan(Date.now()); - } - }); - }); - }); - - it("should filter by multiple statuses", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?status=upcoming,past`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - // Verify all bookings are either upcoming (future) or past - const now = Date.now(); - data.forEach((booking) => { - if (!Array.isArray(booking)) { - const bookingEnd = new Date(booking.end).getTime(); - // Booking is either upcoming (starts in future) or past (ended in past) - const isUpcoming = new Date(booking.start).getTime() > now; - const isPast = bookingEnd < now; - expect(isUpcoming || isPast).toBe(true); - } - }); - }); - }); - - it("should get team bookings with eventTypeIds filter", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?eventTypeIds=${teamEventTypeId}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: ( - | BookingOutput_2024_08_13 - | RecurringBookingOutput_2024_08_13 - | GetSeatedBookingOutput_2024_08_13 - )[] = responseBody.data; - expect(data.length).toBeGreaterThanOrEqual(1); - data.forEach((booking) => { - expect(booking.eventTypeId).toEqual(teamEventTypeId); - }); - }); - }); - - it("should filter by multiple eventTypeIds", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?eventTypeIds=${teamEventTypeId},${teamEventTypeId2}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data: BookingData[] = responseBody.data; - expect(data.length).toBeGreaterThanOrEqual(2); - data.forEach((booking) => { - expect([teamEventTypeId, teamEventTypeId2]).toContain(booking.eventTypeId); - }); - }); - }); - - it("should filter by attendeeEmail", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?attendeeEmail=${attendeeEmail1}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: BookingData[] = responseBody.data; - expect(data.length).toBeGreaterThanOrEqual(1); - data.forEach((booking) => { - if ("attendees" in booking) { - expect(booking.attendees.some((attendee) => attendee.email === attendeeEmail1)).toBe(true); - } - }); - }); - }); - - it("should filter by attendeeName", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?attendeeName=${encodeURIComponent(attendeeName1)}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: BookingData[] = responseBody.data; - expect(data.length).toBeGreaterThanOrEqual(1); - data.forEach((booking) => { - if ("attendees" in booking) { - expect(booking.attendees.some((attendee) => attendee.name === attendeeName1)).toBe(true); - } - }); - }); - }); - - it("should filter by bookingUid", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?bookingUid=${createdBookingUid}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: BookingData[] = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].uid).toEqual(createdBookingUid); - }); - }); - - it("should filter by date range (afterStart and beforeEnd)", async () => { - const afterStartDate = new Date(Date.UTC(2030, 0, 9, 0, 0, 0)).toISOString(); - const beforeEndDate = new Date(Date.UTC(2030, 0, 12, 0, 0, 0)).toISOString(); - return request(app.getHttpServer()) - .get( - `/v2/teams/${standaloneTeam.id}/bookings?afterStart=${afterStartDate}&beforeEnd=${beforeEndDate}` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.length).toBeGreaterThanOrEqual(2); - }); - }); - }); - - describe("sorting", () => { - it("should sort by start ascending", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?sortStart=asc`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data: BookingData[] = responseBody.data; - for (let i = 1; i < data.length; i++) { - const prevBooking = data[i - 1]; - const currBooking = data[i]; - if ("start" in prevBooking && "start" in currBooking) { - expect(new Date(prevBooking.start).getTime()).toBeLessThanOrEqual( - new Date(currBooking.start).getTime() - ); - } - } - }); - }); - - it("should sort by start descending", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?sortStart=desc`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data: BookingData[] = responseBody.data; - for (let i = 1; i < data.length; i++) { - const prevBooking = data[i - 1]; - const currBooking = data[i]; - if ("start" in prevBooking && "start" in currBooking) { - expect(new Date(prevBooking.start).getTime()).toBeGreaterThanOrEqual( - new Date(currBooking.start).getTime() - ); - } - } - }); - }); - - it("should sort by end ascending", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?sortEnd=asc`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data: BookingData[] = responseBody.data; - for (let i = 1; i < data.length; i++) { - const prevBooking = data[i - 1]; - const currBooking = data[i]; - if ("end" in prevBooking && "end" in currBooking) { - expect(new Date(prevBooking.end).getTime()).toBeLessThanOrEqual( - new Date(currBooking.end).getTime() - ); - } - } - }); - }); - - it("should sort by created ascending", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?sortCreated=asc`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data: BookingData[] = responseBody.data; - for (let i = 1; i < data.length; i++) { - const prevBooking = data[i - 1]; - const currBooking = data[i]; - if ("createdAt" in prevBooking && "createdAt" in currBooking) { - expect(new Date(prevBooking.createdAt).getTime()).toBeLessThanOrEqual( - new Date(currBooking.createdAt).getTime() - ); - } - } - }); - }); - }); - - describe("pagination", () => { - it("should return paginated results with take parameter", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?take=1`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.length).toEqual(1); - expect(responseBody.pagination).toBeDefined(); - }); - }); - - it("should return paginated results with skip parameter", async () => { - // First get all bookings to know total - const allBookingsResponse = await request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200); - - const allBookings: BookingData[] = allBookingsResponse.body.data; - const totalCount = allBookings.length; - - // Skip first booking - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?skip=1`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.length).toEqual(totalCount - 1); - }); - }); - - it("should return paginated results with take and skip parameters", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${standaloneTeam.id}/bookings?take=1&skip=1`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.length).toEqual(1); - expect(responseBody.pagination).toBeDefined(); - }); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(teamAdminEmail); - await userRepositoryFixture.deleteByEmail(teamOwnerEmail); - await userRepositoryFixture.deleteByEmail(teamMemberEmail); - await userRepositoryFixture.deleteByEmail(nonTeamUserEmail); - await teamRepositoryFixture.delete(standaloneTeam.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/teams/bookings/teams-bookings.controller.ts b/apps/api/v2/src/modules/teams/bookings/teams-bookings.controller.ts deleted file mode 100644 index f01a632d947f37..00000000000000 --- a/apps/api/v2/src/modules/teams/bookings/teams-bookings.controller.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { GetTeamsBookingsInput_2024_08_13 } from "@/modules/teams/bookings/inputs/get-teams-bookings.input"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { Controller, UseGuards, Get, Param, ParseIntPipe, Query, HttpStatus, HttpCode } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/teams/:teamId/bookings", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, RolesGuard) -@DocsTags("Teams / Bookings") -@ApiHeader(API_KEY_HEADER) -export class TeamsBookingsController { - constructor(private readonly bookingsService: BookingsService_2024_08_13) {} - - @Get("/") - @ApiOperation({ summary: "Get team bookings" }) - @Roles("TEAM_ADMIN") - @HttpCode(HttpStatus.OK) - async getAllTeamBookings( - @Query() queryParams: GetTeamsBookingsInput_2024_08_13, - @Param("teamId", ParseIntPipe) teamId: number, - @GetUser() user: UserWithProfile - ): Promise { - const { bookings, pagination } = await this.bookingsService.getBookings( - { ...queryParams, teamId }, - { email: user.email, id: user.id } - ); - - return { - status: SUCCESS_STATUS, - data: bookings, - pagination, - }; - } -} diff --git a/apps/api/v2/src/modules/teams/bookings/teams-bookings.module.ts b/apps/api/v2/src/modules/teams/bookings/teams-bookings.module.ts deleted file mode 100644 index abb471ed4229ec..00000000000000 --- a/apps/api/v2/src/modules/teams/bookings/teams-bookings.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module"; -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { TeamsBookingsController } from "@/modules/teams/bookings/teams-bookings.controller"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [BookingsModule_2024_08_13, PrismaModule, StripeModule, RedisModule, MembershipsModule], - controllers: [TeamsBookingsController], -}) -export class TeamsBookingsModule {} diff --git a/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types-webhooks.controller.e2e-spec.ts b/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types-webhooks.controller.e2e-spec.ts deleted file mode 100644 index 88efd49035c2b0..00000000000000 --- a/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types-webhooks.controller.e2e-spec.ts +++ /dev/null @@ -1,327 +0,0 @@ -import type { EventType, Team, User, 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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { TeamRepositoryFixture } from "test/fixtures/repository/team.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"; -import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input"; -import { - EventTypeWebhookOutputResponseDto, - EventTypeWebhooksOutputResponseDto, -} from "@/modules/webhooks/outputs/event-type-webhook.output"; -import { DeleteManyWebhooksOutputResponseDto } from "@/modules/webhooks/outputs/webhook.output"; - -describe("Teams EventTypes WebhooksController (e2e)", () => { - let app: INestApplication; - const userEmail = `teams-event-types-webhooks-user-${randomString()}@api.com`; - let userAdmin: User; - let otherUser: User; - let team: Team; - let otherTeam: Team; - let teamEventType: EventType; - let teamEventType2: EventType; - let otherTeamEventType: EventType; - - let eventTypeRepositoryFixture: EventTypesRepositoryFixture; - let userRepositoryFixture: UserRepositoryFixture; - let webhookRepositoryFixture: WebhookRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let webhook: EventTypeWebhookOutputResponseDto["data"]; - let otherWebhook: Webhook; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef); - eventTypeRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - userAdmin = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - otherUser = await userRepositoryFixture.create({ - email: `teams-event-types-webhooks-other-user-${randomString()}@api.com`, - username: `teams-event-types-webhooks-other-user-${randomString()}@api.com`, - }); - - team = await teamsRepositoryFixture.create({ - name: `teams-event-types-webhooks-team-${randomString()}`, - isOrganization: false, - }); - - otherTeam = await teamsRepositoryFixture.create({ - name: `teams-event-types-webhooks-other-team-${randomString()}`, - isOrganization: false, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: userAdmin.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: otherUser.id } }, - team: { connect: { id: otherTeam.id } }, - accepted: true, - }); - - teamEventType = await eventTypeRepositoryFixture.createTeamEventType({ - team: { connect: { id: team.id } }, - title: "Team Event Type 1", - slug: `teams-event-types-webhooks-event-type-${randomString()}`, - length: 60, - schedulingType: "COLLECTIVE", - }); - - teamEventType2 = await eventTypeRepositoryFixture.createTeamEventType({ - team: { connect: { id: team.id } }, - title: "Team Event Type 2", - slug: `teams-event-types-webhooks-event-type-${randomString()}`, - length: 60, - schedulingType: "COLLECTIVE", - }); - - otherTeamEventType = await eventTypeRepositoryFixture.createTeamEventType({ - team: { connect: { id: otherTeam.id } }, - title: "Other Team Event Type", - slug: `teams-event-types-webhooks-other-event-type-${randomString()}`, - length: 60, - schedulingType: "COLLECTIVE", - }); - - otherWebhook = await webhookRepositoryFixture.create({ - id: `teams-webhooks-${randomString()}`, - subscriberUrl: "https://example.com/other", - eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: true, - payloadTemplate: "string", - eventType: { connect: { id: otherTeamEventType.id } }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(userAdmin.email); - await userRepositoryFixture.deleteByEmail(otherUser.email); - await webhookRepositoryFixture.delete(otherWebhook.id); - await teamsRepositoryFixture.delete(team.id); - await teamsRepositoryFixture.delete(otherTeam.id); - await app.close(); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks (POST)", () => { - return request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types/${teamEventType.id}/webhooks`) - .send({ - subscriberUrl: "https://example.com", - triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: true, - payloadTemplate: "string", - } satisfies CreateWebhookInputDto) - .expect(201) - .then(async (res) => { - expect(res.body).toMatchObject({ - status: "success", - data: { - id: expect.any(String), - subscriberUrl: "https://example.com", - triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: true, - payloadTemplate: "string", - eventTypeId: teamEventType.id, - }, - } satisfies EventTypeWebhookOutputResponseDto); - webhook = res.body.data; - }); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks (POST) - create webhook for second event type", () => { - return request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types/${teamEventType2.id}/webhooks`) - .send({ - subscriberUrl: "https://example.com", - triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: true, - payloadTemplate: "string", - } satisfies CreateWebhookInputDto) - .expect(201) - .then(async (res) => { - expect(res.body).toMatchObject({ - status: "success", - data: { - id: expect.any(String), - subscriberUrl: "https://example.com", - triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: true, - payloadTemplate: "string", - eventTypeId: teamEventType2.id, - }, - } satisfies EventTypeWebhookOutputResponseDto); - }); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks (POST) should fail to create a webhook for an event-type that does not belong to user's team", () => { - return request(app.getHttpServer()) - .post(`/v2/teams/${otherTeam.id}/event-types/${otherTeamEventType.id}/webhooks`) - .send({ - subscriberUrl: "https://example.com", - triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: true, - payloadTemplate: "string", - } satisfies CreateWebhookInputDto) - .expect(403); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks/:webhookId (PATCH)", () => { - return request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${teamEventType.id}/webhooks/${webhook.id}`) - .send({ - active: false, - } satisfies UpdateWebhookInputDto) - .expect(200) - .then((res) => { - expect(res.body.data.active).toBe(false); - }); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks/:webhookId (PATCH) should fail to patch a webhook for an event-type that does not belong to user's team", () => { - return request(app.getHttpServer()) - .patch(`/v2/teams/${otherTeam.id}/event-types/${otherTeamEventType.id}/webhooks/${otherWebhook.id}`) - .send({ - active: false, - } satisfies UpdateWebhookInputDto) - .expect(403); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks/:webhookId (GET)", () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/event-types/${teamEventType.id}/webhooks/${webhook.id}`) - .expect(200) - .then((res) => { - expect(res.body).toMatchObject({ - status: "success", - data: { - id: expect.any(String), - subscriberUrl: "https://example.com", - triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: false, - payloadTemplate: "string", - eventTypeId: teamEventType.id, - }, - } satisfies EventTypeWebhookOutputResponseDto); - }); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks/:webhookId (GET) should fail to get a webhook that does not exist", () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/event-types/${teamEventType.id}/webhooks/90284`) - .expect(404); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks/:webhookId (GET) should fail to get a webhook of an eventType that does not belong to user's team", () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${otherTeam.id}/event-types/${otherTeamEventType.id}/webhooks/${otherWebhook.id}`) - .expect(403); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks/:webhookId (GET) should fail to get a webhook that does not belong to the eventType", () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/event-types/${teamEventType.id}/webhooks/${otherWebhook.id}`) - .expect(403); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks (GET)", () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/event-types/${teamEventType.id}/webhooks`) - .expect(200) - .then((res) => { - const responseBody = res.body as EventTypeWebhooksOutputResponseDto; - responseBody.data.forEach((webhook) => { - expect(webhook.eventTypeId).toBe(teamEventType.id); - }); - }); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks (GET) - list webhooks for second event type", () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/event-types/${teamEventType2.id}/webhooks`) - .expect(200) - .then((res) => { - const responseBody = res.body as EventTypeWebhooksOutputResponseDto; - responseBody.data.forEach((webhook) => { - expect(webhook.eventTypeId).toBe(teamEventType2.id); - }); - }); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks/:webhookId (DELETE)", () => { - return request(app.getHttpServer()) - .delete(`/v2/teams/${team.id}/event-types/${teamEventType.id}/webhooks/${webhook.id}`) - .expect(200) - .then((res) => { - expect(res.body).toMatchObject({ - status: "success", - data: { - id: expect.any(String), - subscriberUrl: "https://example.com", - triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"], - active: false, - payloadTemplate: "string", - eventTypeId: teamEventType.id, - }, - } satisfies EventTypeWebhookOutputResponseDto); - }); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks (DELETE)", () => { - return request(app.getHttpServer()) - .delete(`/v2/teams/${team.id}/event-types/${teamEventType2.id}/webhooks`) - .expect(200) - .then((res) => { - expect(res.body).toMatchObject({ - status: "success", - data: "1 webhooks deleted", - } satisfies DeleteManyWebhooksOutputResponseDto); - }); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks/:webhookId (DELETE) should fail to delete a webhook that does not exist", () => { - return request(app.getHttpServer()) - .delete(`/v2/teams/${team.id}/event-types/${teamEventType.id}/webhooks/1234453`) - .expect(404); - }); - - it("/teams/:teamId/event-types/:eventTypeId/webhooks/:webhookId (DELETE) should fail to delete a webhook that does not belong to user's team", () => { - return request(app.getHttpServer()) - .delete(`/v2/teams/${otherTeam.id}/event-types/${otherTeamEventType.id}/webhooks/${otherWebhook.id}`) - .expect(403); - }); -}); diff --git a/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types-webhooks.controller.ts b/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types-webhooks.controller.ts deleted file mode 100644 index b38f1f63716476..00000000000000 --- a/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types-webhooks.controller.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { SkipTakePagination } from "@calcom/platform-types"; -import type { Webhook } from "@calcom/prisma/client"; -import { - Body, - Controller, - Delete, - Get, - Param, - ParseIntPipe, - Patch, - Post, - Query, - UseGuards, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { GetWebhook } from "@/modules/webhooks/decorators/get-webhook-decorator"; -import { IsTeamEventTypeWebhookGuard } from "@/modules/webhooks/guards/is-team-event-type-webhook-guard"; -import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input"; -import { - EventTypeWebhookOutputDto, - type EventTypeWebhookOutputResponseDto, - type EventTypeWebhooksOutputResponseDto, -} from "@/modules/webhooks/outputs/event-type-webhook.output"; -import type { DeleteManyWebhooksOutputResponseDto } from "@/modules/webhooks/outputs/webhook.output"; -import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe"; -import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe"; -import { TeamEventTypeWebhooksService } from "@/modules/webhooks/services/team-event-type-webhooks.service"; -import { WebhooksService } from "@/modules/webhooks/services/webhooks.service"; - -@Controller({ - path: "/v2/teams/:teamId/event-types/:eventTypeId/webhooks", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, RolesGuard, IsTeamEventTypeWebhookGuard) -@DocsTags("Teams / Event Types / Webhooks") -@ApiHeader(API_KEY_HEADER) -@ApiParam({ name: "teamId", type: Number, required: true }) -@ApiParam({ name: "eventTypeId", type: Number, required: true }) -export class TeamsEventTypesWebhooksController { - constructor( - private readonly webhooksService: WebhooksService, - private readonly teamEventTypeWebhooksService: TeamEventTypeWebhooksService - ) {} - - @Post("/") - @ApiOperation({ summary: "Create a webhook for a team event type" }) - @Roles("TEAM_ADMIN") - async createTeamEventTypeWebhook( - @Body() body: CreateWebhookInputDto, - @Param("eventTypeId", ParseIntPipe) eventTypeId: number - ): Promise { - const webhook = await this.teamEventTypeWebhooksService.createTeamEventTypeWebhook( - eventTypeId, - new WebhookInputPipe().transform(body) - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { - strategy: "excludeAll", - }), - }; - } - - @Patch("/:webhookId") - @ApiOperation({ summary: "Update a webhook for a team event type" }) - @Roles("TEAM_ADMIN") - async updateTeamEventTypeWebhook( - @Body() body: UpdateWebhookInputDto, - @Param("webhookId") webhookId: string - ): Promise { - const webhook = await this.webhooksService.updateWebhook( - webhookId, - new PartialWebhookInputPipe().transform(body) - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { - strategy: "excludeAll", - }), - }; - } - - @Get("/:webhookId") - @ApiOperation({ summary: "Get a webhook for a team event type" }) - @ApiParam({ name: "webhookId", type: String, required: true }) - @Roles("TEAM_MEMBER") - async getTeamEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise { - return { - status: SUCCESS_STATUS, - data: plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { - strategy: "excludeAll", - }), - }; - } - - @Get("/") - @Roles("TEAM_MEMBER") - @ApiOperation({ summary: "Get all webhooks for a team event type" }) - async getTeamEventTypeWebhooks( - @Param("eventTypeId", ParseIntPipe) eventTypeId: number, - @Query() pagination: SkipTakePagination - ): Promise { - const webhooks = await this.teamEventTypeWebhooksService.getTeamEventTypeWebhooksPaginated( - eventTypeId, - pagination.skip ?? 0, - pagination.take ?? 250 - ); - return { - status: SUCCESS_STATUS, - data: webhooks.map((webhook) => - plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { - strategy: "excludeAll", - }) - ), - }; - } - - @Delete("/:webhookId") - @Roles("TEAM_ADMIN") - @ApiOperation({ summary: "Delete a webhook for a team event type" }) - @ApiParam({ name: "webhookId", type: String, required: true }) - async deleteTeamEventTypeWebhook( - @GetWebhook() webhook: Webhook - ): Promise { - await this.webhooksService.deleteWebhook(webhook.id); - return { - status: SUCCESS_STATUS, - data: plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { - strategy: "excludeAll", - }), - }; - } - - @Delete("/") - @Roles("TEAM_ADMIN") - @ApiOperation({ summary: "Delete all webhooks for a team event type" }) - async deleteAllTeamEventTypeWebhooks( - @Param("eventTypeId", ParseIntPipe) eventTypeId: number - ): Promise { - const data = await this.teamEventTypeWebhooksService.deleteAllTeamEventTypeWebhooks(eventTypeId); - return { status: SUCCESS_STATUS, data: `${data.count} webhooks deleted` }; - } -} diff --git a/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.e2e-spec.ts b/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.e2e-spec.ts deleted file mode 100644 index 43a3f628650f91..00000000000000 --- a/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.e2e-spec.ts +++ /dev/null @@ -1,1397 +0,0 @@ -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, - NoticeThresholdUnitEnum, -} from "@calcom/platform-enums"; -import type { - ApiSuccessResponse, - CreateTeamEventTypeInput_2024_06_14, - EventTypeOutput_2024_06_14, - Host, - TeamEventTypeOutput_2024_06_14, - UpdateTeamEventTypeInput_2024_06_14, -} 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 { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.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 { HOSTS_REQUIRED_WHEN_SWITCHING_SCHEDULING_TYPE_ERROR } from "@/modules/organizations/event-types/services/input.service"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Organizations Event Types Endpoints", () => { - describe("User Authentication - User is Org Admin", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - - let team: Team; - let falseTestTeam: Team; - - const userEmail = `teams-event-types-user-${randomString()}@api.com`; - let userAdmin: User; - - const teammate1Email = `teams-event-types-teammate1-${randomString()}@api.com`; - const teammate2Email = `teams-event-types-teammate2-${randomString()}@api.com`; - const falseTestUserEmail = `teams-event-types-false-user-${randomString()}@api.com`; - let teamMember1: User; - let teamMember2: User; - let falseTestUser: User; - - let collectiveEventType: TeamEventTypeOutput_2024_06_14; - let managedEventType: TeamEventTypeOutput_2024_06_14; - - async function ensureManagedEventType(): Promise { - if (!managedEventType) { - const setupBody: CreateTeamEventTypeInput_2024_06_14 = { - title: `teams-event-types-managed-${randomString()}`, - slug: `teams-event-types-managed-${randomString()}`, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "MANAGED", - hosts: [ - { - userId: teamMember1.id, - mandatory: true, - priority: "high", - }, - { - userId: teamMember2.id, - mandatory: false, - priority: "low", - }, - ], - }; - - const setupResponse = await request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types`) - .send(setupBody) - .expect(201); - - const setupResponseBody: ApiSuccessResponse = setupResponse.body; - const responseTeamEvent = setupResponseBody.data.find((event) => event.teamId === team.id); - if (responseTeamEvent) { - managedEventType = responseTeamEvent; - } - } - return managedEventType; - } - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - - userAdmin = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - role: "ADMIN", - }); - - teamMember1 = await userRepositoryFixture.create({ - email: teammate1Email, - username: teammate1Email, - name: "alice", - }); - - teamMember2 = await userRepositoryFixture.create({ - email: teammate2Email, - username: teammate2Email, - name: "bob", - }); - - falseTestUser = await userRepositoryFixture.create({ - email: falseTestUserEmail, - username: falseTestUserEmail, - }); - - team = await teamsRepositoryFixture.create({ - name: `teams-event-types-team-${randomString()}`, - isOrganization: false, - }); - - falseTestTeam = await teamsRepositoryFixture.create({ - name: `teams-event-types-false-team-${randomString()}`, - isOrganization: false, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: userAdmin.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teamMember1.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teamMember2.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: falseTestUser.id } }, - team: { connect: { id: falseTestTeam.id } }, - accepted: true, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should not be able to create event-type for user outside team", async () => { - const userId = falseTestUser.id; - - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation", - slug: "coding-consultation", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId, - mandatory: true, - priority: "high", - }, - ], - }; - - return request(app.getHttpServer()).post(`/v2/teams/${team.id}/event-types`).send(body).expect(404); - }); - - it("should not be able to create managed event-type for user outside team", async () => { - const userId = falseTestUser.id; - - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: `managed-outside-team-${randomString()}`, - slug: `managed-outside-team-${randomString()}`, - description: "Managed event type with non-team member.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "MANAGED", - hosts: [ - { - userId, - mandatory: true, - priority: "high", - }, - ], - }; - - return request(app.getHttpServer()).post(`/v2/teams/${team.id}/event-types`).send(body).expect(404); - }); - - it("should not be able to create phone-only event type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Phone coding consultation", - slug: "phone-coding-consultation", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - { - type: "organizersDefaultApp", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teamMember1.id, - }, - { - userId: teamMember2.id, - }, - ], - bookingFields: [ - { - type: "email", - required: false, - label: "Email", - hidden: true, - }, - { - type: "phone", - slug: "attendeePhoneNumber", - required: true, - label: "Phone number", - hidden: false, - }, - ], - }; - - const response = await request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types`) - .send(body) - .expect(400); - expect(response.body.error.message).toBe( - "checkIsEmailUserAccessible - Email booking field must be required and visible" - ); - }); - - it("should not allow creating an event type with integration not installed on team", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation", - slug: "coding-consultation", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "zoom", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teamMember1.id, - }, - { - userId: teamMember2.id, - }, - ], - }; - - return request(app.getHttpServer()).post(`/v2/teams/${team.id}/event-types`).send(body).expect(400); - }); - - it("should create a collective team event-type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: `teams-event-types-collective-${randomString()}`, - slug: `teams-event-types-collective-${randomString()}`, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - bookingFields: [ - { - type: "select", - label: "select which language is your codebase in", - slug: "select-language", - required: true, - placeholder: "select language", - options: ["javascript", "python", "cobol"], - }, - ], - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - schedulingType: "collective", - hosts: [ - { - userId: teamMember1.id, - }, - { - userId: teamMember2.id, - }, - ], - bookingLimitsCount: { - day: 2, - week: 5, - }, - onlyShowFirstAvailableSlot: true, - bookingLimitsDuration: { - day: 60, - week: 100, - }, - offsetStart: 30, - bookingWindow: { - type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays, - value: 30, - rolling: true, - }, - bookerLayouts: { - enabledLayouts: [ - BookerLayoutsInputEnum_2024_06_14.column, - BookerLayoutsInputEnum_2024_06_14.month, - BookerLayoutsInputEnum_2024_06_14.week, - ], - defaultLayout: BookerLayoutsInputEnum_2024_06_14.month, - }, - - confirmationPolicy: { - type: ConfirmationPolicyEnum.TIME, - noticeThreshold: { - count: 60, - unit: NoticeThresholdUnitEnum.MINUTES, - }, - blockUnconfirmedBookingsInBooker: true, - }, - requiresBookerEmailVerification: true, - hideCalendarNotes: true, - hideCalendarEventDetails: true, - hideOrganizerEmail: true, - lockTimeZoneToggleOnBookingPage: true, - color: { - darkThemeHex: "#292929", - lightThemeHex: "#fafafa", - }, - }; - - return request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(body.title); - expect(data.hosts.length).toEqual(2); - expect(data.schedulingType).toEqual("collective"); - evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]); - evaluateHost(body.hosts?.[1] || { userId: -1 }, data.hosts[1]); - expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount); - expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot); - expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration); - expect(data.offsetStart).toEqual(body.offsetStart); - expect(data.bookingWindow).toEqual(body.bookingWindow); - expect(data.bookerLayouts).toEqual(body.bookerLayouts); - expect(data.confirmationPolicy).toEqual(body.confirmationPolicy); - expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification); - expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes); - expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails); - expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail); - expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage); - expect(data.color).toEqual(body.color); - - collectiveEventType = responseBody.data; - }); - }); - - it("should create a managed team event-type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: `teams-event-types-managed-${randomString()}`, - slug: `teams-event-types-managed-${randomString()}`, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "MANAGED", - hosts: [ - { - userId: teamMember1.id, - mandatory: true, - priority: "high", - }, - { - userId: teamMember2.id, - mandatory: false, - priority: "low", - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(3); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - - const teammate1ManagedEvents = teammate1EventTypes.filter((et) => et.title === body.title); - const teammate2ManagedEvents = teammate2EventTypes.filter((et) => et.title === body.title); - const managedTeamEvents = teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" && eventType.title === body.title - ); - - expect(teammate1ManagedEvents.length).toEqual(1); - expect(teammate1ManagedEvents[0].title).toEqual(body.title); - expect(teammate2ManagedEvents.length).toEqual(1); - expect(managedTeamEvents.length).toEqual(1); - - const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.hosts).toHaveLength(2); - expect(responseTeamEvent?.hosts).toEqual( - expect.arrayContaining([ - { - userId: teamMember1.id, - name: teamMember1.name, - username: teamMember1.username, - avatarUrl: teamMember1.avatarUrl, - }, - { - userId: teamMember2.id, - name: teamMember2.name, - username: teamMember2.username, - avatarUrl: teamMember2.avatarUrl, - }, - ]) - ); - - if (!responseTeamEvent) { - throw new Error("Team event not found"); - } - - const responseTeammate1Event = responseBody.data.find((event) => event.ownerId === teamMember1.id); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - const responseTeammate2Event = responseBody.data.find((event) => event.ownerId === teamMember2.id); - expect(responseTeammate2Event).toBeDefined(); - expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - managedEventType = responseTeamEvent; - }); - }); - - it("managed team event types should be returned when fetching event types of users", async () => { - await ensureManagedEventType(); - - return request(app.getHttpServer()) - .get(`/v2/event-types?username=${teamMember1.username}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - const managedEvents = data.filter((event) => event.slug === managedEventType.slug); - expect(managedEvents.length).toEqual(1); - expect(managedEvents[0].slug).toEqual(managedEventType.slug); - expect(managedEvents[0].ownerId).toEqual(teamMember1.id); - expect(managedEvents[0].id).not.toEqual(managedEventType.id); - }); - }); - - it("managed team event type should be returned when fetching event types of users", async () => { - await ensureManagedEventType(); - - return request(app.getHttpServer()) - .get(`/v2/event-types?username=${teamMember1.username}&eventSlug=${managedEventType?.slug}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - const managedEventTypes = data.filter((et) => et.slug === managedEventType?.slug); - expect(managedEventTypes.length).toEqual(1); - expect(managedEventTypes[0].slug).toEqual(managedEventType?.slug); - expect(managedEventTypes[0].ownerId).toEqual(teamMember1.id); - expect(managedEventTypes[0].id).not.toEqual(managedEventType?.id); - }); - }); - - it("should not get a non existing event-type", async () => { - return request(app.getHttpServer()).get(`/v2/teams/${team.id}/event-types/999999`).expect(404); - }); - - it("should get a team event-type", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(collectiveEventType.title); - expect(data.hosts.length).toEqual(2); - evaluateHost(collectiveEventType.hosts[0], data.hosts[0]); - evaluateHost(collectiveEventType.hosts[1], data.hosts[1]); - - collectiveEventType = responseBody.data; - }); - }); - - it("should get team event-types", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/event-types`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(2); - - const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "collective"); - const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "managed"); - - expect(eventTypeCollective?.title).toEqual(collectiveEventType.title); - expect(eventTypeCollective?.hosts.length).toEqual(2); - - expect(eventTypeManaged?.title).toEqual(managedEventType.title); - expect(eventTypeManaged?.hosts.length).toEqual(2); - evaluateHost(collectiveEventType.hosts[0], eventTypeCollective?.hosts[0]); - evaluateHost(collectiveEventType.hosts[1], eventTypeCollective?.hosts[1]); - }); - }); - - it("should not be able to update managed event-type with user outside team", async () => { - await ensureManagedEventType(); - - const body: UpdateTeamEventTypeInput_2024_06_14 = { - hosts: [ - { - userId: falseTestUser.id, - mandatory: true, - priority: "high", - }, - ], - }; - - return request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${managedEventType?.id}`) - .send(body) - .expect(404); - }); - - it("should not be able to update non existing event-type", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - title: "Clean code consultation", - }; - - return request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/999999`) - .send(body) - .expect(400); - }); - - it("should not allow to update event type with integration not installed on team", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - locations: [ - { - type: "integration", - integration: "office365-video", - }, - ], - }; - - return request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`) - .send(body) - .expect(400); - }); - - it("should update collective event-type", async () => { - const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [ - { - userId: teamMember1.id, - }, - ]; - - const body: UpdateTeamEventTypeInput_2024_06_14 = { - hosts: newHosts, - }; - - return request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`) - .send(body) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const eventType = responseBody.data; - expect(eventType.title).toEqual(collectiveEventType.title); - expect(eventType.hosts.length).toEqual(1); - evaluateHost(eventType.hosts[0], newHosts[0]); - }); - }); - - it("should update managed event-type", async () => { - await ensureManagedEventType(); - - const newTitle = `teams-event-types-managed-updated-${randomString()}`; - const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [ - { - userId: teamMember1.id, - mandatory: true, - priority: "medium", - }, - ]; - - const body: UpdateTeamEventTypeInput_2024_06_14 = { - title: newTitle, - hosts: newHosts, - }; - - return request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${managedEventType?.id}`) - .send(body) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(2); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - const managedTeamEventTypes = teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" - ); - - expect(teammate1EventTypes.length).toEqual(1); - expect(teammate1EventTypes[0].title).toEqual(newTitle); - expect(teammate2EventTypes.length).toEqual(0); - expect(managedTeamEventTypes.length).toEqual(1); - expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(false); - expect( - teamEventTypes.filter((eventType) => eventType.schedulingType === "MANAGED")?.[0]?.title - ).toEqual(newTitle); - - const responseTeamEvent = responseBody.data.find( - (eventType) => eventType.schedulingType === "managed" - ); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.title).toEqual(newTitle); - expect(responseTeamEvent?.assignAllTeamMembers).toEqual(false); - - const responseTeammate1Event = responseBody.data.find( - (eventType) => eventType.ownerId === teamMember1.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.title).toEqual(newTitle); - - managedEventType = responseBody.data[0]; - }); - }); - - it("should assign all members to managed event-type", async () => { - await ensureManagedEventType(); - - const body: UpdateTeamEventTypeInput_2024_06_14 = { - assignAllTeamMembers: true, - }; - - return request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${managedEventType?.id}`) - .send(body) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - // note(Lauris): we expect 4 because we have 2 team members, 1 team admin and 4th is the event object itself. - expect(data.length).toEqual(4); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - const managedTeamEventTypes = teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" - ); - - expect(teammate1EventTypes.length).toEqual(1); - expect(teammate2EventTypes.length).toEqual(1); - expect(managedTeamEventTypes.length).toEqual(1); - expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true); - - const responseTeamEvent = responseBody.data.find( - (eventType) => eventType.schedulingType === "managed" - ); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.teamId).toEqual(team.id); - expect(responseTeamEvent?.assignAllTeamMembers).toEqual(true); - - const responseTeammate1Event = responseBody.data.find( - (eventType) => eventType.ownerId === teamMember1.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - const responseTeammate2Event = responseBody.data.find( - (eventType) => eventType.ownerId === teamMember2.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - if (responseTeamEvent) { - managedEventType = responseTeamEvent; - } - }); - }); - - it("should delete event-type not part of the team", async () => { - return request(app.getHttpServer()).delete(`/v2/teams/${team.id}/event-types/99999`).expect(404); - }); - - it("should delete collective event-type", async () => { - return request(app.getHttpServer()) - .delete(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`) - .expect(200); - }); - - it("should delete managed event-type", async () => { - await ensureManagedEventType(); - - return request(app.getHttpServer()) - .delete(`/v2/teams/${team.id}/event-types/${managedEventType?.id}`) - .expect(200); - }); - - function evaluateHost(expected: Host, received: Host | undefined) { - expect(expected.userId).toEqual(received?.userId); - expect(expected.mandatory).toEqual(received?.mandatory); - expect(expected.priority).toEqual(received?.priority); - } - - describe("updating scheduling type", () => { - it("should return 400 error if schedulingType: managed is passed", async () => { - const createBody: CreateTeamEventTypeInput_2024_06_14 = { - title: `teams-event-types-scheduling-collective-${randomString()}`, - slug: `teams-event-types-scheduling-collective-${randomString()}`, - description: "Test collective event type", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teamMember1.id, - }, - ], - }; - - const createResponse = await request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types`) - .send(createBody) - .expect(201); - - const createdEventType: ApiSuccessResponse = createResponse.body; - - const updateBody = { - schedulingType: "managed", - }; - - await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) - .send(updateBody) - .expect(400); - - await eventTypesRepositoryFixture.delete(createdEventType.data.id); - }); - - it("should require hosts when changing round robin event type to collective without providing hosts", async () => { - const createBody: CreateTeamEventTypeInput_2024_06_14 = { - title: `teams-event-types-scheduling-roundrobin-${randomString()}`, - slug: `teams-event-types-scheduling-roundrobin-${randomString()}`, - description: "Test round robin event type", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "ROUND_ROBIN", - hosts: [ - { - userId: teamMember1.id, - priority: "high", - }, - { - userId: teamMember2.id, - priority: "low", - }, - ], - }; - - const createResponse = await request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types`) - .send(createBody) - .expect(201); - - const createdEventType: ApiSuccessResponse = createResponse.body; - - const updateBody = { - schedulingType: "collective", - }; - - const response = await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) - .send(updateBody); - - expect(response.status).toBe(400); - expect(response.body.error.message).toBe(HOSTS_REQUIRED_WHEN_SWITCHING_SCHEDULING_TYPE_ERROR); - - await eventTypesRepositoryFixture.delete(createdEventType.data.id); - }); - - it("should require hosts when changing collective event type to roundRobin without providing hosts", async () => { - const createBody: CreateTeamEventTypeInput_2024_06_14 = { - title: `teams-event-types-scheduling-collective-${randomString()}`, - slug: `teams-event-types-scheduling-collective-${randomString()}`, - description: "Test collective event type", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teamMember1.id, - }, - { - userId: teamMember2.id, - }, - ], - }; - - const createResponse = await request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types`) - .send(createBody) - .expect(201); - - const createdEventType: ApiSuccessResponse = createResponse.body; - - const updateBody = { - schedulingType: "roundRobin", - }; - - const response = await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) - .send(updateBody); - - expect(response.status).toBe(400); - expect(response.body.error.message).toBe(HOSTS_REQUIRED_WHEN_SWITCHING_SCHEDULING_TYPE_ERROR); - - await eventTypesRepositoryFixture.delete(createdEventType.data.id); - }); - - it("should change round robin event type to collective and pass new hosts", async () => { - const createBody: CreateTeamEventTypeInput_2024_06_14 = { - title: `teams-event-types-scheduling-roundrobin-${randomString()}`, - slug: `teams-event-types-scheduling-roundrobin-${randomString()}`, - description: "Test round robin event type", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "ROUND_ROBIN", - hosts: [ - { - userId: teamMember1.id, - priority: "high", - }, - ], - }; - - const createResponse = await request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types`) - .send(createBody) - .expect(201); - - const createdEventType: ApiSuccessResponse = createResponse.body; - - const updateBody = { - schedulingType: "collective", - hosts: [ - { - userId: teamMember2.id, - }, - ], - }; - - const updateResponse = await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) - .send(updateBody) - .expect(200); - - const responseBody: ApiSuccessResponse = updateResponse.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.schedulingType).toEqual("collective"); - expect(responseBody.data.hosts).toHaveLength(1); - expect(responseBody.data.hosts[0].userId).toEqual(teamMember2.id); - - await eventTypesRepositoryFixture.delete(createdEventType.data.id); - }); - - it("should change collective event type to roundRobin and pass new hosts", async () => { - const createBody = { - title: `teams-event-types-scheduling-collective-${randomString()}`, - slug: `teams-event-types-scheduling-collective-${randomString()}`, - description: "Test collective event type", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "collective", - hosts: [ - { - userId: teamMember1.id, - }, - ], - }; - - const createResponse = await request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types`) - .send(createBody) - .expect(201); - - const createdEventType: ApiSuccessResponse = createResponse.body; - - const updateBody = { - schedulingType: "roundRobin", - hosts: [ - { - userId: teamMember2.id, - priority: "medium", - }, - ], - }; - - const updateResponse = await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) - .send(updateBody) - .expect(200); - - const responseBody: ApiSuccessResponse = updateResponse.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.schedulingType).toEqual("roundRobin"); - expect(responseBody.data.hosts).toHaveLength(1); - expect(responseBody.data.hosts[0].userId).toEqual(teamMember2.id); - - await eventTypesRepositoryFixture.delete(createdEventType.data.id); - }); - - it("should change collective event type to roundRobin with assignAllTeamMembers: true", async () => { - const createBody: CreateTeamEventTypeInput_2024_06_14 = { - title: `teams-event-types-scheduling-collective-${randomString()}`, - slug: `teams-event-types-scheduling-collective-${randomString()}`, - description: "Test collective event type", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teamMember1.id, - }, - ], - }; - - const createResponse = await request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types`) - .send(createBody) - .expect(201); - - const createdEventType: ApiSuccessResponse = createResponse.body; - - const updateBody = { - schedulingType: "roundRobin", - assignAllTeamMembers: true, - }; - - const updateResponse = await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) - .send(updateBody) - .expect(200); - - const responseBody: ApiSuccessResponse = updateResponse.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.schedulingType).toEqual("roundRobin"); - expect(responseBody.data.hosts).toHaveLength(3); - const hostUserIds = responseBody.data.hosts.map((host) => host.userId); - expect(hostUserIds).toContain(teamMember1.id); - expect(hostUserIds).toContain(teamMember2.id); - expect(hostUserIds).toContain(userAdmin.id); - - await eventTypesRepositoryFixture.delete(createdEventType.data.id); - }); - - it("should change round robin event type to collective with assignAllTeamMembers: true", async () => { - const createBody: CreateTeamEventTypeInput_2024_06_14 = { - title: `teams-event-types-scheduling-roundrobin-${randomString()}`, - slug: `teams-event-types-scheduling-roundrobin-${randomString()}`, - description: "Test round robin event type", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "ROUND_ROBIN", - hosts: [ - { - userId: teamMember1.id, - priority: "high", - }, - ], - }; - - const createResponse = await request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types`) - .send(createBody) - .expect(201); - - const createdEventType: ApiSuccessResponse = createResponse.body; - - const updateBody = { - schedulingType: "collective", - assignAllTeamMembers: true, - }; - - const updateResponse = await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) - .send(updateBody) - .expect(200); - - const responseBody: ApiSuccessResponse = updateResponse.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.schedulingType).toEqual("collective"); - expect(responseBody.data.hosts).toHaveLength(3); - const hostUserIds = responseBody.data.hosts.map((host) => host.userId); - expect(hostUserIds).toContain(teamMember1.id); - expect(hostUserIds).toContain(teamMember2.id); - expect(hostUserIds).toContain(userAdmin.id); - - await eventTypesRepositoryFixture.delete(createdEventType.data.id); - }); - - it("should preserve existing hosts when updating without changing scheduling type", async () => { - const createBody = { - title: `teams-event-types-scheduling-preserve-${randomString()}`, - slug: `teams-event-types-scheduling-preserve-${randomString()}`, - description: "Test preserve hosts", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "collective", - hosts: [ - { - userId: teamMember1.id, - }, - { - userId: teamMember2.id, - }, - ], - }; - - const createResponse = await request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/event-types`) - .send(createBody) - .expect(201); - - const createdEventType: ApiSuccessResponse = createResponse.body; - - const updateBody = { - title: "Updated title", - }; - - const updateResponse = await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) - .send(updateBody) - .expect(200); - - const responseBody: ApiSuccessResponse = updateResponse.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.title).toEqual("Updated title"); - expect(responseBody.data.schedulingType).toEqual("collective"); - expect(responseBody.data.hosts).toHaveLength(2); - expect(responseBody.data.hosts.map((h) => h.userId)).toContain(teamMember1.id); - expect(responseBody.data.hosts.map((h) => h.userId)).toContain(teamMember2.id); - - await eventTypesRepositoryFixture.delete(createdEventType.data.id); - }); - }); - - describe("should update event type title", () => { - it("should preserve hosts when updating collective event type title", async () => { - const collectiveEventType = await eventTypesRepositoryFixture.create( - { - title: `collective-preserve-hosts-${randomString()}`, - slug: `collective-preserve-hosts-${randomString()}`, - length: 60, - team: { - connect: { - id: team.id, - }, - }, - schedulingType: "COLLECTIVE", - hosts: { - create: [ - { - userId: teamMember1.id, - isFixed: true, - }, - { - userId: teamMember2.id, - isFixed: true, - }, - ], - }, - }, - userAdmin.id - ); - - const newTitle = `updated-collective-title-${randomString()}`; - const updateBody = { - title: newTitle, - }; - - const response = await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`) - .send(updateBody) - .expect(200); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.title).toEqual(newTitle); - expect(responseBody.data.schedulingType).toEqual("collective"); - expect(responseBody.data.hosts).toHaveLength(2); - expect(responseBody.data.hosts.map((h) => h.userId)).toContain(teamMember1.id); - expect(responseBody.data.hosts.map((h) => h.userId)).toContain(teamMember2.id); - - await eventTypesRepositoryFixture.delete(collectiveEventType.id); - }); - - it("should preserve hosts when updating round robin event type title", async () => { - const roundRobinEventType = await eventTypesRepositoryFixture.create( - { - title: `roundrobin-preserve-hosts-${randomString()}`, - slug: `roundrobin-preserve-hosts-${randomString()}`, - length: 60, - team: { - connect: { - id: team.id, - }, - }, - schedulingType: "ROUND_ROBIN", - hosts: { - create: [ - { - userId: teamMember1.id, - isFixed: false, - priority: 2, - }, - { - userId: teamMember2.id, - isFixed: true, - priority: 1, - }, - ], - }, - }, - userAdmin.id - ); - - const newTitle = `updated-roundrobin-title-${randomString()}`; - const updateBody = { - title: newTitle, - }; - - const response = await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${roundRobinEventType.id}`) - .send(updateBody) - .expect(200); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.title).toEqual(newTitle); - expect(responseBody.data.schedulingType).toEqual("roundRobin"); - expect(responseBody.data.hosts).toHaveLength(2); - expect(responseBody.data.hosts.map((h) => h.userId)).toContain(teamMember1.id); - expect(responseBody.data.hosts.map((h) => h.userId)).toContain(teamMember2.id); - - await eventTypesRepositoryFixture.delete(roundRobinEventType.id); - }); - }); - - describe("should update event type hosts", () => { - it("should update collective event type hosts", async () => { - const collectiveEventType = await eventTypesRepositoryFixture.create( - { - title: `collective-update-hosts-${randomString()}`, - slug: `collective-update-hosts-${randomString()}`, - length: 60, - team: { - connect: { - id: team.id, - }, - }, - schedulingType: "COLLECTIVE", - hosts: { - create: [ - { - userId: teamMember1.id, - isFixed: true, - }, - ], - }, - }, - userAdmin.id - ); - - const updateBody = { - hosts: [ - { - userId: teamMember2.id, - }, - ], - }; - - const response = await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`) - .send(updateBody) - .expect(200); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.schedulingType).toEqual("collective"); - expect(responseBody.data.hosts).toHaveLength(1); - expect(responseBody.data.hosts[0].userId).toEqual(teamMember2.id); - - await eventTypesRepositoryFixture.delete(collectiveEventType.id); - }); - - it("should update round robin event type hosts", async () => { - const roundRobinEventType = await eventTypesRepositoryFixture.create( - { - title: `roundrobin-update-hosts-${randomString()}`, - slug: `roundrobin-update-hosts-${randomString()}`, - length: 60, - team: { - connect: { - id: team.id, - }, - }, - schedulingType: "ROUND_ROBIN", - hosts: { - create: [ - { - userId: teamMember1.id, - isFixed: true, - priority: 1, - }, - ], - }, - }, - userAdmin.id - ); - - const updateBody = { - hosts: [ - { - userId: teamMember2.id, - priority: "high", - }, - ], - }; - - const response = await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/event-types/${roundRobinEventType.id}`) - .send(updateBody) - .expect(200); - - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.schedulingType).toEqual("roundRobin"); - expect(responseBody.data.hosts).toHaveLength(1); - expect(responseBody.data.hosts[0].userId).toEqual(teamMember2.id); - - await eventTypesRepositoryFixture.delete(roundRobinEventType.id); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(userAdmin.email); - await userRepositoryFixture.deleteByEmail(teamMember1.email); - await userRepositoryFixture.deleteByEmail(teamMember2.email); - await userRepositoryFixture.deleteByEmail(falseTestUser.email); - await teamsRepositoryFixture.delete(team.id); - await teamsRepositoryFixture.delete(falseTestTeam.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.ts b/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.ts deleted file mode 100644 index ca3f1e2bebbcca..00000000000000 --- a/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input"; -import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer"; -import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service"; -import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service"; -import { CreateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/create-team-event-type.output"; -import { DeleteTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/delete-team-event-type.output"; -import { GetTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/get-team-event-type.output"; -import { GetTeamEventTypesOutput } from "@/modules/teams/event-types/outputs/get-team-event-types.output"; -import { UpdateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/update-team-event-type.output"; -import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { - Controller, - UseGuards, - Get, - Post, - Param, - ParseIntPipe, - Body, - Patch, - Delete, - HttpCode, - HttpStatus, - NotFoundException, - Query, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import { handleCreatePhoneCall } from "@calcom/platform-libraries"; -import { - CreateTeamEventTypeInput_2024_06_14, - GetTeamEventTypesQuery_2024_06_14, - SkipTakePagination, - TeamEventTypeOutput_2024_06_14, - UpdateTeamEventTypeInput_2024_06_14, -} from "@calcom/platform-types"; - -export type EventTypeHandlerResponse = { - data: DatabaseTeamEventType[] | DatabaseTeamEventType; - status: typeof SUCCESS_STATUS | typeof ERROR_STATUS; -}; - -@Controller({ - path: "/v2/teams/:teamId/event-types", - version: API_VERSIONS_VALUES, -}) -@DocsTags("Teams / Event Types") -@ApiParam({ name: "teamId", type: Number, required: true }) -export class TeamsEventTypesController { - constructor( - private readonly teamsEventTypesService: TeamsEventTypesService, - private readonly inputService: InputOrganizationsEventTypesService, - private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe - ) {} - - @Roles("TEAM_ADMIN") - @PlatformPlan("ESSENTIALS") - @UseGuards(ApiAuthGuard, RolesGuard) - @ApiHeader(API_KEY_HEADER) - @Post("/") - @ApiOperation({ summary: "Create an event type" }) - async createTeamEventType( - @GetUser() user: UserWithProfile, - @Param("teamId", ParseIntPipe) teamId: number, - @Body() bodyEventType: CreateTeamEventTypeInput_2024_06_14 - ): Promise { - const transformedBody = await this.inputService.transformAndValidateCreateTeamEventTypeInput( - user.id, - teamId, - bodyEventType - ); - - const eventType = await this.teamsEventTypesService.createTeamEventType(user, teamId, transformedBody); - - return { - status: SUCCESS_STATUS, - data: await this.outputTeamEventTypesResponsePipe.transform(eventType), - }; - } - - @Roles("TEAM_ADMIN") - @UseGuards(ApiAuthGuard, RolesGuard) - @ApiHeader(API_KEY_HEADER) - @Get("/:eventTypeId") - @ApiOperation({ summary: "Get an event type" }) - async getTeamEventType( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("eventTypeId") eventTypeId: number - ): Promise { - const eventType = await this.teamsEventTypesService.getTeamEventType(teamId, eventTypeId); - - if (!eventType) { - throw new NotFoundException(`Event type with id ${eventTypeId} not found`); - } - - return { - status: SUCCESS_STATUS, - data: (await this.outputTeamEventTypesResponsePipe.transform( - eventType - )) as TeamEventTypeOutput_2024_06_14, - }; - } - - @Roles("TEAM_ADMIN") - @Post("/:eventTypeId/create-phone-call") - @UseGuards(ApiAuthGuard, RolesGuard) - @ApiHeader(API_KEY_HEADER) - @ApiOperation({ summary: "Create a phone call" }) - async createPhoneCall( - @Param("eventTypeId") eventTypeId: number, - @Param("orgId", ParseIntPipe) orgId: number, - @Body() body: CreatePhoneCallInput, - @GetUser() user: UserWithProfile - ): Promise { - const data = await handleCreatePhoneCall({ - user: { - id: user.id, - timeZone: user.timeZone, - profile: { organization: { id: orgId } }, - }, - input: { ...body, eventTypeId }, - }); - - return { - status: SUCCESS_STATUS, - data, - }; - } - - @Get("/") - @ApiOperation({ - summary: "Get team event types", - description: - 'Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.', - }) - async getTeamEventTypes( - @Param("teamId", ParseIntPipe) teamId: number, - @Query() queryParams: GetTeamEventTypesQuery_2024_06_14 - ): Promise { - const { eventSlug, hostsLimit, sortCreatedAt } = queryParams; - - if (eventSlug) { - const eventType = await this.teamsEventTypesService.getTeamEventTypeBySlug( - teamId, - eventSlug, - hostsLimit - ); - - return { - status: SUCCESS_STATUS, - data: await this.outputTeamEventTypesResponsePipe.transform(eventType ? [eventType] : []), - }; - } - - const eventTypes = await this.teamsEventTypesService.getTeamEventTypes(teamId, sortCreatedAt); - - return { - status: SUCCESS_STATUS, - data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes), - }; - } - - @Roles("TEAM_ADMIN") - @UseGuards(ApiAuthGuard, RolesGuard) - @ApiHeader(API_KEY_HEADER) - @Patch("/:eventTypeId") - @ApiOperation({ summary: "Update a team event type" }) - async updateTeamEventType( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("eventTypeId", ParseIntPipe) eventTypeId: number, - @GetUser() user: UserWithProfile, - @Body() bodyEventType: UpdateTeamEventTypeInput_2024_06_14 - ): Promise { - const transformedBody = await this.inputService.transformAndValidateUpdateTeamEventTypeInput( - user.id, - eventTypeId, - teamId, - bodyEventType - ); - - const eventType = await this.teamsEventTypesService.updateTeamEventType( - eventTypeId, - teamId, - transformedBody, - user, - false - ); - - return { - status: SUCCESS_STATUS, - data: await this.outputTeamEventTypesResponsePipe.transform(eventType), - }; - } - - @Roles("TEAM_ADMIN") - @UseGuards(ApiAuthGuard, RolesGuard) - @ApiHeader(API_KEY_HEADER) - @Delete("/:eventTypeId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Delete a team event type" }) - async deleteTeamEventType( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("eventTypeId", ParseIntPipe) eventTypeId: number - ): Promise { - const eventType = await this.teamsEventTypesService.deleteTeamEventType(teamId, eventTypeId); - - return { - status: SUCCESS_STATUS, - data: { - id: eventTypeId, - title: eventType.title, - }, - }; - } -} diff --git a/apps/api/v2/src/modules/teams/event-types/outputs/create-team-event-type.output.ts b/apps/api/v2/src/modules/teams/event-types/outputs/create-team-event-type.output.ts deleted file mode 100644 index 856fc7d9bee5ea..00000000000000 --- a/apps/api/v2/src/modules/teams/event-types/outputs/create-team-event-type.output.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types"; - -@ApiExtraModels(TeamEventTypeOutput_2024_06_14) -export class CreateTeamEventTypeOutput extends ApiResponseWithoutData { - @IsNotEmptyObject() - @ValidateNested() - @ApiProperty({ - oneOf: [ - { $ref: getSchemaPath(TeamEventTypeOutput_2024_06_14) }, - { - type: "array", - items: { $ref: getSchemaPath(TeamEventTypeOutput_2024_06_14) }, - }, - ], - }) - @Type(() => TeamEventTypeOutput_2024_06_14) - data!: TeamEventTypeOutput_2024_06_14 | TeamEventTypeOutput_2024_06_14[]; -} diff --git a/apps/api/v2/src/modules/teams/event-types/outputs/delete-team-event-type.output.ts b/apps/api/v2/src/modules/teams/event-types/outputs/delete-team-event-type.output.ts deleted file mode 100644 index 300728c55fb03a..00000000000000 --- a/apps/api/v2/src/modules/teams/event-types/outputs/delete-team-event-type.output.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types"; - -export class DeleteTeamEventTypeOutput extends ApiResponseWithoutData { - @ValidateNested() - @Type(() => TeamEventTypeOutput_2024_06_14) - data!: Pick; -} diff --git a/apps/api/v2/src/modules/teams/event-types/outputs/get-team-event-type.output.ts b/apps/api/v2/src/modules/teams/event-types/outputs/get-team-event-type.output.ts deleted file mode 100644 index 3206c66a584f7b..00000000000000 --- a/apps/api/v2/src/modules/teams/event-types/outputs/get-team-event-type.output.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types"; - -export class GetTeamEventTypeOutput extends ApiResponseWithoutData { - @ValidateNested() - @Type(() => TeamEventTypeOutput_2024_06_14) - data!: TeamEventTypeOutput_2024_06_14; -} diff --git a/apps/api/v2/src/modules/teams/event-types/outputs/get-team-event-types.output.ts b/apps/api/v2/src/modules/teams/event-types/outputs/get-team-event-types.output.ts deleted file mode 100644 index 939c74c369f11d..00000000000000 --- a/apps/api/v2/src/modules/teams/event-types/outputs/get-team-event-types.output.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types"; - -export class GetTeamEventTypesOutput extends ApiResponseWithoutData { - @ValidateNested({ each: true }) - @Type(() => TeamEventTypeOutput_2024_06_14) - data!: TeamEventTypeOutput_2024_06_14[]; -} diff --git a/apps/api/v2/src/modules/teams/event-types/outputs/update-team-event-type.output.ts b/apps/api/v2/src/modules/teams/event-types/outputs/update-team-event-type.output.ts deleted file mode 100644 index 55874ec3af3277..00000000000000 --- a/apps/api/v2/src/modules/teams/event-types/outputs/update-team-event-type.output.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types"; - -@ApiExtraModels(TeamEventTypeOutput_2024_06_14) -export class UpdateTeamEventTypeOutput extends ApiResponseWithoutData { - @IsNotEmptyObject() - @ValidateNested() - @ApiProperty({ - oneOf: [ - { $ref: getSchemaPath(TeamEventTypeOutput_2024_06_14) }, - { - type: "array", - items: { $ref: getSchemaPath(TeamEventTypeOutput_2024_06_14) }, - }, - ], - }) - @Type(() => TeamEventTypeOutput_2024_06_14) - data!: TeamEventTypeOutput_2024_06_14 | TeamEventTypeOutput_2024_06_14[]; -} diff --git a/apps/api/v2/src/modules/organizations/event-types/pipes/team-event-types-response.transformer.ts b/apps/api/v2/src/modules/teams/event-types/pipes/output-team-event-types-response.pipe.ts similarity index 75% rename from apps/api/v2/src/modules/organizations/event-types/pipes/team-event-types-response.transformer.ts rename to apps/api/v2/src/modules/teams/event-types/pipes/output-team-event-types-response.pipe.ts index 0aa4061ac82eb8..7e136c7275cb8d 100644 --- a/apps/api/v2/src/modules/organizations/event-types/pipes/team-event-types-response.transformer.ts +++ b/apps/api/v2/src/modules/teams/event-types/pipes/output-team-event-types-response.pipe.ts @@ -1,18 +1,19 @@ -import { OutputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/output.service"; -import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service"; +import { TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types"; import { Injectable, PipeTransform } from "@nestjs/common"; import { plainToClass } from "class-transformer"; - -import { TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types"; +import { + DatabaseTeamEventType, + OutputTeamEventTypesService, +} from "@/modules/teams/event-types/services/output-team-event-types.service"; @Injectable() export class OutputTeamEventTypesResponsePipe implements PipeTransform { - constructor(private readonly outputOrganizationsEventTypesService: OutputOrganizationsEventTypesService) {} + constructor(private readonly outputTeamEventTypesService: OutputTeamEventTypesService) {} private async transformEventType(item: DatabaseTeamEventType): Promise { return plainToClass( TeamEventTypeOutput_2024_06_14, - await this.outputOrganizationsEventTypesService.getResponseTeamEventType(item, true), + await this.outputTeamEventTypesService.getResponseTeamEventType(item, true), { strategy: "exposeAll" } ); } diff --git a/apps/api/v2/src/modules/organizations/event-types/services/output.service.ts b/apps/api/v2/src/modules/teams/event-types/services/output-team-event-types.service.ts similarity index 97% rename from apps/api/v2/src/modules/organizations/event-types/services/output.service.ts rename to apps/api/v2/src/modules/teams/event-types/services/output-team-event-types.service.ts index dc48db092a4dd8..6cd1f1eddae8e2 100644 --- a/apps/api/v2/src/modules/organizations/event-types/services/output.service.ts +++ b/apps/api/v2/src/modules/teams/event-types/services/output-team-event-types.service.ts @@ -1,20 +1,19 @@ -import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; -import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; -import { UsersRepository } from "@/modules/users/users.repository"; -import { Injectable } from "@nestjs/common"; - import { SchedulingType } from "@calcom/platform-libraries"; import { EventTypeMetadata } from "@calcom/platform-libraries/event-types"; import type { HostPriority, TeamEventTypeResponseHost } from "@calcom/platform-types"; import type { - Team, + CalVideoSettings, + DestinationCalendar, EventType, - User, - Schedule, Host, - DestinationCalendar, - CalVideoSettings, + Schedule, + Team, + User, } from "@calcom/prisma/client"; +import { Injectable } from "@nestjs/common"; +import { OutputEventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/output-event-types.service"; +import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; +import { UsersRepository } from "@/modules/users/users.repository"; type EventTypeRelations = { users: User[]; @@ -100,7 +99,7 @@ type Input = Pick< >; @Injectable() -export class OutputOrganizationsEventTypesService { +export class OutputTeamEventTypesService { constructor( private readonly outputEventTypesService: OutputEventTypesService_2024_06_14, private readonly teamsEventTypesRepository: TeamsEventTypesRepository, diff --git a/apps/api/v2/src/modules/teams/event-types/services/teams-event-types.service.ts b/apps/api/v2/src/modules/teams/event-types/services/teams-event-types.service.ts index 95e775d59c81e7..dcf57519f53935 100644 --- a/apps/api/v2/src/modules/teams/event-types/services/teams-event-types.service.ts +++ b/apps/api/v2/src/modules/teams/event-types/services/teams-event-types.service.ts @@ -1,19 +1,36 @@ -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; -import { - TransformedCreateTeamEventTypeInput, - TransformedUpdateTeamEventTypeInput, -} from "@/modules/organizations/event-types/services/input.service"; -import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; +import { EventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/event-types.service"; +import { InputEventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/input-event-types.service"; +import type { CustomField, SystemField } from "@/platform/event-types/event-types_2024_06_14/transformers"; +import { DatabaseTeamEventType } from "@/modules/teams/event-types/services/output-team-event-types.service"; + +type BaseTransformedEventType = ReturnType< + InstanceType["transformInputCreateEventType"] +>; + +type TransformedCreateTeamEventTypeInput = BaseTransformedEventType & { + bookingFields?: (SystemField | CustomField)[]; + hosts?: { userId: number; isFixed?: boolean; priority?: number }[]; + children?: { + hidden: boolean; + owner: { id: number; name: string; email: string; eventTypeSlugs: string[] }; + }[]; + destinationCalendar?: { integration: string; externalId: string }; + assignAllTeamMembers?: boolean; +}; + +type TransformedUpdateTeamEventTypeInput = Partial & { + bookingFields?: (SystemField | CustomField)[]; + id?: number; +}; + +import { createEventType, updateEventType } from "@calcom/platform-libraries/event-types"; +import type { SortOrderType } from "@calcom/platform-types"; +import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; import { UsersService } from "@/modules/users/services/users.service"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable, NotFoundException, Logger } from "@nestjs/common"; - -import type { SortOrderType } from "@calcom/platform-types"; - -import { createEventType, updateEventType } from "@calcom/platform-libraries/event-types"; @Injectable() export class TeamsEventTypesService { @@ -44,8 +61,7 @@ export class TeamsEventTypesService { input: { teamId: teamId, ...rest }, ctx: { user: eventTypeUser, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + // @ts-ignore - prisma type mismatch between PrismaClient versions prisma: this.dbWrite.prisma, }, }); @@ -128,8 +144,7 @@ export class TeamsEventTypesService { }, ctx: { user: eventTypeUser, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + // @ts-ignore - prisma type mismatch between PrismaClient versions prisma: this.dbWrite.prisma, }, }); diff --git a/apps/api/v2/src/modules/teams/event-types/teams-event-types.module.ts b/apps/api/v2/src/modules/teams/event-types/teams-event-types.module.ts deleted file mode 100644 index c4b8d01e68aef1..00000000000000 --- a/apps/api/v2/src/modules/teams/event-types/teams-event-types.module.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Module } from "@nestjs/common"; -import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; -import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository"; -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OrganizationsConferencingModule } from "@/modules/organizations/conferencing/organizations-conferencing.module"; -import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer"; -import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service"; -import { OutputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/output.service"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { TeamsEventTypesController } from "@/modules/teams/event-types/controllers/teams-event-types.controller"; -import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; -import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; -import { TeamsModule } from "@/modules/teams/teams/teams.module"; -import { UsersModule } from "@/modules/users/users.module"; - -@Module({ - imports: [ - PrismaModule, - RedisModule, - MembershipsModule, - EventTypesModule_2024_06_14, - UsersModule, - TeamsModule, - OrganizationsConferencingModule, - ], - providers: [ - TeamsEventTypesRepository, - TeamsEventTypesService, - InputOrganizationsEventTypesService, - OrganizationsTeamsRepository, - OutputTeamEventTypesResponsePipe, - OutputOrganizationsEventTypesService, - ConferencingRepository, - ], - exports: [TeamsEventTypesRepository, TeamsEventTypesService], - controllers: [TeamsEventTypesController], -}) -export class TeamsEventTypesModule {} diff --git a/apps/api/v2/src/modules/teams/event-types/teams-event-types.repository.ts b/apps/api/v2/src/modules/teams/event-types/teams-event-types.repository.ts index 067ef98d2ec350..c202e701f2d952 100644 --- a/apps/api/v2/src/modules/teams/event-types/teams-event-types.repository.ts +++ b/apps/api/v2/src/modules/teams/event-types/teams-event-types.repository.ts @@ -1,12 +1,14 @@ +import type { SortOrderType } from "@calcom/platform-types"; +import { Injectable } from "@nestjs/common"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -import type { SortOrderType } from "@calcom/platform-types"; @Injectable() export class TeamsEventTypesRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} + constructor( + private readonly dbRead: PrismaReadService, + private readonly dbWrite: PrismaWriteService + ) {} async getTeamEventType(teamId: number, eventTypeId: number) { return this.dbRead.prisma.eventType.findUnique({ diff --git a/apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.e2e-spec.ts b/apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.e2e-spec.ts deleted file mode 100644 index ffb36119e85532..00000000000000 --- a/apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.e2e-spec.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { bootstrap } from "@/bootstrap"; -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 { 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 { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; -import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; -import { randomString } from "test/utils/randomString"; -import { withApiAuth } from "test/utils/withApiAuth"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { Team, User } from "@calcom/prisma/client"; - -describe("Teams Invite Endpoints", () => { - describe("User Authentication - User is Team Admin", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let team: Team; - - const userEmail = `teams-invite-admin-${randomString()}@api.com`; - - let user: User; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - team = await teamsRepositoryFixture.create({ - name: `teams-invite-team-${randomString()}`, - isOrganization: false, - }); - - // Admin of the team - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: team.id } }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - it("should create a team invite", async () => { - return request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/invite`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - expect(response.body.data.token.length).toBeGreaterThan(0); - expect(response.body.data.inviteLink).toEqual(expect.any(String)); - expect(response.body.data.inviteLink).toContain(response.body.data.token); - }); - }); - - it("should create a new invite on each request", async () => { - const first = await request(app.getHttpServer()).post(`/v2/teams/${team.id}/invite`).expect(200); - const firstToken = first.body.data.token as string; - - return request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/invite`) - .expect(200) - .then((response) => { - expect(response.body.status).toEqual(SUCCESS_STATUS); - expect(response.body.data.token).not.toEqual(firstToken); - expect(response.body.data.inviteLink).toEqual(expect.any(String)); - expect(response.body.data.inviteLink).toContain(response.body.data.token); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(user.email); - await teamsRepositoryFixture.delete(team.id); - await app.close(); - }); - }); - - describe("User Authentication - User is Team Member (not Admin)", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let team: Team; - - const userEmail = `teams-invite-member-${randomString()}@api.com`; - - let user: User; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - team = await teamsRepositoryFixture.create({ - name: `teams-invite-member-team-${randomString()}`, - isOrganization: false, - }); - - // Regular member of the team (not admin) - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: user.id } }, - team: { connect: { id: team.id } }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - it("should fail to create invite as non-admin member", async () => { - return request(app.getHttpServer()).post(`/v2/teams/${team.id}/invite`).expect(403); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(user.email); - await teamsRepositoryFixture.delete(team.id); - await app.close(); - }); - }); - - describe("User Authentication - User is not a Team Member", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - - let team: Team; - - const userEmail = `teams-invite-non-member-${randomString()}@api.com`; - - let user: User; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - - team = await teamsRepositoryFixture.create({ - name: `teams-invite-non-member-team-${randomString()}`, - isOrganization: false, - }); - - // User is NOT a member of this team - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - await app.init(); - }); - - it("should fail to create invite as non-member", async () => { - return request(app.getHttpServer()).post(`/v2/teams/${team.id}/invite`).expect(403); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(user.email); - await teamsRepositoryFixture.delete(team.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts b/apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts deleted file mode 100644 index e5190ca5e4aebb..00000000000000 --- a/apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { CreateInviteOutputDto } from "@/modules/teams/invite/outputs/invite.output"; - -import { - Controller, - UseGuards, - Post, - Param, - ParseIntPipe, - HttpCode, - HttpStatus, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { TeamService } from "@calcom/platform-libraries"; - -@Controller({ - path: "/v2/teams/:teamId", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, RolesGuard) -@DocsTags("Teams / Invite") -@ApiHeader(API_KEY_HEADER) -export class TeamsInviteController { - @Post("/invite") - @Roles("TEAM_ADMIN") - @ApiOperation({ summary: "Create team invite link" }) - @HttpCode(HttpStatus.OK) - async createInvite( - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - const result = await TeamService.createInvite(teamId); - return { status: SUCCESS_STATUS, data: result }; - } -} diff --git a/apps/api/v2/src/modules/teams/invite/outputs/invite.output.ts b/apps/api/v2/src/modules/teams/invite/outputs/invite.output.ts deleted file mode 100644 index 05ddc3ab15218e..00000000000000 --- a/apps/api/v2/src/modules/teams/invite/outputs/invite.output.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum, IsString, ValidateNested } from "class-validator"; - -export class InviteDataDto { - @IsString() - @Expose() - @ApiProperty({ - description: - "Unique invitation token for this team. Share this token with prospective members to allow them to join the team.", - example: "f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2", - }) - token!: string; - - @IsString() - @Expose() - @ApiProperty({ - description: - "Complete invitation URL that can be shared with prospective members. Opens the signup page with the token and redirects to getting started after signup.", - example: - "http://app.cal.com/signup?token=f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2&callbackUrl=/getting-started", - }) - inviteLink!: string; -} - -export class CreateInviteOutputDto { - @Expose() - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @Expose() - @ValidateNested() - @Type(() => InviteDataDto) - @ApiProperty({ type: InviteDataDto }) - data!: InviteDataDto; -} diff --git a/apps/api/v2/src/modules/teams/invite/teams-invite.module.ts b/apps/api/v2/src/modules/teams/invite/teams-invite.module.ts deleted file mode 100644 index 8ab7c971004647..00000000000000 --- a/apps/api/v2/src/modules/teams/invite/teams-invite.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { TeamsInviteController } from "@/modules/teams/invite/controllers/teams-invite.controller"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [PrismaModule, RedisModule, MembershipsModule], - controllers: [TeamsInviteController], -}) -export class TeamsInviteModule {} diff --git a/apps/api/v2/src/modules/teams/memberships/controllers/teams-memberships.controller.e2e-spec.ts b/apps/api/v2/src/modules/teams/memberships/controllers/teams-memberships.controller.e2e-spec.ts deleted file mode 100644 index 2cebd8f8837df3..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/controllers/teams-memberships.controller.e2e-spec.ts +++ /dev/null @@ -1,737 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { ApiSuccessResponse } from "@calcom/platform-types"; -import type { EventType, Membership, 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 { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.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 { withApiAuth } from "test/utils/withApiAuth"; -import { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input"; -import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input"; -import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output"; -import { GetTeamMembershipOutput } from "@/modules/teams/memberships/outputs/get-team-membership.output"; -import { GetTeamMembershipsOutput } from "@/modules/teams/memberships/outputs/get-team-memberships.output"; -import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; -import { UpdateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/update-team-membership.output"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; - -describe("Teams Memberships Endpoints", () => { - describe("User Authentication - User is Team Admin", () => { - let app: INestApplication; - - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let userRepositoryFixture: UserRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let team: Team; - let teamEventType: EventType; - let managedEventType: EventType; - let teamAdminMembership: Membership; - let teamMemberMembership: Membership; - let membershipCreatedViaApi: TeamMembershipOutput; - - const teamAdminEmail = `alice-admin-${randomString()}@api.com`; - const teamMemberEmail = `bob-member-${randomString()}@api.com`; - const nonTeamUserEmail = `charlie-outsider-${randomString()}@api`; - - const invitedUserEmail = `david-invited-${randomString()}@api.com`; - - let teamAdmin: User; - let teamMember: User; - let nonTeamUser: User; - - let teammateInvitedViaApi: User; - - const metadata = { - some: "key", - }; - const bio = "This is a bio"; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - teamAdminEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - - teamAdmin = await userRepositoryFixture.create({ - email: teamAdminEmail, - username: teamAdminEmail, - bio, - metadata, - }); - - teamMember = await userRepositoryFixture.create({ - email: teamMemberEmail, - username: teamMemberEmail, - bio, - metadata, - }); - - nonTeamUser = await userRepositoryFixture.create({ - email: nonTeamUserEmail, - username: nonTeamUserEmail, - }); - - teammateInvitedViaApi = await userRepositoryFixture.create({ - email: invitedUserEmail, - username: invitedUserEmail, - bio, - metadata, - }); - - team = await teamsRepositoryFixture.create({ - name: `Team-${randomString()}`, - isOrganization: false, - }); - - teamEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team.id }, - }, - title: "Collective Event Type", - slug: "collective-event-type", - length: 30, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - managedEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "MANAGED", - team: { - connect: { id: team.id }, - }, - title: "Managed Event Type", - slug: "managed-event-type", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - teamAdminMembership = await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: teamAdmin.id } }, - team: { connect: { id: team.id } }, - }); - - teamMemberMembership = await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teamMember.id } }, - team: { connect: { id: team.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teamAdmin.id}`, - username: teamAdminEmail, - organization: { - connect: { - id: team.id, - }, - }, - user: { - connect: { - id: teamAdmin.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teamMember.id}`, - username: teamMemberEmail, - organization: { - connect: { - id: team.id, - }, - }, - user: { - connect: { - id: teamMember.id, - }, - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should get all the memberships of the team", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships`) - .expect(200) - .then((response) => { - const responseBody: GetTeamMembershipsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.length).toEqual(2); - expect(responseBody.data[0].id).toEqual(teamAdminMembership.id); - expect(responseBody.data[1].id).toEqual(teamMemberMembership.id); - expect(responseBody.data[0].teamId).toEqual(team.id); - expect(responseBody.data[1].teamId).toEqual(team.id); - expect(responseBody.data[0].userId).toEqual(teamAdmin.id); - expect(responseBody.data[1].userId).toEqual(teamMember.id); - expect(responseBody.data[0].role).toEqual("ADMIN"); - expect(responseBody.data[1].role).toEqual("MEMBER"); - expect(responseBody.data[0].user.bio).toEqual(teamAdmin.bio); - expect(responseBody.data[1].user.bio).toEqual(teamMember.bio); - expect(responseBody.data[0].user.metadata).toEqual(teamAdmin.metadata); - expect(responseBody.data[1].user.metadata).toEqual(teamMember.metadata); - expect(responseBody.data[0].user.email).toEqual(teamAdmin.email); - expect(responseBody.data[1].user.email).toEqual(teamMember.email); - expect(responseBody.data[0].user.username).toEqual(teamAdmin.username); - expect(responseBody.data[1].user.username).toEqual(teamMember.username); - }); - }); - - it("should not be able to access memberships if not part of the team", async () => { - return request(app.getHttpServer()).get(`/v2/teams/9999/memberships`).expect(403); - }); - - it("should get all the memberships of the org's team paginated", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships?skip=1&take=1`) - .expect(200) - .then((response) => { - const responseBody: GetTeamMembershipsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data[0].id).toEqual(teamMemberMembership.id); - expect(responseBody.data[0].userId).toEqual(teamMember.id); - expect(responseBody.data[0].role).toEqual("MEMBER"); - expect(responseBody.data[0].user.bio).toEqual(teamMember.bio); - expect(responseBody.data[0].user.metadata).toEqual(teamMember.metadata); - expect(responseBody.data[0].user.email).toEqual(teamMember.email); - expect(responseBody.data[0].user.username).toEqual(teamMember.username); - expect(responseBody.data.length).toEqual(1); - expect(responseBody.data[0].teamId).toEqual(team.id); - }); - }); - - it("should get membership of the team", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships/${teamAdminMembership.id}`) - .expect(200) - .then((response) => { - const responseBody: GetTeamMembershipOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(teamAdminMembership.id); - expect(responseBody.data.userId).toEqual(teamAdmin.id); - expect(responseBody.data.role).toEqual("ADMIN"); - expect(responseBody.data.user.bio).toEqual(teamAdmin.bio); - expect(responseBody.data.user.metadata).toEqual(teamAdmin.metadata); - expect(responseBody.data.user.email).toEqual(teamAdmin.email); - expect(responseBody.data.user.username).toEqual(teamAdmin.username); - }); - }); - - it("should have created the membership of the org's team and assigned team wide events", async () => { - const createTeamMembershipBody: CreateTeamMembershipInput = { - userId: teammateInvitedViaApi.id, - accepted: true, - role: "MEMBER", - }; - - return request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/memberships`) - .send(createTeamMembershipBody) - .expect(201) - .then((response) => { - const responseBody: CreateTeamMembershipOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - membershipCreatedViaApi = responseBody.data; - expect(membershipCreatedViaApi.teamId).toEqual(team.id); - expect(membershipCreatedViaApi.role).toEqual("MEMBER"); - expect(membershipCreatedViaApi.user.bio).toEqual(teammateInvitedViaApi.bio); - expect(membershipCreatedViaApi.user.metadata).toEqual(teammateInvitedViaApi.metadata); - expect(membershipCreatedViaApi.user.email).toEqual(teammateInvitedViaApi.email); - expect(membershipCreatedViaApi.user.username).toEqual(teammateInvitedViaApi.username); - expect(membershipCreatedViaApi.userId).toEqual(teammateInvitedViaApi.id); - userHasCorrectEventTypes(membershipCreatedViaApi.userId); - }); - }); - - async function userHasCorrectEventTypes(userId: number) { - const managedEventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(userId); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - expect(managedEventTypes?.length).toEqual(1); - expect(teamEventTypes?.length).toEqual(2); - const collectiveEvenType = teamEventTypes?.find((eventType) => eventType.slug === teamEventType.slug); - expect(collectiveEvenType).toBeTruthy(); - const userHost = collectiveEvenType?.hosts.find((host) => host.userId === userId); - expect(userHost).toBeTruthy(); - expect(managedEventTypes?.find((eventType) => eventType.slug === managedEventType.slug)).toBeTruthy(); - } - - it("should update the membership of the org's team", async () => { - const updateTeamMembershipBody: UpdateTeamMembershipInput = { - role: "OWNER", - }; - - return request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/memberships/${membershipCreatedViaApi.id}`) - .send(updateTeamMembershipBody) - .expect(200) - .then((response) => { - const responseBody: UpdateTeamMembershipOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - membershipCreatedViaApi = responseBody.data; - expect(membershipCreatedViaApi.role).toEqual("OWNER"); - expect(membershipCreatedViaApi.user.bio).toEqual(teammateInvitedViaApi.bio); - expect(membershipCreatedViaApi.user.metadata).toEqual(teammateInvitedViaApi.metadata); - expect(membershipCreatedViaApi.user.email).toEqual(teammateInvitedViaApi.email); - expect(membershipCreatedViaApi.user.username).toEqual(teammateInvitedViaApi.username); - }); - }); - - it("should assign team wide events when membership transitions from accepted=false to accepted=true via PATCH", async () => { - const pendingUserEmail = `pending-user-${randomString()}@api.com`; - const pendingUser = await userRepositoryFixture.create({ - email: pendingUserEmail, - username: pendingUserEmail, - bio, - metadata, - }); - - const createPendingMembershipBody: CreateTeamMembershipInput = { - userId: pendingUser.id, - accepted: false, - role: "MEMBER", - }; - - const createResponse = await request(app.getHttpServer()) - .post(`/v2/teams/${team.id}/memberships`) - .send(createPendingMembershipBody) - .expect(201); - - const pendingMembership: TeamMembershipOutput = createResponse.body.data; - expect(pendingMembership.accepted).toEqual(false); - - const teamEventTypesBeforePatch = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - const collectiveEventBeforePatch = teamEventTypesBeforePatch?.find( - (eventType) => eventType.slug === teamEventType.slug - ); - const userHostBeforePatch = collectiveEventBeforePatch?.hosts.find( - (host) => host.userId === pendingUser.id - ); - expect(userHostBeforePatch).toBeFalsy(); - - const updateToAcceptedBody: UpdateTeamMembershipInput = { - accepted: true, - }; - - const patchResponse = await request(app.getHttpServer()) - .patch(`/v2/teams/${team.id}/memberships/${pendingMembership.id}`) - .send(updateToAcceptedBody) - .expect(200); - - const acceptedMembership: TeamMembershipOutput = patchResponse.body.data; - expect(acceptedMembership.accepted).toEqual(true); - - await userHasCorrectEventTypes(pendingUser.id); - - await request(app.getHttpServer()) - .delete(`/v2/teams/${team.id}/memberships/${pendingMembership.id}`) - .expect(200); - - await userRepositoryFixture.deleteByEmail(pendingUserEmail); - }); - - it("should delete the membership of the org's team we created via api", async () => { - return request(app.getHttpServer()) - .delete(`/v2/teams/${team.id}/memberships/${membershipCreatedViaApi.id}`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.id).toEqual(membershipCreatedViaApi.id); - }); - }); - - it("should fail to get the membership of the org's team we just deleted", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships/${membershipCreatedViaApi.id}`) - .expect(404); - }); - - it("should fail if the membership does not exist", async () => { - return request(app.getHttpServer()).get(`/v2/teams/${team.id}/memberships/123132145`).expect(404); - }); - - it("should filter memberships by single email", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships?emails=${teamAdminEmail}`) - .expect(200) - .then((response) => { - const responseBody: GetTeamMembershipsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.length).toEqual(1); - expect(responseBody.data[0].user.email).toEqual(teamAdminEmail); - expect(responseBody.data[0].userId).toEqual(teamAdmin.id); - expect(responseBody.data[0].role).toEqual("ADMIN"); - }); - }); - - it("should filter memberships by multiple emails", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships?emails=${teamAdminEmail},${teamMemberEmail}`) - .expect(200) - .then((response) => { - const responseBody: GetTeamMembershipsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.length).toEqual(2); - - const emails = responseBody.data.map((membership) => membership.user.email); - expect(emails).toContain(teamAdminEmail); - expect(emails).toContain(teamMemberEmail); - - const adminMembership = responseBody.data.find((m) => m.user.email === teamAdminEmail); - const memberMembership = responseBody.data.find((m) => m.user.email === teamMemberEmail); - - expect(adminMembership).toBeDefined(); - expect(memberMembership).toBeDefined(); - expect(adminMembership?.role).toEqual("ADMIN"); - expect(memberMembership?.role).toEqual("MEMBER"); - }); - }); - - it("should return empty array when filtering by non-existent email", async () => { - const nonExistentEmail = `nonexistent-${randomString()}@test.com`; - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships?emails=${nonExistentEmail}`) - .expect(200) - .then((response) => { - const responseBody: GetTeamMembershipsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.length).toEqual(0); - }); - }); - - it("should return partial results when filtering by mix of existing and non-existent emails", async () => { - const nonExistentEmail = `nonexistent-${randomString()}@test.com`; - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships?emails=${teamAdminEmail},${nonExistentEmail}`) - .expect(200) - .then((response) => { - const responseBody: GetTeamMembershipsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.length).toEqual(1); - expect(responseBody.data[0].user.email).toEqual(teamAdminEmail); - }); - }); - - it("should work with pagination and email filtering combined", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships?emails=${teamAdminEmail},${teamMemberEmail}&skip=1&take=1`) - .expect(200) - .then((response) => { - const responseBody: GetTeamMembershipsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.length).toEqual(1); - const returnedEmail = responseBody.data[0].user.email; - expect([teamAdminEmail, teamMemberEmail]).toContain(returnedEmail); - }); - }); - - it("should handle empty emails array gracefully", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships?emails=`) - .expect(200) - .then((response) => { - const responseBody: GetTeamMembershipsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.length).toEqual(2); - }); - }); - - it("should handle URL encoded email addresses in filter", async () => { - const encodedEmail = encodeURIComponent(teamAdminEmail); - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships?emails=${encodedEmail}`) - .expect(200) - .then((response) => { - const responseBody: GetTeamMembershipsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.length).toEqual(1); - expect(responseBody.data[0].user.email).toEqual(teamAdminEmail); - }); - }); - - it("should filter by email and maintain all user properties", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships?emails=${teamMemberEmail}`) - .expect(200) - .then((response) => { - const responseBody: GetTeamMembershipsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data.length).toEqual(1); - const membership = responseBody.data[0]; - expect(membership.user.email).toEqual(teamMemberEmail); - expect(membership.user.bio).toEqual(teamMember.bio); - expect(membership.user.metadata).toEqual(teamMember.metadata); - expect(membership.user.username).toEqual(teamMember.username); - expect(membership.teamId).toEqual(team.id); - expect(membership.userId).toEqual(teamMember.id); - expect(membership.role).toEqual("MEMBER"); - }); - }); - - it("should validate email array size limits", async () => { - const tooManyEmails = Array.from({ length: 21 }, (_, i) => `test${i}@example.com`).join(","); - return request(app.getHttpServer()) - .get(`/v2/teams/${team.id}/memberships?emails=${tooManyEmails}`) - .expect(400); - }); - - // Auto-accept tests for sub-teams of organizations - describe("auto-accept based on email domain for org sub-teams", () => { - let orgWithAutoAccept: Team; - let subteamWithAutoAccept: Team; - let subteamEventType: EventType; - let userWithMatchingEmail: User; - let userWithUppercaseEmail: User; - let userWithMatchingEmailForOverride: User; - let userWithNonMatchingEmail: User; - - beforeAll(async () => { - // Create org with auto-accept settings - orgWithAutoAccept = await teamsRepositoryFixture.create({ - name: `auto-accept-org-${randomString()}`, - isOrganization: true, - }); - - // Create organization settings with orgAutoAcceptEmail - await teamsRepositoryFixture.createOrgSettings(orgWithAutoAccept.id, { - orgAutoAcceptEmail: "acme.com", - isOrganizationVerified: true, - isOrganizationConfigured: true, - isAdminAPIEnabled: true, - }); - - // Create subteam - subteamWithAutoAccept = await teamsRepositoryFixture.create({ - name: `auto-accept-subteam-${randomString()}`, - isOrganization: false, - parent: { connect: { id: orgWithAutoAccept.id } }, - }); - - // Create event type with assignAllTeamMembers - subteamEventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { connect: { id: subteamWithAutoAccept.id } }, - title: "Auto Accept Event Type", - slug: "auto-accept-event-type", - length: 30, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - // Create users with different email domains - userWithMatchingEmail = await userRepositoryFixture.create({ - email: `alice-${randomString()}@acme.com`, - username: `alice-${randomString()}`, - }); - - userWithUppercaseEmail = await userRepositoryFixture.create({ - email: `bob-${randomString()}@ACME.COM`, - username: `bob-${randomString()}`, - }); - - userWithMatchingEmailForOverride = await userRepositoryFixture.create({ - email: `david-${randomString()}@acme.com`, - username: `david-${randomString()}`, - }); - - userWithNonMatchingEmail = await userRepositoryFixture.create({ - email: `charlie-${randomString()}@external.com`, - username: `charlie-${randomString()}`, - }); - - // Add users to org - await membershipsRepositoryFixture.create({ - role: "MEMBER", - accepted: true, - user: { connect: { id: userWithMatchingEmail.id } }, - team: { connect: { id: orgWithAutoAccept.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - accepted: true, - user: { connect: { id: userWithUppercaseEmail.id } }, - team: { connect: { id: orgWithAutoAccept.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - accepted: true, - user: { connect: { id: userWithMatchingEmailForOverride.id } }, - team: { connect: { id: orgWithAutoAccept.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - accepted: true, - user: { connect: { id: userWithNonMatchingEmail.id } }, - team: { connect: { id: orgWithAutoAccept.id } }, - }); - - // Create profiles for users - await profileRepositoryFixture.create({ - uid: `usr-${userWithMatchingEmail.id}`, - username: userWithMatchingEmail.username || `user-${userWithMatchingEmail.id}`, - organization: { connect: { id: orgWithAutoAccept.id } }, - user: { connect: { id: userWithMatchingEmail.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${userWithUppercaseEmail.id}`, - username: userWithUppercaseEmail.username || `user-${userWithUppercaseEmail.id}`, - organization: { connect: { id: orgWithAutoAccept.id } }, - user: { connect: { id: userWithUppercaseEmail.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${userWithMatchingEmailForOverride.id}`, - username: - userWithMatchingEmailForOverride.username || `user-${userWithMatchingEmailForOverride.id}`, - organization: { connect: { id: orgWithAutoAccept.id } }, - user: { connect: { id: userWithMatchingEmailForOverride.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${userWithNonMatchingEmail.id}`, - username: userWithNonMatchingEmail.username || `user-${userWithNonMatchingEmail.id}`, - organization: { connect: { id: orgWithAutoAccept.id } }, - user: { connect: { id: userWithNonMatchingEmail.id } }, - }); - - // Make teamAdmin an admin of the org and subteam for API access - await membershipsRepositoryFixture.create({ - role: "ADMIN", - accepted: true, - user: { connect: { id: teamAdmin.id } }, - team: { connect: { id: orgWithAutoAccept.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - accepted: true, - user: { connect: { id: teamAdmin.id } }, - team: { connect: { id: subteamWithAutoAccept.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-org-${teamAdmin.id}`, - username: teamAdmin.username || `admin-${teamAdmin.id}`, - organization: { connect: { id: orgWithAutoAccept.id } }, - user: { connect: { id: teamAdmin.id } }, - }); - }); - - it("should auto-accept when email matches orgAutoAcceptEmail for sub-team", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/teams/${subteamWithAutoAccept.id}/memberships`) - .send({ - userId: userWithMatchingEmail.id, - role: "MEMBER", - } satisfies CreateTeamMembershipInput) - .expect(201); - - const responseBody: CreateTeamMembershipOutput = response.body; - expect(responseBody.data.accepted).toBe(true); - - // Verify EventTypes assignment - const eventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(subteamWithAutoAccept.id); - const eventTypeWithAssignAll = eventTypes.find((et) => et.assignAllTeamMembers); - expect(eventTypeWithAssignAll).toBeTruthy(); - const userIsHost = eventTypeWithAssignAll?.hosts.some((h) => h.userId === userWithMatchingEmail.id); - expect(userIsHost).toBe(true); - }); - - it("should handle case-insensitive email domain matching for sub-team", async () => { - // User with email="bob@ACME.COM" should match orgAutoAcceptEmail="acme.com" - const response = await request(app.getHttpServer()) - .post(`/v2/teams/${subteamWithAutoAccept.id}/memberships`) - .send({ - userId: userWithUppercaseEmail.id, - role: "MEMBER", - } satisfies CreateTeamMembershipInput) - .expect(201); - - const responseBody: CreateTeamMembershipOutput = response.body; - expect(responseBody.data.accepted).toBe(true); - }); - - it("should ALWAYS auto-accept when email matches, even if accepted:false for sub-team", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/teams/${subteamWithAutoAccept.id}/memberships`) - .send({ - userId: userWithMatchingEmailForOverride.id, - role: "MEMBER", - accepted: false, - } satisfies CreateTeamMembershipInput) - .expect(201); - - const responseBody: CreateTeamMembershipOutput = response.body; - // Should override to true because email matches - expect(responseBody.data.accepted).toBe(true); - }); - - it("should NOT auto-accept when email does not match orgAutoAcceptEmail for sub-team", async () => { - const response = await request(app.getHttpServer()) - .post(`/v2/teams/${subteamWithAutoAccept.id}/memberships`) - .send({ - userId: userWithNonMatchingEmail.id, - role: "MEMBER", - } satisfies CreateTeamMembershipInput) - .expect(201); - - const responseBody: CreateTeamMembershipOutput = response.body; - expect(responseBody.data.accepted).toBe(false); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(userWithMatchingEmail.email); - await userRepositoryFixture.deleteByEmail(userWithUppercaseEmail.email); - await userRepositoryFixture.deleteByEmail(userWithMatchingEmailForOverride.email); - await userRepositoryFixture.deleteByEmail(userWithNonMatchingEmail.email); - await teamsRepositoryFixture.deleteOrgSettings(orgWithAutoAccept.id); - await teamsRepositoryFixture.delete(subteamWithAutoAccept.id); - await teamsRepositoryFixture.delete(orgWithAutoAccept.id); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(teamAdmin.email); - await userRepositoryFixture.deleteByEmail(teammateInvitedViaApi.email); - await userRepositoryFixture.deleteByEmail(nonTeamUser.email); - await userRepositoryFixture.deleteByEmail(teamMember.email); - await teamsRepositoryFixture.delete(team.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/teams/memberships/controllers/teams-memberships.controller.ts b/apps/api/v2/src/modules/teams/memberships/controllers/teams-memberships.controller.ts deleted file mode 100644 index ef635a69a9de97..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/controllers/teams-memberships.controller.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries/event-types"; -import { - Body, - Controller, - Delete, - Get, - HttpCode, - HttpStatus, - Logger, - Param, - ParseIntPipe, - Patch, - Post, - Query, - UseGuards, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input"; -import { GetTeamMembershipsInput } from "@/modules/teams/memberships/inputs/get-team-memberships.input"; -import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input"; -import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output"; -import { DeleteTeamMembershipOutput } from "@/modules/teams/memberships/outputs/delete-team-membership.output"; -import { GetTeamMembershipOutput } from "@/modules/teams/memberships/outputs/get-team-membership.output"; -import { GetTeamMembershipsOutput } from "@/modules/teams/memberships/outputs/get-team-memberships.output"; -import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; -import { UpdateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/update-team-membership.output"; -import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service"; - -@Controller({ - path: "/v2/teams/:teamId/memberships", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, RolesGuard) -@DocsTags("Teams / Memberships") -@ApiHeader(API_KEY_HEADER) -export class TeamsMembershipsController { - private logger = new Logger("TeamsMembershipsController"); - - constructor(private teamsMembershipsService: TeamsMembershipsService) {} - - @Roles("TEAM_ADMIN") - @Post("/") - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: "Create a membership" }) - async createTeamMembership( - @Param("teamId", ParseIntPipe) teamId: number, - @Body() body: CreateTeamMembershipInput - ): Promise { - const membership = await this.teamsMembershipsService.createTeamMembership(teamId, body); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }), - }; - } - - @Get("/:membershipId") - @ApiOperation({ summary: "Get a membership" }) - @Roles("TEAM_ADMIN") - @HttpCode(HttpStatus.OK) - async getTeamMembership( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("membershipId", ParseIntPipe) membershipId: number - ): Promise { - const orgTeamMembership = await this.teamsMembershipsService.getTeamMembership(teamId, membershipId); - - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamMembershipOutput, orgTeamMembership, { strategy: "excludeAll" }), - }; - } - - @Get("/") - @ApiOperation({ - summary: "Get all memberships", - description: "Retrieve team memberships with optional filtering by email addresses. Supports pagination.", - }) - @Roles("TEAM_ADMIN") - @HttpCode(HttpStatus.OK) - async getTeamMemberships( - @Param("teamId", ParseIntPipe) teamId: number, - @Query() queryParams: GetTeamMembershipsInput - ): Promise { - const { skip, take, emails } = queryParams; - const orgTeamMemberships = await this.teamsMembershipsService.getPaginatedTeamMemberships( - teamId, - emails, - skip ?? 0, - take ?? 250 - ); - return { - status: SUCCESS_STATUS, - data: orgTeamMemberships.map((membership) => - plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }) - ), - }; - } - - @Roles("TEAM_ADMIN") - @Patch("/:membershipId") - @ApiOperation({ summary: "Update membership" }) - async updateTeamMembership( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("membershipId", ParseIntPipe) membershipId: number, - @Body() body: UpdateTeamMembershipInput - ): Promise { - const currentMembership = await this.teamsMembershipsService.getTeamMembership(teamId, membershipId); - - const updatedMembership = await this.teamsMembershipsService.updateTeamMembership( - teamId, - membershipId, - body - ); - - if (!currentMembership.accepted && updatedMembership.accepted) { - try { - await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId); - } catch (err) { - this.logger.error("Could not update new team member eventTypes", err); - } - } - - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamMembershipOutput, updatedMembership, { strategy: "excludeAll" }), - }; - } - - @Roles("TEAM_ADMIN") - @Delete("/:membershipId") - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Delete a membership" }) - async deleteTeamMembership( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("membershipId", ParseIntPipe) membershipId: number - ): Promise { - const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId); - - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }), - }; - } -} diff --git a/apps/api/v2/src/modules/teams/memberships/inputs/create-team-membership.input.ts b/apps/api/v2/src/modules/teams/memberships/inputs/create-team-membership.input.ts deleted file mode 100644 index 282526aa9b3164..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/inputs/create-team-membership.input.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsBoolean, IsOptional, IsEnum, IsInt } from "class-validator"; - -import { MembershipRole } from "@calcom/platform-libraries"; - -export class CreateTeamMembershipInput { - @IsInt() - @ApiProperty({ type: Number }) - readonly userId!: number; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ type: Boolean, default: false }) - readonly accepted?: boolean = false; - - @IsOptional() - @IsEnum(MembershipRole) - @ApiPropertyOptional({ enum: ["MEMBER", "OWNER", "ADMIN"], default: "MEMBER" }) - readonly role: MembershipRole = MembershipRole.MEMBER; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ type: Boolean, default: false }) - readonly disableImpersonation?: boolean = false; -} diff --git a/apps/api/v2/src/modules/teams/memberships/inputs/get-team-memberships.input.ts b/apps/api/v2/src/modules/teams/memberships/inputs/get-team-memberships.input.ts deleted file mode 100644 index b817f2134a0b4d..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/inputs/get-team-memberships.input.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { Transform } from "class-transformer"; -import { ArrayMaxSize, ArrayNotEmpty, IsEmail, IsOptional } from "class-validator"; - -import { SkipTakePagination } from "@calcom/platform-types"; - -export class GetTeamMembershipsInput extends SkipTakePagination { - @IsOptional() - @Transform(({ value }) => { - if (value == null) return undefined; - const rawValues = (Array.isArray(value) ? value : [value]).flatMap((entry) => - typeof entry === "string" ? entry.split(",") : [] - ); - const normalized = rawValues - .map((email) => email.trim()) - .filter((email) => email.length > 0) - .map((email) => email.toLowerCase()); - const deduplicated = [...new Set(normalized)]; - return deduplicated.length > 0 ? deduplicated : undefined; - }) - @ArrayNotEmpty({ message: "emails cannot be empty." }) - @ArrayMaxSize(20, { - message: "emails array cannot contain more than 20 email addresses for team membership filtering", - }) - @IsEmail({}, { each: true, message: "Each email must be a valid email address" }) - @ApiPropertyOptional({ - type: [String], - description: - "Filter team memberships by email addresses. If you want to filter by multiple emails, separate them with a comma (max 20 emails for performance).", - example: "?emails=user1@example.com,user2@example.com", - }) - emails?: string[]; -} diff --git a/apps/api/v2/src/modules/teams/memberships/inputs/update-team-membership.input.ts b/apps/api/v2/src/modules/teams/memberships/inputs/update-team-membership.input.ts deleted file mode 100644 index c0121935bb06d3..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/inputs/update-team-membership.input.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsBoolean, IsOptional, IsEnum } from "class-validator"; - -import { MembershipRole } from "@calcom/platform-libraries"; - -export class UpdateTeamMembershipInput { - @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ type: Boolean }) - readonly accepted?: boolean; - - @IsOptional() - @IsEnum(MembershipRole) - @ApiPropertyOptional({ enum: ["MEMBER", "OWNER", "ADMIN"] }) - readonly role?: MembershipRole; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ type: Boolean }) - readonly disableImpersonation?: boolean; -} diff --git a/apps/api/v2/src/modules/teams/memberships/outputs/create-team-membership.output.ts b/apps/api/v2/src/modules/teams/memberships/outputs/create-team-membership.output.ts deleted file mode 100644 index 41bc5611ba583c..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/outputs/create-team-membership.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class CreateTeamMembershipOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: TeamMembershipOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => TeamMembershipOutput) - data!: TeamMembershipOutput; -} diff --git a/apps/api/v2/src/modules/teams/memberships/outputs/delete-team-membership.output.ts b/apps/api/v2/src/modules/teams/memberships/outputs/delete-team-membership.output.ts deleted file mode 100644 index 40cde0710b810b..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/outputs/delete-team-membership.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class DeleteTeamMembershipOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: TeamMembershipOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => TeamMembershipOutput) - data!: TeamMembershipOutput; -} diff --git a/apps/api/v2/src/modules/teams/memberships/outputs/get-team-membership.output.ts b/apps/api/v2/src/modules/teams/memberships/outputs/get-team-membership.output.ts deleted file mode 100644 index 4cfc4c44795c31..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/outputs/get-team-membership.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class GetTeamMembershipOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: TeamMembershipOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => TeamMembershipOutput) - data!: TeamMembershipOutput; -} diff --git a/apps/api/v2/src/modules/teams/memberships/outputs/get-team-memberships.output.ts b/apps/api/v2/src/modules/teams/memberships/outputs/get-team-memberships.output.ts deleted file mode 100644 index 14dc8a8f981d2f..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/outputs/get-team-memberships.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class GetTeamMembershipsOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: TeamMembershipOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => TeamMembershipOutput) - data!: TeamMembershipOutput[]; -} diff --git a/apps/api/v2/src/modules/teams/memberships/outputs/team-membership.output.ts b/apps/api/v2/src/modules/teams/memberships/outputs/team-membership.output.ts deleted file mode 100644 index 93c78152738e87..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/outputs/team-membership.output.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { Expose, Transform, Type } from "class-transformer"; -import { IsBoolean, IsInt, IsObject, IsOptional, IsString, ValidateNested } from "class-validator"; - -import { MembershipRole } from "@calcom/platform-libraries"; - -class MembershipUserOutputDto { - @IsOptional() - @IsString() - @Expose() - @ApiPropertyOptional() - readonly avatarUrl?: string; - - @IsOptional() - @IsString() - @Expose() - @ApiPropertyOptional() - readonly username?: string; - - @IsOptional() - @IsString() - @Expose() - @ApiPropertyOptional() - readonly name?: string; - - @IsBoolean() - @Expose() - @ApiProperty() - readonly email!: string; - - @IsOptional() - @IsString() - @Expose() - @ApiPropertyOptional() - readonly bio?: string; - - @ApiPropertyOptional({ - type: Object, - example: { key: "value" }, - }) - @IsObject() - @IsOptional() - @Expose() - @Transform( - // note(Lauris): added this transform because without it metadata is removed for some reason - ({ obj }: { obj: { metadata: Record | null | undefined } }) => { - return obj.metadata || undefined; - } - ) - metadata?: Record; -} - -export class TeamMembershipOutput { - @IsInt() - @Expose() - @ApiProperty() - readonly id!: number; - - @IsInt() - @Expose() - @ApiProperty() - readonly userId!: number; - - @IsInt() - @Expose() - @ApiProperty() - readonly teamId!: number; - - @IsBoolean() - @Expose() - @ApiProperty() - readonly accepted!: boolean; - - @IsString() - @ApiProperty({ enum: ["MEMBER", "OWNER", "ADMIN"] }) - @Expose() - readonly role!: MembershipRole; - - @IsOptional() - @IsBoolean() - @Expose() - @ApiPropertyOptional() - readonly disableImpersonation?: boolean; - - @ValidateNested() - @Type(() => MembershipUserOutputDto) - @Expose() - @ApiProperty({ type: MembershipUserOutputDto }) - user!: MembershipUserOutputDto; -} diff --git a/apps/api/v2/src/modules/teams/memberships/outputs/update-team-membership.output.ts b/apps/api/v2/src/modules/teams/memberships/outputs/update-team-membership.output.ts deleted file mode 100644 index a839d593bfcaff..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/outputs/update-team-membership.output.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -export class UpdateTeamMembershipOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - type: TeamMembershipOutput, - }) - @IsNotEmptyObject() - @ValidateNested() - @Type(() => TeamMembershipOutput) - data!: TeamMembershipOutput; -} diff --git a/apps/api/v2/src/modules/teams/memberships/services/teams-memberships.service.ts b/apps/api/v2/src/modules/teams/memberships/services/teams-memberships.service.ts deleted file mode 100644 index db3e22b1e14492..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/services/teams-memberships.service.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { TeamService } from "@calcom/platform-libraries"; -import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries/event-types"; -import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; -import { OrganizationMembershipService } from "@/lib/services/organization-membership.service"; -import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; -import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input"; -import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input"; -import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository"; -import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; -import { UsersRepository } from "@/modules/users/users.repository"; - -export const PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR = `Can't add user to team - the user is platform managed user but team is not because team probably was not created using OAuth credentials.`; -export const REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR = `Can't add user to team - the user is not platform managed user but team is platform managed. Both have to be created using OAuth credentials.`; -export const PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR = `Can't add user to team - managed user and team were created using different OAuth clients.`; - -@Injectable() -export class TeamsMembershipsService { - constructor( - private readonly teamsMembershipsRepository: TeamsMembershipsRepository, - private readonly oAuthClientsRepository: OAuthClientRepository, - private readonly teamsRepository: TeamsRepository, - private readonly usersRepository: UsersRepository, - private readonly orgMembershipService: OrganizationMembershipService, - private readonly logger: Logger - ) {} - - private async shouldAutoAccept({ teamId, userId }: { teamId: number; userId: number }): Promise { - const team = await this.teamsRepository.getById(teamId); - - if (team?.parentId) { - const user = await this.usersRepository.findById(userId); - if (user) { - const shouldAutoAccept = await this.orgMembershipService.shouldAutoAccept({ - organizationId: team.parentId, - userEmail: user.email, - }); - - return shouldAutoAccept; - } - } - - return false; - } - async createTeamMembership(teamId: number, data: CreateTeamMembershipInput) { - await this.canUserBeAddedToTeam(data.userId, teamId); - const shouldAutoAccept = await this.shouldAutoAccept({ teamId, userId: data.userId }); - if (shouldAutoAccept) { - data = { ...data, accepted: true }; - } - - if (data.accepted) { - try { - await updateNewTeamMemberEventTypes(data.userId, teamId); - } catch (err) { - this.logger.error("Could not update new team member eventTypes", err); - } - } - const teamMembership = await this.teamsMembershipsRepository.createTeamMembership(teamId, data); - return teamMembership; - } - - async getPaginatedTeamMemberships(teamId: number, emails?: string[], skip = 0, take = 250) { - const emailArray = !emails ? [] : emails; - - return await this.teamsMembershipsRepository.findTeamMembershipsPaginatedWithFilters( - teamId, - { emails: emailArray }, - skip, - take - ); - } - - async getTeamMembership(teamId: number, membershipId: number) { - const teamMemberships = await this.teamsMembershipsRepository.findTeamMembership(teamId, membershipId); - - if (!teamMemberships) { - throw new NotFoundException("Organization's Team membership not found"); - } - - return teamMemberships; - } - - async updateTeamMembership(teamId: number, membershipId: number, data: UpdateTeamMembershipInput) { - const teamMembership = await this.teamsMembershipsRepository.updateTeamMembershipById( - teamId, - membershipId, - data - ); - return teamMembership; - } - - async deleteTeamMembership(teamId: number, membershipId: number) { - // First get the membership to get the userId - const teamMembership = await this.teamsMembershipsRepository.findTeamMembership(teamId, membershipId); - - if (!teamMembership) { - throw new NotFoundException(`Membership with id ${membershipId} not found in team ${teamId}`); - } - - await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false }); - - return teamMembership; - } - - async canUserBeAddedToTeam(userId: number, teamId: number) { - const [userOAuthClient, teamOAuthClient] = await Promise.all([ - this.oAuthClientsRepository.getByUserId(userId), - this.oAuthClientsRepository.getByTeamId(teamId), - ]); - - if (!userOAuthClient && !teamOAuthClient) { - return true; - } - - if (userOAuthClient && teamOAuthClient && userOAuthClient.id === teamOAuthClient.id) { - return true; - } - - if (!teamOAuthClient) { - throw new BadRequestException(PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR); - } - - if (!userOAuthClient) { - throw new BadRequestException(REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR); - } - - throw new BadRequestException(PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR); - } -} diff --git a/apps/api/v2/src/modules/teams/memberships/teams-memberships.module.ts b/apps/api/v2/src/modules/teams/memberships/teams-memberships.module.ts deleted file mode 100644 index 20c1cda63b6a85..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/teams-memberships.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OrganizationsModule } from "@/modules/organizations/organizations.module"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; -import { TeamsMembershipsController } from "@/modules/teams/memberships/controllers/teams-memberships.controller"; -import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service"; -import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository"; -import { TeamsModule } from "@/modules/teams/teams/teams.module"; -import { UsersModule } from "@/modules/users/users.module"; -import { Logger, Module } from "@nestjs/common"; - -@Module({ - imports: [ - PrismaModule, - RedisModule, - OrganizationsModule, - MembershipsModule, - TeamsEventTypesModule, - TeamsModule, - UsersModule, - ], - providers: [TeamsMembershipsRepository, TeamsMembershipsService, Logger], - controllers: [TeamsMembershipsController], - exports: [TeamsMembershipsService], -}) -export class TeamsMembershipsModule {} diff --git a/apps/api/v2/src/modules/teams/memberships/teams-memberships.repository.ts b/apps/api/v2/src/modules/teams/memberships/teams-memberships.repository.ts deleted file mode 100644 index 3c1c3c7271cc50..00000000000000 --- a/apps/api/v2/src/modules/teams/memberships/teams-memberships.repository.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input"; -import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input"; -import { Injectable } from "@nestjs/common"; - -import type { Prisma } from "@calcom/prisma/client"; - -export interface TeamMembershipFilters { - emails?: string[]; -} - -export const MembershipUserSelect: Prisma.UserSelect = { - username: true, - email: true, - avatarUrl: true, - name: true, - metadata: true, - bio: true, -} satisfies Prisma.UserSelect; - -@Injectable() -export class TeamsMembershipsRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async createTeamMembership(teamId: number, data: CreateTeamMembershipInput) { - return this.dbWrite.prisma.membership.create({ - data: { - createdAt: new Date(), - ...data, - teamId: teamId, - }, - include: { user: { select: MembershipUserSelect } }, - }); - } - - async findTeamMembershipsPaginated(teamId: number, skip: number, take: number) { - return await this.dbRead.prisma.membership.findMany({ - where: { - teamId: teamId, - }, - include: { user: { select: MembershipUserSelect } }, - skip, - take, - }); - } - - async findTeamMembershipsPaginatedWithFilters( - teamId: number, - filters: TeamMembershipFilters, - skip: number, - take: number - ) { - const whereClause: Prisma.MembershipWhereInput = { - teamId: teamId, - }; - - if (filters.emails && filters.emails.length > 0) { - whereClause.user = { - email: { in: filters.emails }, - }; - } - - return await this.dbRead.prisma.membership.findMany({ - where: whereClause, - include: { user: { select: MembershipUserSelect } }, - skip, - take, - }); - } - - async findTeamMembership(teamId: number, membershipId: number) { - return this.dbRead.prisma.membership.findUnique({ - where: { - id: membershipId, - teamId: teamId, - }, - include: { user: { select: MembershipUserSelect } }, - }); - } - - async findTeamMembershipsByNameAndUser(teamName: string, userId: number) { - return this.dbRead.prisma.membership.findFirst({ - where: { - team: { - name: teamName, - }, - userId, - }, - }); - } - - async deleteTeamMembershipById(teamId: number, membershipId: number) { - return this.dbWrite.prisma.membership.delete({ - where: { - id: membershipId, - teamId: teamId, - }, - }); - } - - async updateTeamMembershipById(teamId: number, membershipId: number, data: UpdateTeamMembershipInput) { - return this.dbWrite.prisma.membership.update({ - data: { ...data }, - where: { - id: membershipId, - teamId: teamId, - }, - include: { user: { select: MembershipUserSelect } }, - }); - } -} diff --git a/apps/api/v2/src/modules/teams/schedules/controllers/teams-schedules.controller.ts b/apps/api/v2/src/modules/teams/schedules/controllers/teams-schedules.controller.ts deleted file mode 100644 index f25a24c9b5f94e..00000000000000 --- a/apps/api/v2/src/modules/teams/schedules/controllers/teams-schedules.controller.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service"; -import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/teams/:teamId/schedules", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard, RolesGuard) -@DocsTags("Teams / Schedules") -@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) -export class TeamsSchedulesController { - constructor(private teamsSchedulesService: TeamsSchedulesService) {} - - @Roles("TEAM_ADMIN") - @Get("/") - @ApiOperation({ - summary: "Get all team member schedules", - }) - async getTeamSchedules( - @Param("teamId", ParseIntPipe) teamId: number, - @Query() queryParams: SkipTakePagination - ): Promise { - const { skip, take } = queryParams; - - const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take); - - return { - status: SUCCESS_STATUS, - data: schedules, - }; - } -} diff --git a/apps/api/v2/src/modules/teams/schedules/controllers/teams-schedules.e2e-spec.ts b/apps/api/v2/src/modules/teams/schedules/controllers/teams-schedules.e2e-spec.ts deleted file mode 100644 index 91d6b3a96f8359..00000000000000 --- a/apps/api/v2/src/modules/teams/schedules/controllers/teams-schedules.e2e-spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import type { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types"; -import type { Schedule, 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 { 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 { 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"; - -describe("Teams Schedules Endpoints", () => { - describe("User Authentication - User is Team Admin", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - - let teamsRepositoryFixture: TeamRepositoryFixture; - let scheduleRepositoryFixture: SchedulesRepositoryFixture; - - let membershipsRepositoryFixture: MembershipRepositoryFixture; - - let nonOrgTeam: Team; - - let userSchedule: Schedule; - let user2Schedule: Schedule; - - const userEmail = `teams-schedules-admin-${randomString()}@api.com`; - const userEmail2 = `teams-schedules-member-${randomString()}@api.com`; - - let user: User; - let user2: User; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - - scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); - user2 = await userRepositoryFixture.create({ - email: userEmail2, - username: userEmail2, - }); - - userSchedule = await scheduleRepositoryFixture.create({ - user: { - connect: { - id: user.id, - }, - }, - name: `teams-schedules-user-schedule-${randomString()}`, - timeZone: "America/New_York", - }); - - user2Schedule = await scheduleRepositoryFixture.create({ - user: { - connect: { - id: user2.id, - }, - }, - name: `teams-schedules-user2-schedule-${randomString()}`, - timeZone: "America/New_York", - }); - - nonOrgTeam = await teamsRepositoryFixture.create({ - name: `teams-schedules-non-org-team-${randomString()}`, - isOrganization: false, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: nonOrgTeam.id } }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: user2.id } }, - team: { connect: { id: nonOrgTeam.id } }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should get all the schedules of members in a team", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${nonOrgTeam.id}/schedules`) - .expect(200) - .then((response) => { - const responseBody: GetSchedulesOutput_2024_06_11 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(Array.isArray(responseBody.data)).toBe(true); - expect(responseBody.data.length).toEqual(2); - - const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id); - const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id); - - expect(userOneSchedule).toBeDefined(); - expect(userTwoSchedule).toBeDefined(); - - expect(userOneSchedule?.id).toEqual(userSchedule.id); - expect(userOneSchedule?.name).toEqual(userSchedule.name); - expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone); - - expect(userTwoSchedule?.id).toEqual(user2Schedule.id); - expect(userTwoSchedule?.name).toEqual(user2Schedule.name); - expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(user.email); - await userRepositoryFixture.deleteByEmail(user2.email); - await teamsRepositoryFixture.delete(nonOrgTeam.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/teams/schedules/services/teams-schedules.service.ts b/apps/api/v2/src/modules/teams/schedules/services/teams-schedules.service.ts deleted file mode 100644 index 8523b1e764fd7e..00000000000000 --- a/apps/api/v2/src/modules/teams/schedules/services/teams-schedules.service.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { ScheduleOutput_2024_06_11 } from "@calcom/platform-types"; -import type { Availability, Schedule } from "@calcom/prisma/client"; -import { Injectable, NotFoundException } from "@nestjs/common"; -import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository"; -import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service"; -import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; -import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; - -@Injectable() -export class TeamsSchedulesService { - constructor( - private readonly teamsRepository: TeamsRepository, - private readonly schedulesRepository: SchedulesRepository_2024_06_11, - private readonly outputSchedulesService: OutputSchedulesService_2024_06_11, - private readonly teamsEventTypesRepository: TeamsEventTypesRepository - ) {} - - async getTeamSchedules( - teamId: number, - skip = 0, - take = 250, - eventTypeId?: number - ): Promise { - if (!eventTypeId) { - const userIds = await this.teamsRepository.getTeamUsersIds(teamId); - const schedules = await this.schedulesRepository.getSchedulesByUserIds(userIds, skip, take); - - return this.outputSchedulesService.getResponseSchedules(schedules); - } - - return this.getTeamSchedulesForEventType(teamId, eventTypeId, skip, take); - } - - private async getTeamSchedulesForEventType( - teamId: number, - eventTypeId: number, - skip: number, - take: number - ): Promise { - const eventType = await this.teamsEventTypesRepository.getByIdIncludeHostsAndUserDefaultSchedule( - eventTypeId, - teamId - ); - - if (!eventType) { - throw new NotFoundException(`Event type with id ${eventTypeId} not found in team ${teamId}`); - } - - const effectiveScheduleIds = new Set(); - - for (const host of eventType.hosts) { - const effectiveScheduleId = eventType.scheduleId ?? host.scheduleId ?? host.user.defaultScheduleId; - - if (effectiveScheduleId) { - effectiveScheduleIds.add(effectiveScheduleId); - } - } - - if (effectiveScheduleIds.size === 0) { - return []; - } - - const schedules = await this.schedulesRepository.getSchedulesByIds( - Array.from(effectiveScheduleIds), - skip, - take - ); - - return this.outputSchedulesService.getResponseSchedules( - schedules as (Schedule & { availability: Availability[] })[] - ); - } -} diff --git a/apps/api/v2/src/modules/teams/schedules/teams-schedules.module.ts b/apps/api/v2/src/modules/teams/schedules/teams-schedules.module.ts deleted file mode 100644 index 1a45d2fbd4b41e..00000000000000 --- a/apps/api/v2/src/modules/teams/schedules/teams-schedules.module.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository"; -import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service"; -import { AppsRepository } from "@/modules/apps/apps.repository"; -import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; -import { StripeService } from "@/modules/stripe/stripe.service"; -import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository"; -import { TeamsSchedulesController } from "@/modules/teams/schedules/controllers/teams-schedules.controller"; -import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service"; -import { TeamsController } from "@/modules/teams/teams/controllers/teams.controller"; -import { TeamsService } from "@/modules/teams/teams/services/teams.service"; -import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; -import { UsersRepository } from "@/modules/users/users.repository"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [PrismaModule, MembershipsModule, RedisModule, TeamsEventTypesModule], - providers: [ - TeamsRepository, - TeamsService, - TeamsMembershipsRepository, - OutputSchedulesService_2024_06_11, - OrganizationSchedulesRepository, - StripeService, - UsersRepository, - AppsRepository, - CredentialsRepository, - OrganizationsRepository, - TeamsSchedulesService, - OrganizationsTeamsRepository, - SchedulesRepository_2024_06_11, - ], - controllers: [TeamsController, TeamsSchedulesController], - exports: [TeamsRepository], -}) -export class TeamsSchedulesModule {} diff --git a/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.e2e-spec.ts b/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.e2e-spec.ts deleted file mode 100644 index 84a1c652adf703..00000000000000 --- a/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.e2e-spec.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { slugify } from "@calcom/platform-libraries"; -import { TeamOutputDto } from "@calcom/platform-types"; -import { User } 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 request from "supertest"; -import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.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 { mockThrottlerGuard } from "test/utils/withNoThrottler"; -import { AppModule } from "@/app.module"; -import { bootstrap } from "@/bootstrap"; -import { StripeService } from "@/modules/stripe/stripe.service"; -import { CreateTeamInput } from "@/modules/teams/teams/inputs/create-team.input"; -import { UpdateTeamDto } from "@/modules/teams/teams/inputs/update-team.input"; -import { CreateTeamOutput } from "@/modules/teams/teams/outputs/teams/create-team.output"; -import { GetTeamOutput } from "@/modules/teams/teams/outputs/teams/get-team.output"; -import { GetTeamsOutput } from "@/modules/teams/teams/outputs/teams/get-teams.output"; -import { TeamsModule } from "@/modules/teams/teams/teams.module"; - -describe("Teams endpoint", () => { - let app: INestApplication; - let userRepositoryFixture: UserRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let membershipRepositoryFixture: MembershipRepositoryFixture; - - const aliceEmail = `alice-${randomString()}@api.com`; - let alice: User; - let aliceApiKey: string; - - const bobEmail = `bob-${randomString()}@api.com`; - let bob: User; - let bobApiKey: string; - - let team1: TeamOutputDto; - let team2: TeamOutputDto; - - beforeAll(async () => { - mockThrottlerGuard(); - - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, TeamsModule], - }).compile(); - - jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(() => ({}) as unknown as Stripe); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - - alice = await userRepositoryFixture.create({ - email: aliceEmail, - }); - - bob = await userRepositoryFixture.create({ - email: bobEmail, - }); - - const { keyString } = await apiKeysRepositoryFixture.createApiKey(alice.id, null); - aliceApiKey = keyString; - - const { keyString: bobKeyString } = await apiKeysRepositoryFixture.createApiKey(bob.id, null); - bobApiKey = bobKeyString; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - describe("User has membership in created team", () => { - it("should create first team", async () => { - const body: CreateTeamInput = { - name: `teams dog ${randomString()}`, - metadata: { - teamKey: "teamValue", - }, - }; - - return request(app.getHttpServer()) - .post("/v2/teams") - .send(body) - .set({ Authorization: `Bearer cal_test_${aliceApiKey}` }) - .expect(201) - .then(async (response) => { - const responseBody: CreateTeamOutput = response.body; - const responseData = responseBody.data as TeamOutputDto; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseData).toBeDefined(); - expect(responseData.id).toBeDefined(); - expect(responseData.name).toEqual(body.name); - expect(responseData.slug).toEqual(slugify(body.name)); - expect(responseData.metadata).toEqual(body.metadata); - team1 = responseData; - }); - }); - - it("should not create a team with string metadata", async () => { - const body = { - name: `teams-dog-${randomString()}`, - metadata: JSON.stringify({ - teamKey: "teamValue", - }), - }; - - return request(app.getHttpServer()) - .post("/v2/teams") - .send(body) - .set({ Authorization: `Bearer cal_test_${aliceApiKey}` }) - .expect(400); - }); - - it("should create second team", async () => { - const body: CreateTeamInput = { - name: `teams-cats-${randomString()}`, - metadata: {}, - }; - - return request(app.getHttpServer()) - .post("/v2/teams") - .send(body) - .set({ Authorization: `Bearer cal_test_${aliceApiKey}` }) - .expect(201) - .then(async (response) => { - const responseBody: CreateTeamOutput = response.body; - const responseData = responseBody.data as TeamOutputDto; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseData).toBeDefined(); - expect(responseData.id).toBeDefined(); - expect(responseData.name).toEqual(body.name); - expect(responseData.metadata).toEqual(body.metadata); - team2 = responseData; - }); - }); - - it("should get a team", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team1.id}`) - .set({ Authorization: `Bearer cal_test_${aliceApiKey}` }) - .expect(200) - .then(async (response) => { - const responseBody: GetTeamOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.id).toBeDefined(); - expect(responseBody.data.name).toEqual(team1.name); - }); - }); - - it("should get teams", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams`) - .set({ Authorization: `Bearer cal_test_${aliceApiKey}` }) - .expect(200) - .then(async (response) => { - const responseBody: GetTeamsOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.length).toEqual(2); - - const responseTeam1 = responseBody.data.find((team) => team.id === team1.id); - const responseTeam2 = responseBody.data.find((team) => team.id === team2.id); - - expect(responseTeam1).toBeDefined(); - expect(responseTeam2).toBeDefined(); - }); - }); - - it("should update a team", async () => { - const body: UpdateTeamDto = { - name: `teams-dogs-shepherds-${randomString()}`, - metadata: { - teamKey: `teamValue ${randomString()}`, - }, - }; - - return request(app.getHttpServer()) - .patch(`/v2/teams/${team1.id}`) - .send(body) - .set({ Authorization: `Bearer cal_test_${aliceApiKey}` }) - .expect(200) - .then(async (response) => { - const responseBody: GetTeamOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.id).toBeDefined(); - expect(responseBody.data.name).toEqual(body.name); - expect(responseBody.data.metadata).toEqual(body.metadata); - team1 = responseBody.data; - }); - }); - }); - - describe("User does not have membership in created team", () => { - it("should not be able to get a team", async () => { - return request(app.getHttpServer()) - .get(`/v2/teams/${team2.id}`) - .set({ Authorization: `Bearer cal_test_${bobApiKey}` }) - .expect(403); - }); - }); - - describe("User does not have sufficient membership in created team", () => { - it("should not be able to delete a team", async () => { - await membershipRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: bob.id } }, - team: { connect: { id: team2.id } }, - accepted: true, - }); - return request(app.getHttpServer()) - .delete(`/v2/teams/${team2.id}`) - .set({ Authorization: `Bearer cal_test_${bobApiKey}` }) - .expect(403); - }); - }); - - describe("Delete teams", () => { - it("should delete team", async () => { - return request(app.getHttpServer()) - .delete(`/v2/teams/${team1.id}`) - .set({ Authorization: `Bearer cal_test_${aliceApiKey}` }) - .expect(200) - .then(async (response) => { - const responseBody: GetTeamOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.id).toBeDefined(); - expect(responseBody.data.name).toEqual(team1.name); - }); - }); - - it("should delete team", async () => { - return request(app.getHttpServer()) - .delete(`/v2/teams/${team2.id}`) - .set({ Authorization: `Bearer cal_test_${aliceApiKey}` }) - .expect(200) - .then(async (response) => { - const responseBody: GetTeamOutput = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data.id).toBeDefined(); - expect(responseBody.data.name).toEqual(team2.name); - }); - }); - }); - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(aliceEmail); - await userRepositoryFixture.deleteByEmail(bobEmail); - await teamRepositoryFixture.delete(team1.id); - await teamRepositoryFixture.delete(team2.id); - await app.close(); - }); -}); diff --git a/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.ts b/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.ts deleted file mode 100644 index cf87971c116d17..00000000000000 --- a/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { TeamOutputDto } from "@calcom/platform-types"; -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { API_KEY_HEADER } from "@/lib/docs/headers"; -import { Throttle } from "@/lib/endpoint-throttler-decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { UpdateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/update-organization-team.input"; -import { OrgTeamOutputResponseDto } from "@/modules/organizations/teams/index/outputs/organization-team.output"; -import { CreateTeamInput } from "@/modules/teams/teams/inputs/create-team.input"; -import { CreateTeamOutput } from "@/modules/teams/teams/outputs/teams/create-team.output"; -import { GetTeamOutput } from "@/modules/teams/teams/outputs/teams/get-team.output"; -import { GetTeamsOutput } from "@/modules/teams/teams/outputs/teams/get-teams.output"; -import { UpdateTeamOutput } from "@/modules/teams/teams/outputs/teams/update-team.output"; -import { TeamsService } from "@/modules/teams/teams/services/teams.service"; -import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; -import { UserWithProfile } from "@/modules/users/users.repository"; - -@Controller({ - path: "/v2/teams", - version: API_VERSIONS_VALUES, -}) -@UseGuards(ApiAuthGuard) -@DocsTags("Teams") -@ApiHeader(API_KEY_HEADER) -export class TeamsController { - constructor( - private teamsService: TeamsService, - private teamsRepository: TeamsRepository - ) {} - - @Post() - @ApiOperation({ summary: "Create a team" }) - async createTeam( - @Body() body: CreateTeamInput, - @GetUser() user: UserWithProfile - ): Promise { - const team = await this.teamsService.createTeam(body, user.id); - - if ("paymentLink" in team) { - return { - status: SUCCESS_STATUS, - data: { - pendingTeam: plainToClass(TeamOutputDto, team.pendingTeam, { strategy: "excludeAll" }), - paymentLink: team.paymentLink, - message: team.message, - }, - }; - } - - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamOutputDto, team, { strategy: "excludeAll" }), - }; - } - - @Get("/:teamId") - @ApiOperation({ summary: "Get a team" }) - @UseGuards(RolesGuard) - @Roles("TEAM_MEMBER") - async getTeam(@Param("teamId", ParseIntPipe) teamId: number): Promise { - const team = await this.teamsRepository.getById(teamId); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamOutputDto, team, { strategy: "excludeAll" }), - }; - } - - @Get("/") - @ApiOperation({ summary: "Get teams" }) - async getTeams(@GetUser("id") userId: number): Promise { - const teams = await this.teamsService.getUserTeams(userId); - return { - status: SUCCESS_STATUS, - data: teams.map((team) => plainToClass(TeamOutputDto, team, { strategy: "excludeAll" })), - }; - } - - @Patch("/:teamId") - @ApiOperation({ summary: "Update a team" }) - @UseGuards(RolesGuard) - @Roles("TEAM_OWNER") - async updateTeam( - @Param("teamId", ParseIntPipe) teamId: number, - @Body() body: UpdateOrgTeamDto - ): Promise { - const team = await this.teamsService.updateTeam(teamId, body); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamOutputDto, team, { strategy: "excludeAll" }), - }; - } - - @UseGuards(RolesGuard) - @Delete("/:teamId") - @Throttle({ limit: 1, ttl: 1000, blockDuration: 1000, name: "teams_delete" }) - @ApiOperation({ summary: "Delete a team" }) - @Roles("TEAM_OWNER") - async deleteTeam(@Param("teamId", ParseIntPipe) teamId: number): Promise { - const team = await this.teamsRepository.delete(teamId); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamOutputDto, team, { strategy: "excludeAll" }), - }; - } -} diff --git a/apps/api/v2/src/modules/teams/teams/inputs/create-team.input.ts b/apps/api/v2/src/modules/teams/teams/inputs/create-team.input.ts deleted file mode 100644 index 3ec5fd4adcb57c..00000000000000 --- a/apps/api/v2/src/modules/teams/teams/inputs/create-team.input.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsBoolean, IsObject, IsOptional, IsString, Length, Validate } from "class-validator"; - -import { Metadata, METADATA_DOCS, ValidateMetadata } from "@calcom/platform-types"; - -import { SSRFSafeUrlValidator } from "../validators/ssrfSafeUrlValidator"; - -export class CreateTeamInput { - @IsString() - @Length(1) - @ApiProperty({ description: "Name of the team", example: "CalTeam", required: true }) - readonly name!: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ - type: String, - description: "Team slug in kebab-case - if not provided will be generated automatically based on name.", - example: "caltel", - }) - readonly slug?: string; - - @IsOptional() - @IsString() - @Validate(SSRFSafeUrlValidator) - @ApiPropertyOptional({ - type: String, - example: "https://i.cal.com/api/avatar/b0b58752-68ad-4c0d-8024-4fa382a77752.png", - description: `URL of the teams logo image`, - }) - readonly logoUrl?: string; - - @IsOptional() - @IsString() - @Validate(SSRFSafeUrlValidator) - @ApiPropertyOptional() - readonly calVideoLogo?: string; - - @IsOptional() - @IsString() - @Validate(SSRFSafeUrlValidator) - @ApiPropertyOptional() - readonly appLogo?: string; - - @IsOptional() - @IsString() - @Validate(SSRFSafeUrlValidator) - @ApiPropertyOptional() - readonly appIconLogo?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly bio?: string; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ type: Boolean, default: false }) - readonly hideBranding?: boolean = false; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional() - readonly isPrivate?: boolean; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional() - readonly hideBookATeamMember?: boolean; - - @ApiPropertyOptional({ - type: Object, - description: METADATA_DOCS, - example: { key: "value" }, - }) - @IsObject() - @IsOptional() - @ValidateMetadata() - metadata?: Metadata; - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly theme?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly brandColor?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly darkBrandColor?: string; - - @IsOptional() - @IsString() - @Validate(SSRFSafeUrlValidator) - @ApiPropertyOptional({ - type: String, - example: "https://i.cal.com/api/avatar/949be534-7a88-4185-967c-c020b0c0bef3.png", - description: `URL of the teams banner image which is shown on booker`, - required: false, - }) - readonly bannerUrl?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly timeFormat?: number; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ - type: String, - example: "America/New_York", - description: `Timezone is used to create teams's default schedule from Monday to Friday from 9AM to 5PM. It will default to Europe/London if not passed.`, - required: false, - default: "Europe/London", - }) - readonly timeZone?: string = "Europe/London"; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ - type: String, - example: "Monday", - default: "Sunday", - }) - readonly weekStart?: string = "Sunday"; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ - type: Boolean, - default: true, - description: - "If you are a platform customer, don't pass 'false', because then team creator won't be able to create team event types.", - }) - readonly autoAcceptCreator?: boolean = true; -} diff --git a/apps/api/v2/src/modules/teams/teams/inputs/update-team.input.ts b/apps/api/v2/src/modules/teams/teams/inputs/update-team.input.ts deleted file mode 100644 index 00e5fd3c597ae9..00000000000000 --- a/apps/api/v2/src/modules/teams/teams/inputs/update-team.input.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsBoolean, IsObject, IsOptional, IsString, Length, Validate } from "class-validator"; - -import { Metadata, METADATA_DOCS, ValidateMetadata } from "@calcom/platform-types"; - -import { SSRFSafeUrlValidator } from "../validators/ssrfSafeUrlValidator"; - -export class UpdateTeamDto { - @IsString() - @Length(1) - @ApiPropertyOptional({ description: "Name of the team", example: "CalTeam" }) - readonly name?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ type: String, description: "Team slug", example: "caltel" }) - readonly slug?: string; - - @IsOptional() - @IsString() - @Validate(SSRFSafeUrlValidator) - @ApiPropertyOptional({ - type: String, - example: "https://i.cal.com/api/avatar/b0b58752-68ad-4c0d-8024-4fa382a77752.png", - description: `URL of the teams logo image`, - }) - readonly logoUrl?: string; - - @IsOptional() - @IsString() - @Validate(SSRFSafeUrlValidator) - @ApiPropertyOptional() - readonly calVideoLogo?: string; - - @IsOptional() - @IsString() - @Validate(SSRFSafeUrlValidator) - @ApiPropertyOptional() - readonly appLogo?: string; - - @IsOptional() - @IsString() - @Validate(SSRFSafeUrlValidator) - @ApiPropertyOptional() - readonly appIconLogo?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly bio?: string; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional() - readonly hideBranding?: boolean; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional() - readonly isPrivate?: boolean; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional() - readonly hideBookATeamMember?: boolean; - - @ApiPropertyOptional({ - type: Object, - description: METADATA_DOCS, - example: { key: "value" }, - }) - @IsObject() - @IsOptional() - @ValidateMetadata() - metadata?: Metadata; - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly theme?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly brandColor?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly darkBrandColor?: string; - - @IsOptional() - @IsString() - @Validate(SSRFSafeUrlValidator) - @ApiPropertyOptional({ - type: String, - example: "https://i.cal.com/api/avatar/949be534-7a88-4185-967c-c020b0c0bef3.png", - description: `URL of the teams banner image which is shown on booker`, - }) - readonly bannerUrl?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly timeFormat?: number; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ - type: String, - example: "America/New_York", - description: `Timezone is used to create teams's default schedule from Monday to Friday from 9AM to 5PM. It will default to Europe/London if not passed.`, - }) - readonly timeZone?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional({ - type: String, - example: "Monday", - }) - readonly weekStart?: string; - - @IsOptional() - @IsString() - @ApiPropertyOptional() - readonly bookingLimits?: string; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional() - readonly includeManagedEventsInLimits?: boolean; -} diff --git a/apps/api/v2/src/modules/teams/teams/outputs/teams/create-team.output.ts b/apps/api/v2/src/modules/teams/teams/outputs/teams/create-team.output.ts deleted file mode 100644 index 436e533bf6bf60..00000000000000 --- a/apps/api/v2/src/modules/teams/teams/outputs/teams/create-team.output.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger"; -import { Expose } from "class-transformer"; -import { IsEnum, ValidateNested, IsString, IsUrl } from "class-validator"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import { TeamOutputDto } from "@calcom/platform-types"; - -class CreateTeamOutputData { - @Expose() - @IsString() - message!: string; - - @Expose() - @IsUrl() - paymentLink!: string; - - @Expose() - @ValidateNested() - pendingTeam!: TeamOutputDto; -} - -@ApiExtraModels(TeamOutputDto, CreateTeamOutputData) -export class CreateTeamOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - oneOf: [{ $ref: getSchemaPath(CreateTeamOutputData) }, { $ref: getSchemaPath(TeamOutputDto) }], - description: "Either an Output object or a TeamOutputDto.", - }) - @Expose() - @ValidateNested() - data!: CreateTeamOutputData | TeamOutputDto; -} diff --git a/apps/api/v2/src/modules/teams/teams/outputs/teams/get-team.output.ts b/apps/api/v2/src/modules/teams/teams/outputs/teams/get-team.output.ts deleted file mode 100644 index 58fef199c95d07..00000000000000 --- a/apps/api/v2/src/modules/teams/teams/outputs/teams/get-team.output.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum, ValidateNested } from "class-validator"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import { TeamOutputDto } from "@calcom/platform-types"; - -export class GetTeamOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @Expose() - @ValidateNested() - @Type(() => TeamOutputDto) - data!: TeamOutputDto; -} diff --git a/apps/api/v2/src/modules/teams/teams/outputs/teams/get-teams.output.ts b/apps/api/v2/src/modules/teams/teams/outputs/teams/get-teams.output.ts deleted file mode 100644 index c4036235455e45..00000000000000 --- a/apps/api/v2/src/modules/teams/teams/outputs/teams/get-teams.output.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum, ValidateNested } from "class-validator"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import { TeamOutputDto } from "@calcom/platform-types"; - -export class GetTeamsOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @Expose() - @ValidateNested() - @Type(() => TeamOutputDto) - data!: TeamOutputDto[]; -} diff --git a/apps/api/v2/src/modules/teams/teams/outputs/teams/update-team.output.ts b/apps/api/v2/src/modules/teams/teams/outputs/teams/update-team.output.ts deleted file mode 100644 index 7113e5ecc7c41a..00000000000000 --- a/apps/api/v2/src/modules/teams/teams/outputs/teams/update-team.output.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsEnum, ValidateNested } from "class-validator"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import { TeamOutputDto } from "@calcom/platform-types"; - -export class UpdateTeamOutput { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @Expose() - @ValidateNested() - @Type(() => TeamOutputDto) - data!: TeamOutputDto; -} diff --git a/apps/api/v2/src/modules/teams/teams/services/teams.service.ts b/apps/api/v2/src/modules/teams/teams/services/teams.service.ts deleted file mode 100644 index 383e89a0f91223..00000000000000 --- a/apps/api/v2/src/modules/teams/teams/services/teams.service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { StripeService } from "@/modules/stripe/stripe.service"; -import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository"; -import { CreateTeamInput } from "@/modules/teams/teams/inputs/create-team.input"; -import { UpdateTeamDto } from "@/modules/teams/teams/inputs/update-team.input"; -import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; -import { BadRequestException, Injectable, InternalServerErrorException } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; - -import { slugify } from "@calcom/platform-libraries"; - -@Injectable() -export class TeamsService { - private isTeamBillingEnabled = this.configService.get("stripe.isTeamBillingEnabled"); - - constructor( - private readonly teamsRepository: TeamsRepository, - private readonly teamsMembershipsRepository: TeamsMembershipsRepository, - private readonly stripeService: StripeService, - private readonly configService: ConfigService - ) {} - - async createTeam(input: CreateTeamInput, ownerId: number) { - const { autoAcceptCreator, ...teamData } = input; - if (!teamData.slug) { - teamData.slug = slugify(teamData.name); - } - - const existingTeam = await this.teamsMembershipsRepository.findTeamMembershipsByNameAndUser( - input.name, - ownerId - ); - if (existingTeam) { - throw new BadRequestException({ - message: `You already have created a team with name=${input.name}`, - }); - } - - if (!this.isTeamBillingEnabled) { - const team = await this.teamsRepository.create(teamData); - await this.teamsMembershipsRepository.createTeamMembership(team.id, { - userId: ownerId, - role: "OWNER", - accepted: !!autoAcceptCreator, - }); - return team; - } - - const pendingTeam = await this.teamsRepository.create({ ...teamData, pendingPayment: true }); - - const checkoutSession = await this.stripeService.generateTeamCheckoutSession(pendingTeam.id, ownerId); - - if (!checkoutSession.url) { - await this.teamsRepository.delete(pendingTeam.id); - throw new InternalServerErrorException({ - message: `Failed generating team Stripe checkout session URL which is why team creation was cancelled. Please contact support.`, - }); - } - - return { - message: - "Your team will be created once we receive your payment. Please complete the payment using the payment link.", - paymentLink: checkoutSession.url, - pendingTeam, - }; - } - - async getUserTeams(userId: number) { - const teams = await this.teamsRepository.getTeamsUserIsMemberOf(userId); - return teams; - } - - async updateTeam(teamId: number, data: UpdateTeamDto) { - const team = await this.teamsRepository.update(teamId, data); - return team; - } -} diff --git a/apps/api/v2/src/modules/teams/teams/teams.module.ts b/apps/api/v2/src/modules/teams/teams/teams.module.ts deleted file mode 100644 index db055ec8278431..00000000000000 --- a/apps/api/v2/src/modules/teams/teams/teams.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; -import { StripeModule } from "@/modules/stripe/stripe.module"; -import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository"; -import { TeamsController } from "@/modules/teams/teams/controllers/teams.controller"; -import { TeamsService } from "@/modules/teams/teams/services/teams.service"; -import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; -import { Module } from "@nestjs/common"; - -@Module({ - imports: [PrismaModule, MembershipsModule, RedisModule, StripeModule], - providers: [TeamsRepository, TeamsService, TeamsMembershipsRepository], - controllers: [TeamsController], - exports: [TeamsRepository], -}) -export class TeamsModule {} diff --git a/apps/api/v2/src/modules/teams/teams/teams.repository.ts b/apps/api/v2/src/modules/teams/teams/teams.repository.ts index 2cd9ab58d64e92..ad6a0b01d93e1c 100644 --- a/apps/api/v2/src/modules/teams/teams/teams.repository.ts +++ b/apps/api/v2/src/modules/teams/teams/teams.repository.ts @@ -1,13 +1,15 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable, NotFoundException } from "@nestjs/common"; - import { teamMetadataSchema } from "@calcom/platform-libraries"; import type { Membership, Prisma } from "@calcom/prisma/client"; +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; @Injectable() export class TeamsRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} + constructor( + private readonly dbRead: PrismaReadService, + private readonly dbWrite: PrismaWriteService + ) {} async create(team: Prisma.TeamCreateInput) { return this.dbWrite.prisma.team.create({ diff --git a/apps/api/v2/src/modules/teams/teams/validators/ssrfSafeUrlValidator.spec.ts b/apps/api/v2/src/modules/teams/teams/validators/ssrfSafeUrlValidator.spec.ts deleted file mode 100644 index 6d7a4e4a3d0003..00000000000000 --- a/apps/api/v2/src/modules/teams/teams/validators/ssrfSafeUrlValidator.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { ValidationError } from "class-validator"; -import { IsOptional, Validate, validateSync } from "class-validator"; - -import { SSRFSafeUrlValidator } from "./ssrfSafeUrlValidator"; - -const mockValidateUrlForSSRFSync: jest.Mock = jest.fn(); - -// Mock with minimal logic - actual validation tested in packages/lib -jest.mock("@calcom/platform-libraries", () => ({ - validateUrlForSSRFSync: (url: string) => mockValidateUrlForSSRFSync(url), -})); - -class TestDto { - @IsOptional() - @Validate(SSRFSafeUrlValidator) - url?: string; -} - -describe("SSRFSafeUrlValidator", () => { - const validate = (url?: string): ValidationError[] => { - const dto = new TestDto(); - dto.url = url; - return validateSync(dto); - }; - - beforeEach(() => { - mockValidateUrlForSSRFSync.mockReset(); - }); - - it("accepts undefined values (works with @IsOptional)", () => { - expect(validate(undefined)).toHaveLength(0); - expect(mockValidateUrlForSSRFSync).not.toHaveBeenCalled(); - }); - - it("validates empty strings (does not skip validation)", () => { - mockValidateUrlForSSRFSync.mockReturnValue({ isValid: false }); - - const errors = validate(""); - expect(errors).toHaveLength(1); - expect(mockValidateUrlForSSRFSync).toHaveBeenCalledWith(""); - }); - - it("delegates to validateUrlForSSRFSync and accepts valid URLs", () => { - mockValidateUrlForSSRFSync.mockReturnValue({ isValid: true }); - - expect(validate("https://example.com/logo.png")).toHaveLength(0); - expect(mockValidateUrlForSSRFSync).toHaveBeenCalledWith("https://example.com/logo.png"); - }); - - it("delegates to validateUrlForSSRFSync and rejects invalid URLs", () => { - mockValidateUrlForSSRFSync.mockReturnValue({ isValid: false }); - - const errors = validate("blocked-url"); - expect(errors).toHaveLength(1); - expect(errors[0].constraints).toHaveProperty("ssrfSafeUrl"); - expect(mockValidateUrlForSSRFSync).toHaveBeenCalledWith("blocked-url"); - }); -}); diff --git a/apps/api/v2/src/modules/teams/teams/validators/ssrfSafeUrlValidator.ts b/apps/api/v2/src/modules/teams/teams/validators/ssrfSafeUrlValidator.ts index 75c753e6026284..0265ddf58fc544 100644 --- a/apps/api/v2/src/modules/teams/teams/validators/ssrfSafeUrlValidator.ts +++ b/apps/api/v2/src/modules/teams/teams/validators/ssrfSafeUrlValidator.ts @@ -1,7 +1,6 @@ -import { ValidatorConstraint } from "class-validator"; -import type { ValidatorConstraintInterface } from "class-validator"; - import { validateUrlForSSRFSync } from "@calcom/platform-libraries"; +import type { ValidatorConstraintInterface } from "class-validator"; +import { ValidatorConstraint } from "class-validator"; /** * Validates that a URL is safe for server-side fetching diff --git a/apps/api/v2/src/modules/teams/verified-resources/teams-verified-resources.controller.ts b/apps/api/v2/src/modules/teams/verified-resources/teams-verified-resources.controller.ts deleted file mode 100644 index 697c01d8eeba60..00000000000000 --- a/apps/api/v2/src/modules/teams/verified-resources/teams-verified-resources.controller.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; -import { Throttle } from "@/lib/endpoint-throttler-decorator"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input"; -import { RequestPhoneVerificationInput } from "@/modules/verified-resources/inputs/request-phone-verification.input"; -import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input"; -import { VerifyPhoneInput } from "@/modules/verified-resources/inputs/verify-phone.input"; -import { RequestEmailVerificationOutput } from "@/modules/verified-resources/outputs/request-email-verification-output"; -import { RequestPhoneVerificationOutput } from "@/modules/verified-resources/outputs/request-phone-verification-output"; -import { - TeamVerifiedEmailOutput, - TeamVerifiedEmailOutputData, - TeamVerifiedEmailsOutput, -} from "@/modules/verified-resources/outputs/verified-email.output"; -import { - TeamVerifiedPhoneOutput, - TeamVerifiedPhoneOutputData, - TeamVerifiedPhonesOutput, -} from "@/modules/verified-resources/outputs/verified-phone.output"; -import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service"; -import { - Body, - Controller, - Get, - HttpCode, - HttpStatus, - Param, - ParseIntPipe, - Post, - Query, - UseGuards, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; - -import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; -import { SkipTakePagination } from "@calcom/platform-types"; - -@Controller({ - path: "/v2/teams/:teamId/verified-resources", -}) -@UseGuards(ApiAuthGuard, RolesGuard) -@ApiTags("Teams Verified Resources") -@ApiParam({ name: "teamId", type: Number, required: true }) -export class TeamsVerifiedResourcesController { - constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {} - @ApiOperation({ - summary: "Request email verification code", - description: `Sends a verification code to the Email`, - }) - @Roles("TEAM_ADMIN") - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Post("/emails/verification-code/request") - @Throttle({ limit: 5, ttl: 60000, blockDuration: 60000, name: "teams_verified_resources_emails_requests" }) - @HttpCode(HttpStatus.OK) - async requestEmailVerificationCode( - @Body() body: RequestEmailVerificationInput, - @GetUser("username") username: string, - @GetUser("locale") locale: string - ): Promise { - const verificationCodeRequest = await this.verifiedResourcesService.requestEmailVerificationCode( - { username, locale }, - body.email - ); - - return { - status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS, - }; - } - - @ApiOperation({ - summary: "Request phone number verification code", - description: `Sends a verification code to the phone number`, - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Roles("TEAM_ADMIN") - @Post("/phones/verification-code/request") - @Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "teams_verified_resources_phones_requests" }) - @HttpCode(HttpStatus.OK) - async requestPhoneVerificationCode( - @Body() body: RequestPhoneVerificationInput - ): Promise { - const verificationCodeRequest = await this.verifiedResourcesService.requestPhoneVerificationCode( - body.phone - ); - - return { - status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS, - }; - } - - @ApiOperation({ - summary: "Verify an email for a team", - description: `Use code to verify an email`, - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Roles("TEAM_ADMIN") - @Post("/emails/verification-code/verify") - @HttpCode(HttpStatus.OK) - async verifyEmail( - @Body() body: VerifyEmailInput, - @GetUser("id") userId: number, - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - const verifiedEmail = await this.verifiedResourcesService.verifyEmail( - userId, - body.email, - body.code, - teamId - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail), - }; - } - - @ApiOperation({ - summary: "Verify a phone number for an org team", - description: `Use code to verify a phone number`, - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Roles("TEAM_ADMIN") - @Post("/phones/verification-code/verify") - @Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "teams_verified_resources_phones_verify" }) - @HttpCode(HttpStatus.OK) - async verifyPhoneNumber( - @Body() body: VerifyPhoneInput, - @GetUser("id") userId: number, - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - const verifiedPhone = await this.verifiedResourcesService.verifyPhone( - userId, - body.phone, - body.code, - teamId - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhone), - }; - } - - @ApiOperation({ - summary: "Get list of verified emails of a team", - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Roles("TEAM_ADMIN") - @Get("/emails") - @HttpCode(HttpStatus.OK) - async getVerifiedEmails( - @Param("teamId", ParseIntPipe) teamId: number, - @Query() pagination: SkipTakePagination - ): Promise { - const verifiedEmails = await this.verifiedResourcesService.getTeamVerifiedEmails( - teamId, - pagination?.skip ?? 0, - pagination?.take ?? 250 - ); - return { - status: SUCCESS_STATUS, - data: verifiedEmails.map((verifiedEmail) => plainToClass(TeamVerifiedEmailOutputData, verifiedEmail)), - }; - } - - @ApiOperation({ - summary: "Get list of verified phone numbers of a team", - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Get("/phones") - @Roles("TEAM_ADMIN") - @HttpCode(HttpStatus.OK) - async getVerifiedPhoneNumbers( - @Param("teamId", ParseIntPipe) teamId: number, - @Query() pagination: SkipTakePagination - ): Promise { - const verifiedPhoneNumbers = await this.verifiedResourcesService.getTeamVerifiedPhoneNumbers( - teamId, - pagination?.skip ?? 0, - pagination?.take ?? 250 - ); - return { - status: SUCCESS_STATUS, - data: verifiedPhoneNumbers.map((verifiedPhoneNumber) => - plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber) - ), - }; - } - - @ApiOperation({ - summary: "Get verified email of a team by id", - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Roles("TEAM_ADMIN") - @Get("/emails/:id") - @HttpCode(HttpStatus.OK) - async getVerifiedEmailById( - @Param("id") id: number, - @Param("teamId", ParseIntPipe) teamId: number - ): Promise { - const verifiedEmail = await this.verifiedResourcesService.getTeamVerifiedEmailById(teamId, id); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail), - }; - } - - @ApiOperation({ - summary: "Get verified phone number of a team by id", - }) - @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) - @Roles("TEAM_ADMIN") - @Get("/phones/:id") - @HttpCode(HttpStatus.OK) - async getVerifiedPhoneById( - @Param("teamId", ParseIntPipe) teamId: number, - @Param("id") id: number - ): Promise { - const verifiedPhoneNumber = await this.verifiedResourcesService.getTeamVerifiedPhoneNumberById( - teamId, - id - ); - return { - status: SUCCESS_STATUS, - data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber), - }; - } -} diff --git a/apps/api/v2/src/modules/users/users.module.ts b/apps/api/v2/src/modules/users/users.module.ts index a7eed6c99de5cc..d322ba83edba48 100644 --- a/apps/api/v2/src/modules/users/users.module.ts +++ b/apps/api/v2/src/modules/users/users.module.ts @@ -1,4 +1,4 @@ -import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; +import { EventTypesModule_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; import { UsersService } from "@/modules/users/services/users.service"; diff --git a/apps/api/v2/src/modules/users/validators/timeZoneValidator.ts b/apps/api/v2/src/modules/users/validators/timeZoneValidator.ts index ec60bcc2e68ed7..a2b300d3834966 100644 --- a/apps/api/v2/src/modules/users/validators/timeZoneValidator.ts +++ b/apps/api/v2/src/modules/users/validators/timeZoneValidator.ts @@ -1,14 +1,15 @@ import type { ValidatorConstraintInterface } from "class-validator"; import { ValidatorConstraint } from "class-validator"; -import tzdata from "tzdata"; + @ValidatorConstraint({ name: "timezoneValidator", async: false }) export class TimeZoneValidator implements ValidatorConstraintInterface { validate(timeZone: string): boolean { - const timeZoneList = Object.keys(tzdata.zones); - - if (timeZoneList.includes(timeZone)) return true; - - return false; + try { + Intl.DateTimeFormat(undefined, { timeZone }); + return true; + } catch { + return false; + } } defaultMessage(): string { diff --git a/apps/api/v2/src/modules/verified-resources/services/verified-resources.service.ts b/apps/api/v2/src/modules/verified-resources/services/verified-resources.service.ts index b0ba2b434af10b..e8fffc255d9887 100644 --- a/apps/api/v2/src/modules/verified-resources/services/verified-resources.service.ts +++ b/apps/api/v2/src/modules/verified-resources/services/verified-resources.service.ts @@ -1,12 +1,11 @@ -import { TeamsVerifiedResourcesRepository } from "@/modules/verified-resources/teams-verified-resources.repository"; -import { UsersVerifiedResourcesRepository } from "@/modules/verified-resources/users-verified-resources.repository"; -import { BadRequestException, ConflictException, Injectable } from "@nestjs/common"; - import { - verifyPhoneNumber, sendVerificationCode as sendPhoneVerificationCode, + verifyPhoneNumber, } from "@calcom/platform-libraries"; import { sendEmailVerificationByCode, verifyEmailCodeHandler } from "@calcom/platform-libraries/emails"; +import { BadRequestException, ConflictException, Injectable } from "@nestjs/common"; +import { TeamsVerifiedResourcesRepository } from "@/modules/verified-resources/teams-verified-resources.repository"; +import { UsersVerifiedResourcesRepository } from "@/modules/verified-resources/users-verified-resources.repository"; @Injectable() export class VerifiedResourcesService { diff --git a/apps/api/v2/src/modules/verified-resources/verified-resources.module.ts b/apps/api/v2/src/modules/verified-resources/verified-resources.module.ts index b40c2d32d6b5e4..a1cdd8be14fec8 100644 --- a/apps/api/v2/src/modules/verified-resources/verified-resources.module.ts +++ b/apps/api/v2/src/modules/verified-resources/verified-resources.module.ts @@ -2,12 +2,9 @@ import { AppsRepository } from "@/modules/apps/apps.repository"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; -import { OrgTeamsVerifiedResourcesController } from "@/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; import { StripeService } from "@/modules/stripe/stripe.service"; -import { TeamsVerifiedResourcesController } from "@/modules/teams/verified-resources/teams-verified-resources.controller"; import { UsersRepository } from "@/modules/users/users.repository"; import { UserVerifiedResourcesController } from "@/modules/verified-resources/controllers/users-verified-resources.controller"; import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service"; @@ -19,15 +16,12 @@ import { Module } from "@nestjs/common"; imports: [PrismaModule, RedisModule], controllers: [ UserVerifiedResourcesController, - TeamsVerifiedResourcesController, - OrgTeamsVerifiedResourcesController, ], providers: [ VerifiedResourcesService, UsersVerifiedResourcesRepository, TeamsVerifiedResourcesRepository, MembershipsRepository, - OrganizationsTeamsRepository, OrganizationsRepository, StripeService, AppsRepository, diff --git a/apps/api/v2/src/modules/webhooks/guards/is-team-event-type-webhook-guard.ts b/apps/api/v2/src/modules/webhooks/guards/is-team-event-type-webhook-guard.ts deleted file mode 100644 index f215ee3381d2f1..00000000000000 --- a/apps/api/v2/src/modules/webhooks/guards/is-team-event-type-webhook-guard.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { EventType, Webhook } from "@calcom/prisma/client"; -import { - BadRequestException, - type CanActivate, - type ExecutionContext, - ForbiddenException, - Injectable, - NotFoundException, -} from "@nestjs/common"; -import type { Request } from "express"; -import type { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; -import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; -import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository"; - -type WebhookRequest = Request & { webhook: Webhook; eventType: EventType }; - -@Injectable() -export class IsTeamEventTypeWebhookGuard implements CanActivate { - constructor( - private readonly webhooksRepository: WebhooksRepository, - private readonly teamsEventTypesRepository: TeamsEventTypesRepository, - private readonly membershipsRepository: MembershipsRepository - ) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const user = request.user as ApiAuthGuardUser; - const { webhookId, eventTypeId, teamId } = request.params; - - this.validateInitialRequest(user, teamId, eventTypeId); - - await this.validateTeamMembership(user.id, Number(teamId)); - - request.eventType = await this.validateAndGetEventType(Number(teamId), Number(eventTypeId)); - - if (webhookId) { - request.webhook = await this.validateAndGetWebhook(webhookId, Number(eventTypeId)); - } - - return true; - } - - private validateInitialRequest(user: ApiAuthGuardUser, teamId: string, eventTypeId: string): void { - if (!user) { - throw new ForbiddenException("IsTeamEventTypeWebhookGuard - No user associated with the request."); - } - if (!teamId) { - throw new BadRequestException("IsTeamEventTypeWebhookGuard - Team ID is required."); - } - if (!eventTypeId) { - throw new BadRequestException("IsTeamEventTypeWebhookGuard - Event Type ID is required."); - } - } - - private async validateTeamMembership(userId: number, teamId: number): Promise { - const membership = await this.membershipsRepository.getUserAdminOrOwnerTeamMembership(userId, teamId); - if (!membership) { - throw new ForbiddenException( - `IsTeamEventTypeWebhookGuard - User (${userId}) is not an admin/owner of team (${teamId})` - ); - } - } - - private async validateAndGetEventType(teamId: number, eventTypeId: number): Promise { - const eventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId); - if (!eventType) { - throw new NotFoundException( - `IsTeamEventTypeWebhookGuard - Event type (${eventTypeId}) not found for team (${teamId})` - ); - } - return eventType; - } - - private async validateAndGetWebhook(webhookId: string, eventTypeId: number): Promise { - const webhook = await this.webhooksRepository.getWebhookById(webhookId); - - if (!webhook) { - throw new NotFoundException(`IsTeamEventTypeWebhookGuard - Webhook (${webhookId}) not found`); - } - if (!webhook.eventTypeId) { - throw new BadRequestException(`IsTeamEventTypeWebhookGuard - Webhook (${webhookId}) no event type`); - } - if (webhook.eventTypeId !== eventTypeId) { - throw new ForbiddenException(`IsTeamEventTypeWebhookGuard - Webhook mismatch with event type`); - } - - return webhook; - } -} diff --git a/apps/api/v2/src/modules/webhooks/guards/is-user-event-type-webhook-guard.ts b/apps/api/v2/src/modules/webhooks/guards/is-user-event-type-webhook-guard.ts index 840e15855e70ca..e1c3e019a6572c 100644 --- a/apps/api/v2/src/modules/webhooks/guards/is-user-event-type-webhook-guard.ts +++ b/apps/api/v2/src/modules/webhooks/guards/is-user-event-type-webhook-guard.ts @@ -1,4 +1,4 @@ -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; import { WebhooksService } from "@/modules/webhooks/services/webhooks.service"; import { diff --git a/apps/api/v2/src/modules/webhooks/webhooks.module.ts b/apps/api/v2/src/modules/webhooks/webhooks.module.ts index 3861b9ab469778..3643a295d8f7d8 100644 --- a/apps/api/v2/src/modules/webhooks/webhooks.module.ts +++ b/apps/api/v2/src/modules/webhooks/webhooks.module.ts @@ -1,12 +1,11 @@ import { Module } from "@nestjs/common"; -import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; +import { EventTypesModule_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.module"; import { EventTypeWebhooksController } from "@/modules/event-types/controllers/event-types-webhooks.controller"; import { OAuthClientWebhooksController } from "@/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller"; import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module"; import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; import { MembershipsModule } from "../memberships/memberships.module"; -import { OrganizationsModule } from "../organizations/organizations.module"; import { PrismaModule } from "../prisma/prisma.module"; import { UsersModule } from "../users/users.module"; import { WebhooksController } from "./controllers/webhooks.controller"; @@ -16,7 +15,6 @@ import { TeamEventTypeWebhooksService } from "./services/team-event-type-webhook import { UserWebhooksService } from "./services/user-webhooks.service"; import { WebhooksService } from "./services/webhooks.service"; import { WebhooksRepository } from "./webhooks.repository"; -import { TeamsEventTypesWebhooksController } from "@/modules/teams/event-types/controllers/teams-event-types-webhooks.controller"; import { RedisModule } from "@/modules/redis/redis.module"; @Module({ @@ -26,7 +24,6 @@ import { RedisModule } from "@/modules/redis/redis.module"; UsersModule, EventTypesModule_2024_06_14, OAuthClientModule, - OrganizationsModule, MembershipsModule, OAuthClientModule, ], @@ -34,7 +31,6 @@ import { RedisModule } from "@/modules/redis/redis.module"; WebhooksController, EventTypeWebhooksController, OAuthClientWebhooksController, - TeamsEventTypesWebhooksController, ], providers: [ TeamsEventTypesRepository, diff --git a/apps/api/v2/src/modules/workflows/inputs/create-event-type-workflow.input.ts b/apps/api/v2/src/modules/workflows/inputs/create-event-type-workflow.input.ts deleted file mode 100644 index 3d21a13a48533f..00000000000000 --- a/apps/api/v2/src/modules/workflows/inputs/create-event-type-workflow.input.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { - IsBoolean, - ArrayMinSize, - IsNumber, - IsOptional, - IsString, - ValidateNested, - ValidateIf, -} from "class-validator"; - -import { - BaseWorkflowStepDto, - EMAIL_ADDRESS, - EMAIL_ATTENDEE, - EMAIL_HOST, - SMS_ATTENDEE, - SMS_NUMBER, - STEP_ACTIONS, - WHATSAPP_ATTENDEE, - WHATSAPP_NUMBER, - WorkflowEmailAddressStepDto, - WorkflowEmailAttendeeStepDto, - WorkflowEmailHostStepDto, - WorkflowPhoneAttendeeStepDto, - WorkflowPhoneNumberStepDto, - WorkflowPhoneWhatsAppAttendeeStepDto, - WorkflowPhoneWhatsAppNumberStepDto, -} from "./workflow-step.input"; -import { - AFTER_EVENT, - AFTER_GUESTS_CAL_VIDEO_NO_SHOW, - AFTER_HOSTS_CAL_VIDEO_NO_SHOW, - EventTypeWorkflowTriggerDto, - BEFORE_EVENT, - BOOKING_NO_SHOW_UPDATED, - BOOKING_PAID, - BOOKING_PAYMENT_INITIATED, - BOOKING_REJECTED, - BOOKING_REQUESTED, - EVENT_CANCELLED, - EVENT_TYPE_WORKFLOW_TRIGGER_TYPES, - NEW_EVENT, - OnAfterCalVideoGuestsNoShowTriggerDto, - OnAfterCalVideoHostsNoShowTriggerDto, - OnAfterEventTriggerDto, - OnBeforeEventTriggerDto, - OnCancelTriggerDto, - OnCreationTriggerDto, - OnNoShowUpdateTriggerDto, - OnPaidTriggerDto, - OnPaymentInitiatedTriggerDto, - OnRejectedTriggerDto, - OnRequestedTriggerDto, - OnRescheduleTriggerDto, - RESCHEDULE_EVENT, -} from "./workflow-trigger.input"; - -export class WorkflowActivationDto { - @ApiProperty({ - description: "Whether the workflow is active for all the event-types", - example: false, - type: Boolean, - }) - @IsBoolean() - isActiveOnAllEventTypes = false; - - @ApiPropertyOptional({ - description: - "List of event-types IDs the workflow applies to, required if isActiveOnAllEventTypes is false", - example: [698191], - type: [Number], - }) - @ValidateIf((o) => !o.isActiveOnAllEventTypes) - @IsOptional() - @IsNumber({}, { each: true }) - activeOnEventTypeIds: number[] = []; -} - -@ApiExtraModels( - OnBeforeEventTriggerDto, - OnAfterEventTriggerDto, - OnCancelTriggerDto, - OnCreationTriggerDto, - OnRescheduleTriggerDto, - OnNoShowUpdateTriggerDto, - OnRejectedTriggerDto, - OnRequestedTriggerDto, - OnPaymentInitiatedTriggerDto, - OnPaidTriggerDto, - OnAfterCalVideoGuestsNoShowTriggerDto, - OnAfterCalVideoHostsNoShowTriggerDto, - WorkflowEmailAddressStepDto, - WorkflowEmailAttendeeStepDto, - WorkflowEmailHostStepDto, - WorkflowPhoneWhatsAppAttendeeStepDto, - WorkflowPhoneWhatsAppNumberStepDto, - WorkflowPhoneNumberStepDto, - WorkflowPhoneAttendeeStepDto, - EventTypeWorkflowTriggerDto, - WorkflowActivationDto -) -export class CreateEventTypeWorkflowDto { - @ApiProperty({ description: "Name of the workflow", example: "Platform Test Workflow" }) - @IsString() - name!: string; - - @ApiProperty({ - description: "Activation settings for the workflow", - type: WorkflowActivationDto, - }) - @ValidateNested() - @Type(() => WorkflowActivationDto) - activation!: WorkflowActivationDto; - - @ApiProperty({ - description: `Trigger configuration for the event-type workflow, allowed triggers are ${EVENT_TYPE_WORKFLOW_TRIGGER_TYPES.toString()}`, - oneOf: [ - { $ref: getSchemaPath(OnBeforeEventTriggerDto) }, - { $ref: getSchemaPath(OnAfterEventTriggerDto) }, - { $ref: getSchemaPath(OnCancelTriggerDto) }, - { $ref: getSchemaPath(OnCreationTriggerDto) }, - { $ref: getSchemaPath(OnRescheduleTriggerDto) }, - { $ref: getSchemaPath(OnAfterCalVideoGuestsNoShowTriggerDto) }, - { $ref: getSchemaPath(OnAfterCalVideoHostsNoShowTriggerDto) }, - { $ref: getSchemaPath(OnRejectedTriggerDto) }, - { $ref: getSchemaPath(OnRequestedTriggerDto) }, - { $ref: getSchemaPath(OnPaidTriggerDto) }, - { $ref: getSchemaPath(OnPaymentInitiatedTriggerDto) }, - { $ref: getSchemaPath(OnNoShowUpdateTriggerDto) }, - ], - }) - @ValidateNested() - @Type(() => EventTypeWorkflowTriggerDto, { - keepDiscriminatorProperty: true, - discriminator: { - property: "type", - subTypes: [ - { value: OnBeforeEventTriggerDto, name: BEFORE_EVENT }, - { value: OnAfterEventTriggerDto, name: AFTER_EVENT }, - { value: OnCancelTriggerDto, name: EVENT_CANCELLED }, - { value: OnCreationTriggerDto, name: NEW_EVENT }, - { value: OnRescheduleTriggerDto, name: RESCHEDULE_EVENT }, - { value: OnAfterCalVideoGuestsNoShowTriggerDto, name: AFTER_GUESTS_CAL_VIDEO_NO_SHOW }, - { value: OnAfterCalVideoHostsNoShowTriggerDto, name: AFTER_HOSTS_CAL_VIDEO_NO_SHOW }, - { value: OnRequestedTriggerDto, name: BOOKING_REQUESTED }, - { value: OnRejectedTriggerDto, name: BOOKING_REJECTED }, - { value: OnPaymentInitiatedTriggerDto, name: BOOKING_PAYMENT_INITIATED }, - { value: OnPaidTriggerDto, name: BOOKING_PAID }, - { value: OnNoShowUpdateTriggerDto, name: BOOKING_NO_SHOW_UPDATED }, - ], - }, - }) - trigger!: - | OnAfterEventTriggerDto - | OnBeforeEventTriggerDto - | OnCreationTriggerDto - | OnRescheduleTriggerDto - | OnCancelTriggerDto - | OnRejectedTriggerDto - | OnRequestedTriggerDto - | OnPaidTriggerDto - | OnPaymentInitiatedTriggerDto - | OnNoShowUpdateTriggerDto - | OnAfterCalVideoGuestsNoShowTriggerDto; - - @ApiProperty({ - description: `Steps to execute as part of the event-type workflow, allowed steps are ${STEP_ACTIONS.toString()}`, - oneOf: [ - { $ref: getSchemaPath(WorkflowEmailAddressStepDto) }, - { $ref: getSchemaPath(WorkflowEmailAttendeeStepDto) }, - { $ref: getSchemaPath(WorkflowEmailHostStepDto) }, - { $ref: getSchemaPath(WorkflowPhoneWhatsAppAttendeeStepDto) }, - { $ref: getSchemaPath(WorkflowPhoneWhatsAppNumberStepDto) }, - { $ref: getSchemaPath(WorkflowPhoneNumberStepDto) }, - { $ref: getSchemaPath(WorkflowPhoneAttendeeStepDto) }, - ], - type: "array", - }) - @ValidateNested({ each: true }) - @ArrayMinSize(1, { - message: `Your workflow must contain at least one allowed step. allowed steps are ${STEP_ACTIONS.toString()}`, - }) - @Type(() => BaseWorkflowStepDto, { - keepDiscriminatorProperty: true, - discriminator: { - property: "action", - subTypes: [ - { value: WorkflowEmailAddressStepDto, name: EMAIL_ADDRESS }, - { value: WorkflowEmailAttendeeStepDto, name: EMAIL_ATTENDEE }, - { value: WorkflowEmailHostStepDto, name: EMAIL_HOST }, - { value: WorkflowPhoneWhatsAppAttendeeStepDto, name: WHATSAPP_ATTENDEE }, - { value: WorkflowPhoneWhatsAppNumberStepDto, name: WHATSAPP_NUMBER }, - { value: WorkflowPhoneNumberStepDto, name: SMS_NUMBER }, - { value: WorkflowPhoneAttendeeStepDto, name: SMS_ATTENDEE }, - ], - }, - }) - steps!: ( - | WorkflowEmailAddressStepDto - | WorkflowEmailAttendeeStepDto - | WorkflowEmailHostStepDto - | WorkflowPhoneWhatsAppAttendeeStepDto - | WorkflowPhoneWhatsAppNumberStepDto - | WorkflowPhoneNumberStepDto - | WorkflowPhoneAttendeeStepDto - )[]; -} diff --git a/apps/api/v2/src/modules/workflows/inputs/create-form-workflow.ts b/apps/api/v2/src/modules/workflows/inputs/create-form-workflow.ts deleted file mode 100644 index 75876f620f7eef..00000000000000 --- a/apps/api/v2/src/modules/workflows/inputs/create-form-workflow.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsBoolean, ArrayMinSize, IsOptional, IsString, ValidateNested, ValidateIf } from "class-validator"; - -import { - BaseFormWorkflowStepDto, - EMAIL_ADDRESS, - EMAIL_ATTENDEE, - FORM_ALLOWED_STEP_ACTIONS, - SMS_NUMBER, - WorkflowEmailAddressStepDto, - WorkflowEmailAttendeeStepDto, - WorkflowPhoneNumberStepDto, - WorkflowPhoneAttendeeStepDto, - SMS_ATTENDEE, -} from "./workflow-step.input"; -import { - RoutingFormWorkflowTriggerDto, - FORM_SUBMITTED, - FORM_SUBMITTED_NO_EVENT, - FORM_WORKFLOW_TRIGGER_TYPES, - OnFormSubmittedNoEventTriggerDto, - OnFormSubmittedTriggerDto, -} from "./workflow-trigger.input"; - -export class WorkflowFormActivationDto { - @ApiProperty({ - description: "Whether the workflow is active for all the routing forms", - example: false, - type: Boolean, - }) - @IsBoolean() - isActiveOnAllRoutingForms = false; - - @ApiPropertyOptional({ - description: "List of routing form IDs the workflow applies to", - example: ["abd1-123edf-a213d-123dfwf"], - type: [Number], - }) - @ValidateIf((o) => !o.isActiveOnAllEventTypes) - @IsOptional() - @IsString({ each: true }) - activeOnRoutingFormIds: string[] = []; -} - -@ApiExtraModels( - OnFormSubmittedTriggerDto, - OnFormSubmittedNoEventTriggerDto, - WorkflowEmailAddressStepDto, - WorkflowEmailAttendeeStepDto, - RoutingFormWorkflowTriggerDto, - WorkflowFormActivationDto -) -export class CreateFormWorkflowDto { - @ApiProperty({ description: "Name of the workflow", example: "Platform Test Workflow" }) - @IsString() - name!: string; - - @ApiProperty({ - description: "Activation settings for the workflow", - type: WorkflowFormActivationDto, - }) - @ValidateNested() - @Type(() => WorkflowFormActivationDto) - activation!: WorkflowFormActivationDto; - - @ApiProperty({ - description: `Trigger configuration for the routing-form workflow, allowed triggers are ${FORM_WORKFLOW_TRIGGER_TYPES.toString()}`, - oneOf: [ - { $ref: getSchemaPath(OnFormSubmittedTriggerDto) }, - { $ref: getSchemaPath(OnFormSubmittedNoEventTriggerDto) }, - ], - }) - @ValidateNested() - @Type(() => RoutingFormWorkflowTriggerDto, { - keepDiscriminatorProperty: true, - discriminator: { - property: "type", - subTypes: [ - { value: OnFormSubmittedTriggerDto, name: FORM_SUBMITTED }, - { value: OnFormSubmittedNoEventTriggerDto, name: FORM_SUBMITTED_NO_EVENT }, - ], - }, - }) - trigger!: OnFormSubmittedTriggerDto | OnFormSubmittedNoEventTriggerDto; - - @ApiProperty({ - description: `Steps to execute as part of the routing-form workflow, allowed steps are ${FORM_ALLOWED_STEP_ACTIONS.toString()}`, - oneOf: [ - { $ref: getSchemaPath(WorkflowEmailAddressStepDto) }, - { $ref: getSchemaPath(WorkflowEmailAttendeeStepDto) }, - { $ref: getSchemaPath(WorkflowPhoneAttendeeStepDto) }, - { $ref: getSchemaPath(WorkflowPhoneNumberStepDto) }, - ], - type: "array", - }) - @ValidateNested({ each: true }) - @ArrayMinSize(1, { - message: `Your workflow must contain at least one allowed step. allowed steps are ${FORM_ALLOWED_STEP_ACTIONS.toString()}`, - }) - @Type(() => BaseFormWorkflowStepDto, { - keepDiscriminatorProperty: true, - discriminator: { - property: "action", - subTypes: [ - { value: WorkflowEmailAddressStepDto, name: EMAIL_ADDRESS }, - { value: WorkflowEmailAttendeeStepDto, name: EMAIL_ATTENDEE }, - { value: WorkflowPhoneAttendeeStepDto, name: SMS_ATTENDEE }, - { value: WorkflowPhoneNumberStepDto, name: SMS_NUMBER }, - ], - }, - }) - steps!: ( - | WorkflowEmailAddressStepDto - | WorkflowEmailAttendeeStepDto - | WorkflowPhoneAttendeeStepDto - | WorkflowPhoneNumberStepDto - )[]; -} diff --git a/apps/api/v2/src/modules/workflows/inputs/update-event-type-workflow.input.ts b/apps/api/v2/src/modules/workflows/inputs/update-event-type-workflow.input.ts deleted file mode 100644 index bb3ae3eafb134c..00000000000000 --- a/apps/api/v2/src/modules/workflows/inputs/update-event-type-workflow.input.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsString, IsOptional, ValidateNested, ArrayMinSize } from "class-validator"; - -import { WorkflowActivationDto } from "./create-event-type-workflow.input"; -import { - BaseWorkflowStepDto, - EMAIL_ADDRESS, - EMAIL_ATTENDEE, - EMAIL_HOST, - WHATSAPP_ATTENDEE, - WHATSAPP_NUMBER, - SMS_NUMBER, - SMS_ATTENDEE, - UpdateEmailAddressWorkflowStepDto, - UpdateEmailAttendeeWorkflowStepDto, - UpdateEmailHostWorkflowStepDto, - UpdatePhoneAttendeeWorkflowStepDto, - UpdatePhoneNumberWorkflowStepDto, - UpdatePhoneWhatsAppNumberWorkflowStepDto, - UpdateWhatsAppAttendeePhoneWorkflowStepDto, - STEP_ACTIONS, -} from "./workflow-step.input"; -import { - EventTypeWorkflowTriggerDto, - OnBeforeEventTriggerDto, - BEFORE_EVENT, - OnAfterEventTriggerDto, - AFTER_EVENT, - OnCancelTriggerDto, - EVENT_CANCELLED, - OnCreationTriggerDto, - NEW_EVENT, - OnRescheduleTriggerDto, - RESCHEDULE_EVENT, - OnAfterCalVideoGuestsNoShowTriggerDto, - AFTER_GUESTS_CAL_VIDEO_NO_SHOW, - OnAfterCalVideoHostsNoShowTriggerDto, - AFTER_HOSTS_CAL_VIDEO_NO_SHOW, - OnNoShowUpdateTriggerDto, - OnRejectedTriggerDto, - OnRequestedTriggerDto, - OnPaymentInitiatedTriggerDto, - OnPaidTriggerDto, - BOOKING_REQUESTED, - BOOKING_REJECTED, - BOOKING_PAYMENT_INITIATED, - BOOKING_PAID, - BOOKING_NO_SHOW_UPDATED, - EVENT_TYPE_WORKFLOW_TRIGGER_TYPES, -} from "./workflow-trigger.input"; - -@ApiExtraModels( - OnBeforeEventTriggerDto, - OnAfterEventTriggerDto, - OnCancelTriggerDto, - OnCreationTriggerDto, - OnRescheduleTriggerDto, - OnNoShowUpdateTriggerDto, - OnRejectedTriggerDto, - OnRequestedTriggerDto, - OnPaymentInitiatedTriggerDto, - OnPaidTriggerDto, - OnAfterCalVideoGuestsNoShowTriggerDto, - OnAfterCalVideoHostsNoShowTriggerDto, - UpdateEmailAddressWorkflowStepDto, - UpdateEmailAttendeeWorkflowStepDto, - UpdateEmailHostWorkflowStepDto, - UpdatePhoneAttendeeWorkflowStepDto, - UpdatePhoneWhatsAppNumberWorkflowStepDto, - UpdateWhatsAppAttendeePhoneWorkflowStepDto, - UpdatePhoneNumberWorkflowStepDto, - EventTypeWorkflowTriggerDto, - WorkflowActivationDto -) -export class UpdateEventTypeWorkflowDto { - @ApiPropertyOptional({ description: "Name of the workflow", example: "Platform Test Workflow" }) - @IsString() - @IsOptional() - name?: string; - - @ApiProperty({ - description: "Activation settings for the workflow", - type: WorkflowActivationDto, - }) - @ValidateNested() - @Type(() => WorkflowActivationDto) - @IsOptional() - activation?: WorkflowActivationDto; - - @ApiPropertyOptional({ - description: `Trigger configuration for the event-type workflow, allowed triggers are ${EVENT_TYPE_WORKFLOW_TRIGGER_TYPES.toString()}`, - oneOf: [ - { $ref: getSchemaPath(OnBeforeEventTriggerDto) }, - { $ref: getSchemaPath(OnAfterEventTriggerDto) }, - { $ref: getSchemaPath(OnCancelTriggerDto) }, - { $ref: getSchemaPath(OnCreationTriggerDto) }, - { $ref: getSchemaPath(OnRescheduleTriggerDto) }, - { $ref: getSchemaPath(OnAfterCalVideoGuestsNoShowTriggerDto) }, - { $ref: getSchemaPath(OnAfterCalVideoHostsNoShowTriggerDto) }, - { $ref: getSchemaPath(OnRejectedTriggerDto) }, - { $ref: getSchemaPath(OnRequestedTriggerDto) }, - { $ref: getSchemaPath(OnPaidTriggerDto) }, - { $ref: getSchemaPath(OnPaymentInitiatedTriggerDto) }, - { $ref: getSchemaPath(OnNoShowUpdateTriggerDto) }, - ], - }) - @IsOptional() - @ValidateNested() - @Type(() => EventTypeWorkflowTriggerDto, { - keepDiscriminatorProperty: true, - discriminator: { - property: "type", - subTypes: [ - { value: OnBeforeEventTriggerDto, name: BEFORE_EVENT }, - { value: OnAfterEventTriggerDto, name: AFTER_EVENT }, - { value: OnCancelTriggerDto, name: EVENT_CANCELLED }, - { value: OnCreationTriggerDto, name: NEW_EVENT }, - { value: OnRescheduleTriggerDto, name: RESCHEDULE_EVENT }, - { value: OnAfterCalVideoGuestsNoShowTriggerDto, name: AFTER_GUESTS_CAL_VIDEO_NO_SHOW }, - { value: OnAfterCalVideoHostsNoShowTriggerDto, name: AFTER_HOSTS_CAL_VIDEO_NO_SHOW }, - { value: OnRequestedTriggerDto, name: BOOKING_REQUESTED }, - { value: OnRejectedTriggerDto, name: BOOKING_REJECTED }, - { value: OnPaymentInitiatedTriggerDto, name: BOOKING_PAYMENT_INITIATED }, - { value: OnPaidTriggerDto, name: BOOKING_PAID }, - { value: OnNoShowUpdateTriggerDto, name: BOOKING_NO_SHOW_UPDATED }, - ], - }, - }) - trigger?: - | OnAfterEventTriggerDto - | OnBeforeEventTriggerDto - | OnCreationTriggerDto - | OnRescheduleTriggerDto - | OnCancelTriggerDto - | OnRejectedTriggerDto - | OnRequestedTriggerDto - | OnPaidTriggerDto - | OnPaymentInitiatedTriggerDto - | OnNoShowUpdateTriggerDto - | OnAfterCalVideoGuestsNoShowTriggerDto - | OnAfterCalVideoHostsNoShowTriggerDto; - - @ApiPropertyOptional({ - description: `Steps to execute as part of the event-type workflow, allowed steps are ${STEP_ACTIONS.toString()}`, - oneOf: [ - { $ref: getSchemaPath(UpdateEmailAddressWorkflowStepDto) }, - { $ref: getSchemaPath(UpdateEmailAttendeeWorkflowStepDto) }, - { $ref: getSchemaPath(UpdateEmailHostWorkflowStepDto) }, - { $ref: getSchemaPath(UpdatePhoneAttendeeWorkflowStepDto) }, - { $ref: getSchemaPath(UpdatePhoneWhatsAppNumberWorkflowStepDto) }, - { $ref: getSchemaPath(UpdateWhatsAppAttendeePhoneWorkflowStepDto) }, - { $ref: getSchemaPath(UpdatePhoneNumberWorkflowStepDto) }, - ], - type: "array", - }) - @ValidateNested({ each: true }) - @ArrayMinSize(1, { - message: `Your workflow must contain at least one allowed step. allowed steps are ${STEP_ACTIONS.toString()}`, - }) - @IsOptional() - @Type(() => BaseWorkflowStepDto, { - keepDiscriminatorProperty: true, - discriminator: { - property: "action", - subTypes: [ - { value: UpdateEmailAddressWorkflowStepDto, name: EMAIL_ADDRESS }, - { value: UpdateEmailAttendeeWorkflowStepDto, name: EMAIL_ATTENDEE }, - { value: UpdateEmailHostWorkflowStepDto, name: EMAIL_HOST }, - { value: UpdateWhatsAppAttendeePhoneWorkflowStepDto, name: WHATSAPP_ATTENDEE }, - { value: UpdatePhoneWhatsAppNumberWorkflowStepDto, name: WHATSAPP_NUMBER }, - { value: UpdatePhoneNumberWorkflowStepDto, name: SMS_NUMBER }, - { value: UpdatePhoneAttendeeWorkflowStepDto, name: SMS_ATTENDEE }, - ], - }, - }) - steps?: ( - | UpdateEmailAddressWorkflowStepDto - | UpdateEmailAttendeeWorkflowStepDto - | UpdateEmailHostWorkflowStepDto - | UpdatePhoneAttendeeWorkflowStepDto - | UpdatePhoneWhatsAppNumberWorkflowStepDto - | UpdateWhatsAppAttendeePhoneWorkflowStepDto - | UpdatePhoneNumberWorkflowStepDto - )[]; -} diff --git a/apps/api/v2/src/modules/workflows/inputs/update-form-workflow.input.ts b/apps/api/v2/src/modules/workflows/inputs/update-form-workflow.input.ts deleted file mode 100644 index 30c4b4d7154e12..00000000000000 --- a/apps/api/v2/src/modules/workflows/inputs/update-form-workflow.input.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { WorkflowFormActivationDto } from "@/modules/workflows/inputs/create-form-workflow"; -import { ApiExtraModels, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsString, IsOptional, ValidateNested, ArrayMinSize } from "class-validator"; - -import { - BaseFormWorkflowStepDto, - EMAIL_ADDRESS, - EMAIL_ATTENDEE, - FORM_ALLOWED_STEP_ACTIONS, - SMS_ATTENDEE, - SMS_NUMBER, - UpdateEmailAddressWorkflowStepDto, - UpdateEmailAttendeeWorkflowStepDto, - UpdateEmailHostWorkflowStepDto, - UpdatePhoneAttendeeWorkflowStepDto, - UpdatePhoneNumberWorkflowStepDto, - UpdatePhoneWhatsAppNumberWorkflowStepDto, - UpdateWhatsAppAttendeePhoneWorkflowStepDto, -} from "./workflow-step.input"; -import { - OnFormSubmittedTriggerDto, - OnFormSubmittedNoEventTriggerDto, - FORM_SUBMITTED, - FORM_SUBMITTED_NO_EVENT, - FORM_WORKFLOW_TRIGGER_TYPES, - RoutingFormWorkflowTriggerDto, -} from "./workflow-trigger.input"; - -@ApiExtraModels( - OnFormSubmittedTriggerDto, - OnFormSubmittedNoEventTriggerDto, - UpdateEmailAddressWorkflowStepDto, - UpdateEmailAttendeeWorkflowStepDto, - UpdateEmailHostWorkflowStepDto, - UpdatePhoneAttendeeWorkflowStepDto, - UpdatePhoneWhatsAppNumberWorkflowStepDto, - UpdateWhatsAppAttendeePhoneWorkflowStepDto, - UpdatePhoneNumberWorkflowStepDto, - RoutingFormWorkflowTriggerDto, - WorkflowFormActivationDto -) -export class UpdateFormWorkflowDto { - @ApiPropertyOptional({ description: "Name of the workflow", example: "Rounting-form Test Workflow" }) - @IsString() - @IsOptional() - name?: string; - - @ValidateNested() - @Type(() => WorkflowFormActivationDto) - @IsOptional() - activation?: WorkflowFormActivationDto; - - @ApiPropertyOptional({ - description: `Trigger configuration for the routing-form workflow, allowed triggers are ${FORM_WORKFLOW_TRIGGER_TYPES}`, - oneOf: [ - { $ref: getSchemaPath(OnFormSubmittedTriggerDto) }, - { $ref: getSchemaPath(OnFormSubmittedNoEventTriggerDto) }, - ], - }) - @IsOptional() - @ValidateNested() - @Type(() => RoutingFormWorkflowTriggerDto, { - keepDiscriminatorProperty: true, - discriminator: { - property: "type", - subTypes: [ - { value: OnFormSubmittedTriggerDto, name: FORM_SUBMITTED }, - { value: OnFormSubmittedNoEventTriggerDto, name: FORM_SUBMITTED_NO_EVENT }, - ], - }, - }) - trigger?: OnFormSubmittedTriggerDto | OnFormSubmittedNoEventTriggerDto; - - @ApiPropertyOptional({ - description: `Steps to execute as part of the routing-form workflow, allowed steps are ${FORM_ALLOWED_STEP_ACTIONS.toString()}`, - oneOf: [ - { $ref: getSchemaPath(UpdateEmailAddressWorkflowStepDto) }, - { $ref: getSchemaPath(UpdateEmailAttendeeWorkflowStepDto) }, - { $ref: getSchemaPath(UpdatePhoneAttendeeWorkflowStepDto) }, - { $ref: getSchemaPath(UpdatePhoneNumberWorkflowStepDto) }, - ], - type: "array", - }) - @ValidateNested({ each: true }) - @ArrayMinSize(1, { - message: `Your workflow must contain at least one allowed step. allowed steps are ${FORM_ALLOWED_STEP_ACTIONS.toString()}`, - }) - @IsOptional() - @Type(() => BaseFormWorkflowStepDto, { - keepDiscriminatorProperty: true, - discriminator: { - property: "action", - subTypes: [ - { value: UpdateEmailAddressWorkflowStepDto, name: EMAIL_ADDRESS }, - { value: UpdateEmailAttendeeWorkflowStepDto, name: EMAIL_ATTENDEE }, - { value: UpdatePhoneAttendeeWorkflowStepDto, name: SMS_ATTENDEE }, - { value: UpdatePhoneNumberWorkflowStepDto, name: SMS_NUMBER }, - ], - }, - }) - steps?: ( - | UpdateEmailAddressWorkflowStepDto - | UpdateEmailAttendeeWorkflowStepDto - | UpdatePhoneNumberWorkflowStepDto - | UpdatePhoneAttendeeWorkflowStepDto - )[]; -} diff --git a/apps/api/v2/src/modules/workflows/inputs/workflow-step.input.ts b/apps/api/v2/src/modules/workflows/inputs/workflow-step.input.ts deleted file mode 100644 index d22702d92390ef..00000000000000 --- a/apps/api/v2/src/modules/workflows/inputs/workflow-step.input.ts +++ /dev/null @@ -1,407 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsNumber, IsBoolean, IsString, ValidateNested, IsIn, IsOptional } from "class-validator"; - -import { WorkflowActions, WorkflowTemplates } from "@calcom/platform-libraries"; - -export const EMAIL_HOST = "email_host"; -export const EMAIL_ATTENDEE = "email_attendee"; -export const EMAIL_ADDRESS = "email_address"; -export const SMS_ATTENDEE = "sms_attendee"; -export const SMS_NUMBER = "sms_number"; -export const WHATSAPP_ATTENDEE = "whatsapp_attendee"; -export const WHATSAPP_NUMBER = "whatsapp_number"; -export const CAL_AI_PHONE_CALL = "cal_ai_phone_call"; - -export const STEP_ACTIONS = [ - EMAIL_HOST, - EMAIL_ATTENDEE, - EMAIL_ADDRESS, - SMS_ATTENDEE, - SMS_NUMBER, - WHATSAPP_ATTENDEE, - WHATSAPP_NUMBER, - CAL_AI_PHONE_CALL, -] as const; - -export const FORM_ALLOWED_STEP_ACTIONS = [EMAIL_ATTENDEE, EMAIL_ADDRESS, SMS_ATTENDEE, SMS_NUMBER] as const; - -export const STEP_ACTIONS_TO_ENUM = { - [EMAIL_HOST]: WorkflowActions.EMAIL_HOST, - [EMAIL_ATTENDEE]: WorkflowActions.EMAIL_ATTENDEE, - [EMAIL_ADDRESS]: WorkflowActions.EMAIL_ADDRESS, - [SMS_ATTENDEE]: WorkflowActions.SMS_ATTENDEE, - [WHATSAPP_ATTENDEE]: WorkflowActions.WHATSAPP_ATTENDEE, - [WHATSAPP_NUMBER]: WorkflowActions.WHATSAPP_NUMBER, - [SMS_NUMBER]: WorkflowActions.SMS_NUMBER, - [CAL_AI_PHONE_CALL]: WorkflowActions.CAL_AI_PHONE_CALL, -} as const; - -export const ENUM_TO_STEP_ACTIONS = { - [WorkflowActions.EMAIL_HOST]: EMAIL_HOST, - [WorkflowActions.EMAIL_ATTENDEE]: EMAIL_ATTENDEE, - [WorkflowActions.EMAIL_ADDRESS]: EMAIL_ADDRESS, - [WorkflowActions.SMS_ATTENDEE]: SMS_ATTENDEE, - [WorkflowActions.WHATSAPP_ATTENDEE]: WHATSAPP_ATTENDEE, - [WorkflowActions.WHATSAPP_NUMBER]: WHATSAPP_NUMBER, - [WorkflowActions.SMS_NUMBER]: SMS_NUMBER, - [WorkflowActions.CAL_AI_PHONE_CALL]: CAL_AI_PHONE_CALL, -} as const; - -export type StepAction = (typeof STEP_ACTIONS)[number]; -export type FormAllowedStepAction = (typeof FORM_ALLOWED_STEP_ACTIONS)[number]; - -export const REMINDER = "reminder"; -export const CUSTOM = "custom"; -export const CANCELLED = "cancelled"; -export const RESCHEDULED = "rescheduled"; -export const COMPLETED = "completed"; -export const RATING = "rating"; -export const TEMPLATES = [REMINDER, CUSTOM, RESCHEDULED, COMPLETED, RATING, CANCELLED] as const; -export const TEMPLATES_TO_ENUM = { - [WorkflowTemplates.REMINDER]: REMINDER, - [CUSTOM]: WorkflowTemplates.CUSTOM, - [RESCHEDULED]: WorkflowTemplates.RESCHEDULED, - [CANCELLED]: WorkflowTemplates.CANCELLED, - [COMPLETED]: WorkflowTemplates.COMPLETED, - [RATING]: WorkflowTemplates.RATING, -} as const; - -export const ENUM_TO_TEMPLATES = { - [WorkflowTemplates.REMINDER]: REMINDER, - [WorkflowTemplates.CUSTOM]: CUSTOM, - [WorkflowTemplates.RESCHEDULED]: RESCHEDULED, - [WorkflowTemplates.CANCELLED]: CANCELLED, - [WorkflowTemplates.COMPLETED]: COMPLETED, - [WorkflowTemplates.RATING]: RATING, -} as const; - -export type TemplateType = (typeof TEMPLATES)[number]; - -export const HOST = "const"; -export const ATTENDEE = "attendee"; -export const EMAIL = "email"; -export const PHONE_NUMBER = "phone_number"; - -export const RECIPIENT_TYPES = [HOST, ATTENDEE, EMAIL, PHONE_NUMBER]; -export type RecipientType = (typeof RECIPIENT_TYPES)[number]; - -export class BaseWorkflowMessageDto { - @ApiProperty({ - description: "Subject of the message", - example: "Reminder: Your Meeting {EVENT_NAME} - {EVENT_DATE_ddd, MMM D, YYYY h:mma} with Cal.com", - }) - @IsString() - subject!: string; -} - -export class HtmlWorkflowMessageDto extends BaseWorkflowMessageDto { - @ApiProperty({ - description: "HTML content of the message (used for Emails)", - example: - "

This is a reminder from {ORGANIZER} of {EVENT_NAME} to {ATTENDEE} starting here {LOCATION} {MEETING_URL} at {START_TIME_h:mma} {TIMEZONE}.

", - }) - @IsString() - html!: string; -} - -export class TextWorkflowMessageDto extends BaseWorkflowMessageDto { - @ApiProperty({ - description: "Text content of the message (used for SMS)", - example: - "This is a reminder message from {ORGANIZER} of {EVENT_NAME} to {ATTENDEE} starting here {LOCATION} {MEETING_URL} at {START_TIME_h:mma} {TIMEZONE}.", - }) - @IsString() - text!: string; -} - -export class BaseWorkflowStepDto { - @ApiProperty({ description: "Action to perform", example: EMAIL_HOST, enum: STEP_ACTIONS }) - @IsString() - @IsIn(STEP_ACTIONS) - action!: StepAction; - - @ApiProperty({ description: "Step number in the workflow sequence", example: 1 }) - @IsNumber() - stepNumber!: number; - - @ApiProperty({ description: "Recipient type", example: ATTENDEE, enum: RECIPIENT_TYPES }) - recipient!: RecipientType; - - @ApiProperty({ description: "Template type for the step", example: REMINDER, enum: TEMPLATES }) - template!: TemplateType; - - @ApiProperty({ description: "Displayed sender name.", type: String }) - @IsString() - sender!: string; -} - -export class BaseFormWorkflowStepDto extends BaseWorkflowStepDto { - @ApiProperty({ description: "Action to perform", example: EMAIL_HOST, enum: STEP_ACTIONS }) - @IsString() - @IsIn(FORM_ALLOWED_STEP_ACTIONS) - action!: FormAllowedStepAction; -} - -export class WorkflowEmailHostStepDto extends BaseWorkflowStepDto { - @ApiProperty({ - description: "Action to perform, send an email to the host of the event", - example: EMAIL_HOST, - enum: STEP_ACTIONS, - }) - @IsString() - @IsIn(STEP_ACTIONS) - action: typeof EMAIL_HOST = EMAIL_HOST; - - @ApiProperty({ - description: `Whether to include a calendar event in the notification, can be included with actions ${EMAIL_HOST}, ${EMAIL_ATTENDEE}, ${EMAIL_ADDRESS}`, - example: true, - }) - @IsBoolean() - includeCalendarEvent = false; - - @ApiProperty({ description: "Message content for this step", type: HtmlWorkflowMessageDto }) - @ValidateNested() - @Type(() => HtmlWorkflowMessageDto) - message!: HtmlWorkflowMessageDto; -} - -export class WorkflowEmailAddressStepDto extends BaseWorkflowStepDto { - @ApiProperty({ - description: "Action to perform, send an email to a specific email address", - example: EMAIL_ADDRESS, - enum: STEP_ACTIONS, - }) - @IsString() - @IsIn(STEP_ACTIONS) - action: typeof EMAIL_ADDRESS = EMAIL_ADDRESS; - - @ApiProperty({ - description: "Email address if recipient is EMAIL, required for action EMAIL_ADDRESS", - example: "31214", - externalDocs: { - url: "https://cal.com/docs/api-reference/v2/organization-team-verified-resources/verify-an-email-for-an-org-team", - }, - }) - @IsNumber() - verifiedEmailId!: number; - - @ApiProperty({ - description: `Whether to include a calendar event in the notification, can be included with actions ${EMAIL_HOST}, ${EMAIL_ATTENDEE}, ${EMAIL_ADDRESS}`, - example: true, - }) - @IsBoolean() - includeCalendarEvent = false; - - @ApiProperty({ description: "Message content for this step", type: HtmlWorkflowMessageDto }) - @ValidateNested() - @Type(() => HtmlWorkflowMessageDto) - message!: HtmlWorkflowMessageDto; -} - -export class WorkflowEmailAttendeeStepDto extends BaseWorkflowStepDto { - @ApiProperty({ - description: "Action to perform, send an email to the attendees of the event", - example: EMAIL_ATTENDEE, - enum: STEP_ACTIONS, - }) - @IsString() - @IsIn(STEP_ACTIONS) - action: typeof EMAIL_ATTENDEE = EMAIL_ATTENDEE; - - @ApiProperty({ - description: `Whether to include a calendar event in the notification, can be included with actions ${EMAIL_HOST}, ${EMAIL_ATTENDEE}, ${EMAIL_ADDRESS}`, - example: true, - }) - @IsBoolean() - includeCalendarEvent = false; - - @ApiProperty({ description: "Message content for this step", type: HtmlWorkflowMessageDto }) - @ValidateNested() - @Type(() => HtmlWorkflowMessageDto) - message!: HtmlWorkflowMessageDto; -} - -export class WorkflowPhoneWhatsAppNumberStepDto extends BaseWorkflowStepDto { - @ApiProperty({ - description: "Action to perform, send a text message via whatsapp to a specific phone number", - example: WHATSAPP_NUMBER, - }) - @IsString() - @IsIn(STEP_ACTIONS) - action: typeof WHATSAPP_NUMBER = WHATSAPP_NUMBER; - - @ApiProperty({ - description: - "Phone number if recipient is PHONE_NUMBER, required for actions SMS_NUMBER and WHATSAPP_NUMBER", - example: "3243434", - externalDocs: { - url: "https://cal.com/docs/api-reference/v2/organization-team-verified-resources/verify-a-phone-number-for-an-org-team", - }, - }) - @IsNumber() - verifiedPhoneId!: number; - - @ApiProperty({ description: "Message content for this step", type: TextWorkflowMessageDto }) - @ValidateNested() - @Type(() => TextWorkflowMessageDto) - message!: TextWorkflowMessageDto; -} - -export class WorkflowPhoneAttendeeStepDto extends BaseWorkflowStepDto { - @ApiProperty({ - description: "Action to perform, send a text message to the phone numbers of the attendees", - example: SMS_ATTENDEE, - }) - @IsString() - @IsIn(STEP_ACTIONS) - action: typeof SMS_ATTENDEE = SMS_ATTENDEE; - - @ApiProperty({ - description: - "Phone number if recipient is PHONE_NUMBER, required for actions SMS_NUMBER and WHATSAPP_NUMBER", - example: "3243434", - externalDocs: { - url: "https://cal.com/docs/api-reference/v2/organization-team-verified-resources/verify-a-phone-number-for-an-org-team", - }, - }) - @ApiProperty({ description: "Message content for this step", type: TextWorkflowMessageDto }) - @ValidateNested() - @Type(() => TextWorkflowMessageDto) - message!: TextWorkflowMessageDto; - - @ApiPropertyOptional({ - description: "whether or not the attendees are required to provide their phone numbers when booking", - example: true, - default: false, - }) - @IsBoolean() - @IsOptional() - phoneRequired: boolean = false; -} - -export class WorkflowPhoneNumberStepDto extends BaseWorkflowStepDto { - @ApiProperty({ - description: "Action to perform, send a text message to a specific phone number", - example: SMS_NUMBER, - }) - @IsString() - @IsIn(STEP_ACTIONS) - action: typeof SMS_NUMBER = SMS_NUMBER; - - @ApiProperty({ - description: - "Phone number if recipient is PHONE_NUMBER, required for actions SMS_NUMBER and WHATSAPP_NUMBER", - example: "3243434", - externalDocs: { - url: "https://cal.com/docs/api-reference/v2/organization-team-verified-resources/verify-a-phone-number-for-an-org-team", - }, - }) - @IsNumber() - verifiedPhoneId!: number; - - @ApiProperty({ description: "Message content for this step", type: TextWorkflowMessageDto }) - @ValidateNested() - @Type(() => TextWorkflowMessageDto) - message!: TextWorkflowMessageDto; -} - -export class WorkflowPhoneWhatsAppAttendeeStepDto extends BaseWorkflowStepDto { - @ApiProperty({ description: "Action to perform", example: WHATSAPP_ATTENDEE }) - @IsString() - @IsIn(STEP_ACTIONS) - action: typeof WHATSAPP_ATTENDEE = WHATSAPP_ATTENDEE; - - @ApiProperty({ - description: - "Message content for this step, send a text message via whatsapp to the phone numbers of the attendees", - type: TextWorkflowMessageDto, - }) - @ValidateNested() - @Type(() => TextWorkflowMessageDto) - message!: TextWorkflowMessageDto; - - @ApiPropertyOptional({ - description: "whether or not the attendees are required to provide their phone numbers when booking", - example: true, - default: false, - }) - @IsBoolean() - @IsOptional() - phoneRequired: boolean = false; -} - -export type UpdateWorkflowStepDto = - | UpdateEmailAttendeeWorkflowStepDto - | UpdateEmailAddressWorkflowStepDto - | UpdateEmailHostWorkflowStepDto - | UpdateWhatsAppAttendeePhoneWorkflowStepDto - | UpdatePhoneWhatsAppNumberWorkflowStepDto - | UpdatePhoneAttendeeWorkflowStepDto - | UpdatePhoneNumberWorkflowStepDto; -export class UpdateEmailAttendeeWorkflowStepDto extends WorkflowEmailAttendeeStepDto { - @ApiProperty({ - description: - "Unique identifier of the step you want to update, if adding a new step do not provide this id", - example: 67244, - }) - @IsNumber() - id?: number; -} - -export class UpdateEmailAddressWorkflowStepDto extends WorkflowEmailAddressStepDto { - @ApiProperty({ - description: - "Unique identifier of the step you want to update, if adding a new step do not provide this id", - example: 67244, - }) - @IsNumber() - id?: number; -} - -export class UpdateEmailHostWorkflowStepDto extends WorkflowEmailHostStepDto { - @ApiProperty({ - description: - "Unique identifier of the step you want to update, if adding a new step do not provide this id", - example: 67244, - }) - @IsNumber() - id?: number; -} - -export class UpdatePhoneWhatsAppNumberWorkflowStepDto extends WorkflowPhoneWhatsAppNumberStepDto { - @ApiProperty({ - description: - "Unique identifier of the step you want to update, if adding a new step do not provide this id", - example: 67244, - }) - @IsNumber() - id?: number; -} -export class UpdatePhoneAttendeeWorkflowStepDto extends WorkflowPhoneAttendeeStepDto { - @ApiProperty({ - description: - "Unique identifier of the step you want to update, if adding a new step do not provide this id", - example: 67244, - }) - @IsNumber() - id?: number; -} -export class UpdatePhoneNumberWorkflowStepDto extends WorkflowPhoneNumberStepDto { - @ApiProperty({ - description: - "Unique identifier of the step you want to update, if adding a new step do not provide this id", - example: 67244, - }) - @IsNumber() - id?: number; -} -export class UpdateWhatsAppAttendeePhoneWorkflowStepDto extends WorkflowPhoneWhatsAppAttendeeStepDto { - @ApiProperty({ - description: - "Unique identifier of the step you want to update, if adding a new step do not provide this id", - example: 67244, - }) - @IsNumber() - id?: number; -} diff --git a/apps/api/v2/src/modules/workflows/inputs/workflow-trigger.input.ts b/apps/api/v2/src/modules/workflows/inputs/workflow-trigger.input.ts deleted file mode 100644 index 5c1b970641cb81..00000000000000 --- a/apps/api/v2/src/modules/workflows/inputs/workflow-trigger.input.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsIn, IsNumber, IsString, ValidateNested } from "class-validator"; - -import { TimeUnit, WorkflowTriggerEvents } from "@calcom/platform-libraries"; - -export const BEFORE_EVENT = "beforeEvent"; -export const EVENT_CANCELLED = "eventCancelled"; -export const NEW_EVENT = "newEvent"; -export const AFTER_EVENT = "afterEvent"; -export const RESCHEDULE_EVENT = "rescheduleEvent"; -export const AFTER_HOSTS_CAL_VIDEO_NO_SHOW = "afterHostsCalVideoNoShow"; -export const AFTER_GUESTS_CAL_VIDEO_NO_SHOW = "afterGuestsCalVideoNoShow"; -export const FORM_SUBMITTED = "formSubmitted"; -export const FORM_SUBMITTED_NO_EVENT = "formSubmittedNoEvent"; -export const BOOKING_REJECTED = "bookingRejected"; -export const BOOKING_REQUESTED = "bookingRequested"; -export const BOOKING_PAYMENT_INITIATED = "bookingPaymentInitiated"; -export const BOOKING_PAID = "bookingPaid"; -export const BOOKING_NO_SHOW_UPDATED = "bookingNoShowUpdated"; - -export const FORM_WORKFLOW_TRIGGER_TYPES = [FORM_SUBMITTED, FORM_SUBMITTED_NO_EVENT] as const; - -export const EVENT_TYPE_WORKFLOW_TRIGGER_TYPES = [ - BEFORE_EVENT, - EVENT_CANCELLED, - NEW_EVENT, - AFTER_EVENT, - RESCHEDULE_EVENT, - AFTER_HOSTS_CAL_VIDEO_NO_SHOW, - AFTER_GUESTS_CAL_VIDEO_NO_SHOW, - BOOKING_REJECTED, - BOOKING_REQUESTED, - BOOKING_PAYMENT_INITIATED, - BOOKING_PAID, - BOOKING_NO_SHOW_UPDATED, -] as const; - -export const WORKFLOW_TRIGGER_TYPES = [ - BEFORE_EVENT, - EVENT_CANCELLED, - NEW_EVENT, - AFTER_EVENT, - RESCHEDULE_EVENT, - AFTER_HOSTS_CAL_VIDEO_NO_SHOW, - AFTER_GUESTS_CAL_VIDEO_NO_SHOW, - FORM_SUBMITTED, - FORM_SUBMITTED_NO_EVENT, - BOOKING_REJECTED, - BOOKING_REQUESTED, - BOOKING_PAYMENT_INITIATED, - BOOKING_PAID, - BOOKING_NO_SHOW_UPDATED, -] as const; - -export const WORKFLOW_TRIGGER_TO_ENUM = { - [BEFORE_EVENT]: WorkflowTriggerEvents.BEFORE_EVENT, - [EVENT_CANCELLED]: WorkflowTriggerEvents.EVENT_CANCELLED, - [NEW_EVENT]: WorkflowTriggerEvents.NEW_EVENT, - [AFTER_EVENT]: WorkflowTriggerEvents.AFTER_EVENT, - [RESCHEDULE_EVENT]: WorkflowTriggerEvents.RESCHEDULE_EVENT, - [AFTER_HOSTS_CAL_VIDEO_NO_SHOW]: WorkflowTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, - [AFTER_GUESTS_CAL_VIDEO_NO_SHOW]: WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW, - [FORM_SUBMITTED]: WorkflowTriggerEvents.FORM_SUBMITTED, - [FORM_SUBMITTED_NO_EVENT]: WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT, - [BOOKING_REJECTED]: WorkflowTriggerEvents.BOOKING_REJECTED, - [BOOKING_REQUESTED]: WorkflowTriggerEvents.BOOKING_REQUESTED, - [BOOKING_PAYMENT_INITIATED]: WorkflowTriggerEvents.BOOKING_PAYMENT_INITIATED, - [BOOKING_NO_SHOW_UPDATED]: WorkflowTriggerEvents.BOOKING_NO_SHOW_UPDATED, - [BOOKING_PAID]: WorkflowTriggerEvents.BOOKING_PAID, -} as const; - -export const ENUM_ROUTING_FORM_WORFLOW_TRIGGERS = [ - WORKFLOW_TRIGGER_TO_ENUM[FORM_SUBMITTED_NO_EVENT], - WORKFLOW_TRIGGER_TO_ENUM[FORM_SUBMITTED], -]; - -export const ENUM_OFFSET_WORFLOW_TRIGGERS = [ - WORKFLOW_TRIGGER_TO_ENUM[FORM_SUBMITTED_NO_EVENT], - WORKFLOW_TRIGGER_TO_ENUM[BEFORE_EVENT], - WORKFLOW_TRIGGER_TO_ENUM[AFTER_EVENT], - WORKFLOW_TRIGGER_TO_ENUM[AFTER_GUESTS_CAL_VIDEO_NO_SHOW], - WORKFLOW_TRIGGER_TO_ENUM[AFTER_HOSTS_CAL_VIDEO_NO_SHOW], -]; - -export const ENUM_TO_WORKFLOW_TRIGGER = { - [WorkflowTriggerEvents.BEFORE_EVENT]: BEFORE_EVENT, - [WorkflowTriggerEvents.EVENT_CANCELLED]: EVENT_CANCELLED, - [WorkflowTriggerEvents.NEW_EVENT]: NEW_EVENT, - [WorkflowTriggerEvents.AFTER_EVENT]: AFTER_EVENT, - [WorkflowTriggerEvents.RESCHEDULE_EVENT]: RESCHEDULE_EVENT, - [WorkflowTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW]: AFTER_HOSTS_CAL_VIDEO_NO_SHOW, - [WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW]: AFTER_GUESTS_CAL_VIDEO_NO_SHOW, - [WorkflowTriggerEvents.FORM_SUBMITTED]: FORM_SUBMITTED, - [WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT]: FORM_SUBMITTED_NO_EVENT, - [WorkflowTriggerEvents.BOOKING_REJECTED]: BOOKING_REJECTED, - [WorkflowTriggerEvents.BOOKING_REQUESTED]: BOOKING_REQUESTED, - [WorkflowTriggerEvents.BOOKING_PAYMENT_INITIATED]: BOOKING_PAYMENT_INITIATED, - [WorkflowTriggerEvents.BOOKING_PAID]: BOOKING_PAID, - [WorkflowTriggerEvents.BOOKING_NO_SHOW_UPDATED]: BOOKING_NO_SHOW_UPDATED, -} as const; - -export const ENUM_TO_ROUNTING_FORM_WORKFLOW_TRIGGER = { - [WorkflowTriggerEvents.FORM_SUBMITTED]: FORM_SUBMITTED, -} as const; - -export const HOUR = "hour"; -export const MINUTE = "minute"; -export const DAY = "day"; - -export const TIME_UNITS = [HOUR, MINUTE, DAY] as const; - -export type TimeUnitType = (typeof TIME_UNITS)[number]; - -export const TIME_UNIT_TO_ENUM = { - [HOUR]: TimeUnit.HOUR, - [MINUTE]: TimeUnit.MINUTE, - [DAY]: TimeUnit.DAY, -} as const; - -export const ENUM_TO_TIME_UNIT = { - [TimeUnit.HOUR]: HOUR, - [TimeUnit.MINUTE]: MINUTE, - [TimeUnit.DAY]: DAY, -} as const; - -export type WorkflowTriggerType = (typeof WORKFLOW_TRIGGER_TYPES)[number]; -export type WorkflowEventTypeTriggerType = (typeof EVENT_TYPE_WORKFLOW_TRIGGER_TYPES)[number]; -export type WorkflowFormTriggerType = (typeof FORM_WORKFLOW_TRIGGER_TYPES)[number]; - -export class WorkflowTriggerOffsetDto { - @ApiProperty({ description: "Time value for offset before/after event trigger", example: 24, type: Number }) - @IsNumber() - value!: number; - - @ApiProperty({ description: "Unit for the offset time", example: HOUR }) - @IsString() - @IsIn(TIME_UNITS) - unit!: TimeUnitType; -} - -export class EventTypeWorkflowTriggerDto { - @ApiProperty({ - description: "Trigger type for the event-type workflow", - example: "beforeEvent", - }) - @IsString() - @IsIn(EVENT_TYPE_WORKFLOW_TRIGGER_TYPES) - type!: WorkflowEventTypeTriggerType; -} - -export class RoutingFormWorkflowTriggerDto { - @ApiProperty({ - description: "Trigger type for the routing-form workflow", - example: "formSubmitted", - }) - @IsString() - @IsIn(FORM_WORKFLOW_TRIGGER_TYPES) - type!: WorkflowFormTriggerType; -} - -export class OnCreationTriggerDto { - @ApiProperty({ - description: "Trigger type for the workflow", - }) - @IsString() - @IsIn([NEW_EVENT]) - type: typeof NEW_EVENT = NEW_EVENT; -} - -export class OnRescheduleTriggerDto { - @ApiProperty({ - description: "Trigger type for the workflow", - }) - @IsString() - @IsIn([RESCHEDULE_EVENT]) - type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT; -} -export class OnCancelTriggerDto { - @ApiProperty({ - description: "Trigger type for the workflow", - }) - @IsString() - @IsIn([EVENT_CANCELLED]) - type: typeof EVENT_CANCELLED = EVENT_CANCELLED; -} - -export class OnRejectedTriggerDto { - @ApiProperty({ - description: "Trigger type for the workflow", - }) - type: typeof BOOKING_REJECTED = BOOKING_REJECTED; -} - -export class OnRequestedTriggerDto { - @ApiProperty({ - description: "Trigger type for the workflow", - }) - type: typeof BOOKING_REQUESTED = BOOKING_REQUESTED; -} - -export class OnPaymentInitiatedTriggerDto { - @ApiProperty({ - description: "Trigger type for the workflow", - }) - type: typeof BOOKING_PAYMENT_INITIATED = BOOKING_PAYMENT_INITIATED; -} - -export class OnPaidTriggerDto { - @ApiProperty({ - description: "Trigger type for the workflow", - }) - type: typeof BOOKING_PAID = BOOKING_PAID; -} - -export class OnNoShowUpdateTriggerDto { - @ApiProperty({ - description: "Trigger type for the workflow", - }) - type: typeof BOOKING_NO_SHOW_UPDATED = BOOKING_NO_SHOW_UPDATED; -} - -export class TriggerOffsetDTO { - @ApiProperty({ - description: - "Offset before/after the trigger time; required for BEFORE_EVENT, AFTER_EVENT, and FORM_SUBMITTED_NO_EVENT", - type: WorkflowTriggerOffsetDto, - }) - @ValidateNested() - @Type(() => WorkflowTriggerOffsetDto) - offset!: WorkflowTriggerOffsetDto; -} - -export class OnBeforeEventTriggerDto extends TriggerOffsetDTO { - @ApiProperty({ - description: "Trigger type for the workflow", - example: BEFORE_EVENT, - }) - @IsString() - @IsIn([BEFORE_EVENT]) - type: typeof BEFORE_EVENT = BEFORE_EVENT; -} - -export class OnAfterEventTriggerDto extends TriggerOffsetDTO { - @ApiProperty({ - description: "Trigger type for the workflow", - example: AFTER_EVENT, - }) - @IsString() - @IsIn([AFTER_EVENT]) - type: typeof AFTER_EVENT = AFTER_EVENT; -} - -export class OnAfterCalVideoGuestsNoShowTriggerDto extends TriggerOffsetDTO { - @ApiProperty({ - description: "Trigger type for the workflow", - example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW, - }) - @IsString() - @IsIn([AFTER_GUESTS_CAL_VIDEO_NO_SHOW]) - type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW; -} - -export class OnAfterCalVideoHostsNoShowTriggerDto extends TriggerOffsetDTO { - @ApiProperty({ - description: "Trigger type for the workflow", - example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW, - }) - @IsString() - @IsIn([AFTER_HOSTS_CAL_VIDEO_NO_SHOW]) - type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW; -} -export class OnFormSubmittedTriggerDto { - @ApiProperty({ - description: "Trigger type for the workflow", - example: FORM_SUBMITTED, - }) - @IsString() - @IsIn([FORM_SUBMITTED]) - type: typeof FORM_SUBMITTED = FORM_SUBMITTED; -} - -export class OnFormSubmittedNoEventTriggerDto extends TriggerOffsetDTO { - @ApiProperty({ - description: "Trigger type for the workflow", - example: FORM_SUBMITTED_NO_EVENT, - }) - @IsString() - @IsIn([FORM_SUBMITTED_NO_EVENT]) - type: typeof FORM_SUBMITTED_NO_EVENT = FORM_SUBMITTED_NO_EVENT; -} - -export const OffsetTriggerDTOInstances = [ - OnFormSubmittedNoEventTriggerDto, - OnBeforeEventTriggerDto, - OnAfterEventTriggerDto, - OnAfterCalVideoGuestsNoShowTriggerDto, - OnAfterEventTriggerDto, -]; -export type OffsetTriggerDTOInstancesType = InstanceType<(typeof OffsetTriggerDTOInstances)[number]>; diff --git a/apps/api/v2/src/modules/workflows/outputs/base-workflow.output.ts b/apps/api/v2/src/modules/workflows/outputs/base-workflow.output.ts deleted file mode 100644 index 67332defb20339..00000000000000 --- a/apps/api/v2/src/modules/workflows/outputs/base-workflow.output.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { - HOST, - RECIPIENT_TYPES, - RecipientType, - REMINDER, - TEMPLATES, - TemplateType, -} from "@/modules/workflows/inputs/workflow-step.input"; -import { HOUR, TIME_UNITS, TimeUnitType } from "@/modules/workflows/inputs/workflow-trigger.input"; -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsBoolean, IsOptional, ValidateNested } from "class-validator"; - -export class WorkflowMessageOutputDto { - @ApiProperty({ - description: "Subject of the message", - example: "Reminder: Your Meeting {EVENT_NAME} - {EVENT_DATE_ddd, MMM D, YYYY h:mma} with Cal.com", - }) - @Expose() - subject!: string; - - @ApiPropertyOptional({ - description: "HTML content of the message", - example: "

Reminder for {EVENT_NAME}.

", - }) - @Expose() - html?: string; - - @ApiPropertyOptional({ - description: "Text content of the message (used for SMS/WhatsApp)", - example: "Reminder for {EVENT_NAME}.", - }) - @Expose() - text?: string; -} - -export class BaseWorkflowStepOutputDto { - @ApiProperty({ description: "Unique identifier of the step", example: 67244 }) - @Expose() - id!: number; - - @ApiProperty({ description: "Step number in the workflow sequence", example: 1 }) - @Expose() - stepNumber!: number; - - @ApiProperty({ description: "Intended recipient type", example: HOST, enum: RECIPIENT_TYPES }) - @Expose() - recipient!: RecipientType; - - @ApiPropertyOptional({ description: "Verified Email if action is EMAIL_ADDRESS", example: 31214 }) - @Expose() - email?: string; - - @ApiPropertyOptional({ - description: "Verified Phone if action is SMS_NUMBER or WHATSAPP_NUMBER", - }) - @Expose() - phone?: string; - - @ApiPropertyOptional({ - description: "whether or not the attendees are required to provide their phone numbers when booking", - example: true, - default: false, - }) - @IsBoolean() - @Expose() - @IsOptional() - phoneRequired?: boolean; - - @ApiProperty({ description: "Template type used", example: REMINDER, enum: TEMPLATES }) - @Expose() - template!: TemplateType; - - @ApiPropertyOptional({ - description: "Whether a calendar event (.ics) was included (for email actions)", - example: true, - }) - @Expose() - includeCalendarEvent = false; - - @ApiProperty({ description: "Displayed sender name used for this step", example: "Cal.com Notifications" }) - @Expose() - sender!: string; - - @ApiProperty({ description: "Message content for this step", type: WorkflowMessageOutputDto }) - @Expose() - @ValidateNested() - @Type(() => WorkflowMessageOutputDto) - message!: WorkflowMessageOutputDto; -} - -export class WorkflowTriggerOffsetOutputDto { - @ApiProperty({ description: "Time value for offset", example: 24 }) - @Expose() - value!: number; - - @ApiProperty({ - description: "Unit for the offset time", - example: HOUR, - enum: TIME_UNITS, - }) - @Expose() - unit!: TimeUnitType; -} - -export class BaseWorkflowOutput { - @ApiProperty({ description: "Unique identifier of the workflow", example: 101 }) - @Expose() - id!: number; - - @ApiProperty({ description: "Name of the workflow", example: "Platform Test Workflow" }) - @Expose() - name!: string; - - @ApiPropertyOptional({ - description: "ID of the user who owns the workflow (if not team-owned)", - example: 2313, - }) - @Expose() - userId?: number; - - @ApiPropertyOptional({ description: "ID of the team owning the workflow", example: 4214321 }) - @Expose() - teamId?: number; - - @ApiPropertyOptional({ description: "Timestamp of creation", example: "2024-05-12T10:00:00.000Z" }) - @Expose() - createdAt?: Date | string; - - @ApiPropertyOptional({ description: "Timestamp of last update", example: "2024-05-12T11:30:00.000Z" }) - @Expose() - updatedAt?: Date | string; -} diff --git a/apps/api/v2/src/modules/workflows/outputs/event-type-workflow.output.ts b/apps/api/v2/src/modules/workflows/outputs/event-type-workflow.output.ts deleted file mode 100644 index b52e3c219064ad..00000000000000 --- a/apps/api/v2/src/modules/workflows/outputs/event-type-workflow.output.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { - BaseWorkflowStepOutputDto, - WorkflowTriggerOffsetOutputDto, - BaseWorkflowOutput, -} from "@/modules/workflows/outputs/base-workflow.output"; -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsArray, IsEnum, IsIn, IsString, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -import { EMAIL_HOST, STEP_ACTIONS, StepAction } from "../inputs/workflow-step.input"; -import { - BEFORE_EVENT, - EVENT_TYPE_WORKFLOW_TRIGGER_TYPES, - WorkflowEventTypeTriggerType, -} from "../inputs/workflow-trigger.input"; - -export const WORKFLOW_TYPE_FORM = "routing-form"; -export const WORKFLOW_TYPE_EVENT_TYPE = "event-type"; - -export class EventTypeWorkflowStepOutputDto extends BaseWorkflowStepOutputDto { - @ApiProperty({ description: "Action to perform", example: EMAIL_HOST, enum: STEP_ACTIONS }) - @Expose() - action!: StepAction; -} - -export class EventTypeWorkflowTriggerOutputDto { - @ApiProperty({ - description: "Trigger type for the workflow", - example: BEFORE_EVENT, - enum: EVENT_TYPE_WORKFLOW_TRIGGER_TYPES, - }) - @Expose() - type!: WorkflowEventTypeTriggerType; - - @ApiPropertyOptional({ - description: "Offset details (present for BEFORE_EVENT/AFTER_EVENT)", - type: WorkflowTriggerOffsetOutputDto, - }) - @Expose() - @ValidateNested() - @Type(() => WorkflowTriggerOffsetOutputDto) - offset?: WorkflowTriggerOffsetOutputDto; -} - -export class EventTypeWorkflowActivationOutputDto { - @ApiProperty({ - description: "Whether the workflow is active for all event types associated with the team/user", - example: false, - }) - @Expose() - isActiveOnAllEventTypes?: boolean = false; - - @ApiPropertyOptional({ - description: "List of Event Type IDs the workflow is specifically active on (if not active on all)", - example: [698191, 698192], - }) - @Expose() - @IsArray() - activeOnEventTypeIds?: number[]; -} - -// --- Main Workflow Output DTO --- - -export class EventTypeWorkflowOutput extends BaseWorkflowOutput { - @ApiProperty({ - description: "type of the workflow", - example: WORKFLOW_TYPE_EVENT_TYPE, - default: WORKFLOW_TYPE_EVENT_TYPE, - }) - @IsString() - @IsIn([WORKFLOW_TYPE_EVENT_TYPE]) - type!: typeof WORKFLOW_TYPE_EVENT_TYPE; - - @ApiProperty({ - description: "Activation settings for the workflow", - }) - @Expose() - @ValidateNested() - @Type(() => EventTypeWorkflowActivationOutputDto) - activation!: EventTypeWorkflowActivationOutputDto; - - @ApiProperty({ description: "Trigger configuration", type: EventTypeWorkflowTriggerOutputDto }) - @Expose() - @ValidateNested() - @Type(() => EventTypeWorkflowTriggerOutputDto) - trigger!: EventTypeWorkflowTriggerOutputDto; - - @ApiProperty({ description: "Steps comprising the workflow", type: [EventTypeWorkflowStepOutputDto] }) - @Expose() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => EventTypeWorkflowStepOutputDto) - steps!: EventTypeWorkflowStepOutputDto[]; -} - -// --- List Response Output DTO --- - -export class GetEventTypeWorkflowsOutput { - @ApiProperty({ - description: "Indicates the status of the response", - example: SUCCESS_STATUS, - enum: [SUCCESS_STATUS, ERROR_STATUS], - }) - @Expose() - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - description: "List of workflows", - type: [EventTypeWorkflowOutput], - }) - @Expose() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => EventTypeWorkflowOutput) - data!: EventTypeWorkflowOutput[]; -} - -export class GetEventTypeWorkflowOutput { - @ApiProperty({ - description: "Indicates the status of the response", - example: SUCCESS_STATUS, - enum: [SUCCESS_STATUS, ERROR_STATUS], - }) - @Expose() - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - description: "workflow", - type: [EventTypeWorkflowOutput], - }) - @Expose() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => EventTypeWorkflowOutput) - data!: EventTypeWorkflowOutput; -} diff --git a/apps/api/v2/src/modules/workflows/outputs/routing-form-workflow.output.ts b/apps/api/v2/src/modules/workflows/outputs/routing-form-workflow.output.ts deleted file mode 100644 index d44184ae05c6a2..00000000000000 --- a/apps/api/v2/src/modules/workflows/outputs/routing-form-workflow.output.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { - BaseWorkflowOutput, - BaseWorkflowStepOutputDto, - WorkflowTriggerOffsetOutputDto, -} from "@/modules/workflows/outputs/base-workflow.output"; -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { Expose, Type } from "class-transformer"; -import { IsArray, IsEnum, IsIn, IsString, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - -import { EMAIL_HOST, FORM_ALLOWED_STEP_ACTIONS, FormAllowedStepAction } from "../inputs/workflow-step.input"; -import { - FORM_SUBMITTED, - FORM_WORKFLOW_TRIGGER_TYPES, - WorkflowFormTriggerType, -} from "../inputs/workflow-trigger.input"; - -export const WORKFLOW_TYPE_FORM = "routing-form"; -export const WORKFLOW_TYPE_EVENT_TYPE = "event-type"; - -export class RoutingFormWorkflowStepOutputDto extends BaseWorkflowStepOutputDto { - @ApiProperty({ description: "Action to perform", example: EMAIL_HOST, enum: FORM_ALLOWED_STEP_ACTIONS }) - @Expose() - action!: FormAllowedStepAction; -} - -export class RoutingFormWorkflowTriggerOutputDto { - @ApiProperty({ - description: "Trigger type for the workflow", - example: FORM_SUBMITTED, - enum: FORM_WORKFLOW_TRIGGER_TYPES, - }) - @Expose() - type!: WorkflowFormTriggerType; - - @ApiPropertyOptional({ - description: "Offset details (present for BEFORE_EVENT/AFTER_EVENT/FORM_SUBMITTED_NO_EVENT)", - type: WorkflowTriggerOffsetOutputDto, - }) - @Expose() - @ValidateNested() - @Type(() => WorkflowTriggerOffsetOutputDto) - offset?: WorkflowTriggerOffsetOutputDto; -} - -export class RoutingFormWorkflowActivationOutputDto { - @ApiProperty({ - description: "Whether the workflow is active for all routing forms associated with the team/user", - example: false, - }) - @Expose() - isActiveOnAllRoutingForms?: boolean = false; - - @ApiPropertyOptional({ - description: "List of Event Type IDs the workflow is specifically active on (if not active on all)", - example: ["5cacdec7-1234-6e1b-78d9-7bcda8a1b332"], - }) - @Expose() - @IsArray() - activeOnRoutingFormIds?: string[]; -} - -// --- Main Workflow Output DTO --- - -export class RoutingFormWorkflowOutput extends BaseWorkflowOutput { - @ApiProperty({ - description: "type of the workflow", - example: WORKFLOW_TYPE_FORM, - default: WORKFLOW_TYPE_FORM, - }) - @IsString() - @IsIn([WORKFLOW_TYPE_FORM]) - type!: typeof WORKFLOW_TYPE_FORM; - - @ApiProperty({ - description: "Activation settings for the workflow", - }) - @Expose() - @Type(() => RoutingFormWorkflowActivationOutputDto) - @ValidateNested() - activation!: RoutingFormWorkflowActivationOutputDto; - - @ApiProperty({ description: "Trigger configuration", type: RoutingFormWorkflowTriggerOutputDto }) - @Expose() - @ValidateNested() - @Type(() => RoutingFormWorkflowTriggerOutputDto) - trigger!: RoutingFormWorkflowTriggerOutputDto; - - @ApiProperty({ description: "Steps comprising the workflow", type: [RoutingFormWorkflowStepOutputDto] }) - @Expose() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => RoutingFormWorkflowStepOutputDto) - steps!: RoutingFormWorkflowStepOutputDto[]; -} - -export class GetRoutingFormWorkflowsOutput { - @ApiProperty({ - description: "Indicates the status of the response", - example: SUCCESS_STATUS, - enum: [SUCCESS_STATUS, ERROR_STATUS], - }) - @Expose() - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - description: "List of workflows", - type: [RoutingFormWorkflowOutput], - }) - @Expose() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => RoutingFormWorkflowOutput) - data!: RoutingFormWorkflowOutput[]; -} - -export class GetRoutingFormWorkflowOutput { - @ApiProperty({ - description: "Indicates the status of the response", - example: SUCCESS_STATUS, - enum: [SUCCESS_STATUS, ERROR_STATUS], - }) - @Expose() - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ApiProperty({ - description: "workflow", - type: [RoutingFormWorkflowOutput], - }) - @Expose() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => RoutingFormWorkflowOutput) - data!: RoutingFormWorkflowOutput; -} diff --git a/apps/api/v2/src/modules/workflows/services/team-event-type-workflows.service.ts b/apps/api/v2/src/modules/workflows/services/team-event-type-workflows.service.ts deleted file mode 100644 index 3d2dffff0fe982..00000000000000 --- a/apps/api/v2/src/modules/workflows/services/team-event-type-workflows.service.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { UserWithProfile } from "@/modules/users/users.repository"; -import { TeamsVerifiedResourcesRepository } from "@/modules/verified-resources/teams-verified-resources.repository"; -import { CreateEventTypeWorkflowDto } from "@/modules/workflows/inputs/create-event-type-workflow.input"; -import { UpdateEventTypeWorkflowDto } from "@/modules/workflows/inputs/update-event-type-workflow.input"; -import { WorkflowsInputService } from "@/modules/workflows/services/workflows.input.service"; -import { WorkflowsOutputService } from "@/modules/workflows/services/workflows.output.service"; -import { WorkflowsRepository } from "@/modules/workflows/workflows.repository"; -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; - -@Injectable() -export class TeamEventTypeWorkflowsService { - constructor( - private readonly workflowsRepository: WorkflowsRepository, - private readonly teamsVerifiedResourcesRepository: TeamsVerifiedResourcesRepository, - private readonly workflowInputService: WorkflowsInputService, - private readonly workflowOutputService: WorkflowsOutputService - ) {} - - async getEventTypeTeamWorkflows(teamId: number, skip: number, take: number) { - const workflows = await this.workflowsRepository.getEventTypeTeamWorkflows(teamId, skip, take); - - return workflows.map((workflow) => { - const output = this.workflowOutputService.toEventTypeOutputDto(workflow); - - if (!output) { - throw new BadRequestException(`Could not format workflow for response.`); - } - return output; - }); - } - - async getEventTypeTeamWorkflowById(teamId: number, workflowId: number) { - const workflow = await this.workflowsRepository.getEventTypeTeamWorkflowById(teamId, workflowId); - - if (!workflow) { - throw new NotFoundException(`Workflow with ID ${workflowId} not found for team ${teamId}`); - } - - const output = this.workflowOutputService.toEventTypeOutputDto(workflow); - - if (!output) { - throw new BadRequestException(`Could not format workflow for response.`); - } - return output; - } - - async createEventTypeTeamWorkflow(user: UserWithProfile, teamId: number, data: CreateEventTypeWorkflowDto) { - const workflowHusk = await this.workflowsRepository.createTeamWorkflowHusk(teamId); - const mappedData = await this.workflowInputService.mapEventTypeUpdateDtoToZodSchema( - data, - workflowHusk.id, - teamId, - workflowHusk - ); - - const createdWorkflow = await this.workflowsRepository.updateEventTypeTeamWorkflow( - user, - teamId, - workflowHusk.id, - mappedData - ); - if (!createdWorkflow) { - throw new BadRequestException(`Could not create Workflow in team ${teamId}`); - } - - const output = this.workflowOutputService.toEventTypeOutputDto(createdWorkflow); - - if (!output) { - throw new BadRequestException(`Could not format workflow for response.`); - } - return output; - } - - async updateEventTypeTeamWorkflow( - user: UserWithProfile, - teamId: number, - workflowId: number, - data: UpdateEventTypeWorkflowDto - ) { - const currentWorkflow = await this.workflowsRepository.getEventTypeTeamWorkflowById(teamId, workflowId); - - if (!currentWorkflow) { - throw new NotFoundException(`Workflow with ID ${workflowId} not found for team ${teamId}`); - } - const mappedData = await this.workflowInputService.mapEventTypeUpdateDtoToZodSchema( - data, - workflowId, - teamId, - currentWorkflow - ); - - const updatedWorkflow = await this.workflowsRepository.updateEventTypeTeamWorkflow( - user, - teamId, - workflowId, - mappedData - ); - - if (!updatedWorkflow) { - throw new BadRequestException(`Could not update Workflow with ID ${workflowId} in team ${teamId}`); - } - const output = this.workflowOutputService.toEventTypeOutputDto(updatedWorkflow); - - if (!output) { - throw new BadRequestException(`Could not format workflow for response.`); - } - return output; - } - - async deleteTeamEventTypeWorkflow(teamId: number, workflowId: number) { - return await this.workflowsRepository.deleteTeamWorkflowById(teamId, workflowId); - } -} diff --git a/apps/api/v2/src/modules/workflows/services/team-routing-form-workflows.service.ts b/apps/api/v2/src/modules/workflows/services/team-routing-form-workflows.service.ts deleted file mode 100644 index 3de1cd0b38922d..00000000000000 --- a/apps/api/v2/src/modules/workflows/services/team-routing-form-workflows.service.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { UserWithProfile } from "@/modules/users/users.repository"; -import { TeamsVerifiedResourcesRepository } from "@/modules/verified-resources/teams-verified-resources.repository"; -import { CreateFormWorkflowDto } from "@/modules/workflows/inputs/create-form-workflow"; -import { UpdateFormWorkflowDto } from "@/modules/workflows/inputs/update-form-workflow.input"; -import { WorkflowsInputService } from "@/modules/workflows/services/workflows.input.service"; -import { WorkflowsOutputService } from "@/modules/workflows/services/workflows.output.service"; -import { WorkflowsRepository } from "@/modules/workflows/workflows.repository"; -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; - -@Injectable() -export class TeamRoutingFormWorkflowsService { - constructor( - private readonly workflowsRepository: WorkflowsRepository, - private readonly teamsVerifiedResourcesRepository: TeamsVerifiedResourcesRepository, - private readonly workflowInputService: WorkflowsInputService, - private readonly workflowOutputService: WorkflowsOutputService - ) {} - - async getRoutingFormTeamWorkflows(teamId: number, skip: number, take: number) { - const workflows = await this.workflowsRepository.getRoutingFormTeamWorkflows(teamId, skip, take); - - return workflows.map((workflow) => { - const output = this.workflowOutputService.toRoutingFormOutputDto(workflow); - - if (!output) { - throw new BadRequestException(`Could not format workflow for response.`); - } - return output; - }); - } - - async getRoutingFormTeamWorkflowById(teamId: number, workflowId: number) { - const workflow = await this.workflowsRepository.getRoutingFormTeamWorkflowById(teamId, workflowId); - - if (!workflow) { - throw new NotFoundException(`Workflow with ID ${workflowId} not found for team ${teamId}`); - } - - const output = this.workflowOutputService.toRoutingFormOutputDto(workflow); - - if (!output) { - throw new BadRequestException(`Could not format workflow for response.`); - } - return output; - } - - async createFormTeamWorkflow(user: UserWithProfile, teamId: number, data: CreateFormWorkflowDto) { - const workflowHusk = await this.workflowsRepository.createTeamWorkflowHusk(teamId); - const mappedData = await this.workflowInputService.mapFormUpdateDtoToZodSchema( - data, - workflowHusk.id, - teamId, - workflowHusk - ); - - const createdWorkflow = await this.workflowsRepository.updateRoutingFormTeamWorkflow( - user, - teamId, - workflowHusk.id, - mappedData - ); - if (!createdWorkflow) { - throw new BadRequestException(`Could not create Workflow in team ${teamId}`); - } - - const output = this.workflowOutputService.toRoutingFormOutputDto(createdWorkflow); - - if (!output) { - throw new BadRequestException(`Could not format workflow for response.`); - } - return output; - } - - async updateFormTeamWorkflow( - user: UserWithProfile, - teamId: number, - workflowId: number, - data: UpdateFormWorkflowDto - ) { - const currentWorkflow = await this.workflowsRepository.getRoutingFormTeamWorkflowById(teamId, workflowId); - - if (!currentWorkflow) { - throw new NotFoundException(`Workflow with ID ${workflowId} not found for team ${teamId}`); - } - const mappedData = await this.workflowInputService.mapFormUpdateDtoToZodSchema( - data, - workflowId, - teamId, - currentWorkflow - ); - - const updatedWorkflow = await this.workflowsRepository.updateRoutingFormTeamWorkflow( - user, - teamId, - workflowId, - mappedData - ); - - if (!updatedWorkflow) { - throw new BadRequestException(`Could not update Workflow with ID ${workflowId} in team ${teamId}`); - } - - const output = this.workflowOutputService.toRoutingFormOutputDto(updatedWorkflow); - - if (!output) { - throw new BadRequestException(`Could not format workflow for response.`); - } - return output; - } - - async deleteTeamRoutingFormWorkflow(teamId: number, workflowId: number) { - return await this.workflowsRepository.deleteTeamWorkflowById(teamId, workflowId); - } -} diff --git a/apps/api/v2/src/modules/workflows/services/workflows.input.service.ts b/apps/api/v2/src/modules/workflows/services/workflows.input.service.ts deleted file mode 100644 index bb1486a0eb5bf1..00000000000000 --- a/apps/api/v2/src/modules/workflows/services/workflows.input.service.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { TeamsVerifiedResourcesRepository } from "@/modules/verified-resources/teams-verified-resources.repository"; -import { UpdateEventTypeWorkflowDto } from "@/modules/workflows/inputs/update-event-type-workflow.input"; -import { UpdateFormWorkflowDto } from "@/modules/workflows/inputs/update-form-workflow.input"; -import { WorkflowType } from "@/modules/workflows/workflows.repository"; -import { BadRequestException, Injectable } from "@nestjs/common"; - -import { TUpdateInputSchema } from "@calcom/platform-libraries/workflows"; - -import { - EMAIL_ADDRESS, - EMAIL_ATTENDEE, - EMAIL_HOST, - HtmlWorkflowMessageDto, - SMS_ATTENDEE, - SMS_NUMBER, - STEP_ACTIONS_TO_ENUM, - TemplateType, - TextWorkflowMessageDto, - UpdateWorkflowStepDto, - WHATSAPP_ATTENDEE, - WHATSAPP_NUMBER, -} from "../inputs/workflow-step.input"; -import { - OffsetTriggerDTOInstances, - OffsetTriggerDTOInstancesType, - TIME_UNIT_TO_ENUM, - WORKFLOW_TRIGGER_TO_ENUM, -} from "../inputs/workflow-trigger.input"; - -@Injectable() -export class WorkflowsInputService { - constructor(private readonly teamsVerifiedResourcesRepository: TeamsVerifiedResourcesRepository) {} - - private _isOffsetTrigger( - trigger: UpdateEventTypeWorkflowDto["trigger"] | UpdateFormWorkflowDto["trigger"] - ): trigger is OffsetTriggerDTOInstancesType { - return OffsetTriggerDTOInstances.some((Instance) => trigger instanceof Instance); - } - - private async _getTeamPhoneNumberFromVerifiedId(teamId: number, verifiedPhoneId: number) { - const phoneResource = await this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumberById( - verifiedPhoneId, - teamId - ); - - if (!phoneResource?.phoneNumber) { - throw new BadRequestException("Invalid Verified Phone Id."); - } - - return phoneResource.phoneNumber; - } - - private async _getTeamEmailFromVerifiedId(teamId: number, verifiedEmailId: number) { - const emailResource = await this.teamsVerifiedResourcesRepository.getTeamVerifiedEmailById( - verifiedEmailId, - teamId - ); - if (!emailResource?.email) { - throw new BadRequestException("Invalid Verified Email Id."); - } - - return emailResource.email; - } - - private async mapUpdateWorkflowStepToZodUpdateSchema( - stepDto: UpdateWorkflowStepDto, - index: number, - teamId: number, - workflowIdToUse: number - ) { - let reminderBody: string | null = null; - let sendTo: string | null = null; - let phoneRequired: boolean | null = null; - const html = stepDto.message instanceof HtmlWorkflowMessageDto ? stepDto.message.html : null; - const text = stepDto.message instanceof TextWorkflowMessageDto ? stepDto.message.text : null; - let includeCalendarEvent = false; - - switch (stepDto.action) { - case EMAIL_HOST: - case EMAIL_ATTENDEE: - case EMAIL_ADDRESS: - reminderBody = html ?? null; - includeCalendarEvent = stepDto.includeCalendarEvent; - break; - case SMS_ATTENDEE: - phoneRequired = stepDto.phoneRequired ?? false; - break; - case SMS_NUMBER: - break; - case WHATSAPP_ATTENDEE: - phoneRequired = stepDto.phoneRequired ?? false; - break; - case WHATSAPP_NUMBER: - reminderBody = text ?? null; - break; - } - if (stepDto.action === EMAIL_ADDRESS) { - if (stepDto.verifiedEmailId) { - sendTo = await this._getTeamEmailFromVerifiedId(teamId, stepDto.verifiedEmailId); - } - } else if (stepDto.action === SMS_NUMBER || stepDto.action === WHATSAPP_NUMBER) { - if (stepDto.verifiedPhoneId) { - sendTo = await this._getTeamPhoneNumberFromVerifiedId(teamId, stepDto.verifiedPhoneId); - } - } - - const actionForZod = STEP_ACTIONS_TO_ENUM[stepDto.action]; - const templateForZod = stepDto?.template?.toUpperCase() as unknown as Uppercase; - - return { - id: stepDto.id ?? -(index + 1), - stepNumber: stepDto.stepNumber, - action: actionForZod, - workflowId: workflowIdToUse, - sendTo: sendTo, - reminderBody: reminderBody, - emailSubject: stepDto.message.subject ?? null, - template: templateForZod, - numberRequired: phoneRequired, - sender: stepDto.sender ?? null, - senderName: stepDto.sender ?? null, - includeCalendarEvent: includeCalendarEvent, - }; - } - - private async _mapCommonWorkflowProperties( - updateDto: UpdateEventTypeWorkflowDto | UpdateFormWorkflowDto, - currentData: WorkflowType, - teamId: number, - workflowIdToUse: number - ) { - // 1. Map Steps - let mappedSteps; - if (updateDto?.steps) { - mappedSteps = await Promise.all( - updateDto.steps.map(async (stepDto: UpdateWorkflowStepDto, index: number) => - this.mapUpdateWorkflowStepToZodUpdateSchema(stepDto, index, teamId, workflowIdToUse) - ) - ); - } else { - mappedSteps = currentData.steps.map((step) => ({ - ...step, - senderName: step.sender, - })); - } - - // 2. Map Trigger - let triggerForZod = currentData.trigger; - if (updateDto?.trigger?.type) { - triggerForZod = WORKFLOW_TRIGGER_TO_ENUM[updateDto.trigger.type]; - } - - // 3. Map Time and TimeUnit (Keeping currentData if trigger is missing or not an offset) - let timeUnitForZod = (currentData.timeUnit?.toLowerCase() ?? null) as "hour" | "minute" | "day" | null; - let time = currentData.time ?? null; - - if (updateDto.trigger && this._isOffsetTrigger(updateDto.trigger)) { - timeUnitForZod = updateDto.trigger.offset?.unit ?? timeUnitForZod ?? null; - time = updateDto.trigger.offset?.value ?? currentData.time ?? null; - } - - // 4. Final Enum Conversion - let timeUnit = null; - if (timeUnitForZod) { - timeUnit = TIME_UNIT_TO_ENUM[timeUnitForZod]; - } - - return { mappedSteps, triggerForZod, time, timeUnit }; - } - - async mapEventTypeUpdateDtoToZodSchema( - updateDto: UpdateEventTypeWorkflowDto, - workflowIdToUse: number, - teamId: number, - currentData: WorkflowType - ): Promise { - const { mappedSteps, triggerForZod, time, timeUnit } = await this._mapCommonWorkflowProperties( - updateDto, - currentData, - teamId, - workflowIdToUse - ); - - const updateData: TUpdateInputSchema = { - id: workflowIdToUse, - name: updateDto.name ?? currentData.name, - steps: mappedSteps, - trigger: triggerForZod, - time: time, - timeUnit: timeUnit, - - // Event-type specific logic - activeOnEventTypeIds: - updateDto?.activation?.activeOnEventTypeIds ?? - currentData?.activeOn.map((active) => active.eventTypeId) ?? - [], - isActiveOnAll: updateDto?.activation?.isActiveOnAllEventTypes ?? currentData.isActiveOnAll ?? false, - - // Explicitly set form-related fields to their default/empty state - activeOnRoutingFormIds: [], - } as const satisfies TUpdateInputSchema; - - return updateData; - } - - async mapFormUpdateDtoToZodSchema( - updateDto: UpdateFormWorkflowDto, - workflowIdToUse: number, - teamId: number, - currentData: WorkflowType - ): Promise { - const { mappedSteps, triggerForZod, time, timeUnit } = await this._mapCommonWorkflowProperties( - updateDto, - currentData, - teamId, - workflowIdToUse - ); - - const updateData: TUpdateInputSchema = { - id: workflowIdToUse, - name: updateDto.name ?? currentData.name, - steps: mappedSteps, - trigger: triggerForZod, - time: time, - timeUnit: timeUnit, - - // Form-specific logic - activeOnRoutingFormIds: - updateDto?.activation?.activeOnRoutingFormIds ?? - currentData?.activeOnRoutingForms.map((active) => active.routingFormId) ?? - [], - isActiveOnAll: updateDto?.activation?.isActiveOnAllRoutingForms ?? currentData.isActiveOnAll ?? false, - - // Explicitly set event-type-related fields to their default/empty state - activeOnEventTypeIds: [], - } as const satisfies TUpdateInputSchema; - - return updateData; - } -} diff --git a/apps/api/v2/src/modules/workflows/services/workflows.output.service.ts b/apps/api/v2/src/modules/workflows/services/workflows.output.service.ts deleted file mode 100644 index f810662a8b4127..00000000000000 --- a/apps/api/v2/src/modules/workflows/services/workflows.output.service.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { WorkflowActivationDto } from "@/modules/workflows/inputs/create-event-type-workflow.input"; -import { WorkflowFormActivationDto } from "@/modules/workflows/inputs/create-form-workflow"; -import { - EventTypeWorkflowStepOutputDto, - EventTypeWorkflowOutput, -} from "@/modules/workflows/outputs/event-type-workflow.output"; -import { - RoutingFormWorkflowOutput, - RoutingFormWorkflowStepOutputDto, -} from "@/modules/workflows/outputs/routing-form-workflow.output"; -import { WorkflowType } from "@/modules/workflows/workflows.repository"; -import { Injectable } from "@nestjs/common"; - -import { - ATTENDEE, - CAL_AI_PHONE_CALL, - EMAIL, - EMAIL_ADDRESS, - EMAIL_ATTENDEE, - EMAIL_HOST, - ENUM_TO_STEP_ACTIONS, - ENUM_TO_TEMPLATES, - FORM_ALLOWED_STEP_ACTIONS, - FormAllowedStepAction, - HOST, - PHONE_NUMBER, - RecipientType, - SMS_ATTENDEE, - SMS_NUMBER, - StepAction, - WHATSAPP_ATTENDEE, - WHATSAPP_NUMBER, -} from "../inputs/workflow-step.input"; -import { - ENUM_TO_TIME_UNIT, - ENUM_TO_WORKFLOW_TRIGGER, - HOUR, - OnAfterCalVideoGuestsNoShowTriggerDto, - OnAfterCalVideoHostsNoShowTriggerDto, - OnAfterEventTriggerDto, - OnBeforeEventTriggerDto, - OnCancelTriggerDto, - OnCreationTriggerDto, - OnFormSubmittedTriggerDto, - OnFormSubmittedNoEventTriggerDto, - OnNoShowUpdateTriggerDto, - OnPaidTriggerDto, - OnPaymentInitiatedTriggerDto, - OnRejectedTriggerDto, - OnRequestedTriggerDto, - OnRescheduleTriggerDto, - WORKFLOW_TRIGGER_TO_ENUM, - FORM_WORKFLOW_TRIGGER_TYPES, - ENUM_ROUTING_FORM_WORFLOW_TRIGGERS, - ENUM_OFFSET_WORFLOW_TRIGGERS, -} from "../inputs/workflow-trigger.input"; - -export type TriggerDtoType = - | OnAfterEventTriggerDto - | OnBeforeEventTriggerDto - | OnCreationTriggerDto - | OnRescheduleTriggerDto - | OnCancelTriggerDto - | OnAfterCalVideoGuestsNoShowTriggerDto - | OnFormSubmittedTriggerDto - | OnFormSubmittedNoEventTriggerDto - | OnRejectedTriggerDto - | OnRequestedTriggerDto - | OnPaymentInitiatedTriggerDto - | OnPaidTriggerDto - | OnNoShowUpdateTriggerDto - | OnAfterCalVideoHostsNoShowTriggerDto; - -export type TriggerEventTypeDtoType = - | OnAfterEventTriggerDto - | OnBeforeEventTriggerDto - | OnCreationTriggerDto - | OnRescheduleTriggerDto - | OnCancelTriggerDto - | OnAfterCalVideoGuestsNoShowTriggerDto - | OnRejectedTriggerDto - | OnRequestedTriggerDto - | OnPaymentInitiatedTriggerDto - | OnPaidTriggerDto - | OnNoShowUpdateTriggerDto - | OnAfterCalVideoHostsNoShowTriggerDto; - -type StepConfig = { - recipient: RecipientType; - messageKey: "html" | "text"; - setsCustomRecipient: boolean; - requiresPhone: boolean; -}; - -const ACTION_CONFIG_MAP = { - [EMAIL_HOST]: { - recipient: HOST, - messageKey: "html", - setsCustomRecipient: false, - requiresPhone: false, - } satisfies StepConfig, - [EMAIL_ATTENDEE]: { - recipient: ATTENDEE, - messageKey: "html", - setsCustomRecipient: false, - requiresPhone: false, - }, - [SMS_ATTENDEE]: { - recipient: ATTENDEE, - messageKey: "text", - setsCustomRecipient: false, - requiresPhone: true, - }, - [WHATSAPP_ATTENDEE]: { - recipient: ATTENDEE, - messageKey: "text", - setsCustomRecipient: false, - requiresPhone: true, - }, - [EMAIL_ADDRESS]: { recipient: EMAIL, messageKey: "html", setsCustomRecipient: true, requiresPhone: false }, - [SMS_NUMBER]: { - recipient: PHONE_NUMBER, - messageKey: "text", - setsCustomRecipient: true, - requiresPhone: false, - }, - [WHATSAPP_NUMBER]: { - recipient: PHONE_NUMBER, - messageKey: "text", - setsCustomRecipient: true, - requiresPhone: false, - }, - [CAL_AI_PHONE_CALL]: { - recipient: PHONE_NUMBER, - messageKey: "text", - setsCustomRecipient: true, - requiresPhone: false, - }, -} satisfies Record; - -@Injectable() -export class WorkflowsOutputService { - _isFormAllowedStepAction(action: StepAction): action is FormAllowedStepAction { - return FORM_ALLOWED_STEP_ACTIONS.some((formAction) => formAction === action); - } - _isFormAllowedTrigger( - trigger: WorkflowType["trigger"] - ): trigger is (typeof ENUM_ROUTING_FORM_WORFLOW_TRIGGERS)[number] { - return FORM_WORKFLOW_TRIGGER_TYPES.some( - (formTrigger) => WORKFLOW_TRIGGER_TO_ENUM[formTrigger] === trigger - ); - } - - private _isOffsetTrigger( - trigger: WorkflowType["trigger"] - ): trigger is (typeof ENUM_OFFSET_WORFLOW_TRIGGERS)[number] { - return ENUM_OFFSET_WORFLOW_TRIGGERS.some((offsetTrigger) => offsetTrigger === trigger); - } - - /** - * Maps a single workflow step from the database entity to its DTO representation. - * @param step The workflow step object from the database. - * @returns An EventTypeWorkflowStepOutputDto. - */ - mapStep(step: WorkflowType["steps"][number], _discriminator: "event-type"): EventTypeWorkflowStepOutputDto; - mapStep( - step: WorkflowType["steps"][number], - _discriminator: "routing-form" - ): RoutingFormWorkflowStepOutputDto; - mapStep( - step: WorkflowType["steps"][number], - _discriminator: "event-type" | "routing-form" - ): EventTypeWorkflowStepOutputDto | RoutingFormWorkflowStepOutputDto { - const action = ENUM_TO_STEP_ACTIONS[step.action]; - const config = ACTION_CONFIG_MAP[action] || { - recipient: ATTENDEE, - setsCustomRecipient: false, - requiresPhone: false, - }; - - const customRecipient = step.sendTo ?? ""; - const reminderBody = step.reminderBody ?? ""; - - const baseAction = { - id: step.id, - stepNumber: step.stepNumber, - template: ENUM_TO_TEMPLATES[step.template], - recipient: config.recipient, - sender: step.sender ?? "Default Sender", - includeCalendarEvent: step.includeCalendarEvent, - phoneRequired: config.requiresPhone ? step.numberRequired ?? false : undefined, - email: config.recipient === EMAIL ? customRecipient : "", - phone: config.recipient === PHONE_NUMBER ? customRecipient : "", - message: { - subject: step.emailSubject ?? "", - html: config.messageKey === "html" ? reminderBody : undefined, - text: config.messageKey === "text" ? reminderBody : undefined, - }, - }; - - return this._isFormAllowedStepAction(action) - ? ({ - ...baseAction, - action: action, - } satisfies RoutingFormWorkflowStepOutputDto) - : ({ - ...baseAction, - action: action, - } satisfies EventTypeWorkflowStepOutputDto); - } - - toRoutingFormOutputDto(workflow: WorkflowType): RoutingFormWorkflowOutput | void { - if (workflow.type === "ROUTING_FORM" && this._isFormAllowedTrigger(workflow.trigger)) { - const activation: WorkflowFormActivationDto = { - isActiveOnAllRoutingForms: workflow.isActiveOnAll, - activeOnRoutingFormIds: - workflow.activeOnRoutingForms?.map((relation) => relation.routingFormId) ?? [], - }; - - const trigger: TriggerDtoType = this._isOffsetTrigger(workflow.trigger) - ? { - type: ENUM_TO_WORKFLOW_TRIGGER[workflow.trigger], - offset: { - value: workflow.time ?? 1, - unit: workflow.timeUnit ? ENUM_TO_TIME_UNIT[workflow.timeUnit] : HOUR, - }, - } - : { type: ENUM_TO_WORKFLOW_TRIGGER[workflow.trigger] }; - - const steps: RoutingFormWorkflowStepOutputDto[] = workflow.steps.map((step) => { - return this.mapStep(step, "routing-form"); - }); - - return { - id: workflow.id, - name: workflow.name, - activation: activation, - trigger: trigger, - steps: steps.sort((stepA, stepB) => stepA.stepNumber - stepB.stepNumber), - type: "routing-form", - }; - } - } - - toEventTypeOutputDto(workflow: WorkflowType): EventTypeWorkflowOutput | void { - if (workflow.type === "EVENT_TYPE" && !this._isFormAllowedTrigger(workflow.trigger)) { - const activation: WorkflowActivationDto = { - isActiveOnAllEventTypes: workflow.isActiveOnAll, - activeOnEventTypeIds: workflow.activeOn?.map((relation) => relation.eventTypeId) ?? [], - }; - - const trigger: TriggerEventTypeDtoType = this._isOffsetTrigger(workflow.trigger) - ? { - type: ENUM_TO_WORKFLOW_TRIGGER[workflow.trigger], - offset: { - value: workflow.time ?? 1, - unit: workflow.timeUnit ? ENUM_TO_TIME_UNIT[workflow.timeUnit] : HOUR, - }, - } - : { type: ENUM_TO_WORKFLOW_TRIGGER[workflow.trigger] }; - - const steps: EventTypeWorkflowStepOutputDto[] = workflow.steps.map((step) => { - return this.mapStep(step, "event-type"); - }); - - return { - id: workflow.id, - name: workflow.name, - activation: activation, - trigger: trigger, - steps: steps.sort((stepA, stepB) => stepA.stepNumber - stepB.stepNumber), - type: "event-type", - }; - } - } -} diff --git a/apps/api/v2/src/modules/workflows/workflows.repository.ts b/apps/api/v2/src/modules/workflows/workflows.repository.ts deleted file mode 100644 index 4a341eb32d2860..00000000000000 --- a/apps/api/v2/src/modules/workflows/workflows.repository.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable } from "@nestjs/common"; - -import { TimeUnit, WorkflowTriggerEvents } from "@calcom/platform-libraries"; -import { TUpdateInputSchema } from "@calcom/platform-libraries/workflows"; -import { updateWorkflow } from "@calcom/platform-libraries/workflows"; -import type { PrismaClient } from "@calcom/prisma"; -import type { Workflow, WorkflowStep } from "@calcom/prisma/client"; - -export type WorkflowType = Workflow & { - activeOn: { eventTypeId: number }[]; - steps: WorkflowStep[]; - activeOnRoutingForms: { routingFormId: string }[]; -}; - -@Injectable() -export class WorkflowsRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async deleteTeamWorkflowById(teamId: number, workflowId: number) { - return await this.dbWrite.prisma.workflow.delete({ where: { id: workflowId, teamId } }); - } - - async getEventTypeTeamWorkflowById(teamId: number, id: number): Promise { - const workflow = await this.dbRead.prisma.workflow.findUnique({ - where: { - id: id, - teamId: teamId, - type: "EVENT_TYPE", - }, - include: { - steps: true, - activeOn: { select: { eventTypeId: true } }, - activeOnRoutingForms: { select: { routingFormId: true } }, - }, - }); - - return workflow; - } - - async getRoutingFormTeamWorkflowById(teamId: number, id: number): Promise { - const workflow = await this.dbRead.prisma.workflow.findUnique({ - where: { - id: id, - teamId: teamId, - type: "ROUTING_FORM", - }, - include: { - steps: true, - activeOn: { select: { eventTypeId: true } }, - activeOnRoutingForms: { select: { routingFormId: true } }, - }, - }); - - return workflow; - } - - async getEventTypeTeamWorkflows(teamId: number, skip: number, take: number): Promise { - const workflows = await this.dbRead.prisma.workflow.findMany({ - where: { - teamId: teamId, - type: "EVENT_TYPE", - }, - include: { - steps: true, - activeOn: { select: { eventTypeId: true } }, - activeOnRoutingForms: { select: { routingFormId: true } }, - }, - skip, - take, - }); - - return workflows; - } - - async getRoutingFormTeamWorkflows(teamId: number, skip: number, take: number): Promise { - const workflows = await this.dbRead.prisma.workflow.findMany({ - where: { - teamId: teamId, - type: "ROUTING_FORM", - }, - include: { - steps: true, - activeOn: { select: { eventTypeId: true } }, - activeOnRoutingForms: { select: { routingFormId: true } }, - }, - skip, - take, - }); - - return workflows; - } - - async createTeamWorkflowHusk(teamId: number) { - return this.dbWrite.prisma.workflow.create({ - data: { - name: "", - trigger: WorkflowTriggerEvents.BEFORE_EVENT, - time: 24, - timeUnit: TimeUnit.HOUR, - teamId, - }, - include: { activeOn: true, steps: true, activeOnRoutingForms: true }, - }); - } - - async updateRoutingFormTeamWorkflow( - user: UserWithProfile, - teamId: number, - workflowId: number, - data: TUpdateInputSchema - ) { - await updateWorkflow({ - ctx: { - user: { - ...user, - locale: user?.locale ?? "en", - organizationId: user.profiles?.[0]?.organization?.id ?? null, - }, - prisma: this.dbWrite.prisma as unknown as PrismaClient, - }, - input: data, - }); - - const workflow = await this.getRoutingFormTeamWorkflowById(teamId, workflowId); - return workflow; - } - - async updateEventTypeTeamWorkflow( - user: UserWithProfile, - teamId: number, - workflowId: number, - data: TUpdateInputSchema - ) { - await updateWorkflow({ - ctx: { - user: { - ...user, - locale: user?.locale ?? "en", - organizationId: user.profiles?.[0]?.organization?.id ?? null, - }, - prisma: this.dbWrite.prisma as unknown as PrismaClient, - }, - input: data, - }); - - const workflow = await this.getEventTypeTeamWorkflowById(teamId, workflowId); - return workflow; - } -} diff --git a/apps/api/v2/src/platform/LICENSE b/apps/api/v2/src/platform/LICENSE new file mode 100644 index 00000000000000..d3cfa3cd5af8ec --- /dev/null +++ b/apps/api/v2/src/platform/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020-present Cal.com, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/apps/api/v2/src/platform/README.md b/apps/api/v2/src/platform/README.md new file mode 100644 index 00000000000000..970139ba3d23c1 --- /dev/null +++ b/apps/api/v2/src/platform/README.md @@ -0,0 +1,12 @@ + + + +# API v2 + +This directory contains API endpoints for Cal.diy. + +All code in this repository is licensed under the [MIT License](../../../../LICENSE). diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts b/apps/api/v2/src/platform/bookings/2024-04-15/bookings.module.ts similarity index 71% rename from apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts rename to apps/api/v2/src/platform/bookings/2024-04-15/bookings.module.ts index 03e52b78c51caf..1822209246458d 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts +++ b/apps/api/v2/src/platform/bookings/2024-04-15/bookings.module.ts @@ -1,19 +1,19 @@ -import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller"; -import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; -import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; -import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; -import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; -import { InstantBookingModule } from "@/lib/modules/instant-booking.module"; +import { Module } from "@nestjs/common"; +import { BookingsController_2024_04_15 } from "@/platform/bookings/2024-04-15/controllers/bookings.controller"; +import { PlatformBookingsService } from "@/platform/bookings/shared/platform-bookings.service"; +import { CalendarsRepository } from "@/platform/calendars/calendars.repository"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; +import { EventTypesModule_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/event-types.module"; +import { EventTypesModule_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.module"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.module"; import { RecurringBookingModule } from "@/lib/modules/recurring-booking.module"; import { RegularBookingModule } from "@/lib/modules/regular-booking.module"; import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository"; import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository"; import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; import { AppsRepository } from "@/modules/apps/apps.repository"; -import { BillingModule } from "@/modules/billing/billing.module"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { KyselyModule } from "@/modules/kysely/kysely.module"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; @@ -26,8 +26,6 @@ import { SelectedCalendarsRepository } from "@/modules/selected-calendars/select import { TokensModule } from "@/modules/tokens/tokens.module"; import { TokensRepository } from "@/modules/tokens/tokens.repository"; import { UsersModule } from "@/modules/users/users.module"; -import { Module } from "@nestjs/common"; -import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.module"; @Module({ imports: [ @@ -35,7 +33,6 @@ import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.m KyselyModule, RedisModule, TokensModule, - BillingModule, UsersModule, EventTypesModule_2024_04_15, SchedulesModule_2024_04_15, @@ -43,7 +40,6 @@ import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.m ProfilesModule, RegularBookingModule, RecurringBookingModule, - InstantBookingModule, BookingEventHandlerModule, ], providers: [ diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts similarity index 96% rename from apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts index efe79698ab6716..3b6147f24a39c8 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts @@ -15,13 +15,13 @@ 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"; -import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output"; -import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; -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 { CreateBookingInput_2024_04_15 } from "@/platform/bookings/2024-04-15/inputs/create-booking.input"; +import { CreateRecurringBookingInput_2024_04_15 } from "@/platform/bookings/2024-04-15/inputs/create-recurring-booking.input"; +import { GetBookingOutput_2024_04_15 } from "@/platform/bookings/2024-04-15/outputs/get-booking.output"; +import { GetBookingsOutput_2024_04_15 } from "@/platform/bookings/2024-04-15/outputs/get-bookings.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/platform/bookings/2024-04-15/controllers/bookings.controller.ts similarity index 85% rename from apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts rename to apps/api/v2/src/platform/bookings/2024-04-15/controllers/bookings.controller.ts index 45c8dceac44dd9..85791f56650147 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/platform/bookings/2024-04-15/controllers/bookings.controller.ts @@ -14,7 +14,7 @@ import { handleCancelBooking, handleMarkNoShow, } from "@calcom/platform-libraries"; -import { type InstantBookingCreateResult, makeUserActor } from "@calcom/platform-libraries/bookings"; +import { makeUserActor } from "@calcom/platform-libraries/bookings"; import { ErrorCode, HttpError } from "@calcom/platform-libraries/errors"; import type { ApiResponse } from "@calcom/platform-types"; import { @@ -46,18 +46,17 @@ import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs import { Request } from "express"; import { NextApiRequest } from "next/types"; import { v4 as uuidv4 } from "uuid"; -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 { MarkNoShowInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/mark-no-show.input"; -import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output"; -import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output"; -import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output"; -import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; +import { CreateBookingInput_2024_04_15 } from "@/platform/bookings/2024-04-15/inputs/create-booking.input"; +import { CreateRecurringBookingInput_2024_04_15 } from "@/platform/bookings/2024-04-15/inputs/create-recurring-booking.input"; +import { MarkNoShowInput_2024_04_15 } from "@/platform/bookings/2024-04-15/inputs/mark-no-show.input"; +import { GetBookingOutput_2024_04_15 } from "@/platform/bookings/2024-04-15/outputs/get-booking.output"; +import { GetBookingsOutput_2024_04_15 } from "@/platform/bookings/2024-04-15/outputs/get-bookings.output"; +import { MarkNoShowOutput_2024_04_15 } from "@/platform/bookings/2024-04-15/outputs/mark-no-show.output"; +import { PlatformBookingsService } from "@/platform/bookings/shared/platform-bookings.service"; import { isApiKey, sha256Hash, stripApiKey } from "@/lib/api-key"; import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions"; import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository"; import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository"; -import { InstantBookingCreateService } from "@/lib/services/instant-booking-create.service"; import { RecurringBookingService } from "@/lib/services/recurring-booking.service"; import { RegularBookingService } from "@/lib/services/regular-booking.service"; import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; @@ -70,7 +69,6 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; -import { BillingService } from "@/modules/billing/services/billing.service"; import { KyselyReadService } from "@/modules/kysely/kysely-read.service"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; @@ -117,7 +115,6 @@ export class BookingsController_2024_04_15 { private readonly prismaReadService: PrismaReadService, private readonly kyselyReadService: KyselyReadService, private readonly oAuthClientRepository: OAuthClientRepository, - private readonly billingService: BillingService, private readonly config: ConfigService, private readonly apiKeyRepository: ApiKeysRepository, private readonly platformBookingsService: PlatformBookingsService, @@ -125,7 +122,6 @@ export class BookingsController_2024_04_15 { private readonly usersService: UsersService, private readonly regularBookingService: RegularBookingService, private readonly recurringBookingService: RecurringBookingService, - private readonly instantBookingCreateService: InstantBookingCreateService, private readonly eventTypeRepository: PrismaEventTypeRepository, private readonly teamRepository: PrismaTeamRepository ) {} @@ -223,16 +219,8 @@ export class BookingsController_2024_04_15 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, - impersonatedByUserUuid: null, }, }); - if (booking.userId && booking.uid && booking.startTime && booking.user?.isPlatformManaged) { - void (await this.billingService.increaseUsageByUserId(booking.userId, { - uid: booking.uid, - startTime: booking.startTime, - fromReschedule: booking.fromReschedule, - })); - } return { status: SUCCESS_STATUS, data: booking, @@ -280,12 +268,8 @@ export class BookingsController_2024_04_15 { platformCancelUrl: bookingRequest.platformCancelUrl, platformRescheduleUrl: bookingRequest.platformRescheduleUrl, platformBookingUrl: bookingRequest.platformBookingUrl, - impersonatedByUserUuid: null, actionSource: "API_V2", }); - if (!res.onlyRemovedAttendee && res.isPlatformManagedUserBooking) { - void (await this.billingService.cancelUsageByBookingUid(res.bookingUid)); - } return { status: SUCCESS_STATUS, data: { @@ -317,9 +301,6 @@ export class BookingsController_2024_04_15 { attendees: body.attendees, noShowHost: body.noShowHost, userId: user.id, - actor: makeUserActor(user.uuid), - actionSource: "API_V2", - impersonatedByUserUuid: null, }); return { status: SUCCESS_STATUS, data: markNoShowResponse }; @@ -357,20 +338,10 @@ export class BookingsController_2024_04_15 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, noEmail: bookingRequest.body.noEmail, - impersonatedByUserUuid: null, }, creationSource: "API_V2", }); - createdBookings.forEach(async (booking) => { - if (booking.userId && booking.uid && booking.startTime && booking.user.isPlatformManaged) { - void (await this.billingService.increaseUsageByUserId(booking.userId, { - uid: booking.uid, - startTime: booking.startTime, - })); - } - }); - return { status: SUCCESS_STATUS, data: createdBookings, @@ -381,42 +352,6 @@ export class BookingsController_2024_04_15 { throw new InternalServerErrorException("Could not create recurring booking."); } - @Post("/instant") - async createInstantBooking( - @Req() req: BookingRequest, - @Body() body: CreateBookingInput_2024_04_15, - @Headers(X_CAL_CLIENT_ID) clientId?: string, - @Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string - ): Promise> { - const oAuthClientId = - clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId)); - req.userId = (await this.getOwner(req))?.id ?? -1; - try { - const bookingReq = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed); - const instantMeeting = await this.instantBookingCreateService.createBooking({ - bookingData: bookingReq.body, - }); - - if (instantMeeting.userId && instantMeeting.bookingUid) { - const now = new Date(); - // add a 10 secondes delay to the usage incrementation to give some time to cancel the booking if needed - now.setSeconds(now.getSeconds() + 10); - void (await this.billingService.increaseUsageByUserId(instantMeeting.userId, { - uid: instantMeeting.bookingUid, - startTime: now, - })); - } - - return { - status: SUCCESS_STATUS, - data: instantMeeting, - }; - } catch (err) { - this.handleBookingErrors(err, "instant"); - } - throw new InternalServerErrorException("Could not create instant booking."); - } - private async getOwner(req: Request): Promise<{ id: number; uuid: string } | null> { try { const bearerToken = req.get("Authorization")?.replace("Bearer ", ""); diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts b/apps/api/v2/src/platform/bookings/2024-04-15/inputs/create-booking.input.ts similarity index 87% rename from apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts rename to apps/api/v2/src/platform/bookings/2024-04-15/inputs/create-booking.input.ts index eb12c1e915dab5..5880226487b33f 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts +++ b/apps/api/v2/src/platform/bookings/2024-04-15/inputs/create-booking.input.ts @@ -1,26 +1,26 @@ -import { ApiProperty, ApiPropertyOptional, ApiHideProperty } from "@nestjs/swagger"; +import { RESCHEDULED_BY_DOCS } from "@calcom/platform-types"; +import { ApiHideProperty, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Transform, Type } from "class-transformer"; import { + IsArray, IsBoolean, - IsTimeZone, + IsInt, IsNumber, - IsString, - IsOptional, - IsArray, IsObject, - ValidateNested, + IsOptional, + IsString, + IsTimeZone, isEmail, + registerDecorator, Validate, - IsInt, + ValidateNested, + ValidationOptions, } from "class-validator"; -import { ValidationOptions, registerDecorator } from "class-validator"; - -import { RESCHEDULED_BY_DOCS } from "@calcom/platform-types"; type BookingName = { firstName: string; lastName: string }; function ValidateBookingName(validationOptions?: ValidationOptions) { - return function (target: object, propertyName: string) { + return (target: object, propertyName: string) => { registerDecorator({ name: "validateBookingName", target: target.constructor, @@ -185,11 +185,6 @@ export class CreateBookingInput_2024_04_15 { @ApiHideProperty() routedTeamMemberIds?: number[]; - @IsNumber() - @IsOptional() - @ApiHideProperty() - routingFormResponseId?: number; - @IsBoolean() @IsOptional() @ApiHideProperty() @@ -200,19 +195,6 @@ export class CreateBookingInput_2024_04_15 { @ApiHideProperty() _isDryRun?: boolean; - // reroutingFormResponses is similar to rescheduling which can only be done by the organiser - // won't really be necessary here in our usecase though :- cc @Hariom - @IsObject() - @IsOptional() - @ApiHideProperty() - reroutingFormResponses?: Record< - string, - { - value: (string | number | string[]) & (string | number | string[] | undefined); - label?: string | undefined; - } - >; - @IsString() @IsOptional() @ApiPropertyOptional() diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-recurring-booking.input.ts b/apps/api/v2/src/platform/bookings/2024-04-15/inputs/create-recurring-booking.input.ts similarity index 88% rename from apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-recurring-booking.input.ts rename to apps/api/v2/src/platform/bookings/2024-04-15/inputs/create-recurring-booking.input.ts index 22abf230520acc..31a8607c58024a 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-recurring-booking.input.ts +++ b/apps/api/v2/src/platform/bookings/2024-04-15/inputs/create-recurring-booking.input.ts @@ -1,4 +1,4 @@ -import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input"; +import { CreateBookingInput_2024_04_15 } from "@/platform/bookings/2024-04-15/inputs/create-booking.input"; import { ApiPropertyOptional } from "@nestjs/swagger"; import { IsBoolean, IsString, IsNumber, IsOptional } from "class-validator"; diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/inputs/mark-no-show.input.ts b/apps/api/v2/src/platform/bookings/2024-04-15/inputs/mark-no-show.input.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-04-15/inputs/mark-no-show.input.ts rename to apps/api/v2/src/platform/bookings/2024-04-15/inputs/mark-no-show.input.ts diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-booking.output.ts b/apps/api/v2/src/platform/bookings/2024-04-15/outputs/get-booking.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-booking.output.ts rename to apps/api/v2/src/platform/bookings/2024-04-15/outputs/get-booking.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-bookings.output.ts b/apps/api/v2/src/platform/bookings/2024-04-15/outputs/get-bookings.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-bookings.output.ts rename to apps/api/v2/src/platform/bookings/2024-04-15/outputs/get-bookings.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/outputs/mark-no-show.output.ts b/apps/api/v2/src/platform/bookings/2024-04-15/outputs/mark-no-show.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-04-15/outputs/mark-no-show.output.ts rename to apps/api/v2/src/platform/bookings/2024-04-15/outputs/mark-no-show.output.ts diff --git a/apps/api/v2/src/platform/bookings/2024-08-13/bookings.module.ts b/apps/api/v2/src/platform/bookings/2024-08-13/bookings.module.ts new file mode 100644 index 00000000000000..254e1df9abeb8c --- /dev/null +++ b/apps/api/v2/src/platform/bookings/2024-08-13/bookings.module.ts @@ -0,0 +1,120 @@ +import { Module } from "@nestjs/common"; +import { BookingAttendeesController_2024_08_13 } from "@/platform/bookings/2024-08-13/controllers/booking-attendees.controller"; +import { BookingGuestsController_2024_08_13 } from "@/platform/bookings/2024-08-13/controllers/booking-guests.controller"; +import { BookingLocationController_2024_08_13 } from "@/platform/bookings/2024-08-13/controllers/booking-location.controller"; +import { BookingsController_2024_08_13 } from "@/platform/bookings/2024-08-13/controllers/bookings.controller"; +import { BookingPbacGuard } from "@/platform/bookings/2024-08-13/guards/booking-pbac.guard"; +import { BookingReferencesRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/booking-references.repository"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; +import { BookingAttendeesService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-attendees.service"; +import { BookingGuestsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-guests.service"; +import { BookingLocationService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-location.service"; +import { BookingLocationCalendarSyncService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-location-calendar-sync.service"; +import { BookingLocationCredentialService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-location-credential.service"; +import { BookingLocationIntegrationService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-location-integration.service"; +import { BookingReferencesService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-references.service"; +import { BookingVideoService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-video.service"; +import { BookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/bookings.service"; +import { CalVideoOutputService } from "@/platform/bookings/2024-08-13/services/cal-video.output.service"; +import { CalVideoService } from "@/platform/bookings/2024-08-13/services/cal-video.service"; +import { ErrorsBookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/errors.service"; +import { InputBookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/input.service"; +import { OutputBookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/output.service"; +import { OutputBookingReferencesService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/output-booking-references.service"; +import { PlatformBookingsService } from "@/platform/bookings/shared/platform-bookings.service"; +import { CalendarsRepository } from "@/platform/calendars/calendars.repository"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; +import { EventTypesModule_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/event-types.module"; +import { EventTypesModule_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.module"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; +import { OutputEventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/output-event-types.service"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { BookingAttendeesModule } from "@/lib/modules/booking-attendees.module"; +import { RecurringBookingModule } from "@/lib/modules/recurring-booking.module"; +import { RegularBookingModule } from "@/lib/modules/regular-booking.module"; +import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; +import { AppsRepository } from "@/modules/apps/apps.repository"; +import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module"; +import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository"; +import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; +import { KyselyModule } from "@/modules/kysely/kysely.module"; +import { MembershipsModule } from "@/modules/memberships/memberships.module"; +import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; +import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; +import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { ProfilesModule } from "@/modules/profiles/profiles.module"; +import { RedisModule } from "@/modules/redis/redis.module"; +import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; +import { StripeModule } from "@/modules/stripe/stripe.module"; +import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; +import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { TokensRepository } from "@/modules/tokens/tokens.repository"; +import { UsersModule } from "@/modules/users/users.module"; + +@Module({ + imports: [ + PrismaModule, + KyselyModule, + RedisModule, + TokensModule, + UsersModule, + BookingSeatModule, + SchedulesModule_2024_04_15, + EventTypesModule_2024_04_15, + EventTypesModule_2024_06_14, + StripeModule, + MembershipsModule, + ProfilesModule, + RegularBookingModule, + RecurringBookingModule, + BookingAttendeesModule, + ], + providers: [ + TokensRepository, + OAuthFlowService, + OAuthClientRepository, + OAuthClientUsersService, + BookingsService_2024_08_13, + BookingAttendeesService_2024_08_13, + BookingGuestsService_2024_08_13, + InputBookingsService_2024_08_13, + OutputBookingsService_2024_08_13, + OutputBookingReferencesService_2024_08_13, + OutputEventTypesService_2024_06_14, + BookingsRepository_2024_08_13, + EventTypesRepository_2024_06_14, + BookingSeatRepository, + ApiKeysRepository, + PlatformBookingsService, + CalendarsService, + CalendarsCacheService, + CredentialsRepository, + AppsRepository, + CalendarsRepository, + SelectedCalendarsRepository, + TeamsEventTypesRepository, + TeamsRepository, + ErrorsBookingsService_2024_08_13, + BookingReferencesService_2024_08_13, + BookingReferencesRepository_2024_08_13, + CalVideoService, + CalVideoOutputService, + BookingPbacGuard, + BookingLocationCalendarSyncService_2024_08_13, + BookingLocationCredentialService_2024_08_13, + BookingLocationIntegrationService_2024_08_13, + BookingLocationService_2024_08_13, + BookingVideoService_2024_08_13, + ], + controllers: [ + BookingsController_2024_08_13, + BookingAttendeesController_2024_08_13, + BookingGuestsController_2024_08_13, + BookingLocationController_2024_08_13, + ], + exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13], +}) +export class BookingsModule_2024_08_13 {} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/booking-attendees.controller.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/booking-attendees.controller.ts similarity index 90% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/booking-attendees.controller.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/booking-attendees.controller.ts index 29754ce194debc..330dca553f4789 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/booking-attendees.controller.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/booking-attendees.controller.ts @@ -13,15 +13,15 @@ import { UseGuards, } from "@nestjs/common"; import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; -import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard"; -import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard"; -import { AddAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-attendee.output"; +import { BookingPbacGuard } from "@/platform/bookings/2024-08-13/guards/booking-pbac.guard"; +import { BookingUidGuard } from "@/platform/bookings/2024-08-13/guards/booking-uid.guard"; +import { AddAttendeeOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/add-attendee.output"; import { GetBookingAttendeeOutput_2024_08_13, GetBookingAttendeesOutput_2024_08_13, -} from "@/ee/bookings/2024-08-13/outputs/get-booking-attendees.output"; -import { RemoveAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/remove-attendee.output"; -import { BookingAttendeesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-attendees.service"; +} from "@/platform/bookings/2024-08-13/outputs/get-booking-attendees.output"; +import { RemoveAttendeeOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/remove-attendee.output"; +import { BookingAttendeesService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-attendees.service"; import { VERSION_2024_08_13, VERSION_2024_08_13_VALUE } from "@/lib/api-versions"; import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; import { Throttle } from "@/lib/endpoint-throttler-decorator"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/booking-guests.controller.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/booking-guests.controller.ts similarity index 91% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/booking-guests.controller.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/booking-guests.controller.ts index 015851bbe7b898..9487602fe9f32e 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/booking-guests.controller.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/booking-guests.controller.ts @@ -1,6 +1,6 @@ -import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard"; -import { AddGuestsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-guests.output"; -import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service"; +import { BookingUidGuard } from "@/platform/bookings/2024-08-13/guards/booking-uid.guard"; +import { AddGuestsOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/add-guests.output"; +import { BookingGuestsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-guests.service"; import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions"; import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; import { Throttle } from "@/lib/endpoint-throttler-decorator"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/booking-location.controller.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/booking-location.controller.ts similarity index 91% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/booking-location.controller.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/booking-location.controller.ts index a83bfd26c55805..8af6264c3d803d 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/booking-location.controller.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/booking-location.controller.ts @@ -10,9 +10,9 @@ import { } from "@calcom/platform-types"; import { Body, Controller, HttpCode, HttpStatus, Param, Patch, UseGuards } from "@nestjs/common"; import { ApiExtraModels, ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; -import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard"; -import { UpdateBookingLocationOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/update-location.output"; -import { BookingLocationService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location.service"; +import { BookingUidGuard } from "@/platform/bookings/2024-08-13/guards/booking-uid.guard"; +import { UpdateBookingLocationOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/update-location.output"; +import { BookingLocationService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-location.service"; import { VERSION_2024_08_13, VERSION_2024_08_13_VALUE } from "@/lib/api-versions"; import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; import { Throttle } from "@/lib/endpoint-throttler-decorator"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/bookings.controller.ts similarity index 90% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/bookings.controller.ts index 0153a514fe6311..97874eeb42a14b 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/bookings.controller.ts @@ -7,7 +7,6 @@ import { CreateBookingInput, CreateBookingInput_2024_08_13, CreateBookingInputPipe, - CreateInstantBookingInput_2024_08_13, CreateRecurringBookingInput_2024_08_13, DeclineBookingInput_2024_08_13, GetBookingOutput_2024_08_13, @@ -45,19 +44,19 @@ import { getSchemaPath, } from "@nestjs/swagger"; import { Request } from "express"; -import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard"; -import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard"; -import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input"; -import { BookingReferencesOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/booking-references.output"; -import { CalendarLinksOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output"; -import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output"; -import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; -import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output"; -import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output"; -import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output"; -import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service"; -import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; -import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service"; +import { BookingPbacGuard } from "@/platform/bookings/2024-08-13/guards/booking-pbac.guard"; +import { BookingUidGuard } from "@/platform/bookings/2024-08-13/guards/booking-uid.guard"; +import { BookingReferencesFilterInput_2024_08_13 } from "@/platform/bookings/2024-08-13/inputs/booking-references-filter.input"; +import { BookingReferencesOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/booking-references.output"; +import { CalendarLinksOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/calendar-links.output"; +import { CancelBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/cancel-booking.output"; +import { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; +import { MarkAbsentBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/mark-absent.output"; +import { ReassignBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/reassign-booking.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/reschedule-booking.output"; +import { BookingReferencesService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-references.service"; +import { BookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/bookings.service"; +import { CalVideoService } from "@/platform/bookings/2024-08-13/services/cal-video.service"; import { VERSION_2024_08_13, VERSION_2024_08_13_VALUE } from "@/lib/api-versions"; import { API_KEY_OR_ACCESS_TOKEN_HEADER, @@ -65,7 +64,6 @@ import { OPTIONAL_X_CAL_CLIENT_ID_HEADER, OPTIONAL_X_CAL_SECRET_KEY_HEADER, } from "@/lib/docs/headers"; -import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; import { AuthOptionalUser, GetOptionalUser, @@ -119,8 +117,6 @@ export class BookingsController_2024_08_13 { Meaning that the request bodies are equal but the outcome depends on what kind of event type it is with the goal of making it as seamless for developers as possible. - For team event types it is possible to create instant meeting. To do that just pass \`"instant": true\` to the request body. - The start needs to be in UTC aka if the timezone is GMT+2 in Rome and meeting should start at 11, then UTC time should have hours 09:00 aka without time zone. Finally, there are 2 ways to book an event type belonging to an individual user: @@ -134,7 +130,7 @@ export class BookingsController_2024_08_13 { If you are creating a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner. - For event types that have SMS reminders workflow, you need to pass the attendee's phone number in the request body via \`attendee.phoneNumber\` (e.g., "+19876543210" in international format). This is an optional field, but becomes required when SMS reminders are enabled for the event type. For the complete attendee object structure, see the [attendee object](https://cal.com/docs/api-reference/v2/bookings/create-a-booking#body-attendee) documentation. + For event types that have SMS reminders enabled, you need to pass the attendee's phone number in the request body via \`attendee.phoneNumber\` (e.g., "+19876543210" in international format). This is an optional field, but becomes required when SMS reminders are enabled for the event type. For the complete attendee object structure, see the attendee schema in the \`/docs\` Swagger endpoint. 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. `, @@ -143,18 +139,13 @@ export class BookingsController_2024_08_13 { schema: { oneOf: [ { $ref: getSchemaPath(CreateBookingInput_2024_08_13) }, - { $ref: getSchemaPath(CreateInstantBookingInput_2024_08_13) }, { $ref: getSchemaPath(CreateRecurringBookingInput_2024_08_13) }, ], }, description: - "Accepts different types of booking input: Create Booking (Option 1), Create Instant Booking (Option 2), or Create Recurring Booking (Option 3)", + "Accepts different types of booking input: Create Booking (Option 1) or Create Recurring Booking (Option 2)", }) - @ApiExtraModels( - CreateBookingInput_2024_08_13, - CreateInstantBookingInput_2024_08_13, - CreateRecurringBookingInput_2024_08_13 - ) + @ApiExtraModels(CreateBookingInput_2024_08_13, CreateRecurringBookingInput_2024_08_13) async createBooking( @Body(new CreateBookingInputPipe()) body: CreateBookingInput, @@ -163,12 +154,6 @@ export class BookingsController_2024_08_13 { ): Promise { const booking = await this.bookingsService.createBooking(request, body, user); - if (Array.isArray(booking)) { - await this.bookingsService.billBookings(booking); - } else { - await this.bookingsService.billBooking(booking); - } - return { status: SUCCESS_STATUS, data: booking, @@ -341,7 +326,6 @@ export class BookingsController_2024_08_13 { @GetOptionalUser() user: AuthOptionalUser ): Promise { const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body, user); - await this.bookingsService.billRescheduledBooking(newBooking, bookingUid); return { status: SUCCESS_STATUS, @@ -554,7 +538,6 @@ export class BookingsController_2024_08_13 { } @Get("/:bookingUid/references") - @PlatformPlan("SCALE") @UseGuards(ApiAuthGuard, BookingUidGuard) @Permissions([BOOKING_READ]) @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/add-guests.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/add-guests.e2e-spec.ts similarity index 97% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/add-guests.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/add-guests.e2e-spec.ts index 6450220a98c0ad..4b43dcb7258244 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/add-guests.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/add-guests.e2e-spec.ts @@ -24,11 +24,11 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository 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 { AddGuestsOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/add-guests.output"; +import { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/api-key-bookings.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/api-key-bookings.e2e-spec.ts similarity index 94% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/api-key-bookings.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/api-key-bookings.e2e-spec.ts index 982e9d5829327c..bcf2ea37b86bd0 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/api-key-bookings.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/api-key-bookings.e2e-spec.ts @@ -27,12 +27,12 @@ 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"; -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 { CancelBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/cancel-booking.output"; +import { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/reschedule-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; @@ -215,8 +215,6 @@ describe("Bookings Endpoints 2024-08-13", () => { if (responseDataIsBooking(responseBody.data)) { expect(responseBody.status).toEqual(SUCCESS_STATUS); expect(responseBody.data).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.reschedulingReason).toEqual(body.reschedulingReason); expect(data.start).toEqual(body.start); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/booking-access-auth.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/booking-access-auth.e2e-spec.ts similarity index 95% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/booking-access-auth.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/booking-access-auth.e2e-spec.ts index 7d9313f9c00820..51f67ee1e51fa9 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/booking-access-auth.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/booking-access-auth.e2e-spec.ts @@ -14,10 +14,10 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { randomString } from "test/utils/randomString"; 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 { CalVideoService } from "@/platform/bookings/2024-08-13/services/cal-video.service"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/booking-fields.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/booking-fields.e2e-spec.ts similarity index 98% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/booking-fields.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/booking-fields.e2e-spec.ts index 8e480f4fabc190..8b6371a2d6c825 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/booking-fields.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/booking-fields.e2e-spec.ts @@ -19,10 +19,10 @@ 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 { 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 { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/calendar-links.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/calendar-links.e2e-spec.ts similarity index 95% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/calendar-links.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/calendar-links.e2e-spec.ts index d1f13afb7320fe..9b378713ec88e5 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/calendar-links.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/calendar-links.e2e-spec.ts @@ -13,9 +13,9 @@ 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 { 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 { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/confirm-bookings.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/confirm-bookings.e2e-spec.ts similarity index 96% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/confirm-bookings.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/confirm-bookings.e2e-spec.ts index e1057ebb6dab94..014477b2986c64 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/confirm-bookings.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/confirm-bookings.e2e-spec.ts @@ -14,9 +14,9 @@ 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 { 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 { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/confirm-emails.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/emails/confirm-emails.e2e-spec.ts similarity index 97% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/confirm-emails.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/emails/confirm-emails.e2e-spec.ts index 9f517896796b14..3477764b82859c 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/confirm-emails.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/emails/confirm-emails.e2e-spec.ts @@ -30,11 +30,11 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository 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 { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/reschedule-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/user-emails.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/emails/user-emails.e2e-spec.ts similarity index 96% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/user-emails.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/emails/user-emails.e2e-spec.ts index 9abed33b928f81..e2d634f93897f2 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/user-emails.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/emails/user-emails.e2e-spec.ts @@ -27,12 +27,12 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository 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"; -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 { CancelBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/cancel-booking.output"; +import { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/reschedule-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/services/schedules.service"; 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"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/get-attendees.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/get-attendees.e2e-spec.ts similarity index 96% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/get-attendees.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/get-attendees.e2e-spec.ts index a812a6dabc1424..28998cda7f2ad2 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/get-attendees.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/get-attendees.e2e-spec.ts @@ -15,14 +15,14 @@ import { randomString } from "test/utils/randomString"; import { mockThrottlerGuard } from "test/utils/withNoThrottler"; 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 { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; import { GetBookingAttendeeOutput_2024_08_13, GetBookingAttendeesOutput_2024_08_13, -} from "@/ee/bookings/2024-08-13/outputs/get-booking-attendees.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"; +} from "@/platform/bookings/2024-08-13/outputs/get-booking-attendees.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/recurring-bookings.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/recurring-bookings.e2e-spec.ts similarity index 98% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/recurring-bookings.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/recurring-bookings.e2e-spec.ts index 5c79586bb1ba8a..bb69c7bb7fad49 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/recurring-bookings.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/recurring-bookings.e2e-spec.ts @@ -19,10 +19,10 @@ 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 { 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 { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/remove-attendee.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/remove-attendee.e2e-spec.ts similarity index 96% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/remove-attendee.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/remove-attendee.e2e-spec.ts index 912769cece4b31..00497451181893 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/remove-attendee.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/remove-attendee.e2e-spec.ts @@ -21,12 +21,12 @@ import { randomString } from "test/utils/randomString"; import { mockThrottlerGuard } from "test/utils/withNoThrottler"; 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 { RemoveAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/remove-attendee.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 { AddGuestsOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/add-guests.output"; +import { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; +import { RemoveAttendeeOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/remove-attendee.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/reschedule-bookings.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/reschedule-bookings.e2e-spec.ts similarity index 94% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/reschedule-bookings.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/reschedule-bookings.e2e-spec.ts index 29e27e5dc43f01..5e64b6bfa7bb55 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/reschedule-bookings.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/reschedule-bookings.e2e-spec.ts @@ -16,11 +16,11 @@ 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"; -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 { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/reschedule-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts similarity index 98% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts index ac1be9af05aec8..0f5bfb20e1151b 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts @@ -24,11 +24,11 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository 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 { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/reschedule-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; @@ -555,7 +555,7 @@ describe("Bookings Endpoints 2024-08-13", () => { .expect(400); expect(response.body.error.message).toEqual( - `Booking with uid=${createdSeatedBooking.uid} is a seated booking which means you have to provide seatUid in the request body to specify which seat specifically you want to reschedule. First, fetch the booking using https://cal.com/docs/api-reference/v2/bookings/get-a-booking and then within the attendees array you will find the seatUid of the booking you want to reschedule. Second, provide the seatUid in the request body to specify which seat specifically you want to reschedule using the reschedule endpoint https://cal.com/docs/api-reference/v2/bookings/reschedule-a-booking#option-2` + `Booking with uid=${createdSeatedBooking.uid} is a seated booking which means you have to provide seatUid in the request body to specify which seat specifically you want to reschedule. First, fetch the booking using the GET /v2/bookings/{uid} endpoint and then within the attendees array you will find the seatUid of the booking you want to reschedule. Second, provide the seatUid in the request body to specify which seat specifically you want to reschedule using the POST /v2/bookings/{uid}/reschedule endpoint.` ); }); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/update-booking-location.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/update-booking-location.e2e-spec.ts similarity index 98% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/update-booking-location.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/update-booking-location.e2e-spec.ts index 5c6ce6a894a8b5..914e99f38cb8e2 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/update-booking-location.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/update-booking-location.e2e-spec.ts @@ -78,10 +78,10 @@ import { randomString } from "test/utils/randomString"; import { mockThrottlerGuard } from "test/utils/withNoThrottler"; 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 { UpdateBookingLocationOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/update-location.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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 { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts similarity index 94% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts index 27252cf7e2312c..585394b4299668 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts @@ -20,7 +20,7 @@ import type { 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 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"; @@ -31,21 +31,19 @@ import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-type import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; -import { 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"; -import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output"; -import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service"; -import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output"; -import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; -import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; -import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; +import { CancelBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/cancel-booking.output"; +import { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; +import { MarkAbsentBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/mark-absent.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/reschedule-booking.output"; +import { CalVideoService } from "@/platform/bookings/2024-08-13/services/cal-video.service"; +import { CreateEventTypeOutput_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/outputs/create-event-type.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; @@ -60,8 +58,6 @@ describe("Bookings Endpoints 2024-08-13", () => { let schedulesService: SchedulesService_2024_04_15; let eventTypesRepositoryFixture: EventTypesRepositoryFixture; let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let workflowReminderRepositoryFixture: WorkflowReminderRepositoryFixture; - let workflowRepositoryFixture: WorkflowRepositoryFixture; let oAuthClient: PlatformOAuthClient; let teamRepositoryFixture: TeamRepositoryFixture; @@ -70,8 +66,6 @@ describe("Bookings Endpoints 2024-08-13", () => { let eventTypeId: number; let eventType: EventType; - let eventTypeWithAttendeeSmsReminder: EventType; - let workflow: Workflow; const eventTypeSlug = `user-bookings-event-type-${randomString()}`; let recurringEventTypeId: number; const recurringEventTypeSlug = `user-bookings-event-type-${randomString()}`; @@ -102,8 +96,6 @@ describe("Bookings Endpoints 2024-08-13", () => { oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); schedulesService = moduleRef.get(SchedulesService_2024_04_15); - workflowReminderRepositoryFixture = new WorkflowReminderRepositoryFixture(moduleRef); - workflowRepositoryFixture = new WorkflowRepositoryFixture(moduleRef); organization = await teamRepositoryFixture.create({ name: `user-bookings-organization-${randomString()}`, @@ -195,85 +187,6 @@ describe("Bookings Endpoints 2024-08-13", () => { rating: 10, }); - workflow = await workflowRepositoryFixture.create({ - user: { - connect: { - id: user.id, - }, - }, - trigger: "BEFORE_EVENT", - time: 24, - timeUnit: "HOUR", - position: 0, - isActiveOnAll: false, - name: "Attendee SMS Reminder", - steps: { - create: { - stepNumber: 1, - action: "SMS_ATTENDEE", - sendTo: null, - reminderBody: - "Hi {ATTENDEE}, this is a reminder that your meeting ({EVENT_NAME}) with {ORGANIZER} is on {EVENT_DATE_YYYY MMM D} at {EVENT_TIME_h:mma} {TIMEZONE}.", - emailSubject: "Reminder: {EVENT_NAME} - {EVENT_DATE_ddd, MMM D, YYYY h:mma}", - template: "REMINDER", - numberRequired: true, - sender: "Cal", - numberVerificationPending: false, - includeCalendarEvent: false, - verifiedAt: new Date(), - }, - }, - }); - - eventTypeWithAttendeeSmsReminder = await eventTypesRepositoryFixture.create( - { - title: "event with attendee sms reminder", - slug: "event-with-attendee-sms-reminder", - length: 15, - bookingFields: [ - { - name: "smsReminderNumber", - type: "phone", - sources: [ - { - id: String(workflow.id), - type: "workflow", - label: "Workflow", - editUrl: `/workflows/${workflow.id}`, - fieldRequired: true, - }, - ], - editable: "system", - required: true, - defaultLabel: "number_text_notifications", - defaultPlaceholder: "enter_phone_number", - }, - ], - metadata: { - disableStandardEmails: { - all: { - attendee: true, - host: true, - }, - confirmation: { - host: true, - attendee: true, - }, - }, - }, - workflows: { - create: { - workflow: { - connect: { - id: workflow.id, - }, - }, - }, - }, - }, - user.id - ); - app = moduleRef.createNestApplication(); bootstrap(app as NestExpressApplication); @@ -2471,93 +2384,6 @@ describe("Bookings Endpoints 2024-08-13", () => { }); }); - describe("create booking with attendee sms reminder", () => { - it("should not be able create a booking if attendee sms reminder is missing", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(), - eventTypeId: eventTypeWithAttendeeSmsReminder.id, - attendee: { - name: "Mr Proper", - email: "mr_proper@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - }; - - const response = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(400); - - expect(response.body.error.message).toEqual( - `Missing attendee phone number - it is required by the event type. Pass it as "attendee.phoneNumber" string in the request.` - ); - }); - - it("should be able create a booking if attendee sms reminder is provided", async () => { - const phoneNumber = "+37122222222"; - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(), - eventTypeId: eventTypeWithAttendeeSmsReminder.id, - attendee: { - name: "Mr Proper", - email: "mr_proper@gmail.com", - timeZone: "Europe/Rome", - language: "it", - phoneNumber, - }, - }; - - const response = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(user.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 15, 15, 0)).toISOString()); - expect(data.duration).toEqual(15); - expect(data.eventTypeId).toEqual(eventTypeWithAttendeeSmsReminder.id); - expect(data.attendees.length).toEqual(1); - expect(data.attendees[0]).toEqual({ - name: body.attendee.name, - email: body.attendee.email, - displayEmail: body.attendee.email, - timeZone: body.attendee.timeZone, - language: body.attendee.language, - phoneNumber: body.attendee.phoneNumber, - absent: false, - }); - expect(data.bookingFieldsResponses).toEqual({ - name: body.attendee.name, - email: body.attendee.email, - displayEmail: body.attendee.email, - attendeePhoneNumber: body.attendee.phoneNumber, - smsReminderNumber: body.attendee.phoneNumber, - }); - const dbBooking = await bookingsRepositoryFixture.getById(data.id); - expect(dbBooking?.smsReminderNumber).toEqual(phoneNumber); - const dbWorkflowReminder = await workflowReminderRepositoryFixture.getByBookingUid(data.uid); - expect(dbWorkflowReminder?.method).toEqual("SMS"); - } else { - throw new Error("Invalid response data"); - } - }); - }); - describe("cant't cancel already cancelled booking", () => { it("should not be able to cancel alraedy cancelled booking", async () => { const cancelledBooking = await bookingsRepositoryFixture.create({ diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/variable-length-bookings.e2e-spec.ts b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/variable-length-bookings.e2e-spec.ts similarity index 96% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/variable-length-bookings.e2e-spec.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/variable-length-bookings.e2e-spec.ts index 791eda9a34617d..69eacef6afbd62 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/variable-length-bookings.e2e-spec.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/controllers/e2e/variable-length-bookings.e2e-spec.ts @@ -21,11 +21,11 @@ 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"; -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 { CreateBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/create-booking.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/reschedule-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/guards/booking-pbac.guard.ts b/apps/api/v2/src/platform/bookings/2024-08-13/guards/booking-pbac.guard.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/guards/booking-pbac.guard.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/guards/booking-pbac.guard.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/guards/booking-uid.guard.ts b/apps/api/v2/src/platform/bookings/2024-08-13/guards/booking-uid.guard.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/guards/booking-uid.guard.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/guards/booking-uid.guard.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/inputs/booking-references-filter.input.ts b/apps/api/v2/src/platform/bookings/2024-08-13/inputs/booking-references-filter.input.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/inputs/booking-references-filter.input.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/inputs/booking-references-filter.input.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/add-attendee.output.ts b/apps/api/v2/src/platform/bookings/2024-08-13/outputs/add-attendee.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/add-attendee.output.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/outputs/add-attendee.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/add-guests.output.ts b/apps/api/v2/src/platform/bookings/2024-08-13/outputs/add-guests.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/add-guests.output.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/outputs/add-guests.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/booking-references.output.ts b/apps/api/v2/src/platform/bookings/2024-08-13/outputs/booking-references.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/booking-references.output.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/outputs/booking-references.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/calendar-links.output.ts b/apps/api/v2/src/platform/bookings/2024-08-13/outputs/calendar-links.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/calendar-links.output.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/outputs/calendar-links.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts b/apps/api/v2/src/platform/bookings/2024-08-13/outputs/cancel-booking.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/outputs/cancel-booking.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts b/apps/api/v2/src/platform/bookings/2024-08-13/outputs/create-booking.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/outputs/create-booking.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking-attendees.output.ts b/apps/api/v2/src/platform/bookings/2024-08-13/outputs/get-booking-attendees.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking-attendees.output.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/outputs/get-booking-attendees.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts b/apps/api/v2/src/platform/bookings/2024-08-13/outputs/mark-absent.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/outputs/mark-absent.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reassign-booking.output.ts b/apps/api/v2/src/platform/bookings/2024-08-13/outputs/reassign-booking.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/reassign-booking.output.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/outputs/reassign-booking.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/remove-attendee.output.ts b/apps/api/v2/src/platform/bookings/2024-08-13/outputs/remove-attendee.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/remove-attendee.output.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/outputs/remove-attendee.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts b/apps/api/v2/src/platform/bookings/2024-08-13/outputs/reschedule-booking.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/outputs/reschedule-booking.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/update-location.output.ts b/apps/api/v2/src/platform/bookings/2024-08-13/outputs/update-location.output.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/update-location.output.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/outputs/update-location.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/repositories/booking-references.repository.ts b/apps/api/v2/src/platform/bookings/2024-08-13/repositories/booking-references.repository.ts similarity index 90% rename from apps/api/v2/src/ee/bookings/2024-08-13/repositories/booking-references.repository.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/repositories/booking-references.repository.ts index 76a75130b34958..9f774171d36c89 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/repositories/booking-references.repository.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/repositories/booking-references.repository.ts @@ -1,4 +1,4 @@ -import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input"; +import { BookingReferencesFilterInput_2024_08_13 } from "@/platform/bookings/2024-08-13/inputs/booking-references-filter.input"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { Injectable } from "@nestjs/common"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/repositories/bookings.repository.ts b/apps/api/v2/src/platform/bookings/2024-08-13/repositories/bookings.repository.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/repositories/bookings.repository.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/repositories/bookings.repository.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-attendees.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-attendees.service.ts similarity index 90% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/booking-attendees.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/booking-attendees.service.ts index d8dff6a01004f7..f6492bc3b4a13a 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-attendees.service.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-attendees.service.ts @@ -2,11 +2,11 @@ import { ErrorCode, ErrorWithCode } from "@calcom/platform-libraries/errors"; import type { AddAttendeeInput_2024_08_13 } from "@calcom/platform-types"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { plainToClass } from "class-transformer"; -import { BookingAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-attendee.output"; -import { BookingAttendeeWithId_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking-attendees.output"; -import { RemovedAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/remove-attendee.output"; -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; +import { BookingAttendeeOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/add-attendee.output"; +import { BookingAttendeeWithId_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/get-booking-attendees.output"; +import { RemovedAttendeeOutput_2024_08_13 } from "@/platform/bookings/2024-08-13/outputs/remove-attendee.output"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; +import { PlatformBookingsService } from "@/platform/bookings/shared/platform-bookings.service"; import { BookingAttendeesService } from "@/lib/services/booking-attendees.service"; import type { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; @@ -100,7 +100,6 @@ export class BookingAttendeesService_2024_08_13 { uuid: user.uuid, }, emailsEnabled, - actionSource: "API_V2", }); return plainToClass( @@ -147,7 +146,6 @@ export class BookingAttendeesService_2024_08_13 { uuid: user.uuid, }, emailsEnabled, - actionSource: "API_V2", }); return plainToClass( diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-guests.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-guests.service.ts similarity index 87% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/booking-guests.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/booking-guests.service.ts index d5b7c483f547ff..cd1e57fe1b2f68 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-guests.service.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-guests.service.ts @@ -1,6 +1,6 @@ -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; -import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; +import { BookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/bookings.service"; +import { PlatformBookingsService } from "@/platform/bookings/shared/platform-bookings.service"; import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; import { Injectable, Logger, HttpException, NotFoundException, BadRequestException } from "@nestjs/common"; @@ -48,7 +48,6 @@ export class BookingGuestsService_2024_08_13 { input: { bookingId: booking.id, guests: input.guests }, emailsEnabled, actionSource: "API_V2", - impersonatedByUserUuid: null, }); if (res.message === "Guests added") { return await this.bookingsService.getBooking(bookingUid, user); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location-calendar-sync.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-location-calendar-sync.service.ts similarity index 94% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location-calendar-sync.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/booking-location-calendar-sync.service.ts index bff22d8dbc9966..4b3aab8f2dfbce 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location-calendar-sync.service.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-location-calendar-sync.service.ts @@ -1,8 +1,8 @@ import type { BookingWithUserAndEventDetails, CalendarEvent } from "@calcom/platform-libraries"; import { buildCalEventFromBooking, sendLocationChangeEmailsAndSMS, updateEvent } from "@calcom/platform-libraries"; import { Injectable, Logger } from "@nestjs/common"; -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { BookingLocationCredentialService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location-credential.service"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; +import { BookingLocationCredentialService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-location-credential.service"; @Injectable() export class BookingLocationCalendarSyncService_2024_08_13 { diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location-credential.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-location-credential.service.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location-credential.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/booking-location-credential.service.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location-integration.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-location-integration.service.ts similarity index 88% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location-integration.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/booking-location-integration.service.ts index 9aca9d68de5945..412ea5375dc454 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location-integration.service.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-location-integration.service.ts @@ -1,6 +1,5 @@ import type { BookingWithUserAndEventDetails, CalendarEvent } from "@calcom/platform-libraries"; import { BookingReferenceRepository, updateEvent } from "@calcom/platform-libraries"; -import { makeUserActor } from "@calcom/platform-libraries/bookings"; import { createMeeting, FAKE_DAILY_CREDENTIAL } from "@calcom/platform-libraries/conferencing"; import type { Integration_2024_08_13 } from "@calcom/platform-types"; import type { @@ -11,15 +10,13 @@ import type { } from "@calcom/platform-types/bookings/2024-08-13/outputs/booking.output"; import type { Booking, Prisma } from "@calcom/prisma/client"; import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { BookingLocationCalendarSyncService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location-calendar-sync.service"; -import { BookingLocationCredentialService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location-credential.service"; -import { BookingVideoService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-video.service"; -import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; -import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; -import { apiToInternalintegrationsMapping } from "@/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/locations"; -import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; -import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; +import { BookingLocationCalendarSyncService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-location-calendar-sync.service"; +import { BookingLocationCredentialService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-location-credential.service"; +import { BookingVideoService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-video.service"; +import { BookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/bookings.service"; +import { InputBookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/input.service"; +import { apiToInternalintegrationsMapping } from "@/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/locations"; import type { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; export type BookingLocationResponse = @@ -62,8 +59,6 @@ export class BookingLocationIntegrationService_2024_08_13 { private readonly bookingsService: BookingsService_2024_08_13, private readonly inputService: InputBookingsService_2024_08_13, private readonly bookingVideoService: BookingVideoService_2024_08_13, - private readonly bookingEventHandlerService: BookingEventHandlerService, - private readonly featuresRepository: PrismaFeaturesRepository, private readonly calendarSyncService: BookingLocationCalendarSyncService_2024_08_13, private readonly credentialService: BookingLocationCredentialService_2024_08_13 ) {} @@ -353,25 +348,6 @@ export class BookingLocationIntegrationService_2024_08_13 { bookingLocation: string, evt: CalendarEvent ): Promise { - const organizationId = ctx.existingBookingHost?.organizationId ?? null; - const isBookingAuditEnabled = organizationId - ? await this.featuresRepository.checkIfTeamHasFeature(organizationId, "booking-audit") - : false; - - await this.bookingEventHandlerService.onLocationChanged({ - bookingUid: ctx.existingBooking.uid, - actor: makeUserActor(ctx.user.uuid), - organizationId, - source: "API_V2", - auditData: { - location: { - old: ctx.existingBooking.location, - new: bookingLocation, - }, - }, - isBookingAuditEnabled, - }); - await this.calendarSyncService.sendLocationChangeNotifications( evt, ctx.existingBooking.uid, diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-location.service.ts similarity index 81% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/booking-location.service.ts index 72cbd482e0c044..fa48d3cef5c1c5 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location.service.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-location.service.ts @@ -1,4 +1,3 @@ -import { makeUserActor } from "@calcom/platform-libraries/bookings"; import type { UpdateBookingInputLocation_2024_08_13, UpdateBookingLocationInput_2024_08_13, @@ -11,19 +10,17 @@ import { Logger, NotFoundException, } from "@nestjs/common"; -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { BookingLocationCalendarSyncService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location-calendar-sync.service"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; +import { BookingLocationCalendarSyncService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-location-calendar-sync.service"; import { type BookingForLocationUpdate, BookingLocationIntegrationService_2024_08_13, type BookingLocationResponse, -} from "@/ee/bookings/2024-08-13/services/booking-location-integration.service"; -import { BookingVideoService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-video.service"; -import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; -import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; -import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service"; +} from "@/platform/bookings/2024-08-13/services/booking-location-integration.service"; +import { BookingVideoService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/booking-video.service"; +import { BookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/bookings.service"; +import { InputBookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/input.service"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; import type { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service"; import { UsersRepository } from "@/modules/users/users.repository"; @@ -39,9 +36,7 @@ export class BookingLocationService_2024_08_13 { private readonly inputService: InputBookingsService_2024_08_13, private readonly eventTypesRepository: EventTypesRepository_2024_06_14, private readonly eventTypeAccessService: EventTypeAccessService, - private readonly bookingEventHandlerService: BookingEventHandlerService, private readonly bookingVideoService: BookingVideoService_2024_08_13, - private readonly featuresRepository: PrismaFeaturesRepository, private readonly integrationService: BookingLocationIntegrationService_2024_08_13, private readonly calendarSyncService: BookingLocationCalendarSyncService_2024_08_13 ) {} @@ -141,25 +136,6 @@ export class BookingLocationService_2024_08_13 { metadata: metadataWithoutVideoUrl as Prisma.InputJsonValue, }); - const organizationId = existingBookingHost.organizationId ?? null; - const isBookingAuditEnabled = organizationId - ? await this.featuresRepository.checkIfTeamHasFeature(organizationId, "booking-audit") - : false; - - await this.bookingEventHandlerService.onLocationChanged({ - bookingUid: existingBooking.uid, - actor: makeUserActor(user.uuid), - organizationId, - source: "API_V2", - auditData: { - location: { - old: oldLocation, - new: bookingLocation, - }, - }, - isBookingAuditEnabled, - }); - if (bookingLocation) { await this.sendLocationChangeNotifications(existingBooking.id, existingBooking.uid, bookingLocation); } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-references.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-references.service.ts similarity index 75% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/booking-references.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/booking-references.service.ts index aad7f2b5fafe6d..d7acf6882a5486 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-references.service.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-references.service.ts @@ -1,7 +1,7 @@ -import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository"; -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input"; -import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service"; +import { BookingReferencesRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/booking-references.repository"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; +import { BookingReferencesFilterInput_2024_08_13 } from "@/platform/bookings/2024-08-13/inputs/booking-references-filter.input"; +import { OutputBookingReferencesService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/output-booking-references.service"; import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; @Injectable() diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-video.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-video.service.ts similarity index 97% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/booking-video.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/booking-video.service.ts index 960511c37e01dc..ac1b1d5ce5cf61 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-video.service.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/services/booking-video.service.ts @@ -2,7 +2,7 @@ import type { CredentialForCalendarService } from "@calcom/platform-libraries"; import { CredentialRepository } from "@calcom/platform-libraries"; import { deleteMeeting, FAKE_DAILY_CREDENTIAL } from "@calcom/platform-libraries/conferencing"; import { Injectable, Logger } from "@nestjs/common"; -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; @Injectable() export class BookingVideoService_2024_08_13 { diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/bookings.service.ts similarity index 90% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/bookings.service.ts index 532120f76aaa43..8b96f625a994bd 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/services/bookings.service.ts @@ -16,7 +16,6 @@ import { CancelBookingInput, CreateBookingInput, CreateBookingInput_2024_08_13, - CreateInstantBookingInput_2024_08_13, CreateRecurringBookingInput_2024_08_13, GetBookingsInput_2024_08_13, GetRecurringSeatedBookingOutput_2024_08_13, @@ -41,27 +40,23 @@ import { import { Request } from "express"; import { DateTime } from "luxon"; import { z } from "zod"; -import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output"; -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service"; -import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; -import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; -import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { CalendarLink } from "@/platform/bookings/2024-08-13/outputs/calendar-links.output"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; +import { ErrorsBookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/errors.service"; +import { InputBookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/input.service"; +import { OutputBookingsService_2024_08_13 } from "@/platform/bookings/2024-08-13/services/output.service"; +import { PlatformBookingsService } from "@/platform/bookings/shared/platform-bookings.service"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; import { getPagination } from "@/lib/pagination/pagination"; -import { InstantBookingCreateService } from "@/lib/services/instant-booking-create.service"; import { RecurringBookingService } from "@/lib/services/recurring-booking.service"; import { RegularBookingService } from "@/lib/services/regular-booking.service"; import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator"; import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; -import { BillingService } from "@/modules/billing/services/billing.service"; import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository"; import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service"; import { KyselyReadService } from "@/modules/kysely/kysely-read.service"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; @@ -102,19 +97,15 @@ export class BookingsService_2024_08_13 { private readonly eventTypesRepository: EventTypesRepository_2024_06_14, private readonly prismaReadService: PrismaReadService, private readonly kyselyReadService: KyselyReadService, - private readonly billingService: BillingService, private readonly usersService: UsersService, private readonly usersRepository: UsersRepository, private readonly platformBookingsService: PlatformBookingsService, private readonly oAuthClientRepository: OAuthClientRepository, - private readonly organizationsTeamsRepository: OrganizationsTeamsRepository, - private readonly organizationsRepository: OrganizationsRepository, private readonly teamsRepository: TeamsRepository, private readonly teamsEventTypesRepository: TeamsEventTypesRepository, private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13, private readonly regularBookingService: RegularBookingService, private readonly recurringBookingService: RecurringBookingService, - private readonly instantBookingCreateService: InstantBookingCreateService, private readonly eventTypeAccessService: EventTypeAccessService ) {} @@ -145,10 +136,6 @@ export class BookingsService_2024_08_13 { body.eventTypeId = eventType.id; - if ("instant" in body && body.instant) { - return await this.createInstantBooking(request, body, eventType); - } - const isRecurring = !!eventType?.recurringEvent; const isSeated = !!eventType?.seatsPerTimeSlot; @@ -211,7 +198,7 @@ export class BookingsService_2024_08_13 { body.eventTypeSlug ); } else if (body.teamSlug && body.eventTypeSlug) { - const team = await this.getBookedEventTypeTeam(body.teamSlug, body.organizationSlug); + const team = await this.getBookedEventTypeTeam(body.teamSlug); if (!team) { throw new NotFoundException(`Team with slug ${body.teamSlug} not found`); } @@ -223,19 +210,8 @@ export class BookingsService_2024_08_13 { return null; } - async getBookedEventTypeTeam(teamSlug: string, organizationSlug: string | undefined) { - if (!organizationSlug) { - return await this.teamsRepository.findTeamBySlug(teamSlug); - } - - const organization = await this.organizationsRepository.findOrgBySlug(organizationSlug); - if (!organization) { - throw new NotFoundException( - `slots-input.service.ts: Organization with slug ${organizationSlug} not found` - ); - } - - return await this.organizationsTeamsRepository.findOrgTeamBySlug(organization.id, teamSlug); + async getBookedEventTypeTeam(teamSlug: string) { + return await this.teamsRepository.findTeamBySlug(teamSlug); } async hasRequiredBookingFieldsResponses(body: CreateBookingInput, eventType: EventType | null) { @@ -249,7 +225,7 @@ export class BookingsService_2024_08_13 { } // note(Lauris): we filter out system fields, because some of them are set by default and name and email are passed in the body.attendee. Only exception - // is smsReminderNumber, because if it is required and not passed sms workflow won't work. + // is smsReminderNumber, because if it is required and not passed SMS reminders won't work. const eventTypeBookingFields = eventTypeBookingFieldsSchema .parse(eventType.bookingFields) .filter((field) => !field.editable.startsWith("system") || field.name === "smsReminderNumber"); @@ -420,37 +396,6 @@ export class BookingsService_2024_08_13 { ); } - async createInstantBooking( - request: Request, - body: CreateInstantBookingInput_2024_08_13, - eventType: EventTypeWithOwnerAndTeam - ) { - if (!eventType.team?.id) { - throw new BadRequestException( - "Instant bookings are only supported for team event types, not individual user event types." - ); - } - - const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType); - const booking = await this.instantBookingCreateService.createBooking({ - bookingData: bookingRequest.body, - }); - - const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent( - booking.bookingId - ); - if (!databaseBooking) { - throw new Error(`Booking with id=${booking.bookingId} was not found in the database`); - } - - const outputBooking = await this.outputService.getOutputBooking(databaseBooking); - // Instant bookings don't have a host assigned yet (AWAITING_HOST status), - // so we can't determine isPlatformManaged. Default to false to skip billing. - return Object.assign(outputBooking, { - isPlatformManagedUserBooking: false, - }); - } - async createRecurringBooking( request: Request, body: CreateRecurringBookingInput_2024_08_13, @@ -469,7 +414,6 @@ export class BookingsService_2024_08_13 { platformBookingLocation: bookingRequest.platformBookingLocation, noEmail: bookingRequest.noEmail, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, - impersonatedByUserUuid: null, }, creationSource: "API_V2", }); @@ -499,7 +443,6 @@ export class BookingsService_2024_08_13 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, - impersonatedByUserUuid: null, }, creationSource: "API_V2", }); @@ -530,7 +473,6 @@ export class BookingsService_2024_08_13 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, - impersonatedByUserUuid: null, }, }); @@ -573,7 +515,6 @@ export class BookingsService_2024_08_13 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, - impersonatedByUserUuid: null, }, }); @@ -833,7 +774,6 @@ export class BookingsService_2024_08_13 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, - impersonatedByUserUuid: null, }, }); if (!booking.uid) { @@ -982,13 +922,8 @@ export class BookingsService_2024_08_13 { platformCancelUrl: bookingRequest.platformCancelUrl, platformRescheduleUrl: bookingRequest.platformRescheduleUrl, platformBookingUrl: bookingRequest.platformBookingUrl, - impersonatedByUserUuid: null, }); - if (!res.onlyRemovedAttendee && res.isPlatformManagedUserBooking) { - await this.billingService.cancelUsageByBookingUid(res.bookingUid); - } - if ("cancelSubsequentBookings" in body && body.cancelSubsequentBookings) { return this.getAllRecurringBookingsByIndividualUid(bookingUid, authUser); } @@ -1039,11 +974,7 @@ export class BookingsService_2024_08_13 { attendees: bodyTransformed.attendees, noShowHost: bodyTransformed.noShowHost, userId: bookingOwnerId, - userUuid, platformClientParams, - actor: makeUserActor(userUuid), - actionSource: "API_V2", - impersonatedByUserUuid: null, }); const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid); @@ -1059,47 +990,6 @@ export class BookingsService_2024_08_13 { return this.outputService.getOutputBooking(booking); } - async billBookings(bookings: CreatedBooking[]) { - for (const booking of bookings) { - await this.billBooking(booking); - } - } - - async billBooking(booking: CreatedBooking) { - if (!booking.isPlatformManagedUserBooking) { - return; - } - - const hostId = booking.hosts?.[0]?.id; - if (!hostId) { - this.logger.error(`Booking with uid=${booking.uid} has no host`); - return; - } - - await this.billingService.increaseUsageByUserId(hostId, { - uid: booking.uid, - startTime: new Date(booking.start), - }); - } - - async billRescheduledBooking(newBooking: CreatedBooking, oldBookingUid: string) { - if (!newBooking.isPlatformManagedUserBooking) { - return; - } - - const hostId = newBooking.hosts[0]?.id; - if (!hostId) { - this.logger.error(`Booking with uid=${newBooking.uid} has no host`); - return; - } - - await this.billingService.increaseUsageByUserId(hostId, { - uid: newBooking.uid, - startTime: new Date(newBooking.start), - fromReschedule: oldBookingUid, - }); - } - async reassignBooking(bookingUid: string, reassignedByUser: ApiAuthGuardUser) { const booking = await this.bookingsRepository.getByUidWithEventType(bookingUid); if (!booking) { @@ -1197,7 +1087,7 @@ export class BookingsService_2024_08_13 { const profile = this.usersService.getUserMainProfile(user); try { - const reassigned = await roundRobinManualReassignment({ + await roundRobinManualReassignment({ bookingId: booking.id, newUserId, orgId: profile?.organizationId || null, @@ -1209,12 +1099,19 @@ export class BookingsService_2024_08_13 { reassignedByUuid: reassignedByUser.uuid, }); + const reassigned = await this.bookingsRepository.getByUidWithUser(bookingUid); + if (!reassigned) { + throw new NotFoundException( + `Reassigned booking with uid=${bookingUid} was not found in the database` + ); + } + return this.outputService.getOutputReassignedBooking(reassigned); } catch (error) { if (error instanceof Error) { if (error.message === "invalid_round_robin_host") { throw new BadRequestException( - `User with id=${newUserId} is not a valid Round Robin host - the user to which you reassign this booking must be one of the booking hosts. Fetch the booking using following endpoint and select id of one of the hosts: https://cal.com/docs/api-reference/v2/bookings/get-a-booking` + `User with id=${newUserId} is not a valid Round Robin host - the user to which you reassign this booking must be one of the booking hosts. Fetch the booking using the GET /v2/bookings/{uid} endpoint and select id of one of the hosts.` ); } } @@ -1251,7 +1148,6 @@ export class BookingsService_2024_08_13 { platformClientParams, actionSource: "API_V2", actor: makeUserActor(requestUser.uuid), - impersonatedByUserUuid: null, }, }); @@ -1288,7 +1184,6 @@ export class BookingsService_2024_08_13 { platformClientParams, actionSource: "API_V2", actor: makeUserActor(requestUser.uuid), - impersonatedByUserUuid: null, }, }); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/cal-video.output.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/cal-video.output.service.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/cal-video.output.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/cal-video.output.service.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/cal-video.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/cal-video.service.ts similarity index 94% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/cal-video.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/cal-video.service.ts index 7441352e9ba551..9191cd3c769eb3 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/cal-video.service.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/services/cal-video.service.ts @@ -1,5 +1,5 @@ -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { CalVideoOutputService } from "@/ee/bookings/2024-08-13/services/cal-video.output.service"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; +import { CalVideoOutputService } from "@/platform/bookings/2024-08-13/services/cal-video.output.service"; import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { CAL_VIDEO_TYPE } from "@calcom/platform-constants"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/errors.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/errors.service.ts similarity index 97% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/errors.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/errors.service.ts index beeadd63950dc7..e10031e353d7a3 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/errors.service.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/services/errors.service.ts @@ -42,7 +42,7 @@ export class ErrorsBookingsService_2024_08_13 { throw new BadRequestException("User either already has booking at this time or is not available"); } else if (error.message === "booking_time_out_of_bounds_error") { throw new BadRequestException( - `The event type can't be booked at the "start" time provided. This could be because it's too soon (violating the minimum booking notice) or too far in the future (outside the event's scheduling window). Try fetching available slots first using the GET /v2/slots endpoint and then make a booking with "start" time equal to one of the available slots: https://cal.com/docs/api-reference/v2/slots` + `The event type can't be booked at the "start" time provided. This could be because it's too soon (violating the minimum booking notice) or too far in the future (outside the event's scheduling window). Try fetching available slots first using the GET /v2/slots endpoint and then make a booking with "start" time equal to one of the available slots.` ); } else if (error.message === "Attempting to book a meeting in the past.") { throw new BadRequestException("Attempting to book a meeting in the past."); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/input.service.ts similarity index 78% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/input.service.ts index fe7c9051f3481e..cd54bda2ff4626 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/services/input.service.ts @@ -1,38 +1,3 @@ -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { - eventTypeBookingFieldsSchema, - EventTypeWithOwnerAndTeam, -} from "@/ee/bookings/2024-08-13/services/bookings.service"; -import { - bookingResponsesSchema, - seatedBookingDataSchema, -} from "@/ee/bookings/2024-08-13/services/output.service"; -import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; -import { apiToInternalintegrationsMapping } from "@/ee/event-types/event-types_2024_06_14/transformers"; -import { sha256Hash, isApiKey, stripApiKey } from "@/lib/api-key"; -import { defaultBookingResponses } from "@/lib/safe-parse/default-responses-booking"; -import { safeParse } from "@/lib/safe-parse/safe-parse"; -import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; -import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository"; -import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; -import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; -import { UsersRepository } from "@/modules/users/users.repository"; -import { - BadRequestException, - Injectable, - NotFoundException, -} from "@nestjs/common"; -import { Logger } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import { isURL, isPhoneNumber } from "class-validator"; -import { Request } from "express"; -import { DateInput, DateTime } from "luxon"; -import { NextApiRequest } from "next/types"; -import { v4 as uuidv4 } from "uuid"; -import { z } from "zod"; - import { CreationSource } from "@calcom/platform-libraries"; import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types"; import type { @@ -40,7 +5,6 @@ import type { CancelBookingInput_2024_08_13, CancelSeatedBookingInput_2024_08_13, CreateBookingInput_2024_08_13, - CreateInstantBookingInput_2024_08_13, CreateRecurringBookingInput_2024_08_13, GetBookingsInput_2024_08_13, MarkAbsentBookingInput_2024_08_13, @@ -51,6 +15,35 @@ import type { import type { BookingInputLocation_2024_08_13 } from "@calcom/platform-types/bookings/2024-08-13/inputs/location.input"; import type { UpdateBookingInputLocation_2024_08_13 } from "@calcom/platform-types/bookings/2024-08-13/inputs/update-location.input"; import type { EventType } from "@calcom/prisma/client"; +import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { isPhoneNumber, isURL } from "class-validator"; +import { Request } from "express"; +import { DateInput, DateTime } from "luxon"; +import { NextApiRequest } from "next/types"; +import { v4 as uuidv4 } from "uuid"; +import { z } from "zod"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; +import { + EventTypeWithOwnerAndTeam, + eventTypeBookingFieldsSchema, +} from "@/platform/bookings/2024-08-13/services/bookings.service"; +import { + bookingResponsesSchema, + seatedBookingDataSchema, +} from "@/platform/bookings/2024-08-13/services/output.service"; +import { PlatformBookingsService } from "@/platform/bookings/shared/platform-bookings.service"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; +import { OutputEventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/output-event-types.service"; +import { apiToInternalintegrationsMapping } from "@/platform/event-types/event-types_2024_06_14/transformers"; +import { isApiKey, sha256Hash, stripApiKey } from "@/lib/api-key"; +import { defaultBookingResponses } from "@/lib/safe-parse/default-responses-booking"; +import { safeParse } from "@/lib/safe-parse/safe-parse"; +import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; +import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository"; +import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; +import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; +import { UsersRepository } from "@/modules/users/users.repository"; type BookingRequest = NextApiRequest & { userId: number | undefined; @@ -68,13 +61,13 @@ type OAuthRequestParams = { }; export enum Frequency { - "YEARLY", - "MONTHLY", - "WEEKLY", - "DAILY", - "HOURLY", - "MINUTELY", - "SECONDLY", + YEARLY, + MONTHLY, + WEEKLY, + DAILY, + HOURLY, + MINUTELY, + SECONDLY, } const recurringEventSchema = z.object({ @@ -103,13 +96,10 @@ export class InputBookingsService_2024_08_13 { async createBookingRequest( request: Request, - body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13, + body: CreateBookingInput_2024_08_13, eventType: EventTypeWithOwnerAndTeam ): Promise { - const oAuthClientParams = - await this.platformBookingsService.getOAuthClientParamsForEventType( - eventType - ); + const oAuthClientParams = await this.platformBookingsService.getOAuthClientParamsForEventType(eventType); const bodyTransformed = await this.transformInputCreateBooking( body, eventType, @@ -117,8 +107,7 @@ export class InputBookingsService_2024_08_13 { ); const newRequest = { ...request }; - const userId = - (await this.createBookingRequestOwnerId(request)) ?? undefined; + const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; this.logger.log(`createBookingRequest_2024_08_13`, { requestId: request.get("X-Request-Id"), @@ -160,10 +149,7 @@ export class InputBookingsService_2024_08_13 { const guests = inputBooking.guests && platformClientId - ? await this.platformBookingsService.getPlatformAttendeesEmails( - inputBooking.guests, - platformClientId - ) + ? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId) : inputBooking.guests; const attendeeEmail = inputBooking.attendee.email && platformClientId @@ -175,9 +161,7 @@ export class InputBookingsService_2024_08_13 { const inputLocation = inputBooking.location || inputBooking.meetingUrl; this.isBookingLocationWithEventTypeLocations(inputLocation, eventType); - const location = inputLocation - ? this.transformLocation(inputLocation) - : undefined; + const location = inputLocation ? this.transformLocation(inputLocation) : undefined; const needsSmsReminderNumber = eventType.bookingFields ? eventTypeBookingFieldsSchema @@ -204,9 +188,7 @@ export class InputBookingsService_2024_08_13 { name: inputBooking.attendee.name, email: attendeeEmail ?? "", attendeePhoneNumber: inputBooking.attendee.phoneNumber, - smsReminderNumber: needsSmsReminderNumber - ? inputBooking.attendee.phoneNumber - : undefined, + smsReminderNumber: needsSmsReminderNumber ? inputBooking.attendee.phoneNumber : undefined, guests, location, }, @@ -231,7 +213,6 @@ export class InputBookingsService_2024_08_13 { return { routedTeamMemberIds: routing.teamMemberIds, - routingFormResponseId: routing.responseId, teamMemberEmail: routing.teamMemberEmail, skipContactOwner: routing.skipContactOwner, crmAppSlug: routing.crmAppSlug, @@ -239,10 +220,7 @@ export class InputBookingsService_2024_08_13 { }; } - validateBookingLengthInMinutes( - inputBooking: CreateBookingInput_2024_08_13, - eventType: EventType - ) { + validateBookingLengthInMinutes(inputBooking: CreateBookingInput_2024_08_13, eventType: EventType) { const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType.metadata); if (inputBooking.lengthInMinutes && !eventTypeMetadata?.multipleDuration) { throw new BadRequestException( @@ -251,9 +229,7 @@ export class InputBookingsService_2024_08_13 { } if ( inputBooking.lengthInMinutes && - !eventTypeMetadata?.multipleDuration?.includes( - inputBooking.lengthInMinutes - ) + !eventTypeMetadata?.multipleDuration?.includes(inputBooking.lengthInMinutes) ) { throw new BadRequestException( `Provided 'lengthInMinutes' is not one of the possible lengths for the event type. The possible lengths are: ${eventTypeMetadata?.multipleDuration?.join( @@ -268,10 +244,7 @@ export class InputBookingsService_2024_08_13 { body: CreateRecurringBookingInput_2024_08_13, eventType: EventTypeWithOwnerAndTeam ): Promise { - const oAuthClientParams = - await this.platformBookingsService.getOAuthClientParamsForEventType( - eventType - ); + const oAuthClientParams = await this.platformBookingsService.getOAuthClientParamsForEventType(eventType); // note(Lauris): update to this.transformInputCreate when rescheduling is implemented const bodyTransformed = await this.transformInputCreateRecurringBooking( body, @@ -280,8 +253,7 @@ export class InputBookingsService_2024_08_13 { ); const newRequest = { ...request }; - const userId = - (await this.createBookingRequestOwnerId(request)) ?? undefined; + const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; if (oAuthClientParams) { Object.assign(newRequest, { @@ -307,7 +279,9 @@ export class InputBookingsService_2024_08_13 { } as unknown as BookingRequest; } - transformLocation(location: string | BookingInputLocation_2024_08_13 | UpdateBookingInputLocation_2024_08_13): { + transformLocation( + location: string | BookingInputLocation_2024_08_13 | UpdateBookingInputLocation_2024_08_13 + ): { value: string; optionValue: string; } { @@ -315,10 +289,7 @@ export class InputBookingsService_2024_08_13 { // note(Lauris): this is for backwards compatibility because before switching to booking location objects // we only received a string. If someone is complaining that their location is not displaying as a URL // or whatever check that they are not providing a string for bookign location but one of the input objects. - if ( - isURL(location, { require_protocol: false }) || - location.startsWith("www.") - ) { + if (isURL(location, { require_protocol: false }) || location.startsWith("www.")) { return { value: "link", optionValue: location, @@ -339,12 +310,9 @@ export class InputBookingsService_2024_08_13 { } if (location.type === "integration") { - const integration = - apiToInternalintegrationsMapping[location.integration]; + const integration = apiToInternalintegrationsMapping[location.integration]; if (!integration) { - throw new BadRequestException( - `Invalid integration: ${location.integration}` - ); + throw new BadRequestException(`Invalid integration: ${location.integration}`); } return { value: integration, @@ -402,9 +370,7 @@ export class InputBookingsService_2024_08_13 { } throw new BadRequestException( - `Booking location with type ${ - (location as BookingInputLocation_2024_08_13).type - } not valid.` + `Booking location with type ${(location as BookingInputLocation_2024_08_13).type} not valid.` ); } @@ -417,19 +383,13 @@ export class InputBookingsService_2024_08_13 { return true; } - const eventTypeLocations = this.outputEventTypesService.transformLocations( - dbEventType.locations - ); - const allowedLocationTypes = eventTypeLocations.map( - (location) => location.type - ); + const eventTypeLocations = this.outputEventTypesService.transformLocations(dbEventType.locations); + const allowedLocationTypes = eventTypeLocations.map((location) => location.type); const isAllowed = allowedLocationTypes.includes(inputBookingLocation.type); if (!isAllowed) { throw new BadRequestException( - `Booking location with type ${ - inputBookingLocation.type - } not valid for event type with id=${ + `Booking location with type ${inputBookingLocation.type} not valid for event type with id=${ dbEventType.id }. The event type has following location types: ${allowedLocationTypes.join( ", " @@ -437,17 +397,12 @@ export class InputBookingsService_2024_08_13 { ); } - if ( - inputBookingLocation.type === "integration" && - "integration" in inputBookingLocation - ) { + if (inputBookingLocation.type === "integration" && "integration" in inputBookingLocation) { const allowedIntegrations = eventTypeLocations .filter((location) => location.type === "integration") .map((location) => location.integration); - const isAllowedIntegration = allowedIntegrations.includes( - inputBookingLocation.integration - ); + const isAllowedIntegration = allowedIntegrations.includes(inputBookingLocation.integration); if (!isAllowedIntegration) { throw new BadRequestException( `Booking location with integration ${ @@ -470,9 +425,7 @@ export class InputBookingsService_2024_08_13 { platformClientId?: string ) { if (!eventType.recurringEvent) { - throw new NotFoundException( - `Event type with id=${inputBooking.eventTypeId} is not a recurring event` - ); + throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`); } this.validateBookingLengthInMinutes(inputBooking, eventType); @@ -481,10 +434,7 @@ export class InputBookingsService_2024_08_13 { const occurrence = recurringEventSchema.parse(eventType.recurringEvent); const repeatsEvery = occurrence.interval; - if ( - inputBooking.recurrenceCount && - inputBooking.recurrenceCount > occurrence.count - ) { + if (inputBooking.recurrenceCount && inputBooking.recurrenceCount > occurrence.count) { throw new BadRequestException( "Provided recurrence count is higher than the event type's recurring event count." ); @@ -502,10 +452,7 @@ export class InputBookingsService_2024_08_13 { const guests = inputBooking.guests && platformClientId - ? await this.platformBookingsService.getPlatformAttendeesEmails( - inputBooking.guests, - platformClientId - ) + ? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId) : inputBooking.guests; const attendeeEmail = inputBooking.attendee.email && platformClientId @@ -517,9 +464,7 @@ export class InputBookingsService_2024_08_13 { const inputLocation = inputBooking.location || inputBooking.meetingUrl; this.isBookingLocationWithEventTypeLocations(inputLocation, eventType); - const location = inputLocation - ? this.transformLocation(inputLocation) - : undefined; + const location = inputLocation ? this.transformLocation(inputLocation) : undefined; for (let i = 0; i < repeatsTimes; i++) { const endTime = startTime.plus({ minutes: lengthInMinutes }); @@ -576,19 +521,14 @@ export class InputBookingsService_2024_08_13 { const bodyTransformed = isIndividualSeatReschedule && "seatUid" in body ? await this.transformInputRescheduleSeatedBooking(bookingUid, body) - : await this.transformInputRescheduleBooking( - bookingUid, - body, - isIndividualSeatReschedule - ); - - const oAuthClientParams = - await this.platformBookingsService.getOAuthClientParams( - bodyTransformed.eventTypeId - ); + : await this.transformInputRescheduleBooking(bookingUid, body, isIndividualSeatReschedule); + + const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams( + bodyTransformed.eventTypeId + ); const newRequest = { ...request }; - let userId: number | undefined = undefined; + let userId: number | undefined; if ( oAuthClientParams && @@ -603,9 +543,7 @@ export class InputBookingsService_2024_08_13 { if (request.body.rescheduledBy) { if (request.body.rescheduledBy !== bodyTransformed.responses.email) { - userId = ( - await this.usersRepository.findByEmail(request.body.rescheduledBy) - )?.id; + userId = (await this.usersRepository.findByEmail(request.body.rescheduledBy))?.id; } } @@ -635,9 +573,7 @@ export class InputBookingsService_2024_08_13 { return newRequest as unknown as BookingRequest; } - isRescheduleSeatedBody( - body: RescheduleBookingInput - ): body is RescheduleSeatedBookingInput_2024_08_13 { + isRescheduleSeatedBody(body: RescheduleBookingInput): body is RescheduleSeatedBookingInput_2024_08_13 { return Object.prototype.hasOwnProperty.call(body, "seatUid"); } @@ -645,44 +581,26 @@ export class InputBookingsService_2024_08_13 { bookingUid: string, inputBooking: RescheduleSeatedBookingInput_2024_08_13 ) { - const booking = - await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent( - bookingUid - ); + const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid); // todo create booking seat module, repository and fetch the seat to get info if (!booking) { throw new NotFoundException(`Booking with uid=${bookingUid} not found`); } if (!booking.eventTypeId) { - throw new NotFoundException( - `Booking with uid=${bookingUid} is missing event type` - ); + throw new NotFoundException(`Booking with uid=${bookingUid} is missing event type`); } - const eventType = - await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( - booking.eventTypeId - ); + const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(booking.eventTypeId); if (!eventType) { - throw new NotFoundException( - `Event type with id=${booking.eventTypeId} not found` - ); + throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`); } - const seat = await this.bookingSeatRepository.getByReferenceUid( - inputBooking.seatUid - ); + const seat = await this.bookingSeatRepository.getByReferenceUid(inputBooking.seatUid); if (!seat) { - throw new NotFoundException( - `Seat with uid=${inputBooking.seatUid} does not exist.` - ); + throw new NotFoundException(`Seat with uid=${inputBooking.seatUid} does not exist.`); } - const { responses: bookingResponses } = seatedBookingDataSchema.parse( - seat.data - ); - const attendee = booking.attendees.find( - (attendee) => attendee.email === bookingResponses.email - ); + const { responses: bookingResponses } = seatedBookingDataSchema.parse(seat.data); + const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email); if (!attendee) { throw new NotFoundException( @@ -692,10 +610,7 @@ export class InputBookingsService_2024_08_13 { // preserve the original booking duration instead of using the default event type length // this ensures that bookings with non-default durations (from multi-duration event types) are preserved on reschedule - const originalDurationInMinutes = this.getOriginalBookingDuration( - booking.startTime, - booking.endTime - ); + const originalDurationInMinutes = this.getOriginalBookingDuration(booking.startTime, booking.endTime); const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc", @@ -722,30 +637,20 @@ export class InputBookingsService_2024_08_13 { inputBooking: RescheduleBookingInput_2024_08_13, isIndividualSeatReschedule: boolean ) { - const booking = - await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent( - bookingUid - ); + const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid); if (!booking) { throw new NotFoundException(`Booking with uid=${bookingUid} not found`); } if (!booking.eventTypeId) { - throw new NotFoundException( - `Booking with uid=${bookingUid} is missing event type` - ); + throw new NotFoundException(`Booking with uid=${bookingUid} is missing event type`); } - const eventType = - await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( - booking.eventTypeId - ); + const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(booking.eventTypeId); if (!eventType) { - throw new NotFoundException( - `Event type with id=${booking.eventTypeId} not found` - ); + throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`); } if (eventType.seatsPerTimeSlot && !isIndividualSeatReschedule) { throw new BadRequestException( - `Booking with uid=${bookingUid} is a seated booking which means you have to provide seatUid in the request body to specify which seat specifically you want to reschedule. First, fetch the booking using https://cal.com/docs/api-reference/v2/bookings/get-a-booking and then within the attendees array you will find the seatUid of the booking you want to reschedule. Second, provide the seatUid in the request body to specify which seat specifically you want to reschedule using the reschedule endpoint https://cal.com/docs/api-reference/v2/bookings/reschedule-a-booking#option-2` + `Booking with uid=${bookingUid} is a seated booking which means you have to provide seatUid in the request body to specify which seat specifically you want to reschedule. First, fetch the booking using the GET /v2/bookings/{uid} endpoint and then within the attendees array you will find the seatUid of the booking you want to reschedule. Second, provide the seatUid in the request body to specify which seat specifically you want to reschedule using the POST /v2/bookings/{uid}/reschedule endpoint.` ); } @@ -761,9 +666,7 @@ export class InputBookingsService_2024_08_13 { const attendee = bookingResponsesMissing ? booking.attendees[0] - : booking.attendees.find( - (attendee) => attendee.email === bookingResponses.email - ); + : booking.attendees.find((attendee) => attendee.email === bookingResponses.email); if (!attendee) { throw new NotFoundException( @@ -779,10 +682,7 @@ export class InputBookingsService_2024_08_13 { // preserve the original booking duration instead of using the default event type length // this ensures that bookings with non-default durations (from multi-duration event types) are preserved on reschedule - const originalDurationInMinutes = this.getOriginalBookingDuration( - booking.startTime, - booking.endTime - ); + const originalDurationInMinutes = this.getOriginalBookingDuration(booking.startTime, booking.endTime); const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc", @@ -808,37 +708,21 @@ export class InputBookingsService_2024_08_13 { } async getRescheduleBookingLocation(rescheduleBookingUid: string) { - const booking = await this.bookingsRepository.getByUid( - rescheduleBookingUid - ); + const booking = await this.bookingsRepository.getByUid(rescheduleBookingUid); if (!booking) { - throw new NotFoundException( - `Booking with uid=${rescheduleBookingUid} not found` - ); + throw new NotFoundException(`Booking with uid=${rescheduleBookingUid} not found`); } return booking.location; } - private async createBookingRequestOwnerId( - req: Request - ): Promise { + private async createBookingRequestOwnerId(req: Request): Promise { try { const bearerToken = req.get("Authorization")?.replace("Bearer ", ""); if (bearerToken) { - if ( - isApiKey( - bearerToken, - this.config.get("api.apiKeyPrefix") ?? "cal_" - ) - ) { - const strippedApiKey = stripApiKey( - bearerToken, - this.config.get("api.keyPrefix") - ); + if (isApiKey(bearerToken, this.config.get("api.apiKeyPrefix") ?? "cal_")) { + const strippedApiKey = stripApiKey(bearerToken, this.config.get("api.keyPrefix")); const apiKeyHash = sha256Hash(strippedApiKey); - const keyData = await this.apiKeyRepository.getApiKeyFromHash( - apiKeyHash - ); + const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash); return keyData?.userId; } else { // Access Token @@ -857,12 +741,9 @@ export class InputBookingsService_2024_08_13 { attendeeName: queryParams.attendeeName, afterStartDate: queryParams.afterStart, beforeEndDate: queryParams.beforeEnd, - teamIds: - queryParams.teamsIds || - (queryParams.teamId ? [queryParams.teamId] : undefined), + teamIds: queryParams.teamsIds || (queryParams.teamId ? [queryParams.teamId] : undefined), eventTypeIds: - queryParams.eventTypeIds || - (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined), + queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined), afterUpdatedDate: queryParams.afterUpdatedAt, beforeUpdatedDate: queryParams.beforeUpdatedAt, afterCreatedDate: queryParams.afterCreatedAt, @@ -910,14 +791,11 @@ export class InputBookingsService_2024_08_13 { } const oAuthClientParams = booking.eventTypeId - ? await this.platformBookingsService.getOAuthClientParams( - booking.eventTypeId - ) + ? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId) : undefined; const newRequest = { ...request }; - const userId = - (await this.createBookingRequestOwnerId(request)) ?? undefined; + const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; if (oAuthClientParams) { Object.assign(newRequest, { userId, ...oAuthClientParams }); @@ -933,19 +811,12 @@ export class InputBookingsService_2024_08_13 { return newRequest as unknown as BookingRequest; } - isCancelSeatedBody( - body: CancelBookingInput - ): body is CancelSeatedBookingInput_2024_08_13 { + isCancelSeatedBody(body: CancelBookingInput): body is CancelSeatedBookingInput_2024_08_13 { return Object.prototype.hasOwnProperty.call(body, "seatUid"); } - async transformInputCancelBooking( - bookingUid: string, - inputBooking: CancelBookingInput_2024_08_13 - ) { - const recurringBooking = await this.bookingsRepository.getRecurringByUid( - bookingUid - ); + async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) { + const recurringBooking = await this.bookingsRepository.getRecurringByUid(bookingUid); // note(Lauris): isRecurring means that recurringEventId was passed as uid. isRecurring does not refer to the uid of 1 individual booking within a recurring booking consisting of many bookings. // That is what recurringEventId refers to. const isRecurringUid = !!recurringBooking.length; @@ -994,9 +865,7 @@ export class InputBookingsService_2024_08_13 { }; } - transformInputMarkAbsentBooking( - inputBooking: MarkAbsentBookingInput_2024_08_13 - ) { + transformInputMarkAbsentBooking(inputBooking: MarkAbsentBookingInput_2024_08_13) { return { noShowHost: inputBooking.host, attendees: inputBooking.attendees?.map((attendee) => ({ @@ -1007,7 +876,6 @@ export class InputBookingsService_2024_08_13 { } getOriginalBookingDuration(start: Date, end: Date) { - return DateTime.fromJSDate(end).diff(DateTime.fromJSDate(start), "minutes") - .minutes; + return DateTime.fromJSDate(end).diff(DateTime.fromJSDate(start), "minutes").minutes; } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output-booking-references.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/output-booking-references.service.ts similarity index 100% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/output-booking-references.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/output-booking-references.service.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/platform/bookings/2024-08-13/services/output.service.ts similarity index 99% rename from apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts rename to apps/api/v2/src/platform/bookings/2024-08-13/services/output.service.ts index 1843da15df911d..a79465cd0fc6ca 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/platform/bookings/2024-08-13/services/output.service.ts @@ -1,4 +1,4 @@ -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; import { defaultBookingMetadata, defaultBookingResponses, diff --git a/apps/api/v2/src/ee/bookings/shared/platform-bookings.service.ts b/apps/api/v2/src/platform/bookings/shared/platform-bookings.service.ts similarity index 96% rename from apps/api/v2/src/ee/bookings/shared/platform-bookings.service.ts rename to apps/api/v2/src/platform/bookings/shared/platform-bookings.service.ts index 4ca1c78c2a4f84..e651bd85ee006b 100644 --- a/apps/api/v2/src/ee/bookings/shared/platform-bookings.service.ts +++ b/apps/api/v2/src/platform/bookings/shared/platform-bookings.service.ts @@ -1,4 +1,4 @@ -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; import { UsersRepository } from "@/modules/users/users.repository"; diff --git a/apps/api/v2/src/ee/calendars/calendars.interface.ts b/apps/api/v2/src/platform/calendars/calendars.interface.ts similarity index 89% rename from apps/api/v2/src/ee/calendars/calendars.interface.ts rename to apps/api/v2/src/platform/calendars/calendars.interface.ts index 03fca182dd6ccd..9dcdd2b7b45e0f 100644 --- a/apps/api/v2/src/ee/calendars/calendars.interface.ts +++ b/apps/api/v2/src/platform/calendars/calendars.interface.ts @@ -1,4 +1,4 @@ -import { CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output"; +import { CreateIcsFeedOutputResponseDto } from "@/platform/calendars/input/create-ics.output"; import { Request } from "express"; import { ApiResponse } from "@calcom/platform-types"; diff --git a/apps/api/v2/src/ee/calendars/calendars.module.ts b/apps/api/v2/src/platform/calendars/calendars.module.ts similarity index 53% rename from apps/api/v2/src/ee/calendars/calendars.module.ts rename to apps/api/v2/src/platform/calendars/calendars.module.ts index 33dddedd53fb9f..b6c0705fa5a1f0 100644 --- a/apps/api/v2/src/ee/calendars/calendars.module.ts +++ b/apps/api/v2/src/platform/calendars/calendars.module.ts @@ -1,13 +1,13 @@ -import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository"; -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; -import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller"; -import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service"; -import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service"; -import { OutlookService } from "@/ee/calendars/services/outlook.service"; +import { BookingReferencesRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/booking-references.repository"; +import { BookingsRepository_2024_08_13 } from "@/platform/bookings/2024-08-13/repositories/bookings.repository"; +import { CalendarsRepository } from "@/platform/calendars/calendars.repository"; +import { CalendarsController } from "@/platform/calendars/controllers/calendars.controller"; +import { AppleCalendarService } from "@/platform/calendars/services/apple-calendar.service"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { GoogleCalendarService } from "@/platform/calendars/services/gcal.service"; +import { IcsFeedService } from "@/platform/calendars/services/ics-feed.service"; +import { OutlookService } from "@/platform/calendars/services/outlook.service"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { PrismaModule } from "@/modules/prisma/prisma.module"; diff --git a/apps/api/v2/src/ee/calendars/calendars.repository.ts b/apps/api/v2/src/platform/calendars/calendars.repository.ts similarity index 100% rename from apps/api/v2/src/ee/calendars/calendars.repository.ts rename to apps/api/v2/src/platform/calendars/calendars.repository.ts diff --git a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.e2e-spec.ts b/apps/api/v2/src/platform/calendars/controllers/calendars.controller.e2e-spec.ts similarity index 97% rename from apps/api/v2/src/ee/calendars/controllers/calendars.controller.e2e-spec.ts rename to apps/api/v2/src/platform/calendars/controllers/calendars.controller.e2e-spec.ts index 30b6800773e649..0823329cc694ec 100644 --- a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.e2e-spec.ts +++ b/apps/api/v2/src/platform/calendars/controllers/calendars.controller.e2e-spec.ts @@ -35,10 +35,10 @@ jest.mock("@calcom/platform-libraries/app-store", () => { 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"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { CreateIcsFeedOutput, CreateIcsFeedOutputResponseDto } from "@/platform/calendars/input/create-ics.output"; +import { ConnectedCalendarsData } from "@/platform/calendars/outputs/connected-calendars.output"; +import { DeletedCalendarCredentialsOutputResponseDto } from "@/platform/calendars/outputs/delete-calendar-credentials.output"; +import { CalendarsService } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts b/apps/api/v2/src/platform/calendars/controllers/calendars.controller.ts similarity index 89% rename from apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts rename to apps/api/v2/src/platform/calendars/controllers/calendars.controller.ts index 5e32ebf5335be6..519748b41ae317 100644 --- a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts +++ b/apps/api/v2/src/platform/calendars/controllers/calendars.controller.ts @@ -1,19 +1,19 @@ -import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; -import { CreateIcsFeedInputDto } from "@/ee/calendars/input/create-ics.input"; -import { CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output"; -import { DeleteCalendarCredentialsInputBodyDto } from "@/ee/calendars/input/delete-calendar-credentials.input"; -import { GetBusyTimesOutput } from "@/ee/calendars/outputs/busy-times.output"; -import { ConnectedCalendarsOutput } from "@/ee/calendars/outputs/connected-calendars.output"; +import { CalendarsRepository } from "@/platform/calendars/calendars.repository"; +import { CreateIcsFeedInputDto } from "@/platform/calendars/input/create-ics.input"; +import { CreateIcsFeedOutputResponseDto } from "@/platform/calendars/input/create-ics.output"; +import { DeleteCalendarCredentialsInputBodyDto } from "@/platform/calendars/input/delete-calendar-credentials.input"; +import { GetBusyTimesOutput } from "@/platform/calendars/outputs/busy-times.output"; +import { ConnectedCalendarsOutput } from "@/platform/calendars/outputs/connected-calendars.output"; import { DeletedCalendarCredentialsOutputResponseDto, DeletedCalendarCredentialsOutputDto, -} from "@/ee/calendars/outputs/delete-calendar-credentials.output"; -import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service"; -import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service"; -import { OutlookService } from "@/ee/calendars/services/outlook.service"; +} from "@/platform/calendars/outputs/delete-calendar-credentials.output"; +import { AppleCalendarService } from "@/platform/calendars/services/apple-calendar.service"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { GoogleCalendarService } from "@/platform/calendars/services/gcal.service"; +import { IcsFeedService } from "@/platform/calendars/services/ics-feed.service"; +import { OutlookService } from "@/platform/calendars/services/outlook.service"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; import { ApiAuthGuardOnlyAllow } from "@/modules/auth/decorators/api-auth-guard-only-allow.decorator"; diff --git a/apps/api/v2/src/ee/calendars/input/create-ics.input.ts b/apps/api/v2/src/platform/calendars/input/create-ics.input.ts similarity index 100% rename from apps/api/v2/src/ee/calendars/input/create-ics.input.ts rename to apps/api/v2/src/platform/calendars/input/create-ics.input.ts diff --git a/apps/api/v2/src/ee/calendars/input/create-ics.output.ts b/apps/api/v2/src/platform/calendars/input/create-ics.output.ts similarity index 100% rename from apps/api/v2/src/ee/calendars/input/create-ics.output.ts rename to apps/api/v2/src/platform/calendars/input/create-ics.output.ts diff --git a/apps/api/v2/src/ee/calendars/input/delete-calendar-credentials.input.ts b/apps/api/v2/src/platform/calendars/input/delete-calendar-credentials.input.ts similarity index 100% rename from apps/api/v2/src/ee/calendars/input/delete-calendar-credentials.input.ts rename to apps/api/v2/src/platform/calendars/input/delete-calendar-credentials.input.ts diff --git a/apps/api/v2/src/ee/calendars/outputs/busy-times.output.ts b/apps/api/v2/src/platform/calendars/outputs/busy-times.output.ts similarity index 100% rename from apps/api/v2/src/ee/calendars/outputs/busy-times.output.ts rename to apps/api/v2/src/platform/calendars/outputs/busy-times.output.ts diff --git a/apps/api/v2/src/ee/calendars/outputs/connected-calendars.output.ts b/apps/api/v2/src/platform/calendars/outputs/connected-calendars.output.ts similarity index 100% rename from apps/api/v2/src/ee/calendars/outputs/connected-calendars.output.ts rename to apps/api/v2/src/platform/calendars/outputs/connected-calendars.output.ts diff --git a/apps/api/v2/src/ee/calendars/outputs/delete-calendar-credentials.output.ts b/apps/api/v2/src/platform/calendars/outputs/delete-calendar-credentials.output.ts similarity index 100% rename from apps/api/v2/src/ee/calendars/outputs/delete-calendar-credentials.output.ts rename to apps/api/v2/src/platform/calendars/outputs/delete-calendar-credentials.output.ts diff --git a/apps/api/v2/src/ee/calendars/processors/calendars.processor.ts b/apps/api/v2/src/platform/calendars/processors/calendars.processor.ts similarity index 92% rename from apps/api/v2/src/ee/calendars/processors/calendars.processor.ts rename to apps/api/v2/src/platform/calendars/processors/calendars.processor.ts index 5f550ce6e287ec..bae50e0575e6ec 100644 --- a/apps/api/v2/src/ee/calendars/processors/calendars.processor.ts +++ b/apps/api/v2/src/platform/calendars/processors/calendars.processor.ts @@ -1,7 +1,7 @@ import { Process, Processor } from "@nestjs/bull"; import { Logger } from "@nestjs/common"; import { Job } from "bull"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; export const DEFAULT_CALENDARS_JOB = "default_calendars_job"; export const CALENDARS_QUEUE = "calendars"; diff --git a/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts b/apps/api/v2/src/platform/calendars/services/apple-calendar.service.ts similarity index 96% rename from apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts rename to apps/api/v2/src/platform/calendars/services/apple-calendar.service.ts index 86599d97aaef82..5c83885ab95be7 100644 --- a/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts +++ b/apps/api/v2/src/platform/calendars/services/apple-calendar.service.ts @@ -1,5 +1,5 @@ -import { CredentialSyncCalendarApp } from "@/ee/calendars/calendars.interface"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { CredentialSyncCalendarApp } from "@/platform/calendars/calendars.interface"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { BadRequestException, UnauthorizedException } from "@nestjs/common"; import { Injectable } from "@nestjs/common"; diff --git a/apps/api/v2/src/ee/calendars/services/calendars-cache.service.ts b/apps/api/v2/src/platform/calendars/services/calendars-cache.service.ts similarity index 100% rename from apps/api/v2/src/ee/calendars/services/calendars-cache.service.ts rename to apps/api/v2/src/platform/calendars/services/calendars-cache.service.ts diff --git a/apps/api/v2/src/ee/calendars/services/calendars.service.ts b/apps/api/v2/src/platform/calendars/services/calendars.service.ts similarity index 97% rename from apps/api/v2/src/ee/calendars/services/calendars.service.ts rename to apps/api/v2/src/platform/calendars/services/calendars.service.ts index cae6f09849ce1e..68056d10c13b3b 100644 --- a/apps/api/v2/src/ee/calendars/services/calendars.service.ts +++ b/apps/api/v2/src/platform/calendars/services/calendars.service.ts @@ -16,8 +16,8 @@ import { } from "@nestjs/common"; import { DateTime } from "luxon"; import { z } from "zod"; -import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; +import { CalendarsRepository } from "@/platform/calendars/calendars.repository"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { CredentialsRepository, diff --git a/apps/api/v2/src/ee/calendars/services/gcal.service.ts b/apps/api/v2/src/platform/calendars/services/gcal.service.ts similarity index 96% rename from apps/api/v2/src/ee/calendars/services/gcal.service.ts rename to apps/api/v2/src/platform/calendars/services/gcal.service.ts index 9f4fe145830556..ac20452c7a428a 100644 --- a/apps/api/v2/src/ee/calendars/services/gcal.service.ts +++ b/apps/api/v2/src/platform/calendars/services/gcal.service.ts @@ -1,6 +1,6 @@ -import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface"; -import type { CalendarState } from "@/ee/calendars/controllers/calendars.controller"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { OAuthCalendarApp } from "@/platform/calendars/calendars.interface"; +import type { CalendarState } from "@/platform/calendars/controllers/calendars.controller"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; diff --git a/apps/api/v2/src/ee/calendars/services/ics-feed.service.ts b/apps/api/v2/src/platform/calendars/services/ics-feed.service.ts similarity index 90% rename from apps/api/v2/src/ee/calendars/services/ics-feed.service.ts rename to apps/api/v2/src/platform/calendars/services/ics-feed.service.ts index b1609ddf550af4..ab42e0283936fb 100644 --- a/apps/api/v2/src/ee/calendars/services/ics-feed.service.ts +++ b/apps/api/v2/src/platform/calendars/services/ics-feed.service.ts @@ -1,7 +1,7 @@ -import { ICSFeedCalendarApp } from "@/ee/calendars/calendars.interface"; -import { CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { ICSFeedCalendarApp } from "@/platform/calendars/calendars.interface"; +import { CreateIcsFeedOutputResponseDto } from "@/platform/calendars/input/create-ics.output"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { RedisService } from "@/modules/redis/redis.service"; import { BadRequestException, UnauthorizedException, Logger } from "@nestjs/common"; diff --git a/apps/api/v2/src/ee/calendars/services/outlook.service.ts b/apps/api/v2/src/platform/calendars/services/outlook.service.ts similarity index 96% rename from apps/api/v2/src/ee/calendars/services/outlook.service.ts rename to apps/api/v2/src/platform/calendars/services/outlook.service.ts index 3007a8542b95b6..50d8e0acf54e10 100644 --- a/apps/api/v2/src/ee/calendars/services/outlook.service.ts +++ b/apps/api/v2/src/platform/calendars/services/outlook.service.ts @@ -1,6 +1,6 @@ -import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface"; -import { CalendarState } from "@/ee/calendars/controllers/calendars.controller"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { OAuthCalendarApp } from "@/platform/calendars/calendars.interface"; +import { CalendarState } from "@/platform/calendars/controllers/calendars.controller"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; import { TokensService } from "@/modules/tokens/tokens.service"; diff --git a/apps/api/v2/src/ee/event-types-private-links/controllers/event-types-private-links.controller.e2e-spec.ts b/apps/api/v2/src/platform/event-types-private-links/controllers/event-types-private-links.controller.e2e-spec.ts similarity index 98% rename from apps/api/v2/src/ee/event-types-private-links/controllers/event-types-private-links.controller.e2e-spec.ts rename to apps/api/v2/src/platform/event-types-private-links/controllers/event-types-private-links.controller.e2e-spec.ts index 5c6f4e9f9cebaf..7ef8a5553d8517 100644 --- a/apps/api/v2/src/ee/event-types-private-links/controllers/event-types-private-links.controller.e2e-spec.ts +++ b/apps/api/v2/src/platform/event-types-private-links/controllers/event-types-private-links.controller.e2e-spec.ts @@ -12,7 +12,7 @@ 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 { EventTypesModule_2024_06_14 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/event-types-private-links/controllers/event-types-private-links.controller.ts b/apps/api/v2/src/platform/event-types-private-links/controllers/event-types-private-links.controller.ts similarity index 100% rename from apps/api/v2/src/ee/event-types-private-links/controllers/event-types-private-links.controller.ts rename to apps/api/v2/src/platform/event-types-private-links/controllers/event-types-private-links.controller.ts diff --git a/apps/api/v2/src/ee/event-types-private-links/event-types-private-links.module.ts b/apps/api/v2/src/platform/event-types-private-links/event-types-private-links.module.ts similarity index 90% rename from apps/api/v2/src/ee/event-types-private-links/event-types-private-links.module.ts rename to apps/api/v2/src/platform/event-types-private-links/event-types-private-links.module.ts index d93f9e6e93906f..6a339b6f4ca22b 100644 --- a/apps/api/v2/src/ee/event-types-private-links/event-types-private-links.module.ts +++ b/apps/api/v2/src/platform/event-types-private-links/event-types-private-links.module.ts @@ -1,4 +1,4 @@ -import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; +import { EventTypesModule_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.module"; import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; diff --git a/apps/api/v2/src/ee/event-types-private-links/private-links.repository.ts b/apps/api/v2/src/platform/event-types-private-links/private-links.repository.ts similarity index 100% rename from apps/api/v2/src/ee/event-types-private-links/private-links.repository.ts rename to apps/api/v2/src/platform/event-types-private-links/private-links.repository.ts diff --git a/apps/api/v2/src/ee/event-types-private-links/services/private-links-input.service.ts b/apps/api/v2/src/platform/event-types-private-links/services/private-links-input.service.ts similarity index 100% rename from apps/api/v2/src/ee/event-types-private-links/services/private-links-input.service.ts rename to apps/api/v2/src/platform/event-types-private-links/services/private-links-input.service.ts diff --git a/apps/api/v2/src/ee/event-types-private-links/services/private-links-output.service.ts b/apps/api/v2/src/platform/event-types-private-links/services/private-links-output.service.ts similarity index 100% rename from apps/api/v2/src/ee/event-types-private-links/services/private-links-output.service.ts rename to apps/api/v2/src/platform/event-types-private-links/services/private-links-output.service.ts diff --git a/apps/api/v2/src/ee/event-types-private-links/services/private-links.service.ts b/apps/api/v2/src/platform/event-types-private-links/services/private-links.service.ts similarity index 93% rename from apps/api/v2/src/ee/event-types-private-links/services/private-links.service.ts rename to apps/api/v2/src/platform/event-types-private-links/services/private-links.service.ts index e6161072ebd3bb..8d9cc2f1bd5b89 100644 --- a/apps/api/v2/src/ee/event-types-private-links/services/private-links.service.ts +++ b/apps/api/v2/src/platform/event-types-private-links/services/private-links.service.ts @@ -1,9 +1,9 @@ -import { PrivateLinksRepository } from "@/ee/event-types-private-links/private-links.repository"; -import { PrivateLinksInputService } from "@/ee/event-types-private-links/services/private-links-input.service"; +import { PrivateLinksRepository } from "@/platform/event-types-private-links/private-links.repository"; +import { PrivateLinksInputService } from "@/platform/event-types-private-links/services/private-links-input.service"; import { PrivateLinksOutputService, type PrivateLinkData, -} from "@/ee/event-types-private-links/services/private-links-output.service"; +} from "@/platform/event-types-private-links/services/private-links-output.service"; import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; import { generateHashedLink, isLinkExpired } from "@calcom/platform-libraries/private-links"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/constants/constants.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/constants/constants.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/constants/constants.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/constants/constants.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts similarity index 94% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts index f606f3400e2810..865ea679ce666c 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts @@ -24,14 +24,14 @@ import { randomString } from "test/utils/randomString"; import { withApiAuth } from "test/utils/withApiAuth"; 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 { Editable } from "@/platform/event-types/event-types_2024_04_15//inputs/enums/editable"; +import { EventTypesModule_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/event-types.module"; +import { CreateEventTypeInput_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +import { BaseField } from "@/platform/event-types/event-types_2024_04_15/inputs/enums/field-type"; +import { UpdateEventTypeInput_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/update-event-type.input"; +import { GetEventTypeOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/get-event-type.output"; +import { GetEventTypePublicOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/get-event-type-public.output"; +import { GetEventTypesPublicOutput } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/controllers/event-types.controller.ts similarity index 78% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/controllers/event-types.controller.ts index a1985d5e34e2b4..0e543cdfcab757 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/controllers/event-types.controller.ts @@ -1,53 +1,55 @@ -import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; -import { EventTypeIdParams_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-id.input"; -import { GetPublicEventTypeQueryParams_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input"; -import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input"; -import { CreateEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/create-event-type.output"; -import { DeleteEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/delete-event-type.output"; -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 { - GetEventTypesData, - GetEventTypesOutput, -} from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-types.output"; -import { UpdateEventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/update-event-type.output"; -import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service"; -import { VERSION_2024_04_15, VERSION_2024_06_11 } from "@/lib/api-versions"; -import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; -import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; -import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { UserWithProfile } from "@/modules/users/users.repository"; + EVENT_TYPE_READ, + EVENT_TYPE_WRITE, + SUCCESS_STATUS, + X_CAL_CLIENT_ID, +} from "@calcom/platform-constants"; +import { getEventTypesByViewer, getPublicEvent } from "@calcom/platform-libraries/event-types"; +import type { PrismaClient } from "@calcom/prisma"; import { + Body, Controller, - UseGuards, + Delete, Get, - Param, - Post, - Body, - NotFoundException, - Patch, + Headers, HttpCode, HttpStatus, - Delete, - Query, - Headers, InternalServerErrorException, + NotFoundException, + Param, ParseIntPipe, + Patch, + Post, + Query, + UseGuards, } from "@nestjs/common"; import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger"; - +import { CreateEventTypeInput_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +import { EventTypeIdParams_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/event-type-id.input"; +import { GetPublicEventTypeQueryParams_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input"; +import { UpdateEventTypeInput_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/update-event-type.input"; +import { CreateEventTypeOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/create-event-type.output"; +import { DeleteEventTypeOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/delete-event-type.output"; +import { GetEventTypeOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/get-event-type.output"; import { - EVENT_TYPE_READ, - EVENT_TYPE_WRITE, - SUCCESS_STATUS, - X_CAL_CLIENT_ID, -} from "@calcom/platform-constants"; -import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries/event-types"; -import type { PrismaClient } from "@calcom/prisma"; + GetEventTypePublicOutput, + PublicEventTypeOutput, +} from "@/platform/event-types/event-types_2024_04_15/outputs/get-event-type-public.output"; +import { + GetEventTypesData, + GetEventTypesOutput, +} from "@/platform/event-types/event-types_2024_04_15/outputs/get-event-types.output"; +import { GetEventTypesPublicOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/get-event-types-public.output"; +import { UpdateEventTypeOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/update-event-type.output"; +import { EventTypesService_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/services/event-types.service"; +import { VERSION_2024_04_15, VERSION_2024_06_11 } from "@/lib/api-versions"; +import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; +import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; +import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; +import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { UserWithProfile } from "@/modules/users/users.repository"; @Controller({ path: "/v2/event-types", @@ -144,7 +146,7 @@ export class EventTypesController_2024_04_15 { ); return { - data: event, + data: event as unknown as PublicEventTypeOutput, status: SUCCESS_STATUS, }; } catch (err) { diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.module.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/event-types.module.ts similarity index 70% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.module.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/event-types.module.ts index a2261d69e98e3d..c3c0f6d632eebd 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.module.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/event-types.module.ts @@ -1,6 +1,6 @@ -import { EventTypesController_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/controllers/event-types.controller"; -import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository"; -import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service"; +import { EventTypesController_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/controllers/event-types.controller"; +import { EventTypesRepository_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/event-types.repository"; +import { EventTypesService_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/services/event-types.service"; import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { OrganizationsModule } from "@/modules/organizations/organizations.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/event-types.repository.ts similarity index 95% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/event-types.repository.ts index 0a9c6b0d47aca9..d8beb5b88155f5 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/event-types.repository.ts @@ -1,7 +1,7 @@ import { getEventTypeById } from "@calcom/platform-libraries/event-types"; import type { PrismaClient } from "@calcom/prisma"; import { Injectable } from "@nestjs/common"; -import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +import { CreateEventTypeInput_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/create-event-type.input"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { UsersService } from "@/modules/users/services/users.service"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/create-event-type.input.ts similarity index 93% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/create-event-type.input.ts index 62257716f36c8c..5b1ab9e6044357 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/create-event-type.input.ts @@ -10,7 +10,7 @@ import { Min, ValidateNested, } from "class-validator"; -import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input"; +import { EventTypeLocation_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/event-type-location.input"; export const CREATE_EVENT_LENGTH_EXAMPLE = 60; export const CREATE_EVENT_SLUG_EXAMPLE = "cooking-class"; @@ -99,5 +99,5 @@ export class CreateEventTypeInput_2024_04_15 { // @ApiHideProperty() // @IsOptional() // @IsEnum(SchedulingType) - // schedulingType?: SchedulingType; -> import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type"; + // schedulingType?: SchedulingType; -> import { SchedulingType } from "@/platform/event-types/inputs/enums/scheduling-type"; } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/editable.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/enums/editable.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/editable.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/enums/editable.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/field-type.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/enums/field-type.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/field-type.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/enums/field-type.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/frequency.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/enums/frequency.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/frequency.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/enums/frequency.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/period-type.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/enums/period-type.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/period-type.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/enums/period-type.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/scheduling-type.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/enums/scheduling-type.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/enums/scheduling-type.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/enums/scheduling-type.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/event-type-id.input.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/event-type-id.input.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/event-type-id.input.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/event-type-id.input.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/event-type-location.input.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/event-type-location.input.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/get-public-event-type-query-params.input.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/update-event-type.input.ts similarity index 93% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/update-event-type.input.ts index c662da716f4b3b..9c545c9beba5b7 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/inputs/update-event-type.input.ts @@ -14,10 +14,10 @@ import { Min, ValidateNested, } from "class-validator"; -import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/editable"; -import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type"; -import { Frequency } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/frequency"; -import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input"; +import { Editable } from "@/platform/event-types/event-types_2024_04_15/inputs/enums/editable"; +import { BaseField } from "@/platform/event-types/event-types_2024_04_15/inputs/enums/field-type"; +import { Frequency } from "@/platform/event-types/event-types_2024_04_15/inputs/enums/frequency"; +import { EventTypeLocation_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/event-type-location.input"; // note(Lauris): We will gradually expose more properties if any customer needs them. // Just uncomment any below when requested. Go to bottom of file to see UpdateEventTypeInput. @@ -354,7 +354,7 @@ export class UpdateEventTypeInput_2024_04_15 { // @IsEnum(PeriodType) // @IsOptional() - // periodType?: PeriodType; -> import { PeriodType } from "@/ee/event-types/inputs/enums/period-type"; + // periodType?: PeriodType; -> import { PeriodType } from "@/platform/event-types/inputs/enums/period-type"; // @IsDate() // @IsOptional() @@ -443,7 +443,7 @@ export class UpdateEventTypeInput_2024_04_15 { // @IsEnum(SchedulingType) // @IsOptional() - // schedulingType?: SchedulingType; -> import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type"; + // schedulingType?: SchedulingType; -> import { SchedulingType } from "@/platform/event-types/inputs/enums/scheduling-type"; // @IsInt() // @IsOptional() diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/create-event-type.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/create-event-type.output.ts similarity index 85% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/create-event-type.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/create-event-type.output.ts index c7675708fe99d9..2410a05f7842d1 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/create-event-type.output.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/create-event-type.output.ts @@ -1,4 +1,4 @@ -import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output"; +import { EventTypeOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/event-type.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/delete-event-type.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/delete-event-type.output.ts similarity index 92% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/delete-event-type.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/delete-event-type.output.ts index 76ddc81563d5f1..be9c3489dd79a2 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/delete-event-type.output.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/delete-event-type.output.ts @@ -2,7 +2,7 @@ import { CREATE_EVENT_LENGTH_EXAMPLE, CREATE_EVENT_SLUG_EXAMPLE, CREATE_EVENT_TITLE_EXAMPLE, -} from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +} from "@/platform/event-types/event-types_2024_04_15/inputs/create-event-type.input"; import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty as DocsProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/event-type.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/event-type.output.ts similarity index 89% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/event-type.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/event-type.output.ts index 700f8a4786715f..b8e3d0efc0251f 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/event-type.output.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/event-type.output.ts @@ -3,15 +3,15 @@ import { CREATE_EVENT_LENGTH_EXAMPLE, CREATE_EVENT_SLUG_EXAMPLE, CREATE_EVENT_TITLE_EXAMPLE, -} from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; -import { PeriodType } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/period-type"; -import { SchedulingType } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/scheduling-type"; -import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input"; +} from "@/platform/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +import { PeriodType } from "@/platform/event-types/event-types_2024_04_15/inputs/enums/period-type"; +import { SchedulingType } from "@/platform/event-types/event-types_2024_04_15/inputs/enums/scheduling-type"; +import { EventTypeLocation_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/event-type-location.input"; import { BookingField_2024_04_15, IntervalLimits_2024_04_15, RecurringEvent_2024_04_15, -} from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input"; +} from "@/platform/event-types/event-types_2024_04_15/inputs/update-event-type.input"; import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/get-event-type-public.output.ts similarity index 97% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/get-event-type-public.output.ts index acd4a2918f5107..9244eaa451a47e 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/get-event-type-public.output.ts @@ -1,20 +1,19 @@ +import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { + IsArray, IsBoolean, + IsEnum, IsInt, + IsNumber, + IsObject, IsOptional, IsString, IsUrl, ValidateNested, - IsArray, - IsObject, - IsNumber, - IsEnum, } from "class-validator"; -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; - class Location { @IsString() @ApiProperty() @@ -291,7 +290,7 @@ class Schedule { timeZone!: string | null; } -class PublicEventTypeOutput { +export class PublicEventTypeOutput { @IsInt() @ApiProperty() id!: number; @@ -317,10 +316,6 @@ class PublicEventTypeOutput { @ApiProperty() isInstantEvent!: boolean; - @IsOptional() - @ApiPropertyOptional() - aiPhoneCallConfig?: any; - @IsOptional() @ApiPropertyOptional() schedulingType?: any; @@ -394,10 +389,6 @@ class PublicEventTypeOutput { @ApiPropertyOptional({ type: String, nullable: true }) successRedirectUrl?: string | null; - @IsArray() - @ApiProperty() - workflows!: any[]; - @IsArray() @ApiPropertyOptional() hosts?: any[]; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/get-event-type.output.ts similarity index 86% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/get-event-type.output.ts index 4d0cc43eb41477..fda02150aaa854 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type.output.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/get-event-type.output.ts @@ -1,4 +1,4 @@ -import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output"; +import { EventTypeOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/event-type.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/get-event-types-public.output.ts similarity index 92% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/get-event-types-public.output.ts index 1fecef2ffa7e63..751585d02b01e6 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-types-public.output.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/get-event-types-public.output.ts @@ -2,7 +2,7 @@ import { CREATE_EVENT_LENGTH_EXAMPLE, CREATE_EVENT_SLUG_EXAMPLE, CREATE_EVENT_TITLE_EXAMPLE, -} from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +} from "@/platform/event-types/event-types_2024_04_15/inputs/create-event-type.input"; import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty as DocsProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-types.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/get-event-types.output.ts similarity index 88% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-types.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/get-event-types.output.ts index de791d0a7c54ce..29faffb8f3dd5f 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-types.output.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/get-event-types.output.ts @@ -1,4 +1,4 @@ -import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output"; +import { EventTypeOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/event-type.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsArray, IsEnum, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/update-event-type.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/update-event-type.output.ts similarity index 85% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/update-event-type.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/update-event-type.output.ts index 52b68f4399da5e..0e401acc527f27 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/update-event-type.output.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/outputs/update-event-type.output.ts @@ -1,4 +1,4 @@ -import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output"; +import { EventTypeOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/event-type.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/services/event-types.service.ts similarity index 88% rename from apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_04_15/services/event-types.service.ts index f3b3264483a42e..0b1cc8f189cad3 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_04_15/services/event-types.service.ts @@ -1,11 +1,11 @@ -import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_04_15/constants/constants"; -import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository"; -import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; -import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/editable"; -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 { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output"; -import { systemBeforeFieldEmail } from "@/ee/event-types/event-types_2024_06_14/transformers"; +import { DEFAULT_EVENT_TYPES } from "@/platform/event-types/event-types_2024_04_15/constants/constants"; +import { EventTypesRepository_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/event-types.repository"; +import { CreateEventTypeInput_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +import { Editable } from "@/platform/event-types/event-types_2024_04_15/inputs/enums/editable"; +import { BaseField } from "@/platform/event-types/event-types_2024_04_15/inputs/enums/field-type"; +import { UpdateEventTypeInput_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/update-event-type.input"; +import { EventTypeOutput } from "@/platform/event-types/event-types_2024_04_15/outputs/event-type.output"; +import { systemBeforeFieldEmail } from "@/platform/event-types/event-types_2024_06_14/transformers"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/constants/constants.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/constants/constants.ts similarity index 88% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/constants/constants.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/constants/constants.ts index 10168129330b8d..80adb2b02beec9 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/constants/constants.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/constants/constants.ts @@ -1,4 +1,4 @@ -import { OrganizerIntegrationLocation } from "@/ee/event-types/event-types_2024_06_14/transformers"; +import { OrganizerIntegrationLocation } from "@/platform/event-types/event-types_2024_06_14/transformers"; type BaseEventType = { length: number; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts similarity index 99% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts index 44e9ddef629128..f5133c5bb70d48 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts @@ -39,8 +39,8 @@ import { randomString } from "test/utils/randomString"; import { withApiAuth } from "test/utils/withApiAuth"; 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 { CreateEventTypeInput_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +import { EventTypesModule_2024_06_14 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/controllers/event-types.controller.ts similarity index 84% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/controllers/event-types.controller.ts index 2a1f83cddc1f65..c075c46feada3e 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/controllers/event-types.controller.ts @@ -1,13 +1,40 @@ -import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output"; -import { DeleteEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/delete-event-type.output"; -import { GetEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output"; -import { GetEventTypesOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-types.output"; -import { UpdateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/update-event-type.output"; -import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer"; -import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; -import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service"; -import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; -import type { DatabaseEventType } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; +import { + EVENT_TYPE_READ, + EVENT_TYPE_WRITE, + SUCCESS_STATUS, + VERSION_2024_06_14, +} from "@calcom/platform-constants"; +import { + CreateEventTypeInput_2024_06_14, + GetEventTypesQuery_2024_06_14, + UpdateEventTypeInput_2024_06_14, +} from "@calcom/platform-types"; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + NotFoundException, + Param, + ParseIntPipe, + Patch, + Post, + Query, + UseGuards, +} from "@nestjs/common"; +import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; +import { CreateEventTypeOutput_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/outputs/create-event-type.output"; +import { DeleteEventTypeOutput_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/outputs/delete-event-type.output"; +import { GetEventTypeOutput_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/outputs/get-event-type.output"; +import { GetEventTypesOutput_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/outputs/get-event-types.output"; +import { UpdateEventTypeOutput_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/outputs/update-event-type.output"; +import { EventTypeResponseTransformPipe } from "@/platform/event-types/event-types_2024_06_14/pipes/event-type-response.transformer"; +import { EventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/event-types.service"; +import { InputEventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/input-event-types.service"; +import type { DatabaseEventType } from "@/platform/event-types/event-types_2024_06_14/services/output-event-types.service"; +import { OutputEventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/output-event-types.service"; import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions"; import { API_KEY_OR_ACCESS_TOKEN_HEADER, @@ -25,37 +52,9 @@ import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; -import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer"; -import type { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service"; +import { OutputTeamEventTypesResponsePipe } from "@/modules/teams/event-types/pipes/output-team-event-types-response.pipe"; +import type { DatabaseTeamEventType } from "@/modules/teams/event-types/services/output-team-event-types.service"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { - Controller, - UseGuards, - Get, - Param, - Post, - Body, - NotFoundException, - Patch, - HttpCode, - HttpStatus, - Delete, - Query, - ParseIntPipe, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { - EVENT_TYPE_READ, - EVENT_TYPE_WRITE, - SUCCESS_STATUS, - VERSION_2024_06_14, -} from "@calcom/platform-constants"; -import { - UpdateEventTypeInput_2024_06_14, - GetEventTypesQuery_2024_06_14, - CreateEventTypeInput_2024_06_14, -} from "@calcom/platform-types"; @Controller({ path: "/v2/event-types", diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/event-types.module.ts similarity index 57% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/event-types.module.ts index 1ea35964919cf1..41e1383257939d 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/event-types.module.ts @@ -1,19 +1,19 @@ -import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { EventTypesController_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/controllers/event-types.controller"; -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer"; -import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; -import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service"; -import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; -import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository"; +import { CalendarsRepository } from "@/platform/calendars/calendars.repository"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { EventTypesController_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/controllers/event-types.controller"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; +import { EventTypeResponseTransformPipe } from "@/platform/event-types/event-types_2024_06_14/pipes/event-type-response.transformer"; +import { EventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/event-types.service"; +import { InputEventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/input-event-types.service"; +import { OutputEventTypesService_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/services/output-event-types.service"; +import { SchedulesRepository_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/schedules.repository"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service"; import { MembershipsModule } from "@/modules/memberships/memberships.module"; -import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer"; -import { OutputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/output.service"; +import { OutputTeamEventTypesResponsePipe } from "@/modules/teams/event-types/pipes/output-team-event-types-response.pipe"; +import { OutputTeamEventTypesService } from "@/modules/teams/event-types/services/output-team-event-types.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module"; @@ -43,7 +43,7 @@ import { Module } from "@nestjs/common"; AppsRepository, CalendarsRepository, OutputTeamEventTypesResponsePipe, - OutputOrganizationsEventTypesService, + OutputTeamEventTypesService, TeamsEventTypesRepository, ], controllers: [EventTypesController_2024_06_14], diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/event-types.repository.ts similarity index 98% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/event-types.repository.ts index 54c9bb006b1141..0eed45eeac6f0d 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/event-types.repository.ts @@ -1,7 +1,7 @@ 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 { InputEventTransformed_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/transformed"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/inputs/create-phone-call.input.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/inputs/create-phone-call.input.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/outputs/create-event-type.output.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/outputs/create-event-type.output.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/outputs/create-phone-call.output.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/outputs/create-phone-call.output.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/delete-event-type.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/outputs/delete-event-type.output.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/delete-event-type.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/outputs/delete-event-type.output.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/outputs/get-event-type.output.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/outputs/get-event-type.output.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-types.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/outputs/get-event-types.output.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-types.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/outputs/get-event-types.output.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/update-event-type.output.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/outputs/update-event-type.output.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/update-event-type.output.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/outputs/update-event-type.output.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/pipes/event-type-response.transformer.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/pipes/event-type-response.transformer.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/services/event-types.service.ts similarity index 91% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/services/event-types.service.ts index 550e12f89cfc7f..a24eac3b17d1e8 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/services/event-types.service.ts @@ -1,29 +1,28 @@ -import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_06_14/constants/constants"; -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { DatabaseEventType } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; -import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed"; -import { SystemField, CustomField } from "@/ee/event-types/event-types_2024_06_14/transformers"; -import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository"; -import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator"; -import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; -import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service"; -import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; -import { UsersService } from "@/modules/users/services/users.service"; -import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository"; -import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; - import { dynamicEvent } from "@calcom/platform-libraries"; import { createEventType, - updateEventType, - getEventTypesPublic, EventTypesPublic, + getEventTypesPublic, + updateEventType, } from "@calcom/platform-libraries/event-types"; import type { GetEventTypesQuery_2024_06_14, SortOrderType } from "@calcom/platform-types"; import type { EventType } from "@calcom/prisma/client"; +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; +import { DEFAULT_EVENT_TYPES } from "@/platform/event-types/event-types_2024_06_14/constants/constants"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; +import { DatabaseEventType } from "@/platform/event-types/event-types_2024_06_14/services/output-event-types.service"; +import { InputEventTransformed_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/transformed"; +import { CustomField, SystemField } from "@/platform/event-types/event-types_2024_06_14/transformers"; +import { SchedulesRepository_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/schedules.repository"; +import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator"; +import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; +import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service"; +import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; +import { DatabaseTeamEventType } from "@/modules/teams/event-types/services/output-team-event-types.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; +import { UsersService } from "@/modules/users/services/users.service"; +import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository"; @Injectable() export class EventTypesService_2024_06_14 { @@ -47,14 +46,10 @@ export class EventTypesService_2024_06_14 { const { destinationCalendar: _destinationCalendar, ...rest } = body; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore const { eventType: eventTypeCreated } = await createEventType({ input: rest, ctx: { user: eventTypeUser, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore prisma: this.dbWrite.prisma, }, }); @@ -66,8 +61,6 @@ export class EventTypesService_2024_06_14 { }, ctx: { user: eventTypeUser, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore prisma: this.dbWrite.prisma, }, }); @@ -316,8 +309,6 @@ export class EventTypesService_2024_06_14 { input: { id: eventTypeId, ...body }, ctx: { user: eventTypeUser, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore prisma: this.dbWrite.prisma, }, }); diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/services/input-event-types.service.ts similarity index 95% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/services/input-event-types.service.ts index 2b16fa0d31b904..ab45444695b92a 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/services/input-event-types.service.ts @@ -1,48 +1,48 @@ -import { ConnectedCalendarsData } from "@/ee/calendars/outputs/connected-calendars.output"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed"; -import { - transformBookingFieldsApiToInternal, - transformLocationsApiToInternal, - transformIntervalLimitsApiToInternal, - transformFutureBookingLimitsApiToInternal, - transformRecurrenceApiToInternal, - systemBeforeFieldName, - systemBeforeFieldEmail, - systemBeforeFieldLocation, - systemAfterFieldTitle, - systemAfterFieldNotes, - systemAfterFieldGuests, - systemAfterFieldRescheduleReason, - transformBookerLayoutsApiToInternal, - transformConfirmationPolicyApiToInternal, - transformEventColorsApiToInternal, - transformSeatsApiToInternal, - SystemField, - CustomField, - InternalLocation, - InternalLocationSchema, -} from "@/ee/event-types/event-types_2024_06_14/transformers"; -import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable, BadRequestException } from "@nestjs/common"; - import { slugifyLenient } from "@calcom/platform-libraries"; +import type { CredentialDataWithTeamName } from "@calcom/platform-libraries/app-store"; import { getApps, getUsersCredentialsIncludeServiceAccountKey } from "@calcom/platform-libraries/app-store"; import { - validateCustomEventName, EventTypeMetaDataSchema, EventTypeMetadata, + validateCustomEventName, } from "@calcom/platform-libraries/event-types"; import { CreateEventTypeInput_2024_06_14, DestinationCalendar_2024_06_14, InputBookingField_2024_06_14, OutputUnknownLocation_2024_06_14, - UpdateEventTypeInput_2024_06_14, supportedIntegrations, + UpdateEventTypeInput_2024_06_14, } from "@calcom/platform-types"; import { BookerLayouts } from "@calcom/prisma/zod-utils"; +import { BadRequestException, Injectable } from "@nestjs/common"; +import { ConnectedCalendarsData } from "@/platform/calendars/outputs/connected-calendars.output"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; +import { InputEventTransformed_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/transformed"; +import { + CustomField, + InternalLocation, + InternalLocationSchema, + SystemField, + systemAfterFieldGuests, + systemAfterFieldNotes, + systemAfterFieldRescheduleReason, + systemAfterFieldTitle, + systemBeforeFieldEmail, + systemBeforeFieldLocation, + systemBeforeFieldName, + transformBookerLayoutsApiToInternal, + transformBookingFieldsApiToInternal, + transformConfirmationPolicyApiToInternal, + transformEventColorsApiToInternal, + transformFutureBookingLimitsApiToInternal, + transformIntervalLimitsApiToInternal, + transformLocationsApiToInternal, + transformRecurrenceApiToInternal, + transformSeatsApiToInternal, +} from "@/platform/event-types/event-types_2024_06_14/transformers"; +import { UserWithProfile } from "@/modules/users/users.repository"; interface ValidationContext { eventTypeId?: number; @@ -530,7 +530,7 @@ export class InputEventTypesService_2024_06_14 { ) { const calendars: ConnectedCalendarsData = await this.calendarsService.getCalendars(userId); - const allCals = calendars.connectedCalendars.map((cal) => cal.calendars ?? []).flat(); + const allCals = calendars.connectedCalendars.flatMap((cal) => cal.calendars ?? []); const matchedCalendar = allCals.find( (cal) => @@ -552,7 +552,7 @@ export class InputEventTypesService_2024_06_14 { async validateInputUseDestinationCalendarEmail(userId: number) { const calendars: ConnectedCalendarsData = await this.calendarsService.getCalendars(userId); - const allCals = calendars.connectedCalendars.map((cal) => cal.calendars ?? []).flat(); + const allCals = calendars.connectedCalendars.flatMap((cal) => cal.calendars ?? []); const primaryCalendar = allCals.find((cal) => cal.primary); @@ -607,7 +607,7 @@ export class InputEventTypesService_2024_06_14 { "mirotalk-video": "mirotalk", "jelly-video": "jelly", "jelly-conferencing": "jelly", - "huddle": "huddle01", + huddle: "huddle01", "element-call-video": "element-call", "eightxeight-video": "eightxeight", "discord-video": "discord", @@ -619,7 +619,9 @@ export class InputEventTypesService_2024_06_14 { const credentials = await getUsersCredentialsIncludeServiceAccountKey(user); - const foundApp = getApps(credentials, true).filter((app) => app.slug === appSlug)[0]; + const foundApp = getApps(credentials as CredentialDataWithTeamName[], true).filter( + (app) => app.slug === appSlug + )[0]; const appLocation = foundApp?.appData?.location; @@ -629,7 +631,9 @@ export class InputEventTypesService_2024_06_14 { return foundApp.credential; } - transformInputDisableRescheduling(disableRescheduling: CreateEventTypeInput_2024_06_14["disableRescheduling"]) { + transformInputDisableRescheduling( + disableRescheduling: CreateEventTypeInput_2024_06_14["disableRescheduling"] + ) { if (!disableRescheduling) { return {}; } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.spec.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/services/output-event-types.service.spec.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.spec.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/services/output-event-types.service.spec.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/services/output-event-types.service.ts similarity index 99% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/services/output-event-types.service.ts index 346a4e85ca60d2..a34bd297cfe710 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/services/output-event-types.service.ts @@ -40,7 +40,7 @@ import { transformRecurrenceInternalToApi, transformRequiresConfirmationInternalToApi, transformSeatsInternalToApi, -} from "@/ee/event-types/event-types_2024_06_14/transformers"; +} from "@/platform/event-types/event-types_2024_06_14/transformers"; import { ProfileMinimal, UsersService } from "@/modules/users/services/users.service"; type EventTypeUser = { @@ -354,7 +354,6 @@ export class OutputEventTypesService_2024_06_14 { bookingFields: null, customInputs: [], metadata: null, - workflows: [], isOrgTeamEvent, }); return this.transformBookingFields(defaultBookingFields); diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformed/event-type.tranformed.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformed/event-type.tranformed.ts similarity index 97% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformed/event-type.tranformed.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformed/event-type.tranformed.ts index e8847662777aa2..f70b530a84c4b9 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformed/event-type.tranformed.ts +++ b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformed/event-type.tranformed.ts @@ -6,7 +6,7 @@ import type { transformSeatsApiToInternal, transformBookingFieldsApiToInternal, InternalLocationsSchema, -} from "@/ee/event-types/event-types_2024_06_14/transformers"; +} from "@/platform/event-types/event-types_2024_06_14/transformers"; import type { z } from "zod"; import type { diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformed/index.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformed/index.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformed/index.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformed/index.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/api-to-internal.spec.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/api-to-internal.spec.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/api-to-internal.spec.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/api-to-internal.spec.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/booker-layouts.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/booker-layouts.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/booker-layouts.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/booker-layouts.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/booking-fields.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/booking-fields.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/booking-fields.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/booking-fields.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/confirmation-policy.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/confirmation-policy.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/confirmation-policy.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/confirmation-policy.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/event-colors.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/event-colors.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/event-colors.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/event-colors.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/future-booking-limits.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/future-booking-limits.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/future-booking-limits.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/future-booking-limits.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/index.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/index.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/index.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/index.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/interval-limits.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/interval-limits.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/interval-limits.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/interval-limits.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/locations.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/locations.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/locations.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/locations.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/recurrence.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/recurrence.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/recurrence.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/recurrence.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/seats.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/seats.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/seats.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/api-to-internal/seats.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/index.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/index.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/index.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/index.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/booker-layouts.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/booker-layouts.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/booker-layouts.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/booker-layouts.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/booking-fields.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/booking-fields.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/booking-fields.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/booking-fields.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/event-type-colors.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/event-type-colors.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/event-type-colors.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/event-type-colors.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/future-booking-limits.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/future-booking-limits.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/future-booking-limits.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/future-booking-limits.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/index.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/index.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/index.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/index.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/interval-limits.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/interval-limits.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/interval-limits.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/interval-limits.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/locations.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/locations.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/locations.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/locations.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/recurrence.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/recurrence.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/recurrence.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/recurrence.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/requires-confirmation.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/requires-confirmation.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/requires-confirmation.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/requires-confirmation.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/seats.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/seats.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/seats.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal-to-api/seats.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal/locations.ts b/apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal/locations.ts similarity index 100% rename from apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal/locations.ts rename to apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/internal/locations.ts diff --git a/apps/api/v2/src/ee/gcal/gcal.controller.e2e-spec.ts b/apps/api/v2/src/platform/gcal/gcal.controller.e2e-spec.ts similarity index 98% rename from apps/api/v2/src/ee/gcal/gcal.controller.e2e-spec.ts rename to apps/api/v2/src/platform/gcal/gcal.controller.e2e-spec.ts index e0012f3478954a..7fc4e6ef2a05ab 100644 --- a/apps/api/v2/src/ee/gcal/gcal.controller.e2e-spec.ts +++ b/apps/api/v2/src/platform/gcal/gcal.controller.e2e-spec.ts @@ -11,7 +11,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { CalendarsServiceMock } from "test/mocks/calendars-service-mock"; import { AppModule } from "@/app.module"; import { bootstrap } from "@/bootstrap"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { CalendarsService } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/gcal/gcal.controller.ts b/apps/api/v2/src/platform/gcal/gcal.controller.ts similarity index 92% rename from apps/api/v2/src/ee/gcal/gcal.controller.ts rename to apps/api/v2/src/platform/gcal/gcal.controller.ts index 7ef128ae069024..cfae5d0b9d08e9 100644 --- a/apps/api/v2/src/ee/gcal/gcal.controller.ts +++ b/apps/api/v2/src/platform/gcal/gcal.controller.ts @@ -1,7 +1,7 @@ -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { GcalAuthUrlOutput } from "@/ee/gcal/outputs/auth-url.output"; -import { GcalCheckOutput } from "@/ee/gcal/outputs/check.output"; -import { GcalSaveRedirectOutput } from "@/ee/gcal/outputs/save-redirect.output"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { GcalAuthUrlOutput } from "@/platform/gcal/outputs/auth-url.output"; +import { GcalCheckOutput } from "@/platform/gcal/outputs/check.output"; +import { GcalSaveRedirectOutput } from "@/platform/gcal/outputs/save-redirect.output"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { GCalService } from "@/modules/apps/services/gcal.service"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; diff --git a/apps/api/v2/src/ee/gcal/gcal.module.ts b/apps/api/v2/src/platform/gcal/gcal.module.ts similarity index 77% rename from apps/api/v2/src/ee/gcal/gcal.module.ts rename to apps/api/v2/src/platform/gcal/gcal.module.ts index 8a0319031db75b..cc28a31104d04e 100644 --- a/apps/api/v2/src/ee/gcal/gcal.module.ts +++ b/apps/api/v2/src/platform/gcal/gcal.module.ts @@ -1,7 +1,7 @@ -import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; -import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service"; -import { CalendarsService } from "@/ee/calendars/services/calendars.service"; -import { GcalController } from "@/ee/gcal/gcal.controller"; +import { CalendarsRepository } from "@/platform/calendars/calendars.repository"; +import { CalendarsCacheService } from "@/platform/calendars/services/calendars-cache.service"; +import { CalendarsService } from "@/platform/calendars/services/calendars.service"; +import { GcalController } from "@/platform/gcal/gcal.controller"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { GCalService } from "@/modules/apps/services/gcal.service"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; diff --git a/apps/api/v2/src/ee/gcal/outputs/auth-url.output.ts b/apps/api/v2/src/platform/gcal/outputs/auth-url.output.ts similarity index 100% rename from apps/api/v2/src/ee/gcal/outputs/auth-url.output.ts rename to apps/api/v2/src/platform/gcal/outputs/auth-url.output.ts diff --git a/apps/api/v2/src/ee/gcal/outputs/check.output.ts b/apps/api/v2/src/platform/gcal/outputs/check.output.ts similarity index 100% rename from apps/api/v2/src/ee/gcal/outputs/check.output.ts rename to apps/api/v2/src/platform/gcal/outputs/check.output.ts diff --git a/apps/api/v2/src/ee/gcal/outputs/save-redirect.output.ts b/apps/api/v2/src/platform/gcal/outputs/save-redirect.output.ts similarity index 100% rename from apps/api/v2/src/ee/gcal/outputs/save-redirect.output.ts rename to apps/api/v2/src/platform/gcal/outputs/save-redirect.output.ts diff --git a/apps/api/v2/src/ee/me/me.controller.e2e-spec.ts b/apps/api/v2/src/platform/me/me.controller.e2e-spec.ts similarity index 98% rename from apps/api/v2/src/ee/me/me.controller.e2e-spec.ts rename to apps/api/v2/src/platform/me/me.controller.e2e-spec.ts index e5a7d3b148e55a..ff0eae9aca213f 100644 --- a/apps/api/v2/src/ee/me/me.controller.e2e-spec.ts +++ b/apps/api/v2/src/platform/me/me.controller.e2e-spec.ts @@ -13,7 +13,7 @@ import { randomString } from "test/utils/randomString"; import { withApiAuth } from "test/utils/withApiAuth"; import { AppModule } from "@/app.module"; import { bootstrap } from "@/bootstrap"; -import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesModule_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/me/me.controller.ts b/apps/api/v2/src/platform/me/me.controller.ts similarity index 92% rename from apps/api/v2/src/ee/me/me.controller.ts rename to apps/api/v2/src/platform/me/me.controller.ts index 16602654b46ccd..2ab85caff9b67d 100644 --- a/apps/api/v2/src/ee/me/me.controller.ts +++ b/apps/api/v2/src/platform/me/me.controller.ts @@ -1,6 +1,6 @@ -import { GetMeOutput } from "@/ee/me/outputs/get-me.output"; -import { UpdateMeOutput } from "@/ee/me/outputs/update-me.output"; -import { MeService } from "@/ee/me/services/me.service"; +import { GetMeOutput } from "@/platform/me/outputs/get-me.output"; +import { UpdateMeOutput } from "@/platform/me/outputs/update-me.output"; +import { MeService } from "@/platform/me/services/me.service"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; diff --git a/apps/api/v2/src/ee/me/me.module.ts b/apps/api/v2/src/platform/me/me.module.ts similarity index 73% rename from apps/api/v2/src/ee/me/me.module.ts rename to apps/api/v2/src/platform/me/me.module.ts index ccef7ad838224b..1def351f0a6aa6 100644 --- a/apps/api/v2/src/ee/me/me.module.ts +++ b/apps/api/v2/src/platform/me/me.module.ts @@ -1,6 +1,6 @@ -import { MeController } from "@/ee/me/me.controller"; -import { MeService } from "@/ee/me/services/me.service"; -import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; +import { MeController } from "@/platform/me/me.controller"; +import { MeService } from "@/platform/me/services/me.service"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; import { PrismaWorkerModule } from "@/modules/prisma/prisma-worker.module"; import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module"; diff --git a/apps/api/v2/src/ee/me/outputs/get-me.output.ts b/apps/api/v2/src/platform/me/outputs/get-me.output.ts similarity index 90% rename from apps/api/v2/src/ee/me/outputs/get-me.output.ts rename to apps/api/v2/src/platform/me/outputs/get-me.output.ts index 9f0bdfd4e65cdf..90cb08715a7918 100644 --- a/apps/api/v2/src/ee/me/outputs/get-me.output.ts +++ b/apps/api/v2/src/platform/me/outputs/get-me.output.ts @@ -1,4 +1,4 @@ -import { MeOutput } from "@/ee/me/outputs/me.output"; +import { MeOutput } from "@/platform/me/outputs/me.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/me/outputs/me.output.ts b/apps/api/v2/src/platform/me/outputs/me.output.ts similarity index 100% rename from apps/api/v2/src/ee/me/outputs/me.output.ts rename to apps/api/v2/src/platform/me/outputs/me.output.ts diff --git a/apps/api/v2/src/ee/me/outputs/update-me.output.ts b/apps/api/v2/src/platform/me/outputs/update-me.output.ts similarity index 90% rename from apps/api/v2/src/ee/me/outputs/update-me.output.ts rename to apps/api/v2/src/platform/me/outputs/update-me.output.ts index 53fc44cb26b6e6..81043e9ec56af3 100644 --- a/apps/api/v2/src/ee/me/outputs/update-me.output.ts +++ b/apps/api/v2/src/platform/me/outputs/update-me.output.ts @@ -1,4 +1,4 @@ -import { MeOutput } from "@/ee/me/outputs/me.output"; +import { MeOutput } from "@/platform/me/outputs/me.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/me/services/me.service.ts b/apps/api/v2/src/platform/me/services/me.service.ts similarity index 96% rename from apps/api/v2/src/ee/me/services/me.service.ts rename to apps/api/v2/src/platform/me/services/me.service.ts index 9320b4dcc5f996..5de9887f5be6a0 100644 --- a/apps/api/v2/src/ee/me/services/me.service.ts +++ b/apps/api/v2/src/platform/me/services/me.service.ts @@ -1,4 +1,4 @@ -import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; +import { SchedulesService_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/services/schedules.service"; import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input"; import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository"; diff --git a/apps/api/v2/src/platform/platform-endpoints-module.ts b/apps/api/v2/src/platform/platform-endpoints-module.ts new file mode 100644 index 00000000000000..ab2e120130c401 --- /dev/null +++ b/apps/api/v2/src/platform/platform-endpoints-module.ts @@ -0,0 +1,39 @@ +import { BookingsModule_2024_04_15 } from "@/platform/bookings/2024-04-15/bookings.module"; +import { BookingsModule_2024_08_13 } from "@/platform/bookings/2024-08-13/bookings.module"; +import { CalendarsModule } from "@/platform/calendars/calendars.module"; +import { EventTypesPrivateLinksModule } from "@/platform/event-types-private-links/event-types-private-links.module"; +import { EventTypesModule_2024_04_15 } from "@/platform/event-types/event-types_2024_04_15/event-types.module"; +import { EventTypesModule_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.module"; +import { GcalModule } from "@/platform/gcal/gcal.module"; +import { MeModule } from "@/platform/me/me.module"; +import { ProviderModule } from "@/platform/provider/provider.module"; +import { SchedulesModule_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesModule_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/schedules.module"; +import { SlotsModule_2024_04_15 } from "@/modules/slots/slots-2024-04-15/slots.module"; +import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module"; +import type { MiddlewareConsumer, NestModule } from "@nestjs/common"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [ + GcalModule, + ProviderModule, + SchedulesModule_2024_04_15, + SchedulesModule_2024_06_11, + MeModule, + EventTypesModule_2024_04_15, + EventTypesModule_2024_06_14, + CalendarsModule, + BookingsModule_2024_04_15, + BookingsModule_2024_08_13, + SlotsModule_2024_04_15, + SlotsModule_2024_09_04, + EventTypesPrivateLinksModule, + ], +}) +export class PlatformEndpointsModule implements NestModule { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + configure(_consumer: MiddlewareConsumer) { + // TODO: apply ratelimits + } +} diff --git a/apps/api/v2/src/ee/provider/outputs/verify-access-token.output.ts b/apps/api/v2/src/platform/provider/outputs/verify-access-token.output.ts similarity index 100% rename from apps/api/v2/src/ee/provider/outputs/verify-access-token.output.ts rename to apps/api/v2/src/platform/provider/outputs/verify-access-token.output.ts diff --git a/apps/api/v2/src/ee/provider/outputs/verify-client.output.ts b/apps/api/v2/src/platform/provider/outputs/verify-client.output.ts similarity index 100% rename from apps/api/v2/src/ee/provider/outputs/verify-client.output.ts rename to apps/api/v2/src/platform/provider/outputs/verify-client.output.ts diff --git a/apps/api/v2/src/ee/provider/provider.controller.ts b/apps/api/v2/src/platform/provider/provider.controller.ts similarity index 92% rename from apps/api/v2/src/ee/provider/provider.controller.ts rename to apps/api/v2/src/platform/provider/provider.controller.ts index f10829eaa5cef2..c434d3a4aeb1c9 100644 --- a/apps/api/v2/src/ee/provider/provider.controller.ts +++ b/apps/api/v2/src/platform/provider/provider.controller.ts @@ -11,8 +11,8 @@ import { UseGuards, } from "@nestjs/common"; import { ApiExcludeController, ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; -import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-access-token.output"; -import { ProviderVerifyClientOutput } from "@/ee/provider/outputs/verify-client.output"; +import { ProviderVerifyAccessTokenOutput } from "@/platform/provider/outputs/verify-access-token.output"; +import { ProviderVerifyClientOutput } from "@/platform/provider/outputs/verify-client.output"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; diff --git a/apps/api/v2/src/ee/provider/provider.module.ts b/apps/api/v2/src/platform/provider/provider.module.ts similarity index 86% rename from apps/api/v2/src/ee/provider/provider.module.ts rename to apps/api/v2/src/platform/provider/provider.module.ts index d96be50d3a6fbb..81b7ac42e0d259 100644 --- a/apps/api/v2/src/ee/provider/provider.module.ts +++ b/apps/api/v2/src/platform/provider/provider.module.ts @@ -1,4 +1,4 @@ -import { CalProviderController } from "@/ee/provider/provider.controller"; +import { CalProviderController } from "@/platform/provider/provider.controller"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/controllers/schedules.controller.e2e-spec.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/controllers/schedules.controller.e2e-spec.ts similarity index 93% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/controllers/schedules.controller.e2e-spec.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/controllers/schedules.controller.e2e-spec.ts index e18eb90bc3e068..6bcbb2755b6861 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/controllers/schedules.controller.e2e-spec.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/controllers/schedules.controller.e2e-spec.ts @@ -11,11 +11,11 @@ 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"; -import { UpdateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/update-schedule.output"; -import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { CreateScheduleOutput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/outputs/create-schedule.output"; +import { GetSchedulesOutput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/outputs/get-schedules.output"; +import { UpdateScheduleOutput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/outputs/update-schedule.output"; +import { SchedulesModule_2024_04_15 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/controllers/schedules.controller.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/controllers/schedules.controller.ts similarity index 80% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/controllers/schedules.controller.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/controllers/schedules.controller.ts index 9e8c92f7979163..b6c1a2c4d0db18 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/controllers/schedules.controller.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/controllers/schedules.controller.ts @@ -1,10 +1,10 @@ -import { CreateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/create-schedule.output"; -import { DeleteScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/delete-schedule.output"; -import { GetDefaultScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-default-schedule.output"; -import { GetScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-schedule.output"; -import { GetSchedulesOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/get-schedules.output"; -import { UpdateScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/update-schedule.output"; -import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; +import { CreateScheduleOutput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/outputs/create-schedule.output"; +import { DeleteScheduleOutput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/outputs/delete-schedule.output"; +import { GetDefaultScheduleOutput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/outputs/get-default-schedule.output"; +import { GetScheduleOutput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/outputs/get-schedule.output"; +import { GetSchedulesOutput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/outputs/get-schedules.output"; +import { UpdateScheduleOutput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/outputs/update-schedule.output"; +import { SchedulesService_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/services/schedules.service"; import { VERSION_2024_04_15_VALUE } from "@/lib/api-versions"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/inputs/create-availability.input.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/inputs/create-availability.input.ts similarity index 100% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/inputs/create-availability.input.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/inputs/create-availability.input.ts diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input.ts similarity index 83% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input.ts index 40e0f53abd38fb..4eed78276c50c0 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input.ts @@ -1,4 +1,4 @@ -import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input"; +import { CreateAvailabilityInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-availability.input"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsArray, IsBoolean, IsTimeZone, IsOptional, IsString, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/create-schedule.output.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/create-schedule.output.ts similarity index 86% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/create-schedule.output.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/create-schedule.output.ts index 6a1184683e769c..b86fb05331e41c 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/create-schedule.output.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/create-schedule.output.ts @@ -1,4 +1,4 @@ -import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output"; +import { ScheduleOutput } from "@/platform/schedules/schedules_2024_04_15/outputs/schedule.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/delete-schedule.output.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/delete-schedule.output.ts similarity index 100% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/delete-schedule.output.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/delete-schedule.output.ts diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/get-default-schedule.output.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/get-default-schedule.output.ts similarity index 86% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/get-default-schedule.output.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/get-default-schedule.output.ts index 0d14fb73268fac..d7ed6d9dfb9b1a 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/get-default-schedule.output.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/get-default-schedule.output.ts @@ -1,4 +1,4 @@ -import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output"; +import { ScheduleOutput } from "@/platform/schedules/schedules_2024_04_15/outputs/schedule.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/get-schedule.output.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/get-schedule.output.ts similarity index 85% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/get-schedule.output.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/get-schedule.output.ts index ae7709e0ba3602..4e43e4c1a62352 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/get-schedule.output.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/get-schedule.output.ts @@ -1,4 +1,4 @@ -import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output"; +import { ScheduleOutput } from "@/platform/schedules/schedules_2024_04_15/outputs/schedule.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/get-schedules.output.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/get-schedules.output.ts similarity index 86% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/get-schedules.output.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/get-schedules.output.ts index 81a9b911bd5b03..30f0ea2dee4638 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/get-schedules.output.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/get-schedules.output.ts @@ -1,4 +1,4 @@ -import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output"; +import { ScheduleOutput } from "@/platform/schedules/schedules_2024_04_15/outputs/schedule.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsArray, IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/schedule-updated.output.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/schedule-updated.output.ts similarity index 100% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/schedule-updated.output.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/schedule-updated.output.ts diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/schedule.output.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/schedule.output.ts similarity index 100% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/schedule.output.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/schedule.output.ts diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/update-schedule.output.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/update-schedule.output.ts similarity index 84% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/update-schedule.output.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/update-schedule.output.ts index 8633a77d1205a0..b456642759403c 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/outputs/update-schedule.output.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/outputs/update-schedule.output.ts @@ -1,4 +1,4 @@ -import { UpdatedScheduleOutput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule-updated.output"; +import { UpdatedScheduleOutput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/outputs/schedule-updated.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/schedules.module.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/schedules.module.ts similarity index 64% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/schedules.module.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/schedules.module.ts index 8a8f7d4e0ec821..68854c88e93dab 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/schedules.module.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/schedules.module.ts @@ -1,6 +1,6 @@ -import { SchedulesController_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/controllers/schedules.controller"; -import { SchedulesRepository_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.repository"; -import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; +import { SchedulesController_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/controllers/schedules.controller"; +import { SchedulesRepository_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.repository"; +import { SchedulesService_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/services/schedules.service"; import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/schedules.repository.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/schedules.repository.ts similarity index 89% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/schedules.repository.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/schedules.repository.ts index c35da0721bebda..8084aa5405d87a 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/schedules.repository.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/schedules.repository.ts @@ -1,5 +1,5 @@ -import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input"; -import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { CreateAvailabilityInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-availability.input"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { Injectable } from "@nestjs/common"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/services/schedules.service.ts b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/services/schedules.service.ts similarity index 93% rename from apps/api/v2/src/ee/schedules/schedules_2024_04_15/services/schedules.service.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_04_15/services/schedules.service.ts index 5980a8243a3c65..b9f79d93bf4a8f 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/services/schedules.service.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_04_15/services/schedules.service.ts @@ -1,6 +1,6 @@ -import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input"; -import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; -import { SchedulesRepository_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.repository"; +import { CreateAvailabilityInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-availability.input"; +import { CreateScheduleInput_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesRepository_2024_04_15 } from "@/platform/schedules/schedules_2024_04_15/schedules.repository"; import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.e2e-spec.ts b/apps/api/v2/src/platform/schedules/schedules_2024_06_11/controllers/schedules.controller.e2e-spec.ts similarity index 98% rename from apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.e2e-spec.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_06_11/controllers/schedules.controller.e2e-spec.ts index f774f4418ed973..d63317201e29be 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.e2e-spec.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_06_11/controllers/schedules.controller.e2e-spec.ts @@ -18,7 +18,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository 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 { SchedulesModule_2024_06_11 } from "@/platform/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"; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.ts b/apps/api/v2/src/platform/schedules/schedules_2024_06_11/controllers/schedules.controller.ts similarity index 98% rename from apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_06_11/controllers/schedules.controller.ts index b4c8da13bc5eb7..c0184b6c6d2b9d 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_06_11/controllers/schedules.controller.ts @@ -1,4 +1,4 @@ -import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; +import { SchedulesService_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/services/schedules.service"; import { VERSION_2024_06_14, VERSION_2024_06_11 } from "@/lib/api-versions"; import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; diff --git a/apps/api/v2/src/platform/schedules/schedules_2024_06_11/schedules.module.ts b/apps/api/v2/src/platform/schedules/schedules_2024_06_11/schedules.module.ts new file mode 100644 index 00000000000000..a7b1d761632c81 --- /dev/null +++ b/apps/api/v2/src/platform/schedules/schedules_2024_06_11/schedules.module.ts @@ -0,0 +1,23 @@ +import { EventTypesModule_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.module"; +import { SchedulesController_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/controllers/schedules.controller"; +import { SchedulesRepository_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/schedules.repository"; +import { InputSchedulesService_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/services/input-schedules.service"; +import { OutputSchedulesService_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/services/output-schedules.service"; +import { SchedulesService_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/services/schedules.service"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [PrismaModule, UsersModule, TokensModule, EventTypesModule_2024_06_14], + providers: [ + SchedulesRepository_2024_06_11, + SchedulesService_2024_06_11, + InputSchedulesService_2024_06_11, + OutputSchedulesService_2024_06_11, + ], + controllers: [SchedulesController_2024_06_11], + exports: [SchedulesService_2024_06_11, SchedulesRepository_2024_06_11, OutputSchedulesService_2024_06_11], +}) +export class SchedulesModule_2024_06_11 {} diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.repository.ts b/apps/api/v2/src/platform/schedules/schedules_2024_06_11/schedules.repository.ts similarity index 100% rename from apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.repository.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_06_11/schedules.repository.ts diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/input-schedules.service.ts b/apps/api/v2/src/platform/schedules/schedules_2024_06_11/services/input-schedules.service.ts similarity index 100% rename from apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/input-schedules.service.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_06_11/services/input-schedules.service.ts diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/output-schedules.service.ts b/apps/api/v2/src/platform/schedules/schedules_2024_06_11/services/output-schedules.service.ts similarity index 100% rename from apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/output-schedules.service.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_06_11/services/output-schedules.service.ts diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/schedules.service.ts b/apps/api/v2/src/platform/schedules/schedules_2024_06_11/services/schedules.service.ts similarity index 92% rename from apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/schedules.service.ts rename to apps/api/v2/src/platform/schedules/schedules_2024_06_11/services/schedules.service.ts index 1973dbc5789b74..bc437a9761837f 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/schedules.service.ts +++ b/apps/api/v2/src/platform/schedules/schedules_2024_06_11/services/schedules.service.ts @@ -5,10 +5,10 @@ import type { } from "@calcom/platform-types"; import type { Schedule } from "@calcom/prisma/client"; import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository"; -import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service"; -import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service"; +import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository"; +import { SchedulesRepository_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/schedules.repository"; +import { InputSchedulesService_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/services/input-schedules.service"; +import { OutputSchedulesService_2024_06_11 } from "@/platform/schedules/schedules_2024_06_11/services/output-schedules.service"; import { UsersRepository } from "@/modules/users/users.repository"; @Injectable() diff --git a/apps/api/v2/src/swagger/generate-swagger.ts b/apps/api/v2/src/swagger/generate-swagger.ts index 13e1c90c333083..b08a574cb288ef 100644 --- a/apps/api/v2/src/swagger/generate-swagger.ts +++ b/apps/api/v2/src/swagger/generate-swagger.ts @@ -22,7 +22,7 @@ export async function generateSwaggerForApp(app: NestExpressApplication) const logger = new Logger("App"); logger.log(`Generating Swagger documentation...\n`); - const config = new DocumentBuilder().setTitle("Cal.com API v2").build(); + const config = new DocumentBuilder().setTitle("Cal.diy API v2").build(); const document = SwaggerModule.createDocument(app, config); document.paths = groupAndSortPathsByFirstTag(document.paths); diff --git a/apps/api/v2/test/fixtures/repository/managed-organizations.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/managed-organizations.repository.fixture.ts deleted file mode 100644 index a29a8a2132866f..00000000000000 --- a/apps/api/v2/test/fixtures/repository/managed-organizations.repository.fixture.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { TestingModule } from "@nestjs/testing"; - -export class ManagedOrganizationsRepositoryFixture { - private prismaReadClient: PrismaReadService["prisma"]; - private prismaWriteClient: PrismaWriteService["prisma"]; - - constructor(private readonly module: TestingModule) { - this.prismaReadClient = module.get(PrismaReadService).prisma; - this.prismaWriteClient = module.get(PrismaWriteService).prisma; - } - - async getOrganizationWithManagedOrganizations(organizationId: number) { - return this.prismaReadClient.team.findUnique({ - where: { - id: organizationId, - isOrganization: true, - }, - include: { - managedOrganization: true, - managedOrganizations: true, - }, - }); - } - - async getManagedOrganization(managerOrganizationId: number, managedOrganizationId: number) { - return this.prismaReadClient.managedOrganization.findUnique({ - where: { - managerOrganizationId_managedOrganizationId: { - managerOrganizationId, - managedOrganizationId, - }, - }, - }); - } - - async createManagedOrganization(managerOrganizationId: number, managedOrganizationId: number) { - return this.prismaWriteClient.managedOrganization.create({ - data: { - managerOrganizationId, - managedOrganizationId, - }, - }); - } -} diff --git a/apps/api/v2/test/fixtures/repository/routing-forms.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/routing-forms.repository.fixture.ts deleted file mode 100644 index eedb4a46f99e13..00000000000000 --- a/apps/api/v2/test/fixtures/repository/routing-forms.repository.fixture.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { TestingModule } from "@nestjs/testing"; - -import type { Prisma, App_RoutingForms_Form, App_RoutingForms_FormResponse } from "@calcom/prisma/client"; - -export class RoutingFormsRepositoryFixture { - private prismaReadClient: PrismaReadService["prisma"]; - private prismaWriteClient: PrismaWriteService["prisma"]; - - constructor(module: TestingModule) { - this.prismaReadClient = module.get(PrismaReadService).prisma; - this.prismaWriteClient = module.get(PrismaWriteService).prisma; - } - - async get(routingFormId: App_RoutingForms_Form["id"]) { - return this.prismaReadClient.app_RoutingForms_Form.findUnique({ where: { id: routingFormId } }); - } - - async create(data: Prisma.App_RoutingForms_FormCreateInput) { - return this.prismaWriteClient.app_RoutingForms_Form.create({ data }); - } - - async delete(routingFormId: App_RoutingForms_Form["id"]) { - return this.prismaWriteClient.app_RoutingForms_Form.delete({ where: { id: routingFormId } }); - } - - async deleteResponse(routingFormResponseId: App_RoutingForms_FormResponse["id"]) { - return this.prismaWriteClient.app_RoutingForms_FormResponse.delete({ - where: { id: routingFormResponseId }, - }); - } -} diff --git a/apps/api/v2/test/fixtures/repository/workflow-reminder.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/workflow-reminder.repository.fixture.ts deleted file mode 100644 index b9532176766df3..00000000000000 --- a/apps/api/v2/test/fixtures/repository/workflow-reminder.repository.fixture.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { TestingModule } from "@nestjs/testing"; - -export class WorkflowReminderRepositoryFixture { - private prismaReadClient: PrismaReadService["prisma"]; - private prismaWriteClient: PrismaWriteService["prisma"]; - - constructor(private readonly module: TestingModule) { - this.prismaReadClient = module.get(PrismaReadService).prisma; - this.prismaWriteClient = module.get(PrismaWriteService).prisma; - } - - async getByBookingUid(uid: string) { - return this.prismaReadClient.workflowReminder.findFirst({ where: { bookingUid: uid } }); - } -} diff --git a/apps/api/v2/test/fixtures/repository/workflow.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/workflow.repository.fixture.ts deleted file mode 100644 index 111b3023839e77..00000000000000 --- a/apps/api/v2/test/fixtures/repository/workflow.repository.fixture.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { TestingModule } from "@nestjs/testing"; - -import type { Prisma } from "@calcom/prisma/client"; - -export class WorkflowRepositoryFixture { - private prismaReadClient: PrismaReadService["prisma"]; - private prismaWriteClient: PrismaWriteService["prisma"]; - - constructor(module: TestingModule) { - this.prismaReadClient = module.get(PrismaReadService).prisma; - this.prismaWriteClient = module.get(PrismaWriteService).prisma; - } - - async create(data: Prisma.WorkflowCreateInput) { - return this.prismaWriteClient.workflow.create({ data }); - } - - async delete(id: number) { - return this.prismaWriteClient.workflow.delete({ where: { id } }); - } -} diff --git a/apps/api/v2/test/mocks/calendars-service-mock.ts b/apps/api/v2/test/mocks/calendars-service-mock.ts index ba957cc7b624b3..e6acfab21d2eaa 100644 --- a/apps/api/v2/test/mocks/calendars-service-mock.ts +++ b/apps/api/v2/test/mocks/calendars-service-mock.ts @@ -74,7 +74,6 @@ export class CalendarsServiceMock { userId: null, id: 0, delegationCredentialId: null, - domainWideDelegationCredentialId: null, createdAt: new Date(), updatedAt: new Date(), customCalendarReminder: 10, diff --git a/apps/api/v2/test/utils/withNoThrottler.ts b/apps/api/v2/test/utils/withNoThrottler.ts index ce4e3cdd5da609..08518ba83db91e 100644 --- a/apps/api/v2/test/utils/withNoThrottler.ts +++ b/apps/api/v2/test/utils/withNoThrottler.ts @@ -1,5 +1,10 @@ import { CustomThrottlerGuard } from "@/lib/throttler-guard"; export const mockThrottlerGuard = (): void => { - jest.spyOn(CustomThrottlerGuard.prototype, "handleRequest").mockResolvedValue(true); + jest + .spyOn( + CustomThrottlerGuard.prototype as unknown as { handleRequest: () => Promise }, + "handleRequest" + ) + .mockResolvedValue(true); }; diff --git a/apps/api/v2/tsconfig.json b/apps/api/v2/tsconfig.json index de16359d754ee6..77f60320080228 100644 --- a/apps/api/v2/tsconfig.json +++ b/apps/api/v2/tsconfig.json @@ -26,7 +26,6 @@ "@calcom/platform-libraries/emails": ["../../../packages/platform/libraries/emails.ts"], "@calcom/platform-libraries/schedules": ["../../../packages/platform/libraries/schedules.ts"], "@calcom/platform-libraries/app-store": ["../../../packages/platform/libraries/app-store.ts"], - "@calcom/platform-libraries/workflows": ["../../../packages/platform/libraries/workflows.ts"], "@calcom/platform-libraries/conferencing": ["../../../packages/platform/libraries/conferencing.ts"], "@calcom/platform-libraries/repositories": ["../../../packages/platform/libraries/repositories.ts"], "@calcom/platform-libraries/bookings": ["../../../packages/platform/libraries/bookings.ts"], @@ -34,7 +33,8 @@ "@calcom/platform-libraries/organizations": ["../../../packages/platform/libraries/organizations.ts"], "@calcom/platform-libraries/errors": ["../../../packages/platform/libraries/errors.ts"], "@calcom/platform-libraries/calendars": ["../../../packages/platform/libraries/calendars.ts"], - "@calcom/platform-libraries/tasker": ["../../../packages/platform/libraries/tasker.ts"] + "@calcom/platform-libraries/tasker": ["../../../packages/platform/libraries/tasker.ts"], + "@calcom/platform-libraries/pbac": ["../../../packages/platform/libraries/pbac.ts"] }, "incremental": true, "skipLibCheck": true, diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index 7686097b0a0641..f91358ca07b689 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -51,15 +51,15 @@ const navbar: React.ReactElement = ( const footer: React.ReactElement = (
- Cal.diy is the open source community edition of Cal.com. Cal.com® and Cal® + Cal.diy is the open source community edition of Cal.com. Cal.diy® and Cal® are a registered trademark by Cal.com, Inc. All rights reserved.
); export const metadata: { title: string; description: string } = { - title: "Cal.com Docs", - description: "Cal.com self-hosting documentation", + title: "Cal.diy Docs", + description: "Cal.diy self-hosting documentation", }; export default async function RootLayout({ @@ -79,7 +79,7 @@ export default async function RootLayout({ {children} diff --git a/apps/docs/content/apps/google.mdx b/apps/docs/content/apps/google.mdx index 91ca0963aaa362..39d64d3fb51bd0 100644 --- a/apps/docs/content/apps/google.mdx +++ b/apps/docs/content/apps/google.mdx @@ -21,11 +21,11 @@ 9. **Add Authorized Redirect URIs** - Under Authorized redirect URI's, add the URIs: ``` -/api/integrations/googlecalendar/callback -/api/auth/callback/google +/api/integrations/googlecalendar/callback +/api/auth/callback/google ``` -Replace `` with the URL where your application runs. +Replace `` with the URL where your application runs. 10. **Download the OAuth Client ID JSON** - The key will be created, redirecting you back to the Credentials page. Select the new client ID under "OAuth 2.0 Client IDs", then click "Download JSON". Copy the JSON file contents and paste the entire string into the `.env` and `.env.appStore` files under the `GOOGLE_API_CREDENTIALS` key. @@ -37,7 +37,7 @@ GOOGLE_LOGIN_ENABLED=false This will configure the Google integration as an Internal app, restricting login access. -### Adding Google Calendar to Cal.com App Store +### Adding Google Calendar to Cal.diy App Store After adding Google credentials, you can now add the Google Calendar App to the app store. Repopulate the App store by running: diff --git a/apps/docs/content/apps/hubspot.mdx b/apps/docs/content/apps/hubspot.mdx index 5417d3ec2ea485..3d871f09fa8a3b 100644 --- a/apps/docs/content/apps/hubspot.mdx +++ b/apps/docs/content/apps/hubspot.mdx @@ -22,13 +22,13 @@ HUBSPOT_CLIENT_SECRET 7. **Set the Redirect URL for OAuth** - Set the Redirect URL for OAuth to: ``` -/api/integrations/hubspot/callback +/api/integrations/hubspot/callback ``` -Replace `` with the URL where your application is hosted. +Replace `` with the URL where your application is hosted. 8. **Select Required Scopes** - In the "Scopes" section, select "Read" and "Write" for the scope called `crm.objects.contacts`. 9. **Save the Application** - Click the "Save" button at the bottom of the page. -10. **Complete HubSpot Integration** - You're all set! Any booking in Cal.com will now be created as a meeting in HubSpot for your contacts. +10. **Complete HubSpot Integration** - You're all set! Any booking in Cal.diy will now be created as a meeting in HubSpot for your contacts. diff --git a/apps/docs/content/apps/microsoft.mdx b/apps/docs/content/apps/microsoft.mdx index 8f1957386eeb6e..b3757c400a77c8 100644 --- a/apps/docs/content/apps/microsoft.mdx +++ b/apps/docs/content/apps/microsoft.mdx @@ -11,10 +11,10 @@ 4. **Configure the Web redirect URI** - Set the Web redirect URI to: ``` -/api/integrations/office365calendar/callback +/api/integrations/office365calendar/callback ``` -Replace `` with the URL where your application runs. +Replace `` with the URL where your application runs. 5. **Obtain and set the MS_GRAPH_CLIENT_ID** - Use the Application (client) ID as the value for `MS_GRAPH_CLIENT_ID` in your `.env` file. diff --git a/apps/docs/content/apps/stripe.mdx b/apps/docs/content/apps/stripe.mdx index 6813b350b96ad9..be78118af29abc 100644 --- a/apps/docs/content/apps/stripe.mdx +++ b/apps/docs/content/apps/stripe.mdx @@ -8,10 +8,10 @@ 3. **Activate OAuth for Standard Accounts** - Go to [Stripe Connect Settings](https://dashboard.stripe.com/settings/connect) and activate OAuth for Standard Accounts. -4. **Add the redirect URL** - Add the following redirect URL, replacing `` with your application's URL: +4. **Add the redirect URL** - Add the following redirect URL, replacing `` with your application's URL: ``` -/api/integrations/stripepayment/callback +/api/integrations/stripepayment/callback ``` 5. **Save the Stripe Client ID** - Copy your client ID (`ca_...`) to `STRIPE_CLIENT_ID` in the `.env` file. @@ -19,7 +19,7 @@ 6. **Set up a Stripe webhook** - Open [Stripe Webhooks](https://dashboard.stripe.com/webhooks) and add: ``` -/api/integrations/stripepayment/webhook +/api/integrations/stripepayment/webhook ``` as the webhook for connected applications. diff --git a/apps/docs/content/apps/zoho.mdx b/apps/docs/content/apps/zoho.mdx index 2b2a9efa57a157..94aef108495947 100644 --- a/apps/docs/content/apps/zoho.mdx +++ b/apps/docs/content/apps/zoho.mdx @@ -22,16 +22,16 @@ ZOHOCRM_CLIENT_SECRET 7. **Set the Redirect URL for OAuth** - Set the Redirect URL for OAuth to: ``` -/api/integrations/zohocrm/callback +/api/integrations/zohocrm/callback ``` -Replace `` with your application URL. +Replace `` with your application URL. 8. **Enable Multi-DC Option** - In the "Settings" section, check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers. 9. **Save your settings** - Click "Save" or "UPDATE" at the bottom of the page. -10. **Integration is Ready** - Your ZohoCRM integration can now be easily added in the Cal.com settings. +10. **Integration is Ready** - Your ZohoCRM integration can now be easily added in the Cal.diy settings. #### Obtaining Zoho Calendar Client ID and Secret @@ -40,19 +40,19 @@ Replace `` with your application URL. 2. **Create a New Server-based Application** - Choose "Server-based Applications" and set the Redirect URL for OAuth to: ``` -/api/integrations/zohocalendar/callback +/api/integrations/zohocalendar/callback ``` -Replace `` with your application URL. +Replace `` with your application URL. 3. **Fill in Client Details** - Enter any information you want in the "Client Details" tab. 4. **Go to the Client Secret tab** - Navigate to the "Client Secret" tab. -5. **Save Credentials in Cal.com Admin Panel** - Copy the Client ID and Client Secret to your app keys in the Cal.com admin panel at: +5. **Save Credentials in Cal.diy Admin Panel** - Copy the Client ID and Client Secret to your app keys in the Cal.diy admin panel at: ``` -/settings/admin/apps +/settings/admin/apps ``` 6. **Enable Multi-DC Option** - In the "Settings" section of Zoho API Console, check the "Multi-DC" option if you wish to use the same OAuth credentials across data centers. @@ -78,10 +78,10 @@ Replace `` with your application URL. 3. **Set the Redirect URL for OAuth** - Set the Redirect URL for OAuth to: ``` -/api/integrations/zoho-bigin/callback +/api/integrations/zoho-bigin/callback ``` -Replace `` with your application URL. +Replace `` with your application URL. 4. **Go to the Client Secret tab** - Navigate to the "Client Secret" tab. @@ -94,4 +94,4 @@ ZOHO_BIGIN_CLIENT_SECRET 6. **Enable Multi-DC Option** - In the "Settings" section, check the "Multi-DC" option if you wish to use the same OAuth credentials across data centers. -7. **Complete Zoho Bigin Integration** - Your Zoho Bigin integration is now ready and can be added from the Cal.com app store. +7. **Complete Zoho Bigin Integration** - Your Zoho Bigin integration is now ready and can be added from the Cal.diy app store. diff --git a/apps/docs/content/apps/zoom.mdx b/apps/docs/content/apps/zoom.mdx index 8219afded89c88..4f974a7db913dd 100644 --- a/apps/docs/content/apps/zoom.mdx +++ b/apps/docs/content/apps/zoom.mdx @@ -26,10 +26,10 @@ ZOOM_CLIENT_SECRET 9. **Set the Redirect URL for OAuth** - Set the Redirect URL for OAuth to: ``` -/api/integrations/zoomvideo/callback +/api/integrations/zoomvideo/callback ``` -Replace `` with your application URL. +Replace `` with your application URL. 10. **Configure Allow List and Subdomain Check** - Add the redirect URL to the allow list and enable "Subdomain check". Ensure it displays "saved" below the form. @@ -37,4 +37,4 @@ Replace `` with your application URL. 12. **Save the Scope** - Click "Done" to save the scope settings. -13. **Complete Zoom Integration** - Your Zoom integration is now ready and can be easily added in the Cal.com settings. +13. **Complete Zoom Integration** - Your Zoom integration is now ready and can be easily added in the Cal.diy settings. diff --git a/apps/docs/content/deployments/gcp.mdx b/apps/docs/content/deployments/gcp.mdx index 1a2f3b58db9972..614cf78efa69b5 100644 --- a/apps/docs/content/deployments/gcp.mdx +++ b/apps/docs/content/deployments/gcp.mdx @@ -82,7 +82,7 @@ Now that Docker is installed and running, you can deploy Cal.diy on your virtual Use the following command to pull the Cal.diy Docker image from Docker Hub: ```bash -docker pull cal/calcom +docker pull calcom/cal.diy ``` #### Run the Docker Container @@ -90,7 +90,7 @@ docker pull cal/calcom Run the Docker container using the following command: ```bash -docker run -d -p 80:80 cal/cal.com +docker run -d -p 80:80 calcom/cal.diy ``` This command maps port 80 on your local machine to port 80 inside the container, so you can access Cal.diy from outside the container. diff --git a/apps/docs/content/deployments/vercel.mdx b/apps/docs/content/deployments/vercel.mdx index 6045f392d044d7..7851a7209a2e0a 100644 --- a/apps/docs/content/deployments/vercel.mdx +++ b/apps/docs/content/deployments/vercel.mdx @@ -8,7 +8,7 @@ You need a PostgresDB database hosted somewhere. [Supabase](https://supabase.com ## One Click Deployment -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/calcom/cal.com) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/calcom/cal.diy) ## Manual Deployment @@ -17,7 +17,7 @@ You need a PostgresDB database hosted somewhere. [Supabase](https://supabase.com 1. **Fork and clone the repository** ```bash -git clone https://github.com/<>/cal.com.git +git clone https://github.com/<>/cal.diy.git ``` 2. **Set environment variables** diff --git a/apps/docs/content/index.mdx b/apps/docs/content/index.mdx index 15f09af1dae375..c9dd317198f71c 100644 --- a/apps/docs/content/index.mdx +++ b/apps/docs/content/index.mdx @@ -20,7 +20,7 @@ import { Callout } from "nextra/components"; ## Getting Started -- [Installation](/installation) - Learn how to install and set up Cal.com +- [Installation](/installation) - Learn how to install and set up Cal.diy - [Database Migrations](/database-migrations) - Manage database schema changes - [Upgrading](/upgrading) - Keep your instance up to date - [Docker](/docker) - Deploy with Docker @@ -28,7 +28,7 @@ import { Callout } from "nextra/components"; ## Deployments -Deploy Cal.com on your preferred platform: +Deploy Cal.diy on your preferred platform: - [AWS](/deployments/aws) - [Azure](/deployments/azure) @@ -46,7 +46,7 @@ of monthly bookings by large organizations. Cal.diy is the new open source commu Like any diy project, Cal.com, Inc. does not guarantee security and safety of the open source project. Cal.diy is community maintained and strictly recommended for personal, non-production use. Please use at your own risk. -For any commerical usage, please visit Cal.com or request enterprise access to our on-prem hosting: https://cal.com/sales. +For any commercial usage, please visit Cal.com or request enterprise access to our on-prem hosting: https://cal.com/sales. Find all differences in features below (summarized): @@ -121,4 +121,4 @@ Find all differences in features below (summarized): | Delegation | ❌ | ✅ | | Workspace Platform | ❌ | ✅ | | Admin Panel | ❌ | ✅ | -| | [Install](/installation) |[Sign up](https://cal.com/signup)| \ No newline at end of file +| | [Install](/installation) |[Sign up](https://cal.com/signup)| diff --git a/docs/self-hosting/troubleshooting.mdx b/apps/docs/content/troubleshooting.mdx similarity index 94% rename from docs/self-hosting/troubleshooting.mdx rename to apps/docs/content/troubleshooting.mdx index 3ba12f2fd28f9c..2f7ae3711bb118 100644 --- a/docs/self-hosting/troubleshooting.mdx +++ b/apps/docs/content/troubleshooting.mdx @@ -3,7 +3,7 @@ title: "Troubleshooting" icon: "triangle-exclamation" --- -This guide covers the most common issues encountered when self-hosting Cal.com, along with their solutions. +This guide covers the most common issues encountered when self-hosting Cal.diy, along with their solutions. ## Onboarding / Setup Issues @@ -29,7 +29,7 @@ NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_test_... > **Note:** Replace these with your actual Stripe API keys from the [Stripe Dashboard](https://dashboard.stripe.com/apikeys). If you don't need payment features, you can safely leave these empty — the app will function without them, but Stripe-related features will be disabled. -Related issue: [#25993](https://github.com/calcom/cal.com/issues/25993) +Related issue: [#25993](https://github.com/calcom/cal.diy/issues/25993) --- @@ -37,9 +37,9 @@ Related issue: [#25993](https://github.com/calcom/cal.com/issues/25993) ### Redirect to `localhost:3000` After Deployment -**Symptom:** After deploying Cal.com to a server or domain, login redirects or internal links point back to `http://localhost:3000` instead of your actual domain. +**Symptom:** After deploying Cal.diy to a server or domain, login redirects or internal links point back to `http://localhost:3000` instead of your actual domain. -**Cause:** The environment variables `NEXTAUTH_URL` and `NEXT_PUBLIC_WEBAPP_URL` are not set to your production domain. These variables tell Cal.com and NextAuth where the app is hosted. +**Cause:** The environment variables `NEXTAUTH_URL` and `NEXT_PUBLIC_WEBAPP_URL` are not set to your production domain. These variables tell Cal.diy and NextAuth where the app is hosted. **Solution:** Update your `.env` file to use your actual domain: @@ -55,7 +55,7 @@ NEXTAUTH_URL=https://cal.yourdomain.com - For Docker deployments, these must be set **before** building the image, as `NEXT_PUBLIC_WEBAPP_URL` is a build-time variable (it is inlined by Next.js during the build). Rebuild the image after changing it. - For Vercel deployments, you do **not** need to set `NEXTAUTH_URL` — Vercel automatically infers it from the deployment URL via the `VERCEL_URL` environment variable. -Related issue: [#21921](https://github.com/calcom/cal.com/issues/21921) +Related issue: [#21921](https://github.com/calcom/cal.diy/issues/21921) --- @@ -139,7 +139,7 @@ This allows the container to resolve your public domain internally while keeping ### SSL / HTTPS Issues Behind a Reverse Proxy -**Symptom:** Requests fail with SSL certificate errors when Cal.com is behind a load balancer or reverse proxy that handles HTTPS termination. +**Symptom:** Requests fail with SSL certificate errors when Cal.diy is behind a load balancer or reverse proxy that handles HTTPS termination. **Solution (choose one):** @@ -213,6 +213,6 @@ yarn workspace @calcom/prisma db-deploy If your issue is not listed here: -1. Search the [Cal.com GitHub Issues](https://github.com/calcom/cal.com/issues) — many common problems have documented solutions in issue threads. +1. Search the [Cal.diy GitHub Issues](https://github.com/calcom/cal.diy/issues) — many common problems have documented solutions in issue threads. 2. Check the [Cal.com Community](https://community.cal.com) forum. 3. Review the [Docker configuration](./docker) and [Installation guide](./installation) for any steps you may have missed. diff --git a/apps/web/app/(booking-page-wrapper)/[user]/[type]/page.tsx b/apps/web/app/(booking-page-wrapper)/[user]/[type]/page.tsx index e2da8c5b70cedf..5eb9e6e9a577ec 100644 --- a/apps/web/app/(booking-page-wrapper)/[user]/[type]/page.tsx +++ b/apps/web/app/(booking-page-wrapper)/[user]/[type]/page.tsx @@ -1,31 +1,49 @@ -import { CustomI18nProvider } from "app/CustomI18nProvider"; -import { withAppDirSsr } from "app/WithAppDirSsr"; +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { loadTranslations } from "@calcom/i18n/server"; +import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx"; +import { getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps"; import type { PageProps } from "app/_types"; import { generateMeetingMetadata } from "app/_utils"; -import { headers, cookies } from "next/headers"; +import { CustomI18nProvider } from "app/CustomI18nProvider"; +import { withAppDirSsr } from "app/WithAppDirSsr"; +import type { Metadata } from "next"; +import { cookies, headers } from "next/headers"; +import type React from "react"; +import type { PageProps as LegacyPageProps } from "~/users/views/users-type-public-view"; +import LegacyPage from "~/users/views/users-type-public-view"; -import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { loadTranslations } from "@calcom/i18n/server"; +const getData: (ctx: ReturnType) => Promise = + withAppDirSsr(getServerSideProps); -import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx"; +const ServerPage = async ({ params, searchParams }: PageProps): Promise => { + const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); + const props = await getData(legacyCtx); -import { getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps"; + const locale = props.eventData?.interfaceLanguage; + if (locale) { + const ns = "common"; + const translations = await loadTranslations(locale, ns); + return ( + + + + ); + } -import type { PageProps as LegacyPageProps } from "~/users/views/users-type-public-view"; -import LegacyPage from "~/users/views/users-type-public-view"; + return ; +}; -export const generateMetadata = async ({ params, searchParams }: PageProps) => { +export const generateMetadata = async ({ params, searchParams }: PageProps): Promise => { const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); const props = await getData(legacyCtx); const { booking, isSEOIndexable = true, eventData, isBrandingHidden } = props; const rescheduleUid = booking?.uid; const profileName = eventData?.profile?.name ?? ""; - const profileImage = eventData?.profile.image; const title = eventData?.title ?? ""; const meeting = { title, - profile: { name: profileName, image: profileImage }, + profile: { name: profileName, image: eventData?.profile.image }, users: eventData?.subsetOfUsers.map((user) => ({ name: `${user.name}`, @@ -38,7 +56,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => { (t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`, (t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`, isBrandingHidden, - getOrgFullOrigin(eventData?.entity.orgSlug ?? null), + WEBAPP_URL, `/${decodedParams.user}/${decodedParams.type}` ); @@ -50,24 +68,5 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => { }, }; }; -const getData = withAppDirSsr(getServerSideProps); - -const ServerPage = async ({ params, searchParams }: PageProps) => { - const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); - const props = await getData(legacyCtx); - - const locale = props.eventData?.interfaceLanguage; - if (locale) { - const ns = "common"; - const translations = await loadTranslations(locale, ns); - return ( - - - - ); - } - - return ; -}; export default ServerPage; diff --git a/apps/web/app/(booking-page-wrapper)/[user]/page.tsx b/apps/web/app/(booking-page-wrapper)/[user]/page.tsx index 0e10610b39a155..d08bf51eb5ba9e 100644 --- a/apps/web/app/(booking-page-wrapper)/[user]/page.tsx +++ b/apps/web/app/(booking-page-wrapper)/[user]/page.tsx @@ -1,23 +1,32 @@ -import { withAppDirSsr } from "app/WithAppDirSsr"; +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx"; +import { getServerSideProps } from "@server/lib/[user]/getServerSideProps"; import type { PageProps } from "app/_types"; import { generateMeetingMetadata } from "app/_utils"; -import { headers, cookies } from "next/headers"; - -import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { withAppDirSsr } from "app/WithAppDirSsr"; +import type { Metadata } from "next"; +import { cookies, headers } from "next/headers"; +import type React from "react"; +import type { PageProps as LegacyPageProps } from "~/users/views/users-public-view"; +import LegacyPage from "~/users/views/users-public-view"; -import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx"; +const getData: (ctx: ReturnType) => Promise = + withAppDirSsr(getServerSideProps); -import { getServerSideProps } from "@server/lib/[user]/getServerSideProps"; +const ServerPage = async ({ params, searchParams }: PageProps): Promise => { + const props = await getData( + buildLegacyCtx(await headers(), await cookies(), await params, await searchParams) + ); -import type { PageProps as LegacyPageProps } from "~/users/views/users-public-view"; -import LegacyPage from "~/users/views/users-public-view"; + return ; +}; -export const generateMetadata = async ({ params, searchParams }: PageProps) => { +export const generateMetadata = async ({ params, searchParams }: PageProps): Promise => { const props = await getData( buildLegacyCtx(await headers(), await cookies(), await params, await searchParams) ); - const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props; + const { profile, markdownStrippedBio, isOrgSEOIndexable } = props; const isOrg = !!profile?.organization; const allowSEOIndexing = (!isOrg && profile.allowSEOIndexing) || (isOrg && isOrgSEOIndexable && profile.allowSEOIndexing); @@ -32,7 +41,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => { () => profile.name, () => markdownStrippedBio, false, - getOrgFullOrigin(entity.orgSlug ?? null), + WEBAPP_URL, `/${decodeParams(await params).user}` ); @@ -45,13 +54,4 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => { }; }; -const getData = withAppDirSsr(getServerSideProps); -const ServerPage = async ({ params, searchParams }: PageProps) => { - const props = await getData( - buildLegacyCtx(await headers(), await cookies(), await params, await searchParams) - ); - - return ; -}; - export default ServerPage; diff --git a/apps/web/app/(booking-page-wrapper)/booking/[uid]/page.tsx b/apps/web/app/(booking-page-wrapper)/booking/[uid]/page.tsx index 340fcfee9da8fd..bf47729d2fb240 100644 --- a/apps/web/app/(booking-page-wrapper)/booking/[uid]/page.tsx +++ b/apps/web/app/(booking-page-wrapper)/booking/[uid]/page.tsx @@ -1,19 +1,15 @@ -import { CustomI18nProvider } from "app/CustomI18nProvider"; -import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { PageProps as _PageProps } from "app/_types"; -import { _generateMetadata } from "app/_utils"; -import { cookies, headers } from "next/headers"; - -import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import { loadTranslations } from "@calcom/i18n/server"; import { BookingStatus } from "@calcom/prisma/enums"; - import { buildLegacyCtx } from "@lib/buildLegacyCtx"; - +import type { PageProps as _PageProps } from "app/_types"; +import { _generateMetadata } from "app/_utils"; +import { CustomI18nProvider } from "app/CustomI18nProvider"; +import { withAppDirSsr } from "app/WithAppDirSsr"; +import { cookies, headers } from "next/headers"; import OldPage from "~/bookings/views/bookings-single-view"; import { - getServerSideProps, type PageProps as ClientPageProps, + getServerSideProps, } from "~/bookings/views/bookings-single-view.getServerSideProps"; const getData = withAppDirSsr(getServerSideProps); @@ -30,7 +26,7 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) => (t) => t(`booking_${needsConfirmation ? "submitted" : "confirmed"}${recurringBookings ? "_recurring" : ""}`), false, - getOrgFullOrigin(orgSlug), + process.env.NEXT_PUBLIC_WEBAPP_URL ?? "", `/booking/${(await params).uid}` ); diff --git a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/[user]/[type]/embed/page.tsx b/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/[user]/[type]/embed/page.tsx deleted file mode 100644 index c3c954b4c407fe..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/[user]/[type]/embed/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { CustomI18nProvider } from "app/CustomI18nProvider"; -import withEmbedSsrAppDir from "app/WithEmbedSSR"; -import type { PageProps as ServerPageProps } from "app/_types"; -import { cookies, headers } from "next/headers"; - -import { loadTranslations } from "@calcom/i18n/server"; - -import { buildLegacyCtx } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps"; - -import type { PageProps as TeamTypePageProps } from "~/team/type-view"; -import TeamTypePage from "~/team/type-view"; -import UserTypePage from "~/users/views/users-type-public-view"; -import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view"; - -const getData = withEmbedSsrAppDir(getServerSideProps); - -export type ClientPageProps = UserTypePageProps | TeamTypePageProps; - -export const generateMetadata = async () => { - return { - robots: { - follow: false, - index: false, - }, - }; -}; - -const ServerPage = async ({ params, searchParams }: ServerPageProps) => { - const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); - const props = await getData(context); - - const eventLocale = props.eventData?.interfaceLanguage; - const ns = "common"; - const translations = await loadTranslations(eventLocale ?? "en", ns); - - if ((props as TeamTypePageProps)?.teamId) { - return eventLocale ? ( - - - - ) : ( - - ); - } - - return eventLocale ? ( - - - - ) : ( - - ); -}; - -export default ServerPage; diff --git a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/[user]/[type]/page.tsx b/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/[user]/[type]/page.tsx deleted file mode 100644 index 313e6423492765..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/[user]/[type]/page.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { CustomI18nProvider } from "app/CustomI18nProvider"; -import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { PageProps } from "app/_types"; -import { generateMeetingMetadata } from "app/_utils"; -import { cookies, headers } from "next/headers"; - -import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { loadTranslations } from "@calcom/i18n/server"; - -import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps"; - -import type { PageProps as TeamTypePageProps } from "~/team/type-view"; -import TeamTypePage from "~/team/type-view"; -import UserTypePage from "~/users/views/users-type-public-view"; -import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view"; - -export type OrgTypePageProps = UserTypePageProps | TeamTypePageProps; -const getData = withAppDirSsr(getServerSideProps); - -export const generateMetadata = async ({ params, searchParams }: PageProps) => { - const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); - const props = await getData(legacyCtx); - - const { booking, isSEOIndexable = true, eventData, isBrandingHidden } = props; - const rescheduleUid = booking?.uid; - - const profileName = eventData?.profile?.name ?? ""; - const profileImage = eventData?.profile.image; - const title = eventData?.title ?? ""; - const meeting = { - title, - profile: { name: profileName, image: profileImage }, - users: [ - ...( - (eventData as UserTypePageProps["eventData"])?.subsetOfUsers ?? - (eventData as TeamTypePageProps["eventData"])?.users ?? - [] - ).map((user) => ({ - name: `${user.name}`, - username: `${user.username}`, - })), - ], - }; - const decodedParams = decodeParams(await params); - const metadata = await generateMeetingMetadata( - meeting, - (t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`, - (t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`, - isBrandingHidden, - getOrgFullOrigin(eventData?.entity.orgSlug ?? null), - `/${decodedParams.user}/${decodedParams.type}` - ); - - return { - ...metadata, - robots: { - follow: !(eventData?.hidden || !isSEOIndexable), - index: !(eventData?.hidden || !isSEOIndexable), - }, - }; -}; - -const ServerPage = async ({ params, searchParams }: PageProps) => { - const props = await getData( - buildLegacyCtx(await headers(), await cookies(), await params, await searchParams) - ); - - const eventLocale = props.eventData?.interfaceLanguage; - const ns = "common"; - const translations = await loadTranslations(eventLocale ?? "en", ns); - - if ((props as TeamTypePageProps)?.teamId) { - return eventLocale ? ( - - - - ) : ( - - ); - } - - return eventLocale ? ( - - - - ) : ( - - ); -}; - -export default ServerPage; diff --git a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/[user]/embed/page.tsx b/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/[user]/embed/page.tsx deleted file mode 100644 index 6034af78e3f105..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/[user]/embed/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import withEmbedSsrAppDir from "app/WithEmbedSSR"; -import type { PageProps as ServerPageProps } from "app/_types"; -import { cookies, headers } from "next/headers"; - -import { buildLegacyCtx } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps"; - -import type { PageProps as TeamPageProps } from "~/team/team-view"; -import TeamPage from "~/team/team-view"; -import UserPage from "~/users/views/users-public-view"; -import type { PageProps as UserPageProps } from "~/users/views/users-public-view"; - -const getData = withEmbedSsrAppDir(getServerSideProps); - -export type ClientPageProps = UserPageProps | TeamPageProps; - -export const generateMetadata = async () => { - return { - robots: { - follow: false, - index: false, - }, - }; -}; - -const ServerPage = async ({ params, searchParams }: ServerPageProps) => { - const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); - const props = await getData(context); - if ((props as TeamPageProps)?.team) return ; - return ; -}; - -export default ServerPage; diff --git a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/[user]/page.tsx b/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/[user]/page.tsx deleted file mode 100644 index 28ac508474f47a..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/[user]/page.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { PageProps } from "app/_types"; -import { generateMeetingMetadata } from "app/_utils"; -import { cookies, headers } from "next/headers"; - -import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage"; - -import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps"; - -import type { PageProps as TeamPageProps } from "~/team/team-view"; -import TeamPage from "~/team/team-view"; -import UserPage from "~/users/views/users-public-view"; -import type { PageProps as UserPageProps } from "~/users/views/users-public-view"; - -export type OrgPageProps = UserPageProps | TeamPageProps; -const getData = withAppDirSsr(getServerSideProps); - -export const generateMetadata = async ({ params, searchParams }: PageProps) => { - const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); - const props = await getData(legacyCtx); - const decodedParams = decodeParams(await params); - - if ((props as TeamPageProps)?.team) { - const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = props as TeamPageProps; - const meeting = { - title: markdownStrippedBio ?? "", - profile: { - name: `${team.name}`, - image: getOrgOrTeamAvatar(team), - }, - }; - return { - ...(await generateMeetingMetadata( - meeting, - (t) => team.name ?? t("nameless_team"), - (t) => team.name ?? t("nameless_team"), - false, - getOrgFullOrigin(currentOrgDomain ?? null), - `/${decodedParams.user}` - )), - robots: { - index: isSEOIndexable, - follow: isSEOIndexable, - }, - }; - } else { - const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props as UserPageProps; - - const meeting = { - title: markdownStrippedBio, - profile: { name: `${profile.name}`, image: profile.image }, - users: [ - { - username: `${profile.username ?? ""}`, - name: `${profile.name ?? ""}`, - }, - ], - }; - const isOrg = !!profile?.organization; - const allowSEOIndexing = - (!isOrg && profile.allowSEOIndexing) || (isOrg && isOrgSEOIndexable && profile.allowSEOIndexing); - return { - ...(await generateMeetingMetadata( - meeting, - () => profile.name, - () => markdownStrippedBio, - false, - getOrgFullOrigin(entity.orgSlug ?? null), - `/${decodedParams.user}` - )), - robots: { - index: allowSEOIndexing, - follow: allowSEOIndexing, - }, - }; - } -}; - -const ServerPage = async ({ params, searchParams }: PageProps) => { - const props = await getData( - buildLegacyCtx(await headers(), await cookies(), await params, await searchParams) - ); - if ((props as TeamPageProps)?.team) { - return ; - } - return ; -}; - -export default ServerPage; diff --git a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/embed/page.tsx b/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/embed/page.tsx deleted file mode 100644 index ed971e8f80b416..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/embed/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../../../team/[slug]/embed/page"; diff --git a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/instant-meeting/team/[slug]/[type]/page.tsx b/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/instant-meeting/team/[slug]/[type]/page.tsx deleted file mode 100644 index 89fe3cef6c15e1..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/instant-meeting/team/[slug]/[type]/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { PageProps as _PageProps } from "app/_types"; -import { generateMeetingMetadata } from "app/_utils"; -import { headers, cookies } from "next/headers"; - -import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; - -import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/org/[orgSlug]/instant-meeting/team/[slug]/[type]/getServerSideProps"; - -import type { Props } from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-meeting-view"; -import Page from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-meeting-view"; - -export const generateMetadata = async ({ params, searchParams }: _PageProps) => { - const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); - const { isBrandingHidden, eventData, teamIsPrivate } = await getData(context); - - const profileName = eventData?.profile.name ?? ""; - const profileImage = eventData?.profile.image; - const title = eventData?.title ?? ""; - - const meeting = { - title, - profile: { name: profileName, image: profileImage }, - // Hide team member names in preview if team is private - users: teamIsPrivate - ? [] - : [ - ...(eventData?.users || []).map((user) => ({ - name: `${user.name}`, - username: `${user.username}`, - })), - ], - }; - const decodedParams = decodeParams(await params); - const metadata = await generateMeetingMetadata( - meeting, - () => `${title} | ${profileName}`, - () => `${title}`, - isBrandingHidden, - getOrgFullOrigin(eventData?.entity.orgSlug ?? null), - `/instant-meeting/team/${decodedParams.slug}/${decodedParams.type}` - ); - - return { - ...metadata, - robots: { - follow: !eventData?.hidden, - index: !eventData?.hidden, - }, - }; -}; - -const getData = withAppDirSsr(getServerSideProps); - -const ServerPage = async ({ params, searchParams }: _PageProps) => { - const props = await getData( - buildLegacyCtx(await headers(), await cookies(), await params, await searchParams) - ); - return ; -}; - -export default ServerPage; diff --git a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/page.tsx b/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/page.tsx deleted file mode 100644 index 2359fb25a1fec6..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import Page from "app/(booking-page-wrapper)/team/[slug]/page"; - -export { generateMetadata } from "app/(booking-page-wrapper)/team/[slug]/page"; -export default Page; diff --git a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/team/[slug]/[type]/page.tsx b/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/team/[slug]/[type]/page.tsx deleted file mode 100644 index 26c2c6eb89f39b..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/team/[slug]/[type]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import Page from "app/(booking-page-wrapper)/team/[slug]/[type]/page"; - -export { generateMetadata } from "app/(booking-page-wrapper)/team/[slug]/[type]/page"; -export default Page; diff --git a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/team/[slug]/page.tsx b/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/team/[slug]/page.tsx deleted file mode 100644 index 2359fb25a1fec6..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/org/[orgSlug]/team/[slug]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import Page from "app/(booking-page-wrapper)/team/[slug]/page"; - -export { generateMetadata } from "app/(booking-page-wrapper)/team/[slug]/page"; -export default Page; diff --git a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/actions.ts b/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/actions.ts deleted file mode 100644 index 6ab17439ddc565..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/actions.ts +++ /dev/null @@ -1,27 +0,0 @@ -"use server"; - -import { revalidateTag } from "next/cache"; - -// Coupled with `getCachedTeamData` in `queries.ts` -export async function revalidateTeamDataCache({ - teamSlug, - orgSlug, -}: { - teamSlug: string; - orgSlug: string | null; -}) { - revalidateTag(`team:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}`, "max"); -} - -// Coupled with `getCachedTeamEventType` in `queries.ts` -export async function revalidateTeamEventTypeCache({ - teamSlug, - meetingSlug, - orgSlug, -}: { - teamSlug: string; - meetingSlug: string; - orgSlug: string | null; -}) { - revalidateTag(`event-type:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}:${meetingSlug}`, "max"); -} diff --git a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/embed/page.tsx b/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/embed/page.tsx deleted file mode 100644 index be318f651d4980..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/embed/page.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { CustomI18nProvider } from "app/CustomI18nProvider"; -import withEmbedSsrAppDir from "app/WithEmbedSSR"; -import type { PageProps as ServerPageProps } from "app/_types"; -import { cookies, headers } from "next/headers"; - -import { loadTranslations } from "@calcom/i18n/server"; - -import { buildLegacyCtx } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps"; - -import TypePage, { type PageProps as ClientPageProps } from "~/team/type-view"; - -export const generateMetadata = async () => { - return { - robots: { - follow: false, - index: false, - }, - }; -}; - -const getData = withEmbedSsrAppDir(getServerSideProps); - -const ServerPage = async ({ params, searchParams }: ServerPageProps) => { - const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); - const props = await getData(context); - - const eventLocale = props.eventData?.interfaceLanguage; - if (eventLocale) { - const ns = "common"; - const translations = await loadTranslations(eventLocale, ns); - return ( - - - - ); - } - - return ; -}; - -export default ServerPage; diff --git a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/page.tsx b/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/page.tsx deleted file mode 100644 index dd6e59d3d5fd22..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/page.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { CustomI18nProvider } from "app/CustomI18nProvider"; -import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { PageProps, Params, SearchParams } from "app/_types"; -import { generateMeetingMetadata } from "app/_utils"; -import { cookies, headers } from "next/headers"; - -import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { FeaturesRepository } from "@calcom/features/flags/features.repository"; -import { loadTranslations } from "@calcom/i18n/server"; -import { prisma } from "@calcom/prisma"; - -import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps"; - -import LegacyPage from "~/team/type-view"; -import type { PageProps as LegacyPageProps } from "~/team/type-view"; - -import CachedTeamBooker, { - generateMetadata as generateCachedMetadata, - getOrgContext, -} from "./pageWithCachedData"; -import { getTeamId } from "./queries"; - -async function isCachedTeamBookingEnabled(params: Params, searchParams: SearchParams): Promise { - if (searchParams.experimentalTeamBookingPageCache !== "true") return false; - - const { teamSlug, currentOrgDomain, isValidOrgDomain } = await getOrgContext(params); - const orgSlug = isValidOrgDomain ? currentOrgDomain : null; - const teamId = await getTeamId(teamSlug, orgSlug); - - if (!teamId) return false; - - const featuresRepository = new FeaturesRepository(prisma); - const isTeamFeatureEnabled = await featuresRepository.checkIfTeamHasFeature( - teamId, - "team-booking-page-cache" - ); - return isTeamFeatureEnabled; -} - -export const generateMetadata = async ({ params, searchParams }: PageProps) => { - if (await isCachedTeamBookingEnabled(await params, await searchParams)) { - return await generateCachedMetadata({ params, searchParams }); - } - - const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); - const props = await getData(legacyCtx); - const { booking, isSEOIndexable, eventData, isBrandingHidden } = props; - - const profileName = eventData?.profile?.name ?? ""; - const profileImage = eventData?.profile.image; - const title = eventData?.title ?? ""; - const meeting = { - title, - profile: { name: profileName, image: profileImage }, - users: [ - ...(eventData?.users || []).map((user) => ({ - name: `${user.name}`, - username: `${user.username}`, - })), - ], - }; - const decodedParams = decodeParams(await params); - const metadata = await generateMeetingMetadata( - meeting, - (t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`, - (t) => `${booking?.uid ? t("reschedule") : ""} ${title}`, - isBrandingHidden, - getOrgFullOrigin(eventData.entity.orgSlug ?? null), - `/team/${decodedParams.slug}/${decodedParams.type}` - ); - - return { - ...metadata, - robots: { - follow: !(eventData?.hidden || !isSEOIndexable), - index: !(eventData?.hidden || !isSEOIndexable), - }, - }; -}; - -const getData = withAppDirSsr(getServerSideProps); - -const ServerPage = async ({ params, searchParams }: PageProps) => { - if (await isCachedTeamBookingEnabled(await params, await searchParams)) { - return await CachedTeamBooker({ params, searchParams }); - } - - const props = await getData( - buildLegacyCtx(await headers(), await cookies(), await params, await searchParams) - ); - - const eventLocale = props.eventData?.interfaceLanguage; - if (eventLocale) { - const ns = "common"; - const translations = await loadTranslations(eventLocale, ns); - return ( - - - - ); - } - - return ; -}; - -export default ServerPage; diff --git a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/pageWithCachedData.tsx b/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/pageWithCachedData.tsx deleted file mode 100644 index 2e488238dc1a08..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/pageWithCachedData.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import { CustomI18nProvider } from "app/CustomI18nProvider"; -import type { PageProps, Params } from "app/_types"; -import { generateMeetingMetadata } from "app/_utils"; -import { headers, cookies } from "next/headers"; -import { notFound, redirect } from "next/navigation"; -import { z } from "zod"; - -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { getBookingForReschedule, type GetBookingType } from "@calcom/features/bookings/lib/get-booking"; -import { getOrgFullOrigin, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { getOrganizationSEOSettings } from "@calcom/features/ee/organizations/lib/orgSettings"; -import type { TeamData } from "@calcom/features/ee/teams/lib/getTeamData"; -import { shouldHideBrandingForTeamEvent } from "@calcom/features/profile/lib/hideBranding"; -import { loadTranslations } from "@calcom/i18n/server"; -import slugify from "@calcom/lib/slugify"; -import { BookingStatus, RedirectType } from "@calcom/prisma/enums"; - -import { buildLegacyCtx, buildLegacyRequest } from "@lib/buildLegacyCtx"; -import { handleOrgRedirect } from "@lib/handleOrgRedirect"; - -import CachedClientView, { type TeamBookingPageProps } from "~/team/type-view-cached"; - -import { getCachedTeamData, getEnrichedEventType, getCRMData, shouldUseApiV2ForTeamSlots } from "./queries"; - -const paramsSchema = z.object({ - slug: z.string().transform((s) => slugify(s)), - type: z.string().transform((s) => slugify(s)), -}); - -const _getTeamMetadataForBooking = (teamData: NonNullable, eventTypeId: number) => { - const organizationSettings = getOrganizationSEOSettings(teamData); - const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false; - - return { - orgBannerUrl: teamData.parent?.bannerUrl ?? "", - hideBranding: shouldHideBrandingForTeamEvent({ - eventTypeId, - team: teamData, - }), - isSEOIndexable: allowSEOIndexing, - }; -}; - -export async function getOrgContext(params: Params) { - const result = paramsSchema.safeParse({ - slug: params?.slug, - type: params?.type, - }); - - if (!result.success) return notFound(); // should never happen - - const { slug: teamSlug, type: meetingSlug } = result.data; - const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig( - buildLegacyRequest(await headers(), await cookies()), - params?.orgSlug ?? undefined - ); - - return { - currentOrgDomain, - isValidOrgDomain, - teamSlug, - meetingSlug, - }; -} - -const _getMultipleDurationValue = ( - multipleDurationConfig: number[] | undefined, - queryDuration: string | string[] | null | undefined, - defaultValue: number -) => { - if (!multipleDurationConfig) return null; - if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration); - return defaultValue; -}; - -export const generateMetadata = async ({ params, searchParams }: PageProps) => { - const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await getOrgContext(await params); - - const teamData = await getCachedTeamData(teamSlug, currentOrgDomain); - if (!teamData) return {}; // should never happen - - const enrichedEventType = await getEnrichedEventType({ - teamSlug, - meetingSlug, - orgSlug: isValidOrgDomain ? currentOrgDomain : null, - fromRedirectOfNonOrgLink: (await searchParams).orgRedirection === "true", - }); - if (!enrichedEventType) return {}; // should never happen - - const title = enrichedEventType.title; - const profileName = enrichedEventType.profile.name ?? ""; - const profileImage = enrichedEventType.profile.image; - - const teamIsPrivate = teamData.isPrivate; - - const meeting = { - title, - profile: { name: profileName, image: profileImage }, - // Hide team member names in preview if team is private - users: teamIsPrivate - ? [] - : [ - ...(enrichedEventType?.subsetOfUsers || []).map((user) => ({ - name: `${user.name}`, - username: `${user.username}`, - })), - ], - }; - - const { hideBranding, isSEOIndexable } = _getTeamMetadataForBooking(teamData, enrichedEventType.id); - - const metadata = await generateMeetingMetadata( - meeting, - () => `${title} | ${profileName}`, - () => title, - hideBranding, - getOrgFullOrigin(enrichedEventType.entity.orgSlug ?? null), - `/team/${teamSlug}/${meetingSlug}` - ); - - return { - ...metadata, - robots: { - follow: !(enrichedEventType.hidden || !isSEOIndexable), - index: !(enrichedEventType.hidden || !isSEOIndexable), - }, - }; -}; - -const CachedTeamBooker = async ({ params, searchParams }: PageProps) => { - const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await getOrgContext(await params); - const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); - - // Handle org redirects - const redirectResult = await handleOrgRedirect({ - slugs: [teamSlug], - redirectType: RedirectType.Team, - eventTypeSlug: meetingSlug, - context: legacyCtx, - currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null, - }); - if (redirectResult) return redirect(redirectResult.redirect.destination); - - const teamData = await getCachedTeamData(teamSlug, currentOrgDomain); - - if (!teamData) return notFound(); - - const enrichedEventType = await getEnrichedEventType({ - teamSlug, - meetingSlug, - orgSlug: isValidOrgDomain ? currentOrgDomain : null, - fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true", - }); - if (!enrichedEventType) return notFound(); - - // Handle rescheduling - const { rescheduleUid } = legacyCtx.query; - let bookingForReschedule: GetBookingType | null = null; - if (rescheduleUid) { - const session = await getServerSession({ req: legacyCtx.req }); - bookingForReschedule = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id); - if (enrichedEventType.disableRescheduling) return redirect(`/booking/${rescheduleUid}`); - - if ( - bookingForReschedule?.status === BookingStatus.CANCELLED && - legacyCtx.query.allowRescheduleForCancelledBooking !== "true" && - !enrichedEventType.allowReschedulingCancelledBookings - ) { - // redirecting to the same booking page without `rescheduleUid` search param - return redirect(`/team/${teamSlug}/${meetingSlug}`); - } - } - - const [crmData, useApiV2] = await Promise.all([ - getCRMData(legacyCtx.query, { - id: enrichedEventType.id, - isInstantEvent: enrichedEventType.isInstantEvent, - schedulingType: enrichedEventType.schedulingType, - metadata: enrichedEventType.metadata, - length: enrichedEventType.length, - }), - shouldUseApiV2ForTeamSlots(teamData.id), - ]); - - const props: TeamBookingPageProps = { - ..._getTeamMetadataForBooking(teamData, enrichedEventType.id), - ...crmData, - useApiV2, - isInstantMeeting: legacyCtx.query.isInstantMeeting === "true", - eventSlug: meetingSlug, - username: teamSlug, - eventData: { - ...enrichedEventType, - }, - entity: { ...enrichedEventType.entity }, - bookingData: bookingForReschedule, - isTeamEvent: true, - durationConfig: enrichedEventType.metadata?.multipleDuration, - duration: _getMultipleDurationValue( - enrichedEventType.metadata?.multipleDuration, - legacyCtx.query.duration, - enrichedEventType.length - ), - }; - const Booker = ; - - const eventLocale = enrichedEventType.interfaceLanguage; - if (eventLocale) { - const ns = "common"; - const translations = await loadTranslations(eventLocale, ns); - return ( - - {Booker} - - ); - } - - return Booker; -}; - -export default CachedTeamBooker; diff --git a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/queries.ts b/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/queries.ts deleted file mode 100644 index 51db301c644895..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/queries.ts +++ /dev/null @@ -1,177 +0,0 @@ -import type { GetServerSidePropsContext } from "next"; -import { unstable_cache } from "next/cache"; - -import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils"; -import { getTeamData } from "@calcom/features/ee/teams/lib/getTeamData"; -import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository"; -import { - getEventTypeHosts, - getProfileFromEvent, - getUsersFromEvent, - processEventDataShared, -} from "@calcom/features/eventtypes/lib/getPublicEvent"; -import { getTeamEventType } from "@calcom/features/eventtypes/lib/getTeamEventType"; -import { FeaturesRepository } from "@calcom/features/flags/features.repository"; -import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; -import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants"; -import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; -import { prisma } from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; -import type { SchedulingType } from "@calcom/prisma/enums"; - -export async function getCachedTeamData(teamSlug: string, orgSlug: string | null) { - return unstable_cache(async () => getTeamData(teamSlug, orgSlug), ["team-data", teamSlug, orgSlug ?? ""], { - revalidate: NEXTJS_CACHE_TTL, - tags: [`team:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}`], - })(); -} - -export async function getCachedTeamEventType(teamSlug: string, meetingSlug: string, orgSlug: string | null) { - return unstable_cache( - async () => getTeamEventType(teamSlug, meetingSlug, orgSlug), - ["team-event-type", teamSlug, meetingSlug, orgSlug ?? ""], - { - revalidate: NEXTJS_CACHE_TTL, - tags: [`event-type:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}:${meetingSlug}`], - } - )(); -} - -export async function getEnrichedEventType({ - teamSlug, - meetingSlug, - orgSlug, - fromRedirectOfNonOrgLink, -}: { - teamSlug: string; - meetingSlug: string; - orgSlug: string | null; - fromRedirectOfNonOrgLink: boolean; -}) { - const [teamData, eventType] = await Promise.all([ - getCachedTeamData(teamSlug, orgSlug), - getCachedTeamEventType(teamSlug, meetingSlug, orgSlug), - ]); - - if (!teamData || !eventType) { - return null; - } - - const { subsetOfHosts, hosts } = await getEventTypeHosts({ - hosts: eventType.hosts, - prisma, - }); - - const enrichedOwner = eventType.owner - ? await new UserRepository(prisma).enrichUserWithItsProfile({ - user: eventType.owner, - }) - : null; - const users = - (await getUsersFromEvent({ ...eventType, owner: enrichedOwner, subsetOfHosts, hosts }, prisma)) ?? []; - const name = teamData.parent?.name ?? teamData.name ?? null; - const isUnpublished = teamData.parent ? !teamData.parent.slug : !teamData.slug; - - const eventMetaData = eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata); - - const eventDataShared = await processEventDataShared({ - eventData: eventType, - metadata: eventMetaData, - prisma, - }); - - return { - ...eventDataShared, - owner: enrichedOwner, - subsetOfHosts, - hosts, - profile: getProfileFromEvent({ ...eventType, owner: enrichedOwner, subsetOfHosts, hosts }), - subsetOfUsers: users, - users, - entity: { - fromRedirectOfNonOrgLink, - considerUnpublished: isUnpublished && !fromRedirectOfNonOrgLink, - orgSlug, - teamSlug: teamData.slug ?? null, - name, - hideProfileLink: false, - logoUrl: teamData.parent - ? getPlaceholderAvatar(teamData.parent.logoUrl, teamData.parent.name) - : getPlaceholderAvatar(teamData.logoUrl, teamData.name), - }, - }; -} - -export async function shouldUseApiV2ForTeamSlots(teamId: number): Promise { - const featureRepo = new FeaturesRepository(prisma); - const teamHasApiV2Route = await featureRepo.checkIfTeamHasFeature(teamId, "use-api-v2-for-team-slots"); - const useApiV2 = teamHasApiV2Route && Boolean(process.env.NEXT_PUBLIC_API_V2_URL); - - return useApiV2; -} - -export async function getCRMData( - query: GetServerSidePropsContext["query"], - eventData: { - id: number; - isInstantEvent: boolean; - schedulingType: SchedulingType | null; - metadata: Prisma.JsonValue | null; - length: number; - } -) { - const crmContactOwnerEmail = query["cal.crmContactOwnerEmail"]; - const crmContactOwnerRecordType = query["cal.crmContactOwnerRecordType"]; - const crmAppSlugParam = query["cal.crmAppSlug"]; - const crmRecordIdParam = query["cal.crmRecordId"]; - const crmLookupDoneParam = query["cal.crmLookupDone"]; - - let teamMemberEmail = Array.isArray(crmContactOwnerEmail) ? crmContactOwnerEmail[0] : crmContactOwnerEmail; - let crmOwnerRecordType = Array.isArray(crmContactOwnerRecordType) - ? crmContactOwnerRecordType[0] - : crmContactOwnerRecordType; - let crmAppSlug = Array.isArray(crmAppSlugParam) ? crmAppSlugParam[0] : crmAppSlugParam; - let crmRecordId = Array.isArray(crmRecordIdParam) ? crmRecordIdParam[0] : crmRecordIdParam; - - // If crmLookupDone is true, the router already performed the CRM lookup, so skip it here - const crmLookupDone = - (Array.isArray(crmLookupDoneParam) ? crmLookupDoneParam[0] : crmLookupDoneParam) === "true"; - - if (!crmLookupDone && (!teamMemberEmail || !crmOwnerRecordType || !crmAppSlug)) { - const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import( - "@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm" - ); - const { - email, - recordType, - crmAppSlug: crmAppSlugQuery, - recordId: crmRecordIdQuery, - } = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({ - query, - eventData, - }); - - teamMemberEmail = email ?? undefined; - crmOwnerRecordType = recordType ?? undefined; - crmAppSlug = crmAppSlugQuery ?? undefined; - crmRecordId = crmRecordIdQuery ?? undefined; - } - - return { - teamMemberEmail, - crmOwnerRecordType, - crmAppSlug, - crmRecordId, - }; -} - -export async function getTeamId(teamSlug: string, orgSlug: string | null): Promise { - const teamRepo = new TeamRepository(prisma); - const team = await teamRepo.findFirstBySlugAndParentSlug({ - slug: teamSlug, - parentSlug: orgSlug, - select: { id: true }, - }); - - return team?.id ?? null; -} diff --git a/apps/web/app/(booking-page-wrapper)/team/[slug]/embed/page.tsx b/apps/web/app/(booking-page-wrapper)/team/[slug]/embed/page.tsx deleted file mode 100644 index 7ad40ee95017b1..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/team/[slug]/embed/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import withEmbedSsrAppDir from "app/WithEmbedSSR"; -import type { PageProps as ServerPageProps } from "app/_types"; -import { cookies, headers } from "next/headers"; - -import { buildLegacyCtx } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps"; - -import TeamPage, { type PageProps as ClientPageProps } from "~/team/team-view"; - -export const generateMetadata = async () => { - return { - robots: { - follow: false, - index: false, - }, - }; -}; - -const getData = withEmbedSsrAppDir(getServerSideProps); - -const ServerPage = async ({ params, searchParams }: ServerPageProps) => { - const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); - const props = await getData(context); - return ; -}; - -export default ServerPage; diff --git a/apps/web/app/(booking-page-wrapper)/team/[slug]/page.tsx b/apps/web/app/(booking-page-wrapper)/team/[slug]/page.tsx deleted file mode 100644 index 5f823ff15cfb6b..00000000000000 --- a/apps/web/app/(booking-page-wrapper)/team/[slug]/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { PageProps as _PageProps } from "app/_types"; -import { generateMeetingMetadata } from "app/_utils"; -import { cookies, headers } from "next/headers"; - -import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage"; - -import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx"; -import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps"; - -import type { PageProps } from "~/team/team-view"; -import LegacyPage from "~/team/team-view"; - -export const generateMetadata = async ({ params, searchParams }: _PageProps) => { - const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = await getData( - buildLegacyCtx(await headers(), await cookies(), await params, await searchParams) - ); - - const meeting = { - title: markdownStrippedBio ?? "", - profile: { - name: `${team.name}`, - image: getOrgOrTeamAvatar(team), - }, - }; - const decodedParams = decodeParams(await params); - const metadata = await generateMeetingMetadata( - meeting, - (t) => team.name || t("nameless_team"), - (t) => team.name || t("nameless_team"), - false, - getOrgFullOrigin(currentOrgDomain ?? null), - `/team/${decodedParams.slug}` - ); - return { - ...metadata, - robots: { - follow: isSEOIndexable, - index: isSEOIndexable, - }, - }; -}; - -const getData = withAppDirSsr(getServerSideProps); - -const ServerPage = async ({ params, searchParams }: _PageProps) => { - const props = await getData( - buildLegacyCtx(await headers(), await cookies(), await params, await searchParams) - ); - return ; -}; -export default ServerPage; diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx index f82d9bc7cde425..58d7849a095b92 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx @@ -1,22 +1,14 @@ +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { getScheduleListItemData } from "@calcom/lib/schedules/transformers/getScheduleListItemData"; +import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router"; +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; import { createRouterCaller, getTRPCContext } from "app/_trpc/context"; import type { PageProps, ReadonlyHeaders, ReadonlyRequestCookies } from "app/_types"; import { _generateMetadata, getTranslate } from "app/_utils"; import { unstable_cache } from "next/cache"; import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; - -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; -import { getScheduleListItemData } from "@calcom/lib/schedules/transformers/getScheduleListItemData"; -import { MembershipRole } from "@calcom/prisma/enums"; -import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router"; -import { AvailabilitySliderTable } from "@calcom/web/modules/timezone-buddy/components/AvailabilitySliderTable"; - -import { buildLegacyRequest } from "@lib/buildLegacyCtx"; - -import { AvailabilityList, AvailabilityCTA } from "~/availability/availability-view"; - +import { AvailabilityCTA, AvailabilityList } from "~/availability/availability-view"; import { ShellMainAppDir } from "../ShellMainAppDir"; export const generateMetadata = async () => { @@ -60,32 +52,12 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => { schedules: cachedAvailabilities.schedules.map((schedule) => getScheduleListItemData(schedule)), }; - const organizationId = session?.user?.profile?.organizationId ?? session?.user.org?.id; - const organizationRepository = getOrganizationRepository(); - const isOrgPrivate = organizationId - ? await organizationRepository.checkIfPrivate({ - orgId: organizationId, - }) - : false; - - const permissionService = new PermissionCheckService(); - const teamIdsWithPermission = await permissionService.getTeamIdsWithPermission({ - userId: session.user.id, - permission: "availability.read", - fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], - }); - const canViewTeamAvailability = teamIdsWithPermission.length > 0 || !isOrgPrivate; - return ( }> - {searchParams?.type === "team" && canViewTeamAvailability ? ( - - ) : ( - - )} + CTA={}> + ); }; diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/availability/skeleton.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/availability/skeleton.tsx index 8f539ed39a389e..295c10abd6b9b9 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/availability/skeleton.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/availability/skeleton.tsx @@ -1,10 +1,8 @@ "use client"; -import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir"; - import SkeletonLoader from "@calcom/features/availability/components/SkeletonLoader"; import { useLocale } from "@calcom/lib/hooks/useLocale"; - +import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir"; import { AvailabilityCTA } from "~/availability/availability-view"; export default function AvailabilityLoader() { @@ -14,7 +12,7 @@ export default function AvailabilityLoader() { }> + CTA={}> ); diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/booking/[uid]/logs/page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/booking/[uid]/logs/page.tsx index 90f4fa30b2f119..7f65a0df25d4bb 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/booking/[uid]/logs/page.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/booking/[uid]/logs/page.tsx @@ -1,5 +1,5 @@ // Added as a separate route for now to ease the testing of the audit logs feature -// It partially matches the figma design - https://www.figma.com/design/wleA2SR6rn60EK7ORxAfMy/Cal.com-New-Features?node-id=5641-6732&p=f +// It partially matches the figma design - https://www.figma.com/design/wleA2SR6rn60EK7ORxAfMy/Cal.diy-New-Features?node-id=5641-6732&p=f // TOOD: Move it to the booking page side bar later import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir"; import type { PageProps } from "app/_types"; diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx index d05fbfabeaa4ed..3b02f951204de4 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx @@ -1,8 +1,6 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { getFeatureOptInService } from "@calcom/features/di/containers/FeatureOptInService"; -import { getUserFeatureRepository } from "@calcom/features/di/containers/UserFeatureRepository"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; -import { MembershipRole } from "@calcom/prisma/enums"; +import { FeaturesRepository } from "@calcom/features/flags/features.repository"; +import { prisma } from "@calcom/prisma"; import { buildLegacyRequest } from "@lib/buildLegacyCtx"; import type { PageProps } from "app/_types"; import { _generateMetadata, getTranslate } from "app/_utils"; @@ -39,32 +37,16 @@ const Page = async ({ params }: PageProps) => { } const userId = session.user.id; - const permissionService = new PermissionCheckService(); - const userFeatureRepository = getUserFeatureRepository(); + const featuresRepository = new FeaturesRepository(prisma); - const teamIdsWithPermission = await permissionService.getTeamIdsWithPermission({ - userId, - permission: "booking.read", - fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], - }); - // We check if teamIdsWithPermission.length > 0. - // While this may not be entirely accurate, it's acceptable - // because we perform a thorough validation on the server side for the actual filter values. - // This variable is primarily for UI purposes. - const canReadOthersBookings = teamIdsWithPermission.length > 0; + // No teams in cal.diy, so canReadOthersBookings is always false. + const canReadOthersBookings = false; - const featureOptInService = getFeatureOptInService(); - - const [bookingAuditEnabled, featureStates] = await Promise.all([ - userFeatureRepository.checkIfUserHasFeature(userId, "booking-audit"), - featureOptInService.resolveFeatureStates({ - userId, - featureIds: ["bookings-v3"], - }), + const [bookingAuditEnabled, bookingsV3Enabled] = await Promise.all([ + featuresRepository.checkIfUserHasFeature(userId, "booking-audit"), + featuresRepository.checkIfUserHasFeature(userId, "bookings-v3"), ]); - const bookingsV3Enabled = featureStates["bookings-v3"]?.effectiveEnabled ?? false; - return ( - await _generateMetadata( - (t) => t("organization_members"), - (t) => t("organization_description"), - undefined, - undefined, - "/members" - ); - -const Page = async () => { - const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); - - if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) { - return redirect("/settings/my-account/profile"); - } - - const { org, teams, facetedTeamValues, attributes, permissions } = await getOrgMembersPageData(session); - const t = await getTranslate(); - - return ( - - - - ); -}; - -export default Page; diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/CTA.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/teams/CTA.tsx deleted file mode 100644 index 5a6921b8f1ce31..00000000000000 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/CTA.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; -import posthog from "posthog-js"; - -import { WEBAPP_URL } from "@calcom/lib/constants"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Button } from "@calcom/ui/components/button"; - -export const TeamsCTA = () => { - const { t } = useLocale(); - return ( - - ); -}; diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/actions.ts b/apps/web/app/(use-page-wrapper)/(main-nav)/teams/actions.ts deleted file mode 100644 index e34a18875cc8b1..00000000000000 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/actions.ts +++ /dev/null @@ -1,7 +0,0 @@ -"use server"; - -import { updateTag } from "next/cache"; - -export async function revalidateTeamsList() { - updateTag("viewer.teams.list"); -} diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/loading.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/teams/loading.tsx deleted file mode 100644 index b663692bba276e..00000000000000 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { TeamsListSkeleton } from "./skeleton"; - -export default function Loading() { - return ; -} diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/teams/page.tsx deleted file mode 100644 index 85fdf0db7b9cb0..00000000000000 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { buildLegacyRequest } from "@lib/buildLegacyCtx"; -import type { PageProps as ServerPageProps } from "app/_types"; -import { _generateMetadata, getTranslate } from "app/_utils"; -import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir"; -import { cookies, headers } from "next/headers"; -import { redirect } from "next/navigation"; -import { ServerTeamsListing } from "./server-page"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("teams"), - (t) => t("create_manage_teams_collaborative"), - undefined, - undefined, - "/teams" - ); - -const ServerPage = async ({ searchParams: _searchParams }: ServerPageProps) => { - const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); - const searchParams = await _searchParams; - const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token; - const autoAccept = Array.isArray(searchParams?.autoAccept) - ? searchParams.autoAccept[0] - : searchParams?.autoAccept; - const callbackUrl = token - ? `/teams?token=${encodeURIComponent(token)}${ - autoAccept ? `&autoAccept=${encodeURIComponent(autoAccept)}` : "" - }` - : null; - - if (!session) { - redirect(callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}` : "/auth/login"); - } - - const t = await getTranslate(); - const { Main, CTA, showHeader } = await ServerTeamsListing({ searchParams, session }); - - return ( - - {Main} - - ); -}; -export default ServerPage; diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/server-page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/teams/server-page.tsx deleted file mode 100644 index c385509478ec02..00000000000000 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/server-page.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository"; -import { TeamService } from "@calcom/features/ee/teams/services/teamService"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; -import { ErrorWithCode } from "@calcom/lib/errors"; -import prisma from "@calcom/prisma"; -import { MembershipRole } from "@calcom/prisma/enums"; -import type { SearchParams } from "app/_types"; -import { unstable_cache } from "next/cache"; -import type { Session } from "next-auth"; -import { TeamsListing } from "~/ee/teams/components/TeamsListing"; -import { TeamsCTA } from "./CTA"; - -const getCachedTeams = unstable_cache( - async (userId: number) => { - const teamRepo = new TeamRepository(prisma); - return await teamRepo.findTeamsByUserId({ - userId, - includeOrgs: true, - }); - }, - undefined, - { revalidate: 3600, tags: ["viewer.teams.list"] } // Cache for 1 hour -); - -export const ServerTeamsListing = async ({ - searchParams, - session, -}: { - searchParams: SearchParams; - session: Session; -}): Promise<{ - Main: JSX.Element; - CTA: JSX.Element | null; - showHeader: boolean; -}> => { - const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token; - const autoAccept = Array.isArray(searchParams?.autoAccept) - ? searchParams.autoAccept[0] - : searchParams?.autoAccept; - const userId = session.user.id; - let invitationAccepted = false; - - let teamNameFromInvite, - errorMsgFromInvite = null; - - if (token) { - try { - if (autoAccept === "true") { - await TeamService.acceptInvitationByToken(token, userId); - invitationAccepted = true; - } else { - teamNameFromInvite = await TeamService.inviteMemberByToken(token, userId); - } - } catch (e) { - errorMsgFromInvite = "Error while fetching teams"; - if (e instanceof ErrorWithCode) errorMsgFromInvite = e.message; - } - } - - const teams = await getCachedTeams(userId); - const userProfile = session?.user?.profile; - const orgId = userProfile?.organizationId ?? session?.user.org?.id; - - // Filter to get accepted non-organization teams (same logic as TeamsListing) - const acceptedTeams = teams.filter((m) => m.accepted && !m.isOrganization); - - // Check if user has a team plan (any accepted team with a slug) - const hasTeamPlan = teams.some((team) => team.accepted === true && team.slug !== null); - - // Show header unless we're showing the upgrade banner (no teams and no team plan) - const showHeader = acceptedTeams.length > 0 || hasTeamPlan; - - const permissionCheckService = new PermissionCheckService(); - const canCreateTeam = orgId - ? await permissionCheckService.checkPermission({ - userId: session.user.id, - teamId: orgId, - permission: "team.create", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }) - : false; - - return { - Main: ( - - ), - CTA: !orgId || canCreateTeam ? : null, - showHeader, - }; -}; diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/skeleton.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/teams/skeleton.tsx deleted file mode 100644 index 7c37672e5283da..00000000000000 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/skeleton.tsx +++ /dev/null @@ -1,14 +0,0 @@ -"use client"; - -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir"; -import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList"; - -export const TeamsListSkeleton = () => { - const { t } = useLocale(); - return ( - - - - ); -}; diff --git a/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/FormEdit.tsx b/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/FormEdit.tsx deleted file mode 100644 index 36889af36b223f..00000000000000 --- a/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/FormEdit.tsx +++ /dev/null @@ -1,393 +0,0 @@ -"use client"; - -import { FieldTypes } from "@calcom/app-store/routing-forms/lib/FieldTypes"; -import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types"; -import { LearnMoreLink } from "@calcom/features/eventtypes/components/LearnMoreLink"; -import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import classNames from "@calcom/ui/classNames"; -import { Button } from "@calcom/ui/components/button"; -import { FormCard, FormCardBody } from "@calcom/ui/components/card"; -import { - BooleanToggleGroupField, - Label, - MultiOptionInput, - SelectField, - TextField, -} from "@calcom/ui/components/form"; -import { Tooltip } from "@calcom/ui/components/tooltip"; -import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm"; -import SingleForm from "@components/apps/routing-forms/SingleForm"; -import { ChevronDownIcon, MenuIcon } from "@coss/ui/icons"; -import { useAutoAnimate } from "@formkit/auto-animate/react"; -import type { inferSSRProps } from "@lib/types/inferSSRProps"; -import type { UseFormReturn } from "react-hook-form"; -import { Controller, useFieldArray, useWatch } from "react-hook-form"; -import { Toaster } from "sonner"; -import { v4 as uuidv4 } from "uuid"; - -type HookForm = UseFormReturn; - -function Field({ - fieldIndex, - hookForm, - hookFieldNamespace, - deleteField, - moveUp, - moveDown, - appUrl, - disableTypeChange, -}: { - fieldIndex: number; - hookForm: HookForm; - hookFieldNamespace: `fields.${number}`; - deleteField: { - check: () => boolean; - fn: () => void; - }; - moveUp: { - check: () => boolean; - fn: () => void; - }; - moveDown: { - check: () => boolean; - fn: () => void; - }; - appUrl: string; - disableTypeChange: boolean; -}) { - const { t } = useLocale(); - - const router = hookForm.getValues(`${hookFieldNamespace}.router`); - const routerField = hookForm.getValues(`${hookFieldNamespace}.routerField`); - - const label = useWatch({ - control: hookForm.control, - name: `${hookFieldNamespace}.label`, - }); - - const identifier = useWatch({ - control: hookForm.control, - name: `${hookFieldNamespace}.identifier`, - }); - - const fieldType = useWatch({ - control: hookForm.control, - name: `${hookFieldNamespace}.type`, - }); - - const preCountFieldLabel = label || routerField?.label || "Field"; - const fieldLabel = `${fieldIndex + 1}. ${preCountFieldLabel}`; - - return ( -
- - -
- { - const newLabel = e.target.value; - // Use label from useWatch which is guaranteed to be the previous value - // since useWatch updates reactively (after re-render), not synchronously - const previousLabel = label || ""; - hookForm.setValue(`${hookFieldNamespace}.label`, newLabel, { - shouldDirty: true, - }); - const currentIdentifier = hookForm.getValues(`${hookFieldNamespace}.identifier`); - // Only auto-update identifier if it was auto-generated from the previous label - // This preserves manual identifier changes - const isIdentifierGeneratedFromPreviousLabel = - currentIdentifier === getFieldIdentifier(previousLabel).toLowerCase(); - if (!currentIdentifier || isIdentifierGeneratedFromPreviousLabel) { - hookForm.setValue( - `${hookFieldNamespace}.identifier`, - getFieldIdentifier(newLabel).toLowerCase(), - { shouldDirty: true } - ); - } - }} - /> -
-
- - } - name={`${hookFieldNamespace}.identifier`} - required - placeholder={t("identifies_name_field")} - value={identifier || routerField?.identifier || label || routerField?.label || ""} - onChange={(e) => { - hookForm.setValue(`${hookFieldNamespace}.identifier`, e.target.value.toLowerCase(), { - shouldDirty: true, - }); - }} - /> -
-
- { - const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value); - if (disableTypeChange) { - return ( -
- - - - -
- ); - } else { - return ( - - Object.assign({}, baseStyles, { - fontSize: "14px", - }), - option: (baseStyles) => - Object.assign({}, baseStyles, { - fontSize: "14px", - }), - }} - label="Type" - isDisabled={!!router} - containerClassName="data-testid-field-type" - options={FieldTypes} - onChange={(option) => { - if (!option) { - return; - } - onChange(option.value); - }} - defaultValue={defaultValue} - /> - ); - } - }} - /> -
- {["select", "multiselect"].includes(fieldType) ? ( -
- - 500"]} - defaultNumberOfOptions={4} - pasteDelimiters={["\n", ","]} - showMoveButtons={true} - minOptions={1} - addOptionLabel={t("add_an_option")} - addOptionButtonColor="minimal" - /> -
- ) : null} - -
- { - return ( - - ); - }} - /> -
-
-
-
- ); -} - -const FormEdit = ({ - hookForm, - form, - appUrl, -}: { - hookForm: HookForm; - form: inferSSRProps["form"]; - appUrl: string; -}) => { - const fieldsNamespace = "fields"; - const { - fields: hookFormFields, - append: appendHookFormField, - remove: removeHookFormField, - swap: swapHookFormField, - } = useFieldArray({ - control: hookForm.control, - name: fieldsNamespace, - keyName: "_id", - }); - - const [animationRef] = useAutoAnimate(); - - const addField = () => { - appendHookFormField({ - id: uuidv4(), - // This is same type from react-awesome-query-builder - type: "text", - label: "", - }); - }; - - // hookForm.reset(form); - if (!form.fields) { - form.fields = []; - } - return hookFormFields.length ? ( -
-
- {hookFormFields.map((field, key) => { - const existingField = Boolean((form.fields || []).find((f) => f.id === field.id)); - const hasFormResponses = (form._count?.responses ?? 0) > 0; - return ( - hookFormFields.length > 1, - fn: () => { - removeHookFormField(key); - }, - }} - moveUp={{ - check: () => key !== 0, - fn: () => { - swapHookFormField(key, key - 1); - }, - }} - moveDown={{ - check: () => key !== hookFormFields.length - 1, - fn: () => { - if (key === hookFormFields.length - 1) { - return; - } - swapHookFormField(key, key + 1); - }, - }} - key={field.id} - /> - ); - })} -
- {hookFormFields.length ? ( -
- -
- ) : null} -
- ) : ( -
- {/* TODO: remake empty screen for V3 */} -
-
- {/* Icon card - Top */} -
-
- -
-
- {/* Left fanned card */} -
- {/* Right fanned card */} -
-
-
-

- Create your first question -

-

- Fields are the form fields that the booker would see. -

-
- -
-
- ); -}; - -export default function FormEditPage({ - appUrl, - permissions, - ...props -}: inferSSRProps & { appUrl: string }) { - return ( - <> - - } - /> - - ); -} diff --git a/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/FormProvider.tsx b/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/FormProvider.tsx deleted file mode 100644 index 1e34195c6ac4d2..00000000000000 --- a/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/FormProvider.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client"; - -import { FormProvider as ReactHookFormProvider, useForm } from "react-hook-form"; - -export default function FormProvider({ children }: { children: React.ReactNode }) { - const methods = useForm(); - - return {children}; -} diff --git a/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/IncompleteBooking.tsx b/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/IncompleteBooking.tsx deleted file mode 100644 index 348d50a487fd1f..00000000000000 --- a/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/IncompleteBooking.tsx +++ /dev/null @@ -1,336 +0,0 @@ -"use client"; - -import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types"; -import { SalesforceFieldType, WhenToWriteToRecord } from "@calcom/app-store/salesforce/lib/enums"; -import type { writeToRecordDataSchema as salesforceWriteToRecordDataSchema } from "@calcom/app-store/salesforce/zod"; -import { routingFormIncompleteBookingDataSchema as salesforceRoutingFormIncompleteBookingDataSchema } from "@calcom/app-store/salesforce/zod"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { IncompleteBookingActionType } from "@calcom/prisma/enums"; -import { trpc } from "@calcom/trpc/react"; -import type { inferSSRProps } from "@calcom/types/inferSSRProps"; -import { Button } from "@calcom/ui/components/button"; -import { InputField, Label, Select, Switch } from "@calcom/ui/components/form"; -import { showToast } from "@calcom/ui/components/toast"; -import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm"; -import SingleForm from "@components/apps/routing-forms/SingleForm"; -import { GlobeIcon } from "@coss/ui/icons"; -import { useEffect, useState } from "react"; -import type z from "zod"; - -function Page({ form }: { form: RoutingFormWithResponseCount }) { - const { t } = useLocale(); - const { data, isLoading } = trpc.viewer.appRoutingForms.getIncompleteBookingSettings.useQuery({ - formId: form.id, - }); - - const mutation = trpc.viewer.appRoutingForms.saveIncompleteBookingSettings.useMutation({ - onSuccess: () => { - showToast(t("success"), "success"); - }, - onError: (error) => { - showToast(t(`error: ${error.message}`), "error"); - }, - }); - - const [salesforceWriteToRecordObject, setSalesforceWriteToRecordObject] = useState< - z.infer - >({}); - - // Handle just Salesforce for now but need to expand this to other apps - const [salesforceActionEnabled, setSalesforceActionEnabled] = useState(false); - - const fieldTypeOptions = [{ label: t("text"), value: SalesforceFieldType.TEXT }]; - - const [selectedFieldType, setSelectedFieldType] = useState(fieldTypeOptions[0]); - - const whenToWriteToRecordOptions = [ - { label: t("on_every_instance"), value: WhenToWriteToRecord.EVERY_BOOKING }, - { label: t("only_if_field_is_empty"), value: WhenToWriteToRecord.FIELD_EMPTY }, - ]; - - const [selectedWhenToWrite, setSelectedWhenToWrite] = useState(whenToWriteToRecordOptions[0]); - - const [newSalesforceAction, setNewSalesforceAction] = useState({ - field: "", - fieldType: selectedFieldType.value, - value: "", - whenToWrite: WhenToWriteToRecord.FIELD_EMPTY, - }); - - const credentialOptions = data?.credentials.map((credential) => ({ - label: credential.team?.name, - value: credential.id, - })); - - const [selectedCredential, setSelectedCredential] = useState( - Array.isArray(credentialOptions) ? credentialOptions[0] : null - ); - - useEffect(() => { - const salesforceAction = data?.incompleteBookingActions.find( - (action) => action.actionType === IncompleteBookingActionType.SALESFORCE - ); - - if (salesforceAction) { - setSalesforceActionEnabled(salesforceAction.enabled); - - const parsedSalesforceActionData = salesforceRoutingFormIncompleteBookingDataSchema.safeParse( - salesforceAction.data - ); - if (parsedSalesforceActionData.success) { - setSalesforceWriteToRecordObject(parsedSalesforceActionData.data?.writeToRecordObject ?? {}); - } - - setSelectedCredential( - credentialOptions - ? (credentialOptions.find((option) => option.value === salesforceAction?.credentialId) ?? - selectedCredential) - : selectedCredential - ); - } - }, [data]); - - if (isLoading) { - return
Loading...
; - } - return ( - <> -
- <> -
-
-
- -
-
- - Write to Salesforce contact/lead record - -
-
- { - setSalesforceActionEnabled(checked); - }} - /> -
- {salesforceActionEnabled ? ( -
- {form.team && ( - <> -
- - option.value === action.fieldType)} - isDisabled={true} - /> -
-
- - -
-
- - { - if (e) { - setSelectedFieldType(e); - setNewSalesforceAction({ - ...newSalesforceAction, - fieldType: e.value, - }); - } - }} - /> -
-
- - - setNewSalesforceAction({ - ...newSalesforceAction, - value: e.target.value, - }) - } - /> -
-
- - page.value === action?.type)} - onChange={(item) => { - if (!item) { - return; - } - const newAction: LocalRoute["action"] = { - type: item.value, - value: "", - }; - - if (newAction.type === "customPageMessage") { - newAction.value = t("default_custom_page_message"); - } else { - newAction.value = ""; - } - - onActionChange(newAction); - }} - options={RoutingPages} - /> -
- {action?.type ? ( - action?.type === "customPageMessage" ? ( -