diff --git a/.changeset/config.json b/.changeset/config.json index c12b327eb7..28258ad3fe 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 4d41267d73..466b4ab480 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 5baff90ab3..9358323124 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 674deef620..b27ab6ad19 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 a071344ccd..e3029227ef 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 995996b7d5..43f7554eed 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 0e47639377..3654aa47c4 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 01c10fab7a..3e5bb3c8c1 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 7fbcb4197c..6459fe9b31 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 4b394599e9..0000000000 --- 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 0611f100e8..e287800d36 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 8eb08509ca..d567973aa2 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 3552cb5a75..b3098d0e10 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 121c5b935e..f550ba4848 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 685646b154..990deccf74 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 739b6d413c..0000000000 --- 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 bddb9fd84e..52e8dab2c7 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 3028842537..b10b5d71b2 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 5a84df5f48..0000000000 --- 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 3b573b7a8a..0000000000 --- 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 196fbac272..8ac3f3cf24 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 cbee6ae91a..cfa810d7cd 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 a8422e68d0..6caa7bc1b1 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 90485e8539..e694acea44 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 d76a18aafd..a097116dd4 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 12682bc614..2c9ca42ab6 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 1dc4b10e2c..1c19c446a3 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 62824f4417..b44abad56b 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 a67e2e9984..02b548529c 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 c709b17c7b..5c9af507af 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 fa686e10c7..cacdc2a1df 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 d79c6bf2c2..c45fe54b93 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 0000000000..215ed8ad22 --- /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 3a109bf8fc..a9c7ffe832 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 58f6575521..ece78b0b0a 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 8b062b7ee2..e9012e3ebc 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 c59d452a07..0df84f682a 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 22d41cb5c1..3148c3ddea 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 b1df3dc09e..4af892454e 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 a49e9dc608..639d0040e1 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 6db124150e..fa3d8e1bd1 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 5032e5b617..064ffc3290 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 b0402b9f21..da3a67fedb 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 6520d20afc..98b51fbaac 100644 --- a/LICENSE +++ b/LICENSE @@ -1,670 +1,21 @@ +MIT License + 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. - - 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. - - 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 3efd4b9862..f45c14d146 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 9fe2b7643b..76ec30e722 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. +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). -**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. - -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 +#### Running Cal.diy with Docker Compose -If you are evaluating Cal.com or running with minimal to no modifications, this option is for you. - -1. Clone calcom/cal.com +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. +5. Start Cal.diy via docker compose - Note: To aid with support, by default Scarf.sh is used as registry proxy for download metrics. - -5. Start Cal.com via docker compose - - (Most basic users, and for First Run) To run the complete stack, which includes a local Postgres database, Cal.com web app, and Prisma Studio: + 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. +Cal.diy is fully open source, licensed under the [MIT License](https://opensource.org/license/mit). -> [!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. - - - -### 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: + -1. Follow semantic versioning when using changesets. -2. Mark breaking changes using `❗️Breaking change` +### Contributors + + + + + + + +### 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 8dead5a027..689fbea928 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 95af227fe0..0000000000 --- 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 acd66c6f1f..5fb9e10ec1 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 ac6b851ce4..ec970777ea 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 a0a70d9f9b..d7bf907275 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 2f7203f951..d327fc9bb1 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 34956c841f..61a804b545 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 52613fcd18..6d517bb0cc 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 d5237f8beb..bd80ab280e 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 5858b1bd65..7a540965c0 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 68e0dc0c78..25a749299b 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 31e979bab8..ccd2a2eec6 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 fd03fa7eb7..c13b1d172e 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 ab3f11b92c..6cd9d17d39 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 5b752c8a3e..89d2e9668c 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 bd35517946..243533b488 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 b2d15236fd..ea86e86f90 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 3db70f081f..b382ff2ee0 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 62ed42e221..6140c87e6f 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 e0b4a6c81d..a20b0d0656 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 8b809d61cb..bfbc93bc0c 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 65a3fcfc98..e6ade05fa7 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 38a68c53e9..0000000000 --- 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 d7fa3f2da0..780f7209ae 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 4563b50eb6..578182bbe5 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 1e5afd1829..a40b1cf04f 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 68289a8cc9..b8aebd874a 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 62bf70e1d6..de84d48a26 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 df55be8f10..767c538ff8 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 1976a6a97d..3a32d2a404 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 f001bcc606..68e1e41b39 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 f2f5c62403..7873762a33 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 06bd13ee91..acccadf451 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 7d9f5937b7..3b1e9e73c6 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 67b40a02f9..9ede706e17 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 33074d4d61..7a1e7d5665 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 5f65fb4e12..ba5818aaa5 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 8a44675c60..da3dbf604c 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 c76be87384..ca23900119 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 a297548bbf..9ab5fe5d29 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 2189bda954..0ee12c41dc 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 9868df60c2..60b3d5fe08 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 e304df56a2..5549dcfe04 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 2c0b8c0a76..0e7b16ef4c 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 ff45e67a92..fa6d1d853f 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 d8d98b68b0..0000000000 --- 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 a2c49f1abb..0000000000 --- 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 e69de29bb2..0000000000 diff --git a/apps/api/v1/LICENSE b/apps/api/v1/LICENSE deleted file mode 100644 index a8c6744758..0000000000 --- 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 071d58879d..0000000000 --- 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 79040c9dbb..0000000000 --- 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 fb04db4a1c..0000000000 --- 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 92d2d6919a..0000000000 --- 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 216276f3fd..0000000000 --- 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 0256bb8345..0000000000 --- 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 d6fff1cbd0..0000000000 --- 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 8003a71984..0000000000 --- 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 656f69a7ba..0000000000 --- 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 444f0f6261..0000000000 --- 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 3681e42273..0000000000 --- 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 bd8c404a04..0000000000 --- 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 b71cdcab31..0000000000 --- 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 b7922ae54d..0000000000 --- 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 ff409fba51..0000000000 --- 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 f29c08829b..0000000000 --- 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 2da41fdb84..0000000000 --- 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 3022c8c0c8..0000000000 --- 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 ae6b0348b0..0000000000 --- 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 8faffc4b98..0000000000 --- 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 1958ee055f..0000000000 --- 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 e0f3d423b0..0000000000 --- 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 8c69df5506..0000000000 --- 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 08bfc37dd7..0000000000 --- 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 ed09856699..0000000000 --- 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 cf21cd804b..0000000000 --- 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 beb7081498..0000000000 --- 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 f2e69cf153..0000000000 --- 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 ffcd622730..0000000000 --- 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 64cef76c14..0000000000 --- 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 220f261b5c..0000000000 --- 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 470aea1bd3..0000000000 --- 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 04e248c047..0000000000 --- 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 7bae6bc5b9..0000000000 --- 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 1426e643c0..0000000000 --- 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 798ad2c442..0000000000 --- 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 906a250dff..0000000000 --- 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 b8d544730a..0000000000 --- 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 4f8af42d77..0000000000 --- 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 25a11ab7aa..0000000000 --- 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 1e77352bc8..0000000000 --- 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 d0482d0d27..0000000000 --- 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 423b28e955..0000000000 --- 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 c90abc5b42..0000000000 --- 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 f6a5115deb..0000000000 --- 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 97bdfa4b15..0000000000 --- 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 ef6d811ea9..0000000000 --- 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 e8fefc3fd0..0000000000 --- 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 f2a80361eb..0000000000 --- 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 49f96f6354..0000000000 --- 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 f187d0f134..0000000000 --- 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 290bea55fa..0000000000 --- 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 a1666a638f..0000000000 --- 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 d9996d36a8..0000000000 --- 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 bd5eefe63e..0000000000 --- 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 52e831b434..0000000000 --- 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 efec7b46cf..0000000000 --- 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 3c4f4ac536..0000000000 --- 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 f8c0495697..0000000000 --- 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 f76ec117c6..0000000000 --- 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 28745ed7c9..0000000000 --- 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 2841ea05de..0000000000 --- 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 259e528c17..0000000000 --- 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 7f401e0b0a..0000000000 --- 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 a60bc93476..0000000000 --- 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 ea4dd0a23e..0000000000 --- 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 d259db60ed..0000000000 --- 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 8f6a5058a5..0000000000 --- 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 65d2acc3ac..0000000000 --- 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 1c03b3fa25..0000000000 --- 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 1c63272f68..0000000000 --- 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 54980cf494..0000000000 --- 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 dc619b0dd1..0000000000 --- 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 3313d742bf..0000000000 --- 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 e89b47c30e..0000000000 --- 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 245f5c9cb0..0000000000 --- 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 fdb121ea89..0000000000 --- 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 fb29f39126..0000000000 --- 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 8757133849..0000000000 --- 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 54980cf494..0000000000 --- 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 e68de943af..0000000000 --- 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 1bda7cf6af..0000000000 --- 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 4352822fd7..0000000000 --- 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 c97e6cd4df..0000000000 --- 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 5dd52285cb..0000000000 --- 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 e3515d5ff5..0000000000 --- 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 1635dee7b0..0000000000 --- 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 fa0096fecd..0000000000 --- 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 54980cf494..0000000000 --- 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 e328929c52..0000000000 --- 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 4c06b7827a..0000000000 --- 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 e89b47c30e..0000000000 --- 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 06a8f9b4e1..0000000000 --- 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 b7321824ca..0000000000 --- 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 a5105c01e7..0000000000 --- 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 0b3ab22691..0000000000 --- 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 082b41ec52..0000000000 --- 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 54980cf494..0000000000 --- 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 2c222e6e30..0000000000 --- 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 9d619d0219..0000000000 --- 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 d57b971142..0000000000 --- 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 1949b5e791..0000000000 --- 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 f4de9c9c73..0000000000 --- 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 9d619d0219..0000000000 --- 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 7968a28b0b..0000000000 --- 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 a1ab66c754..0000000000 --- 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 e89b47c30e..0000000000 --- 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 fa4a8bd5c9..0000000000 --- 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 c97e6cd4df..0000000000 --- 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 d407ee4fda..0000000000 --- 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 810d5c7ee2..0000000000 --- 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 0bcaa3ad50..0000000000 --- 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 2e755dbe56..0000000000 --- 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 d3b1bae7bc..0000000000 --- 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 05243a3922..0000000000 --- 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 81ea4db7b6..0000000000 --- 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 481bae5dca..0000000000 --- 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 b14ee8a4c0..0000000000 --- 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 54980cf494..0000000000 --- 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 0867c2f315..0000000000 --- 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 203d6f8a51..0000000000 --- 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 e89b47c30e..0000000000 --- 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 7878b05b91..0000000000 --- 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 a08562513a..0000000000 --- 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 32b43ca8d2..0000000000 --- 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 23533864aa..0000000000 --- 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 54980cf494..0000000000 --- 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 5a830f6b6f..0000000000 --- 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 74a9a1f71b..0000000000 --- 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 e89b47c30e..0000000000 --- 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 e0f33493d6..0000000000 --- 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 f6af2d58fa..0000000000 --- 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 e80ac95e49..0000000000 --- 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 5755855df9..0000000000 --- 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 ca52c1a036..0000000000 --- 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 7fb26d2ac5..0000000000 --- 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 02a3c4097d..0000000000 --- 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 38e9fe78c2..0000000000 --- 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 edba1dcea4..0000000000 --- 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 176d5a93f7..0000000000 --- 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 f3b29a934e..0000000000 --- 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 1877e3547f..0000000000 --- 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 e89b47c30e..0000000000 --- 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 9f0dfa4829..0000000000 --- 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 67340c8e68..0000000000 --- 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 1bda7cf6af..0000000000 --- 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 a117607011..0000000000 --- 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 c97e6cd4df..0000000000 --- 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 f14317eae3..0000000000 --- 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 17fae079db..0000000000 --- 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 3314b0e11f..0000000000 --- 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 ee7aa19d4d..0000000000 --- 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 54980cf494..0000000000 --- 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 1a36805a16..0000000000 --- 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 bada35b35a..0000000000 --- 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 e89b47c30e..0000000000 --- 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 35ab674c07..0000000000 --- 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 d556245753..0000000000 --- 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 ef44111c2e..0000000000 --- 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 646b3300fa..0000000000 --- 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 ffded6bae1..0000000000 --- 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 7d35096b95..0000000000 --- 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 54980cf494..0000000000 --- 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 a896a4c54b..0000000000 --- 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 7abee87cca..0000000000 --- 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 e89b47c30e..0000000000 --- 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 09ce0c3940..0000000000 --- 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 1d6835c5f2..0000000000 --- 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 86698c6e1c..0000000000 --- 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 1066c6cadd..0000000000 --- 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 54980cf494..0000000000 --- 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 6ce2e40db0..0000000000 --- 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 b80c9a775e..0000000000 --- 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 e89b47c30e..0000000000 --- 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 e7ed5bd126..0000000000 --- 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 c6dd674f1e..0000000000 --- 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 c97e6cd4df..0000000000 --- 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 2749c1b5a8..0000000000 --- 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 356220d939..0000000000 --- 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 96fd8f301d..0000000000 --- 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 b588c0147a..0000000000 --- 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 306145d3a1..0000000000 --- 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 637af631d6..0000000000 --- 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 c97e6cd4df..0000000000 --- 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 54980cf494..0000000000 --- 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 665d5bd391..0000000000 --- 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 4e22acee3e..0000000000 --- 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 133780a7d3..0000000000 --- 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 e89b47c30e..0000000000 --- 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 27c465ea07..0000000000 --- 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 36f799b2a8..0000000000 --- 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 96553d1f43..0000000000 --- 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 306145d3a1..0000000000 --- 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 56329a2980..0000000000 --- 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 9966045ea1..0000000000 --- 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 3d79edde38..0000000000 --- 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 e89b47c30e..0000000000 --- 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 5598f3ed8b..0000000000 --- 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 f36d568dd3..0000000000 --- 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 405027c495..0000000000 --- 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 f78c8a8177..0000000000 --- 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 54980cf494..0000000000 --- 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 6dd8dc8866..0000000000 --- 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 9bb6f29f3a..0000000000 --- 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 e89b47c30e..0000000000 --- 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 2272787139..0000000000 --- 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 689616931c..0000000000 --- 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 938d54f0be..0000000000 --- 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 249ac4df98..0000000000 --- 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 d5fb532a29..0000000000 --- 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 2b69df981c..0000000000 --- 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 2467c80638..0000000000 --- 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 845b16ad6b..0000000000 --- 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 5e66eb52c4..0000000000 --- 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 b32bb45afe..0000000000 --- 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 1179859139..0000000000 --- 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 6a9caae21f..0000000000 --- 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 8b5da8904a..0000000000 --- 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 69b780f9c9..0000000000 --- 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 425dc04af2..0000000000 --- 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 79b1906626..0000000000 --- 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 8ffbec5974..0000000000 --- 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 224d11a450..0000000000 --- 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 1060eaf928..0000000000 --- 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 fa0a180c88..0000000000 --- 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 ecdc1d0ead..0000000000 --- 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 44cb8aa3cd..0000000000 --- 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 ca13d5b4b9..0000000000 --- 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 d2879a24e8..0000000000 --- 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 2fc536c46c..0000000000 --- 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 ce4fde5a20..0000000000 --- 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 b3e150c96f..0000000000 --- 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 e89d447ffb..0000000000 --- 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 5b7549be7b..0000000000 --- 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 d9e1e6bba8..0000000000 --- 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 b191f9c541..0000000000 --- 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 8c58b02c04..0000000000 --- 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 2532163fc6..fd53a2e357 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 45aff8e698..324331be74 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 505d85b580..2e3097d93b 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 1f9ebc4018..4384249793 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 999ac76358..0000000000 --- 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 3bbf063778..0000000000 --- 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 15013b555a..0000000000 --- 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 542bf21c46..0000000000 --- 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 4d7ef0cc33..0000000000 --- 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 d773ae7f9d..0000000000 --- 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 3d950b505c..0000000000 --- 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 3e0284761a..0000000000 --- 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 4fff5d3e08..0000000000 --- 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 72c37b73cf..0000000000 --- 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 4eb60ae073..0000000000 --- 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 b801d4926e..0000000000 --- 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 11a1463083..0000000000 --- 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 47aad9a7b8..0000000000 --- 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 5fbfc6413b..3ce062813e 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 9d5482a380..78c9f7d814 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 d9092ee02e..bc212d0f4c 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 e7b3e65244..0000000000 --- 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 9434cdb622..0c717efd38 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 eedbfec61a..0000000000 --- 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 9a0a8614a4..52b25fded5 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 { RegularBookingModule } from "@/lib/modules/regular-booking.module"; -import { RecurringBookingService } from "@/lib/services/recurring-booking.service"; -import { Module } from "@nestjs/common"; -import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service"; +import { Module, Scope } from "@nestjs/common"; import { Logger } from "@/lib/logger.bridge"; -import { Scope } from "@nestjs/common"; -import { BookingAuditProducerService } from "@/lib/services/booking-audit-producer.service"; -import { HashedLinkService } from "@/lib/services/hashed-link.service"; -import { TaskerService } from "@/lib/services/tasker.service"; +import { RegularBookingModule } from "@/lib/modules/regular-booking.module"; import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository"; +import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.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 { 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 70ffb75f03..6a2cc1ec65 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 8ce416cf2a..7bb51d78a0 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 55e0110bb5..0000000000 --- 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 637fe27c94..0000000000 --- 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 7ff59634ca..0000000000 --- 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 e2d31d4571..26df7b04ea 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 f34a407c2b..e9cccdf989 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 6893875982..31a7d1c64d 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 675d8d4ff7..c18558dd9d 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 a87a9b7d2a..0000000000 --- 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 dc968dd14f..1c91911870 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 edb6a081de..0000000000 --- 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 e09e7f6ab1..0000000000 --- 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 9d05b066f5..f5d0569621 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 12a387012f..c8adb24615 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 7c5b2987ca..18d8badd70 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 8e3df330c6..0000000000 --- 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 59be6027c1..0000000000 --- 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 55906d6e51..44e83f3f9a 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 ffa418a9f8..3775bb68a6 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 9210a3c1f6..ad28c31130 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 44ad35f6fa..0000000000 --- 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 cab4bb5201..0000000000 --- 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 1229b751f1..0000000000 --- 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 73a21ff65f..0000000000 --- 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 ca938e8b76..0000000000 --- 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 749a2dfff1..8218e766ec 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 58b827d352..4be448ed8b 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 ed854dd84e..0000000000 --- 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 36c7dd8245..11492f2ce0 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 1f0dfa701c..0000000000 --- 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 2d80b40ae0..0000000000 --- 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 d4f38e2f90..0000000000 --- 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 1a79e351db..f0a7be9fef 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 6b0fa894f1..c07df68c58 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 e3d427cc6f..649565f6aa 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 e43da04781..0000000000 --- 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 349af67eb8..bc5dd1a1a4 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 33017e4437..0000000000 --- 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 2079c97313..0000000000 --- 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 dbb8274850..0000000000 --- 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 fbdedc0f14..0000000000 --- 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 410ce2902f..0000000000 --- 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 fdf255ab36..0000000000 --- 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 df144a5d88..0000000000 --- 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 ad4042adaf..0000000000 --- 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 7fe3e03e54..0000000000 --- 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 6845c504a1..733fd4dab6 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 { Pbac } from "@/modules/auth/decorators/pbac/pbac.decorator"; -import type { PermissionString } from "@calcom/platform-libraries/pbac"; -import { PermissionCheckService, FeaturesRepository } from "@calcom/platform-libraries/pbac"; - -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 5398b11d7a..0000000000 --- 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 6fe688668d..0000000000 --- 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 9bc2ae28c6..0000000000 --- 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 8e22057d0c..0000000000 --- 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 a446a51200..0000000000 --- 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 2a86cde742..0000000000 --- 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 5c63af9946..5e11844eae 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 905a61e592..4677984f77 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 30bbb16d4f..1638e24d29 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 7d19212110..4712d663c3 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,3 +1,25 @@ +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 { ConfigModule, ConfigService } from "@nestjs/config"; +import { JwtService as NestJwtService } from "@nestjs/jwt"; +import { Test, TestingModule } from "@nestjs/testing"; +import { createRequest } from "node-mocks-http"; +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 { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.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 { MockedRedisService } from "test/mocks/mock-redis-service"; +import { randomString } from "test/utils/randomString"; +import { + ApiAuthGuardRequest, + ApiAuthStrategy, + ONLY_CLIENT_ID_PROVIDED_MESSAGE, + ONLY_CLIENT_SECRET_PROVIDED_MESSAGE, +} 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"; @@ -13,31 +35,6 @@ 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 { ExecutionContext, HttpException } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import { ConfigModule } from "@nestjs/config"; -import { JwtService as NestJwtService } from "@nestjs/jwt"; -import { Test, TestingModule } from "@nestjs/testing"; -import { createRequest } from "node-mocks-http"; -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 { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.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 { 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, - ONLY_CLIENT_ID_PROVIDED_MESSAGE, - ONLY_CLIENT_SECRET_PROVIDED_MESSAGE, -} from "./api-auth.strategy"; -import { ApiAuthStrategy } from "./api-auth.strategy"; 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 9f24bea005..766a233169 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 968d6debc1..0000000000 --- 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 003e28e9cc..0000000000 --- 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 8b7cbef2c6..0000000000 --- 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 93c5bf16c9..0000000000 --- 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 b192561223..0000000000 --- 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 0a4ee10ee2..0000000000 --- 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 c510fe8c6b..0000000000 --- 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 9bae0150dc..0000000000 --- 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 6187650867..0000000000 --- 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 7935e5e0b4..0000000000 --- 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 7e45eff371..0000000000 --- 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 e5b19a09c8..0000000000 --- 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 eeec3a6bbd..0000000000 --- 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 ff446e59e6..0000000000 --- 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 3576eb46c3..0000000000 --- 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 c8d8e5f28c..e56e36fb72 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 6f66a3f172..43d3cf5e8f 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 9b8d576a7e..a6ea7154f8 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 8f2b718375..8602f5312c 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 f0cfa921c5..f15aa30c1c 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 276e893bcd..aa5b9e46b4 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 a67b62482e..1c487798fe 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 e3c513b1c8..b94dcb7a70 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 bbd3c5068e..ca3a3997bc 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 ea635b6349..e22cc52c6d 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 87f383bf91..b81c0381d8 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 828369ca1f..de42ee1972 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 8ea63997f2..db4b82ff3c 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 ea745df74a..38546c70fe 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 00de2547f1..0f68a8a3ce 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 42d2fc489c..0000000000 --- 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 7d0e068a5f..83f0269b7f 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 85a7e3479f..a1bf04aaef 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 7feddb4c31..bfdf1db99f 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 e382c36da9..fd23a012e0 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 2d9d20365c..6e175523a3 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 27aaec6758..8346b959d5 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 2be306c69f..0000000000 --- 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 d0052e9b7e..0000000000 --- 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 dd6e9f047b..0000000000 --- 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 6a5ec4ae8c..0000000000 --- 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 7a6c6d6a2b..0000000000 --- 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 bdf05d3f40..0000000000 --- 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 21bf06f1b7..0000000000 --- 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 d0cafc9ae0..0000000000 --- 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 e036a2666f..0000000000 --- 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 28d85f4553..0000000000 --- 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 d5fb7e9a4a..0000000000 --- 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 97a2b9fc8a..0000000000 --- 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 e45142e846..0000000000 --- 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 71215c12d9..0000000000 --- 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 56a8fe4c06..0000000000 --- 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 ffcf4156ad..0000000000 --- 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 458eedd269..0000000000 --- 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 afda117315..0000000000 --- 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 66afccd388..0000000000 --- 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 7c4ecd05f2..0000000000 --- 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 8da3918bbf..0000000000 --- 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 edc8d7d912..0000000000 --- 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 6f6306494c..0000000000 --- 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 2e31b64bd6..0000000000 --- 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 52a1e8800e..0000000000 --- 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 08408a39d6..0000000000 --- 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 9d1e94bfb9..0000000000 --- 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 07b5c8937d..0000000000 --- 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 cab7ec59ff..0000000000 --- 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 5afb1b519b..0000000000 --- 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 31b8dc7e7d..0000000000 --- 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 6b74863771..0000000000 --- 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 9f6a6f4925..0000000000 --- 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 0d2b76df47..0000000000 --- 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 80fc4e2893..0000000000 --- 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 ad177944d6..0000000000 --- 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 e505ddc16c..0000000000 --- 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 eecb1343c8..0000000000 --- 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 26d2ba7ad3..0000000000 --- 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 0390e9ef55..0000000000 --- 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 7b5ecbad9f..0000000000 --- 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 840e0ed454..0000000000 --- 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 e0ca7b9c94..0000000000 --- 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 c4a2fe54fa..0000000000 --- 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 5033e96a7a..0000000000 --- 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 5e0f571a5e..0000000000 --- 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 2e1e3725dd..0000000000 --- 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 acd24672af..0000000000 --- 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 2145f037eb..0000000000 --- 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 a919b9c454..0000000000 --- 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 c9b09b43c8..0000000000 --- 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 93f6cfd610..0000000000 --- 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 816a37efe9..0000000000 --- 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 64c429c4ef..0000000000 --- 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 10267b55e3..0000000000 --- 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 ba9b0eecde..98b2752203 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 b6e63c4927..0000000000 --- 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 9eec79d243..0000000000 --- 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 527a883931..0000000000 --- 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 838764d186..0000000000 --- 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 2cdf674294..0000000000 --- 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 9c294a5f46..0000000000 --- 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 601092563f..0000000000 --- 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 a0e6cfc755..0000000000 --- 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 e1fb6b053b..0000000000 --- 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 69103cb368..0000000000 --- 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 371247ccb0..0000000000 --- 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 39549ad943..0000000000 --- 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 64eb03a776..0000000000 --- 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 a1f614dbc1..0000000000 --- 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 0e0a6906c1..9b693fc9ed 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 2708d1ccb1..0000000000 --- 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 7bd45ac94b..0000000000 --- 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 9a291ceb35..0000000000 --- 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 bd5adbdf5e..0000000000 --- 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 1b7f4e3282..0000000000 --- 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 b23686c150..0000000000 --- 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 0f7224f480..0000000000 --- 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 60be617211..0000000000 --- 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 651f19c563..0000000000 --- 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 cf52f8d674..0000000000 --- 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 4dd313e2c8..0000000000 --- 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 d688678fc4..0000000000 --- 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 f423fb35ac..0000000000 --- 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 aee4f5cb3f..0000000000 --- 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 75b8397a93..0000000000 --- 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 3d9840187a..0000000000 --- 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 c76024b5dd..0000000000 --- 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 8a95eab6a1..0000000000 --- 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 a9e1d5ecf8..0000000000 --- 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 3a6d062933..0000000000 --- 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 191b712ce1..0000000000 --- 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 62936ee32a..0000000000 --- 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 481047d878..0000000000 --- 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 c2226241dd..0000000000 --- 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 5288cc55e1..0000000000 --- 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 62f7334113..0000000000 --- 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 118b4ef18f..0000000000 --- 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 61c23b7fd0..0000000000 --- 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 6bd8f8c061..0000000000 --- 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 7ab90e2044..0000000000 --- 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 97eb19c9ae..0000000000 --- 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 2927eb636d..0000000000 --- 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 1a929d9cae..0000000000 --- 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 8e8682e752..0000000000 --- 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 fadb51dc11..0000000000 --- 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 cdb68d497c..0000000000 --- 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 dbbb61a4db..0000000000 --- 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 2eb1c49f2c..0000000000 --- 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 3b210e0659..0000000000 --- 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 9b3daa145a..0000000000 --- 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 7e4de86edd..0000000000 --- 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 d329b59b84..0000000000 --- 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 66fe05d83f..0000000000 --- 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 2b7ea1bf79..0000000000 --- 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 c32ebd9d0b..0000000000 --- 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 e09947e06c..0000000000 --- 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 13fe3b8706..0000000000 --- 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 2fb399c43a..0000000000 --- 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 97e178d8a5..0000000000 --- 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 4daab24e29..0000000000 --- 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 07c16c9d0a..0000000000 --- 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 5717967194..0000000000 --- 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 e652a85df7..0000000000 --- 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 e70ad523eb..0000000000 --- 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 543804dd60..0000000000 --- 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 7aaafc42e7..0000000000 --- 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 c416dec0a1..0000000000 --- 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 b344a255fa..0000000000 --- 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 3d9b3a8539..0000000000 --- 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 c4e4eda89d..0000000000 --- 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 c2b026df51..0000000000 --- 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 6837f3b0a7..0000000000 --- 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 87d9402cbf..0000000000 --- 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 7030658c35..0000000000 --- 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 8fa6df8348..0000000000 --- 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 27f905f3a8..0000000000 --- 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 5b8b9b8a58..0000000000 --- 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 5a5ea2e5a2..0000000000 --- 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 baeee396f1..0000000000 --- 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 271ba8066c..0000000000 --- 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 059ca461e1..0000000000 --- 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 c1c5c49c15..0000000000 --- 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 6b5daca93d..0000000000 --- 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 33852e89e7..0000000000 --- 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 da1f11a14e..0000000000 --- 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 72747f4a90..0000000000 --- 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 0019c99946..0000000000 --- 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 a151a4358d..0000000000 --- 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 8a4a474e4f..0000000000 --- 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 f0fad18af6..0000000000 --- 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 35a1e56c66..0000000000 --- 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 7df0cf86d5..0000000000 --- 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 573f4aa973..0000000000 --- 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 8b150a9103..0000000000 --- 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 7321539306..0000000000 --- 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 77fa4d5f86..0000000000 --- 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 fae7ca6d23..0000000000 --- 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 404088d5c9..0000000000 --- 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 8a0b52835b..0000000000 --- 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 98c56d2d3c..0000000000 --- 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 bea1e365d2..0000000000 --- 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 0ae5a2acee..0000000000 --- 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 12c0af3673..0000000000 --- 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 d603355df1..0000000000 --- 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 674a623545..0000000000 --- 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 7a7e6e1208..0000000000 --- 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 f2fc201d9b..0000000000 --- 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 0d9297a52e..0000000000 --- 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 60bbfb889d..0000000000 --- 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 c350c3bb48..0000000000 --- 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 c96c085c03..0000000000 --- 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 b8f2570842..0000000000 --- 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 8d97528e87..0000000000 --- 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 c1527ee6cd..0000000000 --- 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 c32ebd9d0b..0000000000 --- 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 ac288fa903..0000000000 --- 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 1386173222..0000000000 --- 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 ac1f87cd64..0000000000 --- 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 2d9644ca34..0000000000 --- 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 920ec28946..0000000000 --- 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 9f5fde010a..0000000000 --- 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 83db1cf15a..0000000000 --- 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 121716afea..0000000000 --- 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 030d976550..0000000000 --- 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 2a507ea0ad..0000000000 --- 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 2b17a6fefa..0000000000 --- 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 e2cb848917..0000000000 --- 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 9dc3dd25b6..0000000000 --- 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 8ceff75f7f..0000000000 --- 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 5d231f015b..0000000000 --- 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 2acc56bf11..0000000000 --- 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 8574d7faf9..0000000000 --- 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 b0b0a1cd7b..0000000000 --- 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 94b34df576..0000000000 --- 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 a999a123a5..0000000000 --- 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 1cba9543ca..0000000000 --- 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 388da56e58..0000000000 --- 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 5bb90c95d4..0000000000 --- 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 01bccf1f21..0000000000 --- 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 8a28c65043..0000000000 --- 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 95f5d9ea06..0000000000 --- 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 9c4664caa9..0000000000 --- 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 08afa61293..0000000000 --- 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 a048b11780..0000000000 --- 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 f0ba3dff74..0000000000 --- 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 ff929b9841..0000000000 --- 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 22d145ba16..0000000000 --- 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 6d0211055b..0000000000 --- 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 ca4d8fd08d..0000000000 --- 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 adafce6275..0000000000 --- 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 a39f66eacf..0000000000 --- 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 fe2534ed0b..0000000000 --- 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 bb0c403f8d..0000000000 --- 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 7bb2859f9b..0000000000 --- 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 26049b3a36..0000000000 --- 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 c6b642274b..0000000000 --- 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 f3a6d4afda..0000000000 --- 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 198863153d..0000000000 --- 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 f648acff47..0000000000 --- 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 136a05507c..0000000000 --- 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 06e56ae1ad..20d6badb1a 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 86451a6496..c3d7c1c4b8 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 18230a54ad..0dd17e552f 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 236cfc8beb..041f078c3b 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 b77fbf148b..fe8ca53c72 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 acbb06f703..9ae2bc7df7 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 d62c12b0e3..c459f909c6 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 5f62897314..25c14cf7fd 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 b9e5864318..9ec332e6c4 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 b71145d454..0000000000 --- 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 8628a29123..16058830f6 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 f033206095..0000000000 --- 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 57715e5422..a0cc1ccf9e 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 e4169f7b3e..d1ecdc676d 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 { dynamicEvent } from "@calcom/platform-libraries"; +import { + 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, +} 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"; -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { DateTime } from "luxon"; - -import { dynamicEvent } from "@calcom/platform-libraries"; -import { - ByUsernameAndEventTypeSlug_2024_09_04, - ByTeamSlugAndEventTypeSlug_2024_09_04, - 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"; 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 70f6378725..521b5531de 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 2ffc759c9b..73e5f98b92 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 4b1dcad021..3daedcbf98 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 e57a563bc3..685f79b28c 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 fc8ef5b337..0000000000 --- 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 87ee0439e0..0000000000 --- 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 f01a632d94..0000000000 --- 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 abb471ed42..0000000000 --- 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 88efd49035..0000000000 --- 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 b38f1f6371..0000000000 --- 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 43a3f62865..0000000000 --- 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 ca3f1e2beb..0000000000 --- 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 856fc7d9be..0000000000 --- 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 300728c55f..0000000000 --- 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 3206c66a58..0000000000 --- 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 939c74c369..0000000000 --- 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 55874ec3af..0000000000 --- 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 0aa4061ac8..7e136c7275 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 dc48db092a..6cd1f1edda 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, - EventType, - User, - Schedule, - Host, - DestinationCalendar, CalVideoSettings, + DestinationCalendar, + EventType, + Host, + 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 95e775d59c..dcf57519f5 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 c4b8d01e68..0000000000 --- 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 067ef98d2e..c202e701f2 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 ffb36119e8..0000000000 --- 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 e5190ca5e4..0000000000 --- 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 05ddc3ab15..0000000000 --- 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 8ab7c97100..0000000000 --- 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 2cebd8f883..0000000000 --- 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 ef635a69a9..0000000000 --- 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 282526aa9b..0000000000 --- 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 b817f2134a..0000000000 --- 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 c0121935bb..0000000000 --- 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 41bc5611ba..0000000000 --- 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 40cde0710b..0000000000 --- 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 4cfc4c4479..0000000000 --- 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 14dc8a8f98..0000000000 --- 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 93c7815273..0000000000 --- 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 a839d593bf..0000000000 --- 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 db3e22b1e1..0000000000 --- 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 20c1cda63b..0000000000 --- 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 3c1c3c7271..0000000000 --- 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 f25a24c9b5..0000000000 --- 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 91d6b3a96f..0000000000 --- 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 8523b1e764..0000000000 --- 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 1a45d2fbd4..0000000000 --- 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 84a1c652ad..0000000000 --- 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 cf87971c11..0000000000 --- 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 3ec5fd4adc..0000000000 --- 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 00e5fd3c59..0000000000 --- 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 436e533bf6..0000000000 --- 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 58fef199c9..0000000000 --- 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 c403623545..0000000000 --- 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 7113e5ecc7..0000000000 --- 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 383e89a0f9..0000000000 --- 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 db055ec827..0000000000 --- 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 2cd9ab58d6..ad6a0b01d9 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 6d7a4e4a3d..0000000000 --- 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 75c753e602..0265ddf58f 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 697c01d8ee..0000000000 --- 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 a7eed6c99d..d322ba83ed 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 ec60bcc2e6..a2b300d383 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 b0ba2b434a..e8fffc255d 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 b40c2d32d6..a1cdd8be14 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 f215ee3381..0000000000 --- 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 840e15855e..e1c3e019a6 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 3861b9ab46..3643a295d8 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 3d21a13a48..0000000000 --- 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 75876f620f..0000000000 --- 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 bb3ae3eafb..0000000000 --- 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 30c4b4d715..0000000000 --- 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 d22702d923..0000000000 --- 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 5c1b970641..0000000000 --- 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 67332defb2..0000000000 --- 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 b52e3c2190..0000000000 --- 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 d44184ae05..0000000000 --- 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 3d2dffff0f..0000000000 --- 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 3de1cd0b38..0000000000 --- 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 bb1486a0eb..0000000000 --- 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 f810662a8b..0000000000 --- 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 4a341eb32d..0000000000 --- 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 0000000000..d3cfa3cd5a --- /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 0000000000..970139ba3d --- /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 03e52b78c5..1822209246 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 efe79698ab..3b6147f24a 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 45c8dceac4..85791f5665 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 eb12c1e915..5880226487 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 { - IsBoolean, - IsTimeZone, - IsNumber, - IsString, - IsOptional, IsArray, - IsObject, - ValidateNested, - isEmail, - Validate, + IsBoolean, IsInt, + IsNumber, + IsObject, + IsOptional, + IsString, + IsTimeZone, + isEmail, + registerDecorator, + Validate, + 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 22abf23052..31a8607c58 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 0000000000..254e1df9ab --- /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 29754ce194..330dca553f 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 015851bbe7..9487602fe9 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 a83bfd26c5..8af6264c3d 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 0153a514fe..97874eeb42 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 6450220a98..4b43dcb725 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 982e9d5829..bcf2ea37b8 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 7d9313f9c0..51f67ee1e5 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 8e480f4fab..8b6371a2d6 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 d1f13afb73..9b378713ec 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 e1057ebb6d..014477b298 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 9f51789679..3477764b82 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 9abed33b92..e2d634f938 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 a812a6dabc..28998cda7f 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 5c79586bb1..bb69c7bb7f 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 912769cece..0049745118 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 29e27e5dc4..5e64b6bfa7 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 ac1be9af05..0f5bfb20e1 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 5c6ce6a894..914e99f38c 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 27252cf7e2..585394b429 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 791eda9a34..69eacef6af 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 76a75130b3..9f774171d3 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 d8dff6a010..f6492bc3b4 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 d5b7c483f5..cd1e57fe1b 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 bff22d8dbc..4b3aab8f2d 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 9aca9d68de..412ea5375d 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 72cbd482e0..fa48d3cef5 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 aad7f2b5fa..d7acf6882a 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 960511c37e..ac1b1d5ce5 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 532120f76a..8b96f625a9 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 7441352e9b..9191cd3c76 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 beeadd6395..e10031e353 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 fe7c9051f3..cd54bda2ff 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 - ); + : await this.transformInputRescheduleBooking(bookingUid, body, isIndividualSeatReschedule); - const oAuthClientParams = - await this.platformBookingsService.getOAuthClientParams( - bodyTransformed.eventTypeId - ); + 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 1843da15df..a79465cd0f 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 4ca1c78c2a..e651bd85ee 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 03fca182dd..9dcdd2b7b4 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 33dddedd53..b6c0705fa5 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 30b6800773..0823329cc6 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 5e32ebf533..519748b41a 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 5f550ce6e2..bae50e0575 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 86599d97aa..5c83885ab9 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 cae6f09849..68056d10c1 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 9f4fe14583..ac20452c7a 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 b1609ddf55..ab42e02839 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 3007a8542b..50d8e0acf5 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 5c6f4e9f9c..7ef8a5553d 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 d93f9e6e93..6a339b6f4c 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 e6161072eb..8d9cc2f1bd 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 f606f3400e..865ea679ce 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 a1985d5e34..0e543cdfca 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,18 +1,47 @@ -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 { + 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, + Delete, + Get, + Headers, + HttpCode, + HttpStatus, + 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 { + GetEventTypePublicOutput, + PublicEventTypeOutput, +} from "@/platform/event-types/event-types_2024_04_15/outputs/get-event-type-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"; +} 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"; @@ -21,33 +50,6 @@ import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions. import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { - Controller, - UseGuards, - Get, - Param, - Post, - Body, - NotFoundException, - Patch, - HttpCode, - HttpStatus, - Delete, - Query, - Headers, - InternalServerErrorException, - ParseIntPipe, -} from "@nestjs/common"; -import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger"; - -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"; @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 a2261d69e9..c3c0f6d632 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 0a9c6b0d47..d8beb5b881 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 62257716f3..5b1ab9e604 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 c662da716f..9c545c9beb 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 c7675708fe..2410a05f78 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 76ddc81563..be9c3489dd 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 700f8a4786..b8e3d0efc0 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 acd4a2918f..9244eaa451 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 4d0cc43eb4..fda02150aa 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 1fecef2ffa..751585d02b 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 de791d0a7c..29faffb8f3 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 52b68f4399..0e401acc52 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 f3b3264483..0b1cc8f189 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 1016812933..80adb2b02b 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 44e9ddef62..f5133c5bb7 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 2a1f83cddc..c075c46fea 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 1ea3596491..41e1383257 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 54c9bb006b..0eed45eeac 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 550e12f89c..a24eac3b17 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 { dynamicEvent } from "@calcom/platform-libraries"; +import { + createEventType, + 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/organizations/event-types/services/output.service"; +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 { 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, -} 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 { 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 2b16fa0d31..ab45444695 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 346a4e85ca..a34bd297cf 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 e884766277..f70b530a84 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 e0012f3478..7fc4e6ef2a 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 7ef128ae06..cfae5d0b9d 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 8a0319031d..cc28a31104 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 e5a7d3b148..ff0eae9aca 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 16602654b4..2ab85caff9 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 ccef7ad838..1def351f0a 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 9f0bdfd4e6..90cb08715a 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 53fc44cb26..81043e9ec5 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 9320b4dcc5..5de9887f5b 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 0000000000..ab2e120130 --- /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 f10829eaa5..c434d3a4ae 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 d96be50d3a..81b7ac42e0 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 e18eb90bc3..6bcbb2755b 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 9e8c92f797..b6c1a2c4d0 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 40e0f53abd..4eed78276c 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 6a1184683e..b86fb05331 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 0d14fb7326..d7ed6d9dfb 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 ae7709e0ba..4e43e4c1a6 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 81a9b911bd..30f0ea2dee 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 8633a77d12..b456642759 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 8a8f7d4e0e..68854c88e9 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 c35da0721b..8084aa5405 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 5980a8243a..b9f79d93bf 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 f774f4418e..d63317201e 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 b4c8da13bc..c0184b6c6d 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 0000000000..a7b1d76163 --- /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 1973dbc578..bc437a9761 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 13e1c90c33..b08a574cb2 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 a29a8a2132..0000000000 --- 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 eedb4a46f9..0000000000 --- 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 b953217676..0000000000 --- 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 111b302383..0000000000 --- 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 ba957cc7b6..e6acfab21d 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 ce4e3cdd5d..08518ba83d 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 de16359d75..77f6032008 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 7686097b0a..f91358ca07 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 91ca0963aa..39d64d3fb5 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 5417d3ec2e..3d871f09fa 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 8f1957386e..b3757c400a 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 6813b350b9..be78118af2 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 2b2a9efa57..94aef10849 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 8219afded8..4f974a7db9 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 1a2f3b58db..614cf78efa 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 6045f392d0..7851a7209a 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 15f09af1da..c9dd317198 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 3ba12f2fd2..2f7ae3711b 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 e2da8c5b70..5eb9e6e9a5 100644 --- a/apps/web/app/(booking-page-wrapper)/[user]/[type]/page.tsx +++ b/apps/web/app/(booking-page-wrapper)/[user]/[type]/page.tsx @@ -1,58 +1,21 @@ -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 { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { loadTranslations } from "@calcom/i18n/server"; - -import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx"; - -import { getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps"; - +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"; -export const generateMetadata = async ({ params, searchParams }: PageProps) => { - const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); - const props = await getData(legacyCtx); +const getData: (ctx: ReturnType) => Promise = + withAppDirSsr(getServerSideProps); - 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?.subsetOfUsers.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 getData = withAppDirSsr(getServerSideProps); - -const ServerPage = async ({ params, searchParams }: PageProps) => { +const ServerPage = async ({ params, searchParams }: PageProps): Promise => { const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams); const props = await getData(legacyCtx); @@ -70,4 +33,40 @@ const ServerPage = async ({ params, searchParams }: PageProps) => { return ; }; +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 title = eventData?.title ?? ""; + const meeting = { + title, + profile: { name: profileName, image: eventData?.profile.image }, + users: + eventData?.subsetOfUsers.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, + WEBAPP_URL, + `/${decodedParams.user}/${decodedParams.type}` + ); + + return { + ...metadata, + robots: { + follow: !(eventData?.hidden || !isSEOIndexable), + index: !(eventData?.hidden || !isSEOIndexable), + }, + }; +}; + 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 0e10610b39..d08bf51eb5 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 { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx"; - -import { getServerSideProps } from "@server/lib/[user]/getServerSideProps"; - +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"; -export const generateMetadata = async ({ params, searchParams }: PageProps) => { +const getData: (ctx: ReturnType) => Promise = + withAppDirSsr(getServerSideProps); + +const ServerPage = async ({ params, searchParams }: PageProps): Promise => { const props = await getData( buildLegacyCtx(await headers(), await cookies(), await params, await searchParams) ); - const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props; + return ; +}; + +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 } = 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 340fcfee9d..bf47729d2f 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 c3c954b4c4..0000000000 --- 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 313e642349..0000000000 --- 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 6034af78e3..0000000000 --- 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 28ac508474..0000000000 --- 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 ed971e8f80..0000000000 --- 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 89fe3cef6c..0000000000 --- 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 2359fb25a1..0000000000 --- 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 26c2c6eb89..0000000000 --- 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 2359fb25a1..0000000000 --- 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 6ab17439dd..0000000000 --- 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 be318f651d..0000000000 --- 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 dd6e59d3d5..0000000000 --- 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 2e488238dc..0000000000 --- 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 51db301c64..0000000000 --- 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 7ad40ee950..0000000000 --- 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 5f823ff15c..0000000000 --- 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 f82d9bc7cd..58d7849a09 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 8f539ed39a..295c10abd6 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 90f4fa30b2..7f65a0df25 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 d05fbfabea..3b02f95120 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 5a6921b8f1..0000000000 --- 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 e34a18875c..0000000000 --- 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 b663692bba..0000000000 --- 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 85fdf0db7b..0000000000 --- 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 c385509478..0000000000 --- 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 7c37672e52..0000000000 --- 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 36889af36b..0000000000 --- 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 1e34195c6a..0000000000 --- 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 348d50a487..0000000000 --- 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" ? ( -