Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b2161cc19 | ||
|
|
b997c656df | ||
|
|
c9c5493de6 | ||
|
|
20a5775f31 | ||
|
|
b9b0a86b32 | ||
|
|
ad93066cf6 | ||
|
|
e6d76b630c | ||
|
|
b941b716d5 | ||
|
|
1ab9622416 | ||
|
|
15156c2225 | ||
|
|
cc202ed847 | ||
|
|
79d51a4989 | ||
|
|
53a846d10c | ||
|
|
1619f2daf2 | ||
|
|
57bd0b3df3 | ||
|
|
e359d13d9a | ||
|
|
3b7cb542ed | ||
|
|
fd7f6c32ae | ||
|
|
06d8b98b15 | ||
|
|
0c69c5dfea | ||
|
|
00f7d8be0d | ||
|
|
2e9beb8513 | ||
|
|
b6c6fa7365 | ||
|
|
c623785ec2 | ||
|
|
6a381cdc79 | ||
|
|
9d4496133e | ||
|
|
1452672495 | ||
|
|
9159c591ff | ||
|
|
408aee5851 | ||
|
|
29bbada77a | ||
|
|
6178092878 | ||
|
|
6f531d0be5 | ||
|
|
d7b723c914 | ||
|
|
aad3988d60 | ||
|
|
c8172ab790 | ||
|
|
657f9dfd41 | ||
|
|
c48b209c04 | ||
|
|
b9a460d8b4 | ||
|
|
a7727b5ca6 | ||
|
|
9b661b75f1 | ||
|
|
487cc0751d | ||
|
|
f7f36978e1 | ||
|
|
c1dbb7cffd | ||
|
|
e722e75a6f | ||
|
|
455572b5f4 | ||
|
|
2fe4462f62 | ||
|
|
f9dc788007 | ||
|
|
6e293d2985 | ||
|
|
00334c5610 | ||
|
|
2f9c8cb441 | ||
|
|
39173900b1 | ||
|
|
bd61e01a5d | ||
|
|
aa0b3a2778 | ||
|
|
170feeae5e | ||
|
|
a67b57b216 | ||
|
|
821a16648f | ||
|
|
727f7df2c2 | ||
|
|
be5f7f83b5 | ||
|
|
02a49e2b4b | ||
|
|
22f92f1ee3 | ||
|
|
c7a797c0e0 | ||
|
|
447436dfff | ||
|
|
c415b2d7f5 | ||
|
|
c10c87c98e | ||
|
|
85f96f492d | ||
|
|
b1350d0764 | ||
|
|
bac2d1f6f0 | ||
|
|
fec504496a | ||
|
|
890fc92148 | ||
|
|
557bd8bf25 | ||
|
|
d8027fe248 | ||
|
|
116c765c36 | ||
|
|
c6c3e228a3 | ||
|
|
7584645b8c | ||
|
|
32ff7ffed1 | ||
|
|
55113f20fe | ||
|
|
0a293308e7 | ||
|
|
e558daaa0b | ||
|
|
3f0a6718bc | ||
|
|
8ffebdba44 | ||
|
|
cc1101ac43 | ||
|
|
7a80a5f929 | ||
|
|
a27e1498b8 | ||
|
|
a697857fa0 | ||
|
|
e2f45bdf5a | ||
|
|
d2930bc243 | ||
|
|
b0b93f612e | ||
|
|
558c8be740 | ||
|
|
1aee1742d5 | ||
|
|
ab942ed4cd | ||
|
|
b5bec79354 | ||
|
|
b48d0e1f48 | ||
|
|
4a38b4dc89 | ||
|
|
ff12e387b3 | ||
|
|
6b5e144042 | ||
|
|
cd7b93a51e | ||
|
|
a76366cbcb | ||
|
|
860c04bbda | ||
|
+6 |
225313e391 | ||
|
|
3994929a51 | ||
|
|
ae38027d76 | ||
|
|
9036080fcf | ||
|
|
e6ef4e1988 | ||
|
|
3c2619a7a3 | ||
|
|
cfb09a1b21 | ||
|
|
5fe4e09975 | ||
|
|
542799e2cf | ||
|
|
3eda7858c0 | ||
|
|
b59bf7d1ee | ||
|
|
0dadeac352 | ||
|
|
5eb7a4d56e | ||
|
|
43fa944e2f | ||
|
|
1866b2845c | ||
|
|
2cfc63e6e3 | ||
|
|
4f66f3d2c6 | ||
|
|
ca3e7bd97d | ||
|
|
cafa3f9cf3 | ||
|
|
e04faffcbb | ||
|
|
6f1556a2fd | ||
|
|
d72a954896 | ||
|
|
abeb20373c | ||
|
|
422ef2f5b8 | ||
|
|
ec920eb2a8 | ||
|
|
795eecf300 | ||
|
|
8ae3d6d55b | ||
|
|
85070574da | ||
|
|
d1c2872f6e | ||
|
|
42d78f8099 | ||
|
|
f0d7a757d4 | ||
|
|
0b2f09ab7d | ||
|
|
355aaa7010 | ||
|
|
c27b73aa9a | ||
|
|
a622c7b624 | ||
|
|
19bd3e5849 | ||
|
|
7827028e0a | ||
|
|
ebc3ced687 | ||
|
|
b56824e626 | ||
|
|
460e511459 | ||
|
|
b4fd6f3442 | ||
|
|
0a7a0e3687 | ||
|
|
595fffbea0 | ||
|
|
218c9152da | ||
|
|
77fe23da49 | ||
|
|
a583db3fd4 | ||
|
|
cb1f803d0a | ||
|
|
479d6b5961 | ||
|
|
4ab3ec73c0 | ||
|
|
8dda418127 | ||
|
|
a7f24e777b | ||
|
|
f78fca594a | ||
|
|
e6a7ae7da3 | ||
|
|
ad5d92dff0 | ||
|
|
8bf46ab8b6 | ||
|
|
34b0753f8d | ||
|
|
0bd7227150 | ||
|
|
4d192ee328 | ||
|
|
63013d402e | ||
|
|
6b3ccbc61a | ||
|
|
2af5cba0d7 | ||
|
|
ae7bbaf414 | ||
|
|
4df4383e1e | ||
|
|
a0706d9b51 | ||
|
|
2eb385af48 | ||
|
|
a0f986f949 | ||
|
|
b0094bad2d | ||
|
|
c732c06835 | ||
|
|
c1334475c5 | ||
|
|
b35dc43011 | ||
|
|
c7dc3ecc49 | ||
|
|
a613c975d3 | ||
|
|
5fad7c9478 | ||
|
|
318470d866 | ||
|
|
a0ce42e375 | ||
|
|
5293a5cc54 | ||
|
|
42aefc9032 | ||
|
|
14acf4e9bc | ||
|
|
80815bd8a5 | ||
|
|
9ed8face03 | ||
|
|
18a107d081 | ||
|
|
04d7537e2f | ||
|
|
423b17a819 | ||
|
|
0466b7ebd6 | ||
|
|
09329707c1 | ||
|
|
1773aba378 | ||
|
|
f8be2b3833 | ||
|
|
79d488faca | ||
|
|
da2c57c302 | ||
|
|
abaf03adde | ||
|
|
5b353c3d0c | ||
|
|
1f4cda31ac | ||
|
|
73d1a6f468 | ||
|
|
f72748b6f2 | ||
|
|
d12fc78fea | ||
|
|
3d26332443 | ||
|
|
bdb490c894 | ||
|
|
01dbc686b9 | ||
|
|
376961f30d | ||
|
|
18a0eb9736 | ||
|
|
84fb4f68b2 | ||
|
|
b5ab6fe3fa | ||
|
|
645985c783 | ||
|
|
a84513eda5 | ||
|
|
b97331aeb3 | ||
|
|
851d96423b | ||
|
|
ef830f11ec | ||
|
|
1ab4ec9c22 | ||
|
|
656a5bf423 | ||
|
|
8a6450e854 | ||
|
|
51841e5e9c | ||
|
|
59dfcaa2fa | ||
|
|
0d24706380 | ||
|
|
87b6e0b49c | ||
|
|
4244ca3117 | ||
|
|
8d0f0d4899 | ||
|
|
6463983d55 | ||
|
|
c8661563c8 | ||
|
|
2262d56491 | ||
|
|
6b1c747c0e | ||
|
|
04036c32a7 | ||
|
|
49070b96ae | ||
|
|
237fea0a41 | ||
|
|
a7e7561efa | ||
|
|
e366c10521 | ||
|
|
2ace1d1017 | ||
|
|
fbe31ad82d | ||
|
|
ee3c08918a | ||
|
|
b6d6b7cdb4 | ||
|
|
1b82552e76 | ||
|
|
13a90aba30 | ||
|
|
de0de64a16 | ||
|
|
8e772d9046 | ||
|
|
7ac7a14beb | ||
|
|
9c127cbfc2 | ||
|
|
187a2c576e | ||
|
|
5e8d994ac1 | ||
|
|
1de007ad21 | ||
|
|
06494a6999 | ||
|
|
016c47d535 | ||
|
|
2aeb290bda | ||
|
|
95b4f67842 | ||
|
|
1a4afe06c8 | ||
|
|
0eb72d66a7 | ||
|
|
7d2523e823 | ||
|
|
6739ed3a87 | ||
|
|
dd3505ad8b | ||
|
|
ce9df65b5d | ||
|
|
fc6e39c78a | ||
|
|
931f3d3396 | ||
|
|
8a6fac6748 | ||
|
|
af43ede38e | ||
|
|
a66eb9dfac | ||
|
|
5ba676b014 | ||
|
|
c2c082c105 | ||
|
|
44d6c1f3fb | ||
|
|
a466e8eef7 | ||
|
|
370345da22 | ||
|
|
a1fc6668d5 | ||
|
|
351ab0790f | ||
|
|
504600b151 | ||
|
|
8b222490c2 | ||
|
|
0657a96795 | ||
|
|
9c9d7fe0c3 | ||
|
|
950879b9c7 | ||
|
|
55dc4bddb6 | ||
|
|
cbb1b285f5 | ||
|
|
45f8ecbab7 | ||
|
|
1680cba5a5 | ||
|
|
046042a877 | ||
|
|
a926e0b9d6 | ||
|
|
9334b67f2c | ||
|
|
ae1b844bea | ||
|
|
15e93af2ac | ||
|
|
49c97c6380 | ||
|
|
8679d21d78 | ||
|
|
382f00e178 | ||
|
|
e8eab10d17 | ||
|
|
7a7282a7ab | ||
|
|
19b3d6a633 | ||
|
|
046b7a0d08 | ||
|
|
30acf6b196 | ||
|
|
5e95018870 | ||
|
|
e559e7a46f | ||
|
|
3b1123a29a | ||
|
|
be3e580107 | ||
|
|
b325cf8d00 | ||
|
|
6ce9defecf | ||
|
|
8d05656b7b | ||
|
|
091400b7d5 | ||
|
|
6de26b3c43 | ||
|
|
063400fcbc | ||
|
|
44115ac7fa | ||
|
|
7e76770ba6 | ||
|
|
d5ef3e1d72 | ||
|
|
0c19606e19 | ||
|
|
b8219c273d | ||
|
|
f75415e414 | ||
|
|
4bf5e3cdf5 | ||
|
|
4181b5988a | ||
|
|
c41ba9d984 | ||
|
|
2a578ea9d5 | ||
|
|
c96827e1c0 | ||
|
|
3f5363038c | ||
|
|
e2de28b933 | ||
|
|
5d88858995 | ||
|
|
82a500aeaa | ||
|
|
2408337c78 | ||
|
|
119edb5389 | ||
|
|
ae4474cd42 | ||
|
|
3290ae526a | ||
|
|
16a7cf6d04 | ||
|
|
ab2ab31eef | ||
|
|
a08ae43533 | ||
|
|
d60b6c6a6d | ||
|
|
9085287fc6 | ||
|
|
7e2b5b12b4 | ||
|
|
3211c5bcff | ||
|
|
a75a5b72b2 | ||
|
|
509fc4b289 | ||
|
|
f63c06d68a | ||
|
|
3dff77fe59 | ||
|
|
a6f9952d75 | ||
|
|
03704b2e73 | ||
|
|
982a715c4b | ||
|
|
eaff9e8c89 | ||
|
|
e2938ac39a | ||
|
|
b92db55380 | ||
|
|
3015c5a15e | ||
|
|
2444598e38 | ||
|
|
6eadadec73 | ||
|
|
145449e7b4 | ||
|
|
1d8326ed83 |
+20
-17
@@ -107,6 +107,15 @@ NEXT_PUBLIC_POSTHOG_KEY=
|
||||
|
||||
NEXT_PUBLIC_POSTHOG_HOST=
|
||||
|
||||
# plain.com config
|
||||
|
||||
PLAIN_API_KEY=
|
||||
PLAIN_API_URL=https://api.plain.com/v1
|
||||
PLAIN_HMAC_SECRET_KEY=
|
||||
PLAIN_CHAT_ID=
|
||||
PLAIN_CHAT_HMAC_SECRET_KEY=
|
||||
NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS=
|
||||
|
||||
# Zendesk Config
|
||||
NEXT_PUBLIC_ZENDESK_KEY=
|
||||
|
||||
@@ -119,7 +128,7 @@ NEXT_PUBLIC_FRESHCHAT_HOST=
|
||||
|
||||
# Google OAuth credentials
|
||||
# To enable Login with Google you need to:
|
||||
# 1. Set `GOOGLE_API_CREDENTIALS` above
|
||||
# 1. Set `GOOGLE_API_CREDENTIALS` below
|
||||
# 2. Set `GOOGLE_LOGIN_ENABLED` to `true`
|
||||
# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance
|
||||
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
|
||||
@@ -239,7 +248,8 @@ CLOUDFLARE_TURNSTILE_SECRET=
|
||||
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
||||
|
||||
# Close.com internal CRM
|
||||
CLOSECOM_API_KEY=
|
||||
CLOSECOM_CLIENT_ID=
|
||||
CLOSECOM_CLIENT_SECRET=
|
||||
|
||||
# Sendgrid internal sync service
|
||||
SENDGRID_SYNC_API_KEY=
|
||||
@@ -345,9 +355,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
|
||||
# provide a value between 0 and 100 to ensure the percentage of traffic
|
||||
# redirected from the legacy to the future pages
|
||||
AB_TEST_BUCKET_PROBABILITY=50
|
||||
# whether we redirect to the future/event-types from event-types or not
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_DEVELOPER_ENABLED=0
|
||||
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||
@@ -355,16 +362,8 @@ APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
|
||||
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
|
||||
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_BOOKING_ENABLED=0
|
||||
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
|
||||
APP_ROUTER_WORKFLOWS_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
|
||||
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
|
||||
APP_ROUTER_APPS_ENABLED=0
|
||||
APP_ROUTER_VIDEO_ENABLED=0
|
||||
APP_ROUTER_TEAM_ENABLED=0
|
||||
APP_ROUTER_TEAMS_ENABLED=0
|
||||
APP_ROUTER_AVAILABILITY_ENABLED=0
|
||||
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
|
||||
APP_ROUTER_AUTH_LOGIN_ENABLED=0
|
||||
APP_ROUTER_AUTH_LOGOUT_ENABLED=0
|
||||
@@ -373,10 +372,6 @@ APP_ROUTER_AUTH_SAML_ENABLED=0
|
||||
APP_ROUTER_AUTH_ERROR_ENABLED=0
|
||||
APP_ROUTER_AUTH_PLATFORM_ENABLED=0
|
||||
APP_ROUTER_AUTH_OAUTH2_ENABLED=0
|
||||
APP_ROUTER_MORE_ENABLED=0
|
||||
APP_ROUTER_MAINTENANCE_ENABLED=0
|
||||
APP_ROUTER_UPGRADE_ENABLED=0
|
||||
APP_ROUTER_CONNECT_AND_JOIN_ENABLED=0
|
||||
|
||||
# disable setry server source maps
|
||||
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
||||
@@ -416,3 +411,11 @@ NEXT_PUBLIC_LOGGER_LEVEL=
|
||||
|
||||
# Used to use Replexica SDK, a tool for real-time AI-powered localization
|
||||
REPLEXICA_API_KEY=
|
||||
|
||||
# Comma-separated list of DSyncData.directoryId to log SCIM API requests for. It can be enabled temporarily for debugging the requests being sent to SCIM server.
|
||||
DIRECTORY_IDS_TO_LOG=
|
||||
|
||||
|
||||
# Set this when Cal.com is used to serve only one organization's booking pages
|
||||
# Read more about it in the README.md
|
||||
NEXT_PUBLIC_SINGLE_ORG_SLUG=
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/packages/app-store/applecalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/caldavcalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/exchange2013calendar/**/* @calcom/Foundation
|
||||
/packages/app-store/exchange2016calendar/**/* @calcom/Foundation
|
||||
/packages/app-store/exchangecalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/feishucalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/googlecalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/ics-feedcalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/larkcalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/office365calendar/**/* @calcom/Foundation
|
||||
/packages/app-store/zohocalendar/**/* @calcom/Foundation
|
||||
@@ -17,6 +17,7 @@ env:
|
||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
||||
NODE_OPTIONS: --max-old-space-size=29000
|
||||
REDIS_URL: "redis://localhost:6379"
|
||||
REPLEXICA_API_KEY: ${{ secrets.CI_REPLEXICA_API_KEY }}
|
||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
|
||||
@@ -17,7 +17,6 @@ env:
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
@@ -51,7 +50,8 @@ env:
|
||||
jobs:
|
||||
analyze:
|
||||
if: always()
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
@@ -60,6 +60,7 @@ jobs:
|
||||
- name: Analyze bundle
|
||||
run: |
|
||||
cd apps/web
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
npx -p nextjs-bundle-analysis@0.5.0 report
|
||||
|
||||
- name: Upload bundle
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
printf '{
|
||||
"pr_number": ${{ github.event.pull_request.number }}
|
||||
}' >> context.json
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: context.json
|
||||
path: ./
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Unit
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
REPLEXICA_API_KEY: ${{ secrets.CI_REPLEXICA_API_KEY }}
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
undecided:
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
Vendored
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
undecided:
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
@@ -485,6 +485,15 @@ Don't code but still want to contribute? Join our [Discussions](https://github.c
|
||||
|
||||
- Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
||||
|
||||
## Single Org Mode
|
||||
If you want to have booker.yourcompany.com to be the domain used for both dashboard(e.g. https://booker.yourcompany.com/event-types) and booking pages(e.g. https://booker.yourcompany.com/john.joe/15min).
|
||||
- Set the `NEXT_PUBLIC_SINGLE_ORG_SLUG` environment variable to the slug of the organization you want to use. `NEXT_PUBLIC_SINGLE_ORG_SLUG=booker`
|
||||
- Set the `NEXT_PUBLIC_WEBAPP_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXT_PUBLIC_WEBAPP_URL=https://booker.yourcompany.com`.
|
||||
- Set the `NEXT_PUBLIC_WEBSITE_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXT_PUBLIC_WEBSITE_URL=https://booker.yourcompany.com`.
|
||||
- Set the `NEXTAUTH_URL` environment variable to the URL of the Cal.com self-hosted instance e.g. `NEXTAUTH_URL=https://booker.yourcompany.com`.
|
||||
|
||||
Note: It causes root to serve the dashboard and not the organization profile page which shows all bookable users in the organization.
|
||||
|
||||
## Integrations
|
||||
|
||||
### Obtaining the Google API Credentials
|
||||
|
||||
@@ -11,15 +11,15 @@ test.describe("Org", () => {
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
|
||||
test("Org User(Peer) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/peer/embed");
|
||||
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=Peer Richelsen")).toBeVisible();
|
||||
await expect(page.locator("text=Rick in i.cal.com")).toBeVisible();
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
|
||||
test("Org User Event(peer/meet) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/peer/meet/embed");
|
||||
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();
|
||||
|
||||
@@ -61,6 +61,8 @@ describe("rateLimitApiKey middleware", () => {
|
||||
reset: Date.now(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ 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";
|
||||
|
||||
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
|
||||
@@ -24,23 +25,35 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
||||
if (!hasValidLicense && IS_PRODUCTION) {
|
||||
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
|
||||
}
|
||||
// Check if the apiKey query param is provided.
|
||||
|
||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||
// remove the prefix from the user provided api_key. If no env set default to "cal_"
|
||||
|
||||
const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", "");
|
||||
// Hash the key again before matching against the database records.
|
||||
const hashedKey = hashAPIKey(strippedApiKey);
|
||||
// Check if the hashed api key exists in database.
|
||||
const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey } });
|
||||
// If cannot find any api key. Throw a 401 Unauthorized.
|
||||
if (!apiKey) return res.status(401).json({ error: "Your apiKey is not valid" });
|
||||
const apiKey = await prisma.apiKey.findUnique({
|
||||
where: { hashedKey },
|
||||
include: {
|
||||
user: {
|
||||
select: { role: true, locked: true, email: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!apiKey) return res.status(401).json({ error: "Your API key is not valid." });
|
||||
if (apiKey.expiresAt && dateNotInPast(apiKey.expiresAt)) {
|
||||
return res.status(401).json({ error: "This apiKey is expired" });
|
||||
return res.status(401).json({ error: "This API key is expired." });
|
||||
}
|
||||
if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" });
|
||||
if (!apiKey.userId || !apiKey.user)
|
||||
return res.status(404).json({ error: "No user found for this API key." });
|
||||
|
||||
// save the user id in the request for later use
|
||||
req.userId = apiKey.userId;
|
||||
req.user = apiKey.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;
|
||||
|
||||
@@ -6,8 +6,7 @@ import { UserPermissionRole, MembershipRole } from "@calcom/prisma/enums";
|
||||
import { ScopeOfAdmin } from "./scopeOfAdmin";
|
||||
|
||||
export const isAdminGuard = async (req: NextApiRequest) => {
|
||||
const { userId } = req;
|
||||
const user = await prisma.user.findUnique({ where: { id: userId }, select: { role: true } });
|
||||
const { user, userId } = req;
|
||||
if (!user) return { isAdmin: false, scope: null };
|
||||
|
||||
const { role: userRole } = user;
|
||||
@@ -34,7 +33,7 @@ export const isAdminGuard = async (req: NextApiRequest) => {
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!orgOwnerOrAdminMemberships.length) return { isAdmin: false, scope: null };
|
||||
if (orgOwnerOrAdminMemberships.length > 0) return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin };
|
||||
|
||||
return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin };
|
||||
return { isAdmin: false, scope: null };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
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(user.email));
|
||||
}
|
||||
@@ -23,6 +23,7 @@ const schemaBookingBaseBodyParams = Booking.pick({
|
||||
status: true,
|
||||
rescheduledBy: true,
|
||||
cancelledBy: true,
|
||||
createdAt: true,
|
||||
}).partial();
|
||||
|
||||
export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(schemaQueryUserId.partial());
|
||||
@@ -119,4 +120,5 @@ export const schemaBookingReadPublic = Booking.extend({
|
||||
fromReschedule: true,
|
||||
cancelledBy: true,
|
||||
rescheduledBy: true,
|
||||
createdAt: true,
|
||||
});
|
||||
|
||||
@@ -9,11 +9,25 @@ export const schemaSelectedCalendarBaseBodyParams = SelectedCalendar;
|
||||
|
||||
export const schemaSelectedCalendarPublic = SelectedCalendar.omit({});
|
||||
|
||||
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams.partial({
|
||||
userId: true,
|
||||
});
|
||||
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams
|
||||
.partial({
|
||||
userId: true,
|
||||
})
|
||||
.omit({
|
||||
// id will be set by the database
|
||||
id: true,
|
||||
// No eventTypeId support in API v1
|
||||
eventTypeId: true,
|
||||
});
|
||||
|
||||
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams.partial();
|
||||
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 */
|
||||
|
||||
Vendored
+1
@@ -8,6 +8,7 @@ export declare module "next" {
|
||||
session?: Session | null;
|
||||
|
||||
userId: number;
|
||||
user?: { role: string; locked: boolean; email: string } | null;
|
||||
method: string;
|
||||
// session: { user: { id: number } };
|
||||
// query: Partial<{ [key: string]: string | string[] }>;
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"start": "PORT=3003 next start",
|
||||
"docker-start-api": "PORT=80 next start",
|
||||
"type-check": "tsc --pretty --noEmit"
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/tsconfig": "*",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import type { UserWithCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
import { getConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
import { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery";
|
||||
@@ -104,12 +105,8 @@ async function getHandler(req: NextApiRequest) {
|
||||
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
|
||||
const usersWithCalendars = await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
include: {
|
||||
selectedCalendars: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
const usersWithCalendars = await UserRepository.findManyByIdsIncludeDestinationAndSelectedCalendars({
|
||||
ids: userIds,
|
||||
});
|
||||
|
||||
return await getConnectedCalendars(usersWithCalendars);
|
||||
@@ -117,11 +114,12 @@ async function getHandler(req: NextApiRequest) {
|
||||
|
||||
async function getConnectedCalendars(users: UserWithCalendars[]) {
|
||||
const connectedDestinationCalendarsPromises = users.map((user) =>
|
||||
getConnectedDestinationCalendars(user, false, prisma).then((connectedCalendarsResult) =>
|
||||
connectedCalendarsResult.connectedCalendars.map((calendar) => ({
|
||||
userId: user.id,
|
||||
...calendar,
|
||||
}))
|
||||
getConnectedDestinationCalendarsAndEnsureDefaultsInDb({ user, onboarding: false, prisma }).then(
|
||||
(connectedCalendarsResult) =>
|
||||
connectedCalendarsResult.connectedCalendars.map((calendar) => ({
|
||||
userId: user.id,
|
||||
...calendar,
|
||||
}))
|
||||
)
|
||||
);
|
||||
const connectedDestinationCalendars = await Promise.all(connectedDestinationCalendarsPromises);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
|
||||
import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
|
||||
|
||||
@@ -49,7 +49,9 @@ import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
|
||||
await prisma.selectedCalendar.delete({ where: { userId_integration_externalId } });
|
||||
await SelectedCalendarRepository.deleteUserLevel({
|
||||
where: userId_integration_externalId,
|
||||
});
|
||||
return { message: `Selected Calendar with id: ${query.id} deleted successfully` };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
|
||||
import { schemaSelectedCalendarPublic, selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
|
||||
|
||||
@@ -49,8 +49,8 @@ import { schemaSelectedCalendarPublic, selectedCalendarIdSchema } from "~/lib/va
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
|
||||
const data = await prisma.selectedCalendar.findUniqueOrThrow({
|
||||
where: { userId_integration_externalId },
|
||||
const data = await SelectedCalendarRepository.findUserLevelUniqueOrThrow({
|
||||
where: userId_integration_externalId,
|
||||
});
|
||||
return { selected_calendar: schemaSelectedCalendarPublic.parse(data) };
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import type { UpdateArguments } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
@@ -56,7 +58,7 @@ 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: Prisma.SelectedCalendarUpdateArgs = { where: { userId_integration_externalId }, data };
|
||||
const args: UpdateArguments = { where: { ...userId_integration_externalId }, data };
|
||||
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
@@ -67,7 +69,7 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
const result = await prisma.selectedCalendar.update(args);
|
||||
const result = await SelectedCalendarRepository.updateUserLevel(args);
|
||||
return { selected_calendar: schemaSelectedCalendarPublic.parse(result) };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { FindManyArgs } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
|
||||
import { schemaSelectedCalendarPublic } from "~/lib/validations/selected-calendar";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
@@ -34,7 +34,7 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
/* Admin gets all selected calendar by default, otherwise only the user's ones */
|
||||
const args: Prisma.SelectedCalendarFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
const args: FindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
|
||||
/** Only admins can query other users */
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
@@ -46,7 +46,7 @@ async function getHandler(req: NextApiRequest) {
|
||||
if (Array.isArray(query.userId)) args.orderBy = { userId: "asc" };
|
||||
}
|
||||
|
||||
const data = await prisma.selectedCalendar.findMany(args);
|
||||
const data = await SelectedCalendarRepository.findManyUserLevel(args);
|
||||
return { selected_calendars: data.map((v) => schemaSelectedCalendarPublic.parse(v)) };
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
@@ -52,7 +53,11 @@ import {
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { userId: bodyUserId, ...body } = schemaSelectedCalendarBodyParams.parse(req.body);
|
||||
const args: Prisma.SelectedCalendarCreateArgs = { data: { ...body, userId } };
|
||||
const args: {
|
||||
data: Prisma.SelectedCalendarUncheckedCreateInput;
|
||||
} = {
|
||||
data: { ...body, userId },
|
||||
};
|
||||
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
@@ -63,7 +68,7 @@ async function postHandler(req: NextApiRequest) {
|
||||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
const data = await prisma.selectedCalendar.create(args);
|
||||
const data = await SelectedCalendarRepository.create(args.data);
|
||||
|
||||
return {
|
||||
selected_calendar: schemaSelectedCalendarPublic.parse(data),
|
||||
|
||||
@@ -52,6 +52,8 @@ describe("POST /api/attendees", () => {
|
||||
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;
|
||||
@@ -84,7 +86,8 @@ describe("POST /api/attendees", () => {
|
||||
timeZone: "UTC",
|
||||
bookingId: 1,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
prismaMock.attendee.create.mockResolvedValue(attendeeData);
|
||||
|
||||
req.userId = 123;
|
||||
|
||||
@@ -11,7 +11,7 @@ import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Booking } from "@calcom/prisma";
|
||||
import type { Booking } from "@calcom/prisma/client";
|
||||
|
||||
import handler from "../../../pages/api/bookings/_post";
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ describe("DELETE /api/event-types/[id]", () => {
|
||||
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 = [
|
||||
|
||||
@@ -72,6 +72,7 @@ describe("POST /api/event-types", () => {
|
||||
},
|
||||
});
|
||||
req.userId = memberUserId;
|
||||
// @ts-expect-error - Return type is wrong
|
||||
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false);
|
||||
|
||||
await handler(req, res);
|
||||
@@ -114,6 +115,7 @@ describe("POST /api/event-types", () => {
|
||||
req.isSystemWideAdmin = true;
|
||||
req.userId = adminUserId;
|
||||
|
||||
// @ts-expect-error - Return type is wrong
|
||||
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false);
|
||||
|
||||
await handler(req, res);
|
||||
@@ -138,6 +140,7 @@ describe("POST /api/event-types", () => {
|
||||
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);
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock";
|
||||
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, vi, it, expect, afterEach, beforeEach } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ILicenseKeyService } from "@calcom/ee/common/server/LicenseKeyService";
|
||||
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { isAdminGuard } from "~/lib/utils/isAdmin";
|
||||
import { hashAPIKey } from "~/../../../packages/features/ee/api-keys/lib/apiKeys";
|
||||
|
||||
import { verifyApiKey } from "../../../lib/helpers/verifyApiKey";
|
||||
import { ScopeOfAdmin } from "../../../lib/utils/scopeOfAdmin";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
@@ -20,13 +21,8 @@ afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
vi.mock("@calcom/prisma");
|
||||
vi.mock("~/lib/utils/isAdmin", () => ({
|
||||
isAdminGuard: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("Verify API key", () => {
|
||||
let service: LicenseKeyService;
|
||||
let service: ILicenseKeyService;
|
||||
|
||||
beforeEach(async () => {
|
||||
service = await LicenseKeyService.create();
|
||||
@@ -45,7 +41,6 @@ describe("Verify API key", () => {
|
||||
};
|
||||
|
||||
vi.mocked(service.checkLicense).mockResolvedValue(false);
|
||||
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null });
|
||||
|
||||
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||
|
||||
@@ -69,7 +64,6 @@ describe("Verify API key", () => {
|
||||
};
|
||||
|
||||
vi.mocked(service.checkLicense).mockResolvedValue(true);
|
||||
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null });
|
||||
|
||||
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||
|
||||
@@ -91,10 +85,18 @@ describe("Verify API key", () => {
|
||||
},
|
||||
prisma,
|
||||
});
|
||||
|
||||
prismaMock.apiKey.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
userId: 2,
|
||||
const hashedKey = hashAPIKey("test_key");
|
||||
await prismock.apiKey.create({
|
||||
data: {
|
||||
hashedKey,
|
||||
user: {
|
||||
create: {
|
||||
email: "admin@example.com",
|
||||
role: UserPermissionRole.ADMIN,
|
||||
locked: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const middleware = {
|
||||
@@ -102,7 +104,6 @@ describe("Verify API key", () => {
|
||||
};
|
||||
|
||||
vi.mocked(service.checkLicense).mockResolvedValue(true);
|
||||
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.SystemWide });
|
||||
|
||||
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||
|
||||
@@ -125,10 +126,36 @@ describe("Verify API key", () => {
|
||||
},
|
||||
prisma,
|
||||
});
|
||||
|
||||
prismaMock.apiKey.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
userId: 2,
|
||||
const hashedKey = hashAPIKey("test_key");
|
||||
await prismock.apiKey.create({
|
||||
data: {
|
||||
hashedKey,
|
||||
user: {
|
||||
create: {
|
||||
email: "org-admin@acme.com",
|
||||
role: UserPermissionRole.USER,
|
||||
locked: false,
|
||||
teams: {
|
||||
create: {
|
||||
accepted: true,
|
||||
role: MembershipRole.OWNER,
|
||||
team: {
|
||||
create: {
|
||||
name: "ACME",
|
||||
isOrganization: true,
|
||||
organizationSettings: {
|
||||
create: {
|
||||
isAdminAPIEnabled: true,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const middleware = {
|
||||
@@ -136,7 +163,6 @@ describe("Verify API key", () => {
|
||||
};
|
||||
|
||||
vi.mocked(service.checkLicense).mockResolvedValue(true);
|
||||
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin });
|
||||
|
||||
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||
|
||||
@@ -149,4 +175,44 @@ describe("Verify API key", () => {
|
||||
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<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {},
|
||||
query: {
|
||||
apiKey: "cal_test_key",
|
||||
},
|
||||
prisma,
|
||||
});
|
||||
const hashedKey = hashAPIKey("test_key");
|
||||
await prismock.apiKey.create({
|
||||
data: {
|
||||
hashedKey,
|
||||
user: {
|
||||
create: {
|
||||
email: "locked@example.com",
|
||||
role: UserPermissionRole.USER,
|
||||
locked: 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,6 +79,15 @@ describe("POST /api/selected-calendars", () => {
|
||||
integration: "google",
|
||||
externalId: "ext123",
|
||||
userId: 444444,
|
||||
id: "xxx-xxx",
|
||||
eventTypeId: null,
|
||||
domainWideDelegationCredentialId: null,
|
||||
googleChannelId: null,
|
||||
googleChannelKind: null,
|
||||
googleChannelResourceId: null,
|
||||
googleChannelResourceUri: null,
|
||||
googleChannelExpiration: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
@@ -104,10 +113,19 @@ describe("POST /api/selected-calendars", () => {
|
||||
req.userId = 333333;
|
||||
|
||||
prismaMock.selectedCalendar.create.mockResolvedValue({
|
||||
id: "xxx-xxx",
|
||||
credentialId: 1,
|
||||
integration: "google",
|
||||
externalId: "ext123",
|
||||
userId: 333333,
|
||||
googleChannelId: null,
|
||||
googleChannelKind: null,
|
||||
googleChannelResourceId: null,
|
||||
googleChannelResourceUri: null,
|
||||
googleChannelExpiration: null,
|
||||
domainWideDelegationCredentialId: null,
|
||||
eventTypeId: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("isAdmin guard", () => {
|
||||
});
|
||||
|
||||
req.userId = 0;
|
||||
req.user = undefined;
|
||||
|
||||
const { isAdmin, scope } = await isAdminGuard(req);
|
||||
|
||||
@@ -35,6 +36,7 @@ describe("isAdmin guard", () => {
|
||||
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);
|
||||
|
||||
@@ -51,6 +53,7 @@ describe("isAdmin guard", () => {
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "admin@example.com" } });
|
||||
|
||||
req.userId = adminUser.id;
|
||||
req.user = adminUser;
|
||||
|
||||
const { isAdmin, scope } = await isAdminGuard(req);
|
||||
|
||||
@@ -67,6 +70,8 @@ describe("isAdmin guard", () => {
|
||||
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);
|
||||
@@ -81,6 +86,8 @@ describe("isAdmin guard", () => {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { describe, expect, it, beforeEach } from "vitest";
|
||||
|
||||
import { WatchlistSeverity } from "@calcom/prisma/enums";
|
||||
|
||||
import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked";
|
||||
|
||||
describe("isLockedOrBlocked", () => {
|
||||
beforeEach(async () => {
|
||||
await prismock.watchlist.createMany({
|
||||
data: [
|
||||
{
|
||||
type: "DOMAIN",
|
||||
value: "spam.com",
|
||||
createdById: 1,
|
||||
},
|
||||
{
|
||||
type: "DOMAIN",
|
||||
value: "blocked.com",
|
||||
severity: WatchlistSeverity.CRITICAL,
|
||||
createdById: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false if no user in request", async () => {
|
||||
const req = { userId: null, user: null } as any;
|
||||
const result = await isLockedOrBlocked(req);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if user has no email", async () => {
|
||||
const req = { userId: 123, user: { email: null } } as any;
|
||||
const result = await isLockedOrBlocked(req);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true if user is locked", async () => {
|
||||
const req = {
|
||||
userId: 123,
|
||||
user: {
|
||||
locked: true,
|
||||
email: "test@example.com",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = await isLockedOrBlocked(req);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true if user email domain is watchlisted", async () => {
|
||||
const req = {
|
||||
userId: 123,
|
||||
user: {
|
||||
locked: false,
|
||||
email: "test@blocked.com",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = await isLockedOrBlocked(req);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
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 any;
|
||||
|
||||
const result = await isLockedOrBlocked(req);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle email domains case-insensitively", async () => {
|
||||
const req = {
|
||||
userId: 123,
|
||||
user: {
|
||||
locked: false,
|
||||
email: "test@BLOCKED.COM",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = await isLockedOrBlocked(req);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"./next.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"../../../packages/types/*.d.ts",
|
||||
|
||||
@@ -6,6 +6,7 @@ ARG DATABASE_URL
|
||||
WORKDIR /calcom
|
||||
|
||||
RUN set -eux;
|
||||
RUN ln -s /usr/lib/libssl.so.3 /lib/libssl.so.3
|
||||
|
||||
ENV NODE_ENV="production"
|
||||
ENV NODE_OPTIONS="--max-old-space-size=8192"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.63",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.81",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
CAL_API_VERSION_HEADER,
|
||||
X_CAL_CLIENT_ID,
|
||||
X_CAL_SECRET_KEY,
|
||||
X_CAL_PLATFORM_EMBED,
|
||||
} from "@calcom/platform-constants";
|
||||
|
||||
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
||||
@@ -45,6 +46,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
allowedHeaders: [
|
||||
X_CAL_CLIENT_ID,
|
||||
X_CAL_SECRET_KEY,
|
||||
X_CAL_PLATFORM_EMBED,
|
||||
CAL_API_VERSION_HEADER,
|
||||
"Accept",
|
||||
"Authorization",
|
||||
|
||||
@@ -30,6 +30,8 @@ const loadConfig = (): AppConfig => {
|
||||
stripe: {
|
||||
apiKey: getEnv("STRIPE_API_KEY"),
|
||||
webhookSecret: getEnv("STRIPE_WEBHOOK_SECRET"),
|
||||
teamMonthlyPriceId: getEnv("STRIPE_TEAM_MONTHLY_PRICE_ID", "set-team-monthly-price-in-your-env"),
|
||||
isTeamBillingEnabled: getEnv("IS_TEAM_BILLING_ENABLED", true),
|
||||
},
|
||||
app: {
|
||||
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
|
||||
|
||||
@@ -21,6 +21,8 @@ export type AppConfig = {
|
||||
stripe: {
|
||||
apiKey: string;
|
||||
webhookSecret: string;
|
||||
teamMonthlyPriceId: string;
|
||||
isTeamBillingEnabled: boolean;
|
||||
};
|
||||
app: {
|
||||
baseUrl: string;
|
||||
|
||||
@@ -37,7 +37,7 @@ import { Request } from "express";
|
||||
import { NextApiRequest } from "next/types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
||||
import { X_CAL_CLIENT_ID, X_CAL_PLATFORM_EMBED } from "@calcom/platform-constants";
|
||||
import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants";
|
||||
import {
|
||||
handleNewRecurringBooking,
|
||||
@@ -161,14 +161,15 @@ export class BookingsController_2024_04_15 {
|
||||
async createBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() body: CreateBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<Partial<BookingResponse>>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const { orgSlug, locationUrl } = body;
|
||||
req.headers["x-cal-force-slug"] = orgSlug;
|
||||
try {
|
||||
const booking = await handleNewBooking(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed)
|
||||
);
|
||||
if (booking.userId && booking.uid && booking.startTime) {
|
||||
void (await this.billingService.increaseUsageByUserId(booking.userId, {
|
||||
@@ -192,13 +193,16 @@ export class BookingsController_2024_04_15 {
|
||||
@Req() req: BookingRequest,
|
||||
@Param("bookingId") bookingId: string,
|
||||
@Body() _: CancelBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
if (bookingId) {
|
||||
try {
|
||||
req.body.id = parseInt(bookingId);
|
||||
const res = await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
|
||||
const res = await handleCancelBooking(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed)
|
||||
);
|
||||
if (!res.onlyRemovedAttendee) {
|
||||
void (await this.billingService.cancelUsageByBookingUid(res.bookingUid));
|
||||
}
|
||||
@@ -246,7 +250,8 @@ export class BookingsController_2024_04_15 {
|
||||
async createRecurringBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateRecurringBookingInput_2024_04_15[],
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<BookingResponse[]>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
try {
|
||||
@@ -258,7 +263,7 @@ export class BookingsController_2024_04_15 {
|
||||
}
|
||||
|
||||
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
|
||||
await this.createNextApiRecurringBookingRequest(req, oAuthClientId)
|
||||
await this.createNextApiRecurringBookingRequest(req, oAuthClientId, undefined, isEmbed)
|
||||
);
|
||||
|
||||
createdBookings.forEach(async (booking) => {
|
||||
@@ -284,13 +289,14 @@ export class BookingsController_2024_04_15 {
|
||||
async createInstantBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
try {
|
||||
const instantMeeting = await handleInstantMeeting(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId)
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed)
|
||||
);
|
||||
|
||||
if (instantMeeting.userId && instantMeeting.bookingUid) {
|
||||
@@ -332,8 +338,14 @@ export class BookingsController_2024_04_15 {
|
||||
}
|
||||
}
|
||||
|
||||
private async getOAuthClientsParams(clientId: string): Promise<OAuthRequestParams> {
|
||||
private async getOAuthClientsParams(clientId: string, isEmbed = false): Promise<OAuthRequestParams> {
|
||||
const res = { ...DEFAULT_PLATFORM_PARAMS };
|
||||
|
||||
if (isEmbed) {
|
||||
// embed should ignore oauth client settings and enable emails by default
|
||||
return { ...res, arePlatformEmailsEnabled: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
|
||||
// fetch oAuthClient from db and use data stored in db to set these values
|
||||
@@ -354,13 +366,14 @@ export class BookingsController_2024_04_15 {
|
||||
private async createNextApiBookingRequest(
|
||||
req: BookingRequest,
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string
|
||||
platformBookingLocation?: string,
|
||||
isEmbed?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const requestId = req.get("X-Request-Id");
|
||||
const clone = { ...req };
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(oAuthClientId)
|
||||
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
this.logger.log(`createNextApiBookingRequest_2024_04_15`, {
|
||||
requestId,
|
||||
@@ -377,12 +390,13 @@ export class BookingsController_2024_04_15 {
|
||||
private async createNextApiRecurringBookingRequest(
|
||||
req: BookingRequest,
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string
|
||||
platformBookingLocation?: string,
|
||||
isEmbed?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const clone = { ...req };
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(oAuthClientId)
|
||||
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
const requestId = req.get("X-Request-Id");
|
||||
this.logger.log(`createNextApiRecurringBookingRequest_2024_04_15`, {
|
||||
@@ -424,4 +438,8 @@ export class BookingsController_2024_04_15 {
|
||||
|
||||
throw new InternalServerErrorException(errMsg);
|
||||
}
|
||||
|
||||
private transformToBoolean(v?: string): boolean {
|
||||
return v && typeof v === "string" ? v.toLowerCase() === "true" : false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
@@ -13,53 +14,66 @@ import {
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
optionValue!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
value!: string;
|
||||
}
|
||||
|
||||
class Response {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiProperty({ type: [String] })
|
||||
guests!: string[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => Location)
|
||||
@ApiPropertyOptional({ type: Location })
|
||||
location?: Location;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class CreateBookingInput_2024_04_15 {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
end?: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
start!: string;
|
||||
|
||||
@IsNumber()
|
||||
@ApiProperty()
|
||||
eventTypeId!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
eventTypeSlug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
rescheduleUid?: string;
|
||||
|
||||
@IsTimeZone()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
|
||||
@Transform(({ value }: { value: string | string[] }) => {
|
||||
@@ -68,38 +82,48 @@ export class CreateBookingInput_2024_04_15 {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
user?: string[];
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
language!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
bookingUid?: string;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object })
|
||||
metadata!: Record<string, string>;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
hasHashedBookingLink?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
hashedLink!: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
seatReferenceUid?: string;
|
||||
|
||||
@Type(() => Response)
|
||||
@ApiProperty({ type: Response })
|
||||
responses!: Response;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
orgSlug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
locationUrl?: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsString, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
@@ -6,23 +7,29 @@ import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2024_04_15 {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
noEmail?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiPropertyOptional()
|
||||
recurringCount?: number;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Object] })
|
||||
appsStatus?: AppsStatus[] | undefined;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Object] })
|
||||
allRecurringDates?: Record<string, string>[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiPropertyOptional()
|
||||
currentRecurringIndex?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
recurringEventId?: string;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import { ApiPropertyOptional, ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsOptional, IsArray, IsEmail, IsBoolean, ValidateNested } from "class-validator";
|
||||
|
||||
class Attendee {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
noShow!: boolean;
|
||||
}
|
||||
|
||||
export class MarkNoShowInput_2024_04_15 {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
noShowHost?: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Attendee] })
|
||||
attendees?: Attendee[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -17,145 +17,185 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Metadata {
|
||||
@IsUrl()
|
||||
@ApiProperty()
|
||||
videoCallUrl!: string;
|
||||
}
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
optionValue!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
value!: string;
|
||||
}
|
||||
|
||||
class Response {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
notes!: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiProperty({ type: [String] })
|
||||
guests!: string[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Location)
|
||||
@ApiProperty({ type: Location })
|
||||
location!: Location;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
}
|
||||
|
||||
class Attendee {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
}
|
||||
|
||||
class EventType {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
eventName!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
slug!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
timeZone!: string | null;
|
||||
}
|
||||
|
||||
class GetBookingData_2024_04_15 {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
title!: string;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
uid!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
description!: string | null;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object })
|
||||
customInputs!: any;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
smsReminderNumber!: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
recurringEventId!: string | null;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty()
|
||||
startTime!: Date;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty()
|
||||
endTime!: Date;
|
||||
|
||||
@IsUrl()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
location!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
status!: string;
|
||||
|
||||
@Type(() => Metadata)
|
||||
@ApiProperty({ type: Metadata })
|
||||
metadata!: Metadata | any;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
cancellationReason!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Response)
|
||||
@ApiProperty({ type: Response })
|
||||
responses!: Response | any;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
rejectionReason!: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsEmail()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
userPrimaryEmail!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => User)
|
||||
@ApiProperty({ type: User, nullable: true })
|
||||
user!: User | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [Attendee] })
|
||||
attendees!: Attendee[];
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
eventTypeId!: number | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => EventType)
|
||||
@ApiProperty({ type: EventType, nullable: true })
|
||||
eventType!: EventType | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -26,179 +26,230 @@ enum Status {
|
||||
|
||||
class Attendee {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
locale!: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
bookingId!: number | null;
|
||||
}
|
||||
|
||||
class EventType {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional()
|
||||
id?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
eventName?: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
price!: number;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
recurringEvent?: any;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
currency!: string;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty()
|
||||
metadata!: any;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
seatsShowAttendees?: boolean | undefined | null;
|
||||
@ApiPropertyOptional({ type: Boolean, nullable: true })
|
||||
seatsShowAttendees?: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
seatsShowAvailabilityCount?: boolean | undefined | null;
|
||||
@ApiPropertyOptional({ type: Boolean, nullable: true })
|
||||
seatsShowAvailabilityCount?: boolean | null;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
team?: any | null;
|
||||
}
|
||||
|
||||
class Reference {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
uid!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
meetingId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
thirdPartyRecurringEventId?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
meetingPassword!: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
meetingUrl?: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
bookingId!: number | null;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
externalCalendarId!: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
deleted?: any;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
credentialId!: number | null;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
}
|
||||
|
||||
class GetBookingsDataEntry {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
title!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
userPrimaryEmail?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
description!: string | null;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object })
|
||||
customInputs!: object | any;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty()
|
||||
startTime!: string;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty()
|
||||
endTime!: string;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [Attendee] })
|
||||
attendees!: Attendee[];
|
||||
|
||||
@ApiProperty()
|
||||
metadata!: any;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
uid!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
recurringEventId!: string | null;
|
||||
|
||||
@IsUrl()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
location!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => EventType)
|
||||
@ApiProperty({ type: EventType })
|
||||
eventType!: EventType;
|
||||
|
||||
@IsEnum(Status)
|
||||
status!: "CANCELLED" | "REJECTED" | "ACCEPTED" | "PENDING" | "AWAITING_HOST";
|
||||
@ApiProperty({ enum: Status, type: String })
|
||||
status!: Status;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: Boolean })
|
||||
paid!: boolean;
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
payment!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Reference)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [Reference] })
|
||||
references!: Reference[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: Boolean })
|
||||
isRecorded!: boolean;
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
seatsReferences!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => User)
|
||||
@ApiProperty({ type: User })
|
||||
user!: User | null;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
rescheduled?: any;
|
||||
}
|
||||
|
||||
@@ -206,17 +257,20 @@ class GetBookingsData_2024_04_15 {
|
||||
@ValidateNested()
|
||||
@Type(() => GetBookingsDataEntry)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [GetBookingsDataEntry] })
|
||||
bookings!: GetBookingsDataEntry[];
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
recurringInfo!: any[];
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
nextCursor!: number | null;
|
||||
}
|
||||
|
||||
export class GetBookingsOutput_2024_04_15 {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS], type: String })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsString, IsEnum, IsOptional, ValidateNested, IsArray, IsEmail, IsBoolean } from "class-validator";
|
||||
|
||||
@@ -6,24 +6,29 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Attendee {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
noShow!: boolean;
|
||||
}
|
||||
|
||||
class HandleMarkNoShowData_2024_04_15 {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
message!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
noShowHost?: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Attendee] })
|
||||
attendees?: Attendee[];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/out
|
||||
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 { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions";
|
||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-08-13\``,
|
||||
example: VERSION_2024_08_13,
|
||||
required: true,
|
||||
})
|
||||
export class BookingsController_2024_08_13 {
|
||||
|
||||
@@ -169,6 +169,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.eventTypeId).toEqual(eventTypeId);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
|
||||
+8
-1
@@ -34,6 +34,7 @@ import {
|
||||
OrganizerCancelledEmail,
|
||||
AttendeeCancelledEmail,
|
||||
OrganizerReassignedEmail,
|
||||
AttendeeUpdatedEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
@@ -61,10 +62,12 @@ jest
|
||||
jest
|
||||
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
|
||||
jest
|
||||
.spyOn(OrganizerReassignedEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(AttendeeUpdatedEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
|
||||
type EmailSetup = {
|
||||
team: Team;
|
||||
@@ -681,6 +684,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
emailsDisabledSetup.roundRobinEventType.currentHostId = reassignToId;
|
||||
});
|
||||
});
|
||||
@@ -697,6 +701,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerReassignedEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -898,6 +903,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(AttendeeUpdatedEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
emailsDisabledSetup.roundRobinEventType.currentHostId = reassignToId;
|
||||
});
|
||||
});
|
||||
@@ -914,6 +920,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerReassignedEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(AttendeeUpdatedEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -181,6 +181,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -201,6 +202,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.recurringBookingUid).toBeDefined();
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -256,6 +258,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -277,6 +280,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.recurringBookingUid).toBeDefined();
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -296,6 +300,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(thirdBooking.recurringBookingUid).toBeDefined();
|
||||
expect(thirdBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
|
||||
@@ -205,6 +205,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -278,6 +279,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const firstAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeOne);
|
||||
expect(firstAttendee).toEqual({
|
||||
name: createdSeatedBooking.attendees[0].name,
|
||||
email: createdSeatedBooking.attendees[0].email,
|
||||
timeZone: createdSeatedBooking.attendees[0].timeZone,
|
||||
language: createdSeatedBooking.attendees[0].language,
|
||||
absent: false,
|
||||
@@ -291,6 +293,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const secondAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeTwo);
|
||||
expect(secondAttendee).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -368,6 +371,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.attendees.length).toEqual(1);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -402,6 +406,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.attendees.length).toEqual(1);
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -576,6 +581,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const attendee = createdSeatedBooking.attendees.find((a) => a.seatUid === body.seatUid);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: attendee?.name,
|
||||
email: attendee?.email,
|
||||
timeZone: attendee?.timeZone,
|
||||
language: attendee?.language,
|
||||
absent: false,
|
||||
@@ -835,6 +841,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.attendees.length).toEqual(1);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -869,6 +876,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.attendees.length).toEqual(1);
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -944,8 +952,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
});
|
||||
expect(firstBooking.attendees.length).toEqual(2);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
const firstAttendee = firstBooking.attendees.find(
|
||||
(attendee) => attendee.name === nameAttendeeOne
|
||||
);
|
||||
expect(firstAttendee).toEqual({
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -956,8 +968,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
},
|
||||
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
|
||||
});
|
||||
expect(firstBooking.attendees[1]).toEqual({
|
||||
const secondAttendee = firstBooking.attendees.find(
|
||||
(attendee) => attendee.name === nameAttendeeTwo
|
||||
);
|
||||
expect(secondAttendee).toEqual({
|
||||
name: nameAttendeeTwo,
|
||||
email: emailAttendeeTwo,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -992,6 +1008,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.attendees.length).toEqual(2);
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -1004,6 +1021,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
expect(secondBooking.attendees[1]).toEqual({
|
||||
name: nameAttendeeTwo,
|
||||
email: emailAttendeeTwo,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
|
||||
@@ -59,6 +59,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
let team1EventTypeId: number;
|
||||
let team2EventTypeId: number;
|
||||
let phoneOnlyEventTypeId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
@@ -205,6 +206,83 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
team1EventTypeId = team1EventType.id;
|
||||
|
||||
const phoneOnlyEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team1.id },
|
||||
},
|
||||
title: "Phone Only Event Type",
|
||||
slug: "phone-only-event-type",
|
||||
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: {
|
||||
@@ -307,6 +385,61 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: 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 possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a phone based booking", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||
eventTypeId: phoneOnlyEventTypeId,
|
||||
attendee: {
|
||||
name: "alice",
|
||||
phoneNumber: "+919876543210",
|
||||
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",
|
||||
phoneNumber: body.attendee.phoneNumber,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -359,12 +492,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.attendees.length).toEqual(2);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.attendees[1]).toEqual({
|
||||
name: teamUser2.name,
|
||||
email: teamUser2.email,
|
||||
timeZone: teamUser2.timeZone,
|
||||
language: teamUser2.locale,
|
||||
absent: false,
|
||||
@@ -395,7 +530,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].eventTypeId).toEqual(team1EventTypeId);
|
||||
});
|
||||
});
|
||||
@@ -433,7 +568,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data.length).toEqual(3);
|
||||
expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined();
|
||||
expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -364,6 +364,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -434,6 +435,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -455,6 +457,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.recurringBookingUid).toBeDefined();
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -475,6 +478,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(thirdBooking.recurringBookingUid).toBeDefined();
|
||||
expect(thirdBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Request } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
handleNewBooking,
|
||||
handleNewRecurringBooking,
|
||||
getAllUserBookings,
|
||||
handleInstantMeeting,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
handleMarkNoShow,
|
||||
confirmBookingHandler,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateBookingInput,
|
||||
@@ -340,7 +340,10 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
const bookingRequest = await this.inputService.createCancelBookingRequest(request, bookingUid, body);
|
||||
await handleCancelBooking(bookingRequest);
|
||||
const res = await handleCancelBooking(bookingRequest);
|
||||
if (!res.onlyRemovedAttendee) {
|
||||
await this.billingService.cancelUsageByBookingUid(res.bookingUid);
|
||||
}
|
||||
|
||||
if ("cancelSubsequentBookings" in body && body.cancelSubsequentBookings) {
|
||||
return this.getAllRecurringBookingsByIndividualUid(bookingUid);
|
||||
@@ -363,12 +366,17 @@ export class BookingsService_2024_08_13 {
|
||||
|
||||
async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) {
|
||||
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
|
||||
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
|
||||
const platformClientParams = bookingBefore?.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(bookingBefore.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
await handleMarkNoShow({
|
||||
bookingUid,
|
||||
attendees: bodyTransformed.attendees,
|
||||
noShowHost: bodyTransformed.noShowHost,
|
||||
userId: bookingOwnerId,
|
||||
platformClientParams,
|
||||
});
|
||||
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
|
||||
@@ -423,7 +431,11 @@ export class BookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
|
||||
const profile = this.usersService.getUserMainProfile(requestUser);
|
||||
|
||||
@@ -431,6 +443,7 @@ export class BookingsService_2024_08_13 {
|
||||
bookingId: booking.id,
|
||||
orgId: profile?.organizationId || null,
|
||||
emailsEnabled,
|
||||
platformClientParams,
|
||||
});
|
||||
|
||||
const reassigned = await this.bookingsRepository.getByUidWithUser(bookingUid);
|
||||
@@ -441,12 +454,6 @@ export class BookingsService_2024_08_13 {
|
||||
return this.outputService.getOutputReassignedBooking(reassigned);
|
||||
}
|
||||
|
||||
async getEmailsEnabled(eventTypeId: number) {
|
||||
const oAuthParams = await this.inputService.getOAuthClientParams(eventTypeId);
|
||||
const emailsEnabled = oAuthParams ? oAuthParams.arePlatformEmailsEnabled : true;
|
||||
return emailsEnabled;
|
||||
}
|
||||
|
||||
async reassignBookingToUser(
|
||||
bookingUid: string,
|
||||
newUserId: number,
|
||||
@@ -463,7 +470,11 @@ export class BookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`User with id=${newUserId} was not found in the database`);
|
||||
}
|
||||
|
||||
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
|
||||
const profile = this.usersService.getUserMainProfile(user);
|
||||
|
||||
@@ -474,6 +485,7 @@ export class BookingsService_2024_08_13 {
|
||||
reassignReason: body.reason,
|
||||
reassignedById,
|
||||
emailsEnabled,
|
||||
platformClientParams,
|
||||
});
|
||||
|
||||
return this.outputService.getOutputReassignedBooking(reassigned);
|
||||
@@ -485,7 +497,11 @@ export class BookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
|
||||
await confirmBookingHandler({
|
||||
ctx: {
|
||||
@@ -496,6 +512,7 @@ export class BookingsService_2024_08_13 {
|
||||
confirmed: true,
|
||||
recurringEventId: booking.recurringEventId,
|
||||
emailsEnabled,
|
||||
platformClientParams,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -508,7 +525,11 @@ export class BookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
|
||||
await confirmBookingHandler({
|
||||
ctx: {
|
||||
@@ -520,6 +541,7 @@ export class BookingsService_2024_08_13 {
|
||||
recurringEventId: booking.recurringEventId,
|
||||
reason,
|
||||
emailsEnabled,
|
||||
platformClientParams,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import {
|
||||
bookingResponsesSchema,
|
||||
seatedBookingResponsesSchema,
|
||||
seatedBookingDataSchema,
|
||||
} from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
@@ -160,9 +160,14 @@ export class InputBookingsService_2024_08_13 {
|
||||
? {
|
||||
...inputBooking.bookingFieldsResponses,
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email,
|
||||
email: inputBooking.attendee.email ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
}
|
||||
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email },
|
||||
: {
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -340,7 +345,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Seat with uid=${inputBooking.seatUid} does not exist.`);
|
||||
}
|
||||
|
||||
const { responses: bookingResponses } = seatedBookingResponsesSchema.parse(seat.data);
|
||||
const { responses: bookingResponses } = seatedBookingDataSchema.parse(seat.data);
|
||||
const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
|
||||
|
||||
if (!attendee) {
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import {
|
||||
defaultBookingMetadata,
|
||||
defaultBookingResponses,
|
||||
defaultSeatedBookingData,
|
||||
defaultSeatedBookingMetadata,
|
||||
} from "@/lib/safe-parse/default-responses-booking";
|
||||
import { safeParse } from "@/lib/safe-parse/safe-parse";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import { DateTime } from "luxon";
|
||||
@@ -30,9 +37,10 @@ export const bookingResponsesSchema = z
|
||||
guests: z.array(z.string()).optional(),
|
||||
rescheduleReason: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
.describe("BookingResponses");
|
||||
|
||||
export const seatedBookingResponsesSchema = z
|
||||
export const seatedBookingDataSchema = z
|
||||
.object({
|
||||
responses: z
|
||||
.object({
|
||||
@@ -47,7 +55,10 @@ export const seatedBookingResponsesSchema = z
|
||||
})
|
||||
.passthrough(),
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
.describe("SeatedBookingData");
|
||||
|
||||
const seatedBookingMetadataSchema = z.object({}).catchall(z.string()).describe("SeatedBookingMetadata");
|
||||
|
||||
type DatabaseUser = { id: number; name: string | null; email: string; username: string | null };
|
||||
|
||||
@@ -61,6 +72,7 @@ type DatabaseBooking = Booking & {
|
||||
email: string;
|
||||
timeZone: string;
|
||||
locale: string | null;
|
||||
phoneNumber?: string | null;
|
||||
noShow: boolean | null;
|
||||
bookingSeat?: BookingSeat | null;
|
||||
}[];
|
||||
@@ -72,8 +84,6 @@ type BookingWithUser = Booking & { user: DatabaseUser | null };
|
||||
|
||||
type DatabaseMetadata = z.infer<typeof bookingMetadataSchema>;
|
||||
|
||||
const seatedBookingMetadataSchema = z.object({}).catchall(z.string());
|
||||
|
||||
@Injectable()
|
||||
export class OutputBookingsService_2024_08_13 {
|
||||
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
|
||||
@@ -82,8 +92,12 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses);
|
||||
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
|
||||
const bookingResponses = safeParse(
|
||||
bookingResponsesSchema,
|
||||
databaseBooking.responses,
|
||||
defaultBookingResponses
|
||||
);
|
||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
|
||||
const booking = {
|
||||
@@ -108,6 +122,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: !!attendee.noShow,
|
||||
phoneNumber: attendee.phoneNumber ?? undefined,
|
||||
})),
|
||||
guests: bookingResponses.guests,
|
||||
location,
|
||||
@@ -167,8 +182,12 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses);
|
||||
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
|
||||
const bookingResponses = safeParse(
|
||||
bookingResponsesSchema,
|
||||
databaseBooking.responses,
|
||||
defaultBookingResponses
|
||||
);
|
||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
|
||||
const booking = {
|
||||
@@ -225,7 +244,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
|
||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
|
||||
const booking = {
|
||||
@@ -254,7 +273,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
||||
parsed.attendees = databaseBooking.attendees.map((attendee) => {
|
||||
const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data);
|
||||
const { responses } = safeParse(
|
||||
seatedBookingDataSchema,
|
||||
attendee.bookingSeat?.data,
|
||||
defaultSeatedBookingData
|
||||
);
|
||||
|
||||
const attendeeData = {
|
||||
name: attendee.name,
|
||||
@@ -267,7 +290,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
attendeeParsed.metadata = seatedBookingMetadataSchema.parse(attendee.bookingSeat?.metadata);
|
||||
attendeeParsed.metadata = safeParse(
|
||||
seatedBookingMetadataSchema,
|
||||
attendee.bookingSeat?.metadata,
|
||||
defaultSeatedBookingMetadata
|
||||
);
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
|
||||
@@ -320,7 +347,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
|
||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
|
||||
const booking = {
|
||||
@@ -353,7 +380,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
||||
parsed.attendees = databaseBooking.attendees.map((attendee) => {
|
||||
const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data);
|
||||
const { responses } = safeParse(
|
||||
seatedBookingDataSchema,
|
||||
attendee.bookingSeat?.data,
|
||||
defaultSeatedBookingData
|
||||
);
|
||||
|
||||
const attendeeData = {
|
||||
name: attendee.name,
|
||||
@@ -366,7 +397,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
attendeeParsed.metadata = seatedBookingMetadataSchema.parse(attendee.bookingSeat?.metadata);
|
||||
attendeeParsed.metadata = safeParse(
|
||||
seatedBookingMetadataSchema,
|
||||
attendee.bookingSeat?.metadata,
|
||||
defaultSeatedBookingMetadata
|
||||
);
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
return attendeeParsed;
|
||||
|
||||
@@ -113,7 +113,7 @@ describe("Platform Calendars Endpoints", () => {
|
||||
});
|
||||
|
||||
// TODO: Uncomment this once CI is ready to run proper Office365 tests
|
||||
// it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/connect: it should redirect to auth-url for office 365 calendar oauth with valid access token `, async () => {
|
||||
// it(`/GET/v2/calendars/${OFFICE_365_CALENDAR}/connect: it should redirect to auth-url for office 365 calendar OAuth with valid access token `, async () => {
|
||||
// const response = await request(app.getHttpServer())
|
||||
// .get(`/v2/calendars/${OFFICE_365_CALENDAR}/connect`)
|
||||
// .set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
@@ -123,7 +123,7 @@ describe("Platform Calendars Endpoints", () => {
|
||||
// expect(data.authUrl).toBeDefined();
|
||||
// });
|
||||
|
||||
it(`/GET/v2/calendars/${GOOGLE_CALENDAR}/connect: it should redirect to auth-url for google calendar oauth with valid access token `, async () => {
|
||||
it(`/GET/v2/calendars/${GOOGLE_CALENDAR}/connect: it should redirect to auth-url for google calendar OAuth with valid access token `, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/v2/calendars/${GOOGLE_CALENDAR}/connect`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
|
||||
@@ -86,18 +86,16 @@ export class CalendarsController {
|
||||
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@Get("/busy-times")
|
||||
@ApiOperation({ summary: "Get busy times" })
|
||||
@ApiOperation({
|
||||
summary: "Get busy times",
|
||||
description:
|
||||
"Get busy times from a calendar. Example request URL is `https://api.cal.com/v2/calendars/busy-times?loggedInUsersTz=Europe%2FMadrid&dateFrom=2024-12-18&dateTo=2024-12-18&calendarsToLoad[0][credentialId]=135&calendarsToLoad[0][externalId]=skrauciz%40gmail.com`",
|
||||
})
|
||||
async getBusyTimes(
|
||||
@Query() queryParams: CalendarBusyTimesInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<GetBusyTimesOutput> {
|
||||
const { loggedInUsersTz, dateFrom, dateTo, calendarsToLoad } = queryParams;
|
||||
if (!dateFrom || !dateTo) {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
const busyTimes = await this.calendarsService.getBusyTimes(
|
||||
calendarsToLoad,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import {
|
||||
ArrayNotEmpty,
|
||||
IsBoolean,
|
||||
@@ -50,11 +50,10 @@ export class CreateIcsFeedInputDto {
|
||||
urls!: string[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({
|
||||
@ApiPropertyOptional({
|
||||
example: false,
|
||||
description: "Whether to allowing writing to the calendar or not",
|
||||
type: "boolean",
|
||||
required: false,
|
||||
default: true,
|
||||
})
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsDate, IsEnum, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
@@ -6,13 +6,16 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class BusyTimesOutput {
|
||||
@IsDate()
|
||||
@ApiProperty({ type: Date })
|
||||
start!: Date;
|
||||
|
||||
@IsDate()
|
||||
@ApiProperty({ type: Date })
|
||||
end!: Date;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
source?: string | null;
|
||||
}
|
||||
|
||||
@@ -24,5 +27,6 @@ export class GetBusyTimesOutput {
|
||||
@ValidateNested()
|
||||
@Type(() => BusyTimesOutput)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [BusyTimesOutput] })
|
||||
data!: BusyTimesOutput[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
@@ -18,195 +18,246 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
class Integration {
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@ApiPropertyOptional({ type: Object, nullable: true })
|
||||
appData?: object | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
dirName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
__template?: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
description!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
installed?: boolean;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
variant!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
category?: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiProperty({ type: [String] })
|
||||
categories!: string[];
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
logo!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
publisher!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
slug!: string;
|
||||
|
||||
@IsUrl()
|
||||
@ApiProperty()
|
||||
url!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object, nullable: true })
|
||||
locationOption!: object | null;
|
||||
}
|
||||
|
||||
class Primary {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
externalId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
integration?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
@ApiPropertyOptional()
|
||||
name?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: Boolean, nullable: true })
|
||||
primary!: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
readOnly!: boolean;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
email?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isSelected!: boolean;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
credentialId!: number;
|
||||
}
|
||||
|
||||
export class Calendar {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
externalId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
integration?: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ type: Boolean, nullable: true })
|
||||
primary?: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
readOnly!: boolean;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
email?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isSelected!: boolean;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
credentialId!: number;
|
||||
}
|
||||
|
||||
export class ConnectedCalendar {
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Integration })
|
||||
integration!: Integration;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
credentialId!: number;
|
||||
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Primary })
|
||||
primary?: Primary;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Calendar] })
|
||||
calendars?: Calendar[];
|
||||
}
|
||||
|
||||
class DestinationCalendar {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
integration!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
externalId!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
primaryEmail!: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ nullable: true })
|
||||
userId!: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
eventTypeId!: number | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
credentialId!: number | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
name?: string | null;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
primary?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
readOnly?: boolean;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
email?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
integrationTitle?: string;
|
||||
}
|
||||
|
||||
export class ConnectedCalendarsData {
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [ConnectedCalendar] })
|
||||
connectedCalendars!: ConnectedCalendar[];
|
||||
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@ApiProperty({ type: DestinationCalendar })
|
||||
destinationCalendar!: DestinationCalendar;
|
||||
}
|
||||
|
||||
export class ConnectedCalendarsOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
@@ -214,5 +265,6 @@ export class ConnectedCalendarsOutput {
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => ConnectedCalendarsData)
|
||||
@ApiProperty({ type: ConnectedCalendarsData })
|
||||
data!: ConnectedCalendarsData;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@ import { DateTime } from "luxon";
|
||||
import { z } from "zod";
|
||||
|
||||
import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
|
||||
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries";
|
||||
import {
|
||||
getConnectedDestinationCalendarsAndEnsureDefaultsInDb,
|
||||
getBusyCalendarTimes,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { Calendar } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@@ -41,12 +44,18 @@ export class CalendarsService {
|
||||
if (!userWithCalendars) {
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
return getConnectedDestinationCalendars(
|
||||
userWithCalendars,
|
||||
false,
|
||||
this.dbWrite.prisma as unknown as PrismaClient
|
||||
);
|
||||
return getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
|
||||
user: {
|
||||
...userWithCalendars,
|
||||
allSelectedCalendars: userWithCalendars.selectedCalendars,
|
||||
userLevelSelectedCalendars: userWithCalendars.selectedCalendars.filter(
|
||||
(calendar) => !calendar.eventTypeId
|
||||
),
|
||||
},
|
||||
onboarding: false,
|
||||
eventTypeId: null,
|
||||
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
||||
});
|
||||
}
|
||||
|
||||
async getBusyTimes(
|
||||
@@ -64,7 +73,6 @@ export class CalendarsService {
|
||||
);
|
||||
try {
|
||||
const calendarBusyTimes = await getBusyCalendarTimes(
|
||||
"",
|
||||
credentials,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
|
||||
@@ -4,13 +4,14 @@ import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { calendar_v3 } from "@googleapis/calendar";
|
||||
import { Logger, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { google } from "googleapis";
|
||||
import { OAuth2Client } from "googleapis-common";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SUCCESS_STATUS, GOOGLE_CALENDAR_TYPE } from "@calcom/platform-constants";
|
||||
@@ -78,7 +79,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
|
||||
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
||||
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirectUri);
|
||||
return oAuth2Client;
|
||||
}
|
||||
|
||||
@@ -90,7 +91,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
}
|
||||
|
||||
if (gcalCredentials.invalid) {
|
||||
throw new BadRequestException("Invalid google oauth credentials.");
|
||||
throw new BadRequestException("Invalid google OAuth credentials.");
|
||||
}
|
||||
|
||||
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
|
||||
@@ -115,7 +116,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
) {
|
||||
// User chose not to authorize your app or didn't authorize your app
|
||||
// redirect directly without oauth code
|
||||
if (!code) {
|
||||
if (!code || code === "undefined") {
|
||||
return { url: redir || origin };
|
||||
}
|
||||
|
||||
@@ -134,8 +135,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
|
||||
oAuth2Client.setCredentials(key);
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
const calendar = new calendar_v3.Calendar({
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ export class OutlookService implements OAuthCalendarApp {
|
||||
redir?: string
|
||||
) {
|
||||
// if code is not defined, user denied to authorize office 365 app, just redirect straight away
|
||||
if (!code) {
|
||||
if (!code || code === "undefined") {
|
||||
return { url: redir || origin };
|
||||
}
|
||||
|
||||
|
||||
@@ -78,4 +78,11 @@ export class EventTypesRepository_2024_04_15 {
|
||||
async deleteEventType(eventTypeId: number) {
|
||||
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
|
||||
}
|
||||
|
||||
async getEventTypeWithDuration(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
select: { length: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+18
-7
@@ -1,5 +1,5 @@
|
||||
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty, ApiPropertyOptional, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -36,7 +36,10 @@ export class CreateEventTypeInput_2024_04_15 {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: CREATE_EVENT_DESCRIPTION_EXAMPLE,
|
||||
})
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@@ -48,30 +51,38 @@ export class CreateEventTypeInput_2024_04_15 {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation_2024_04_15)
|
||||
@IsArray()
|
||||
@ApiPropertyOptional({
|
||||
type: [EventTypeLocation_2024_04_15],
|
||||
})
|
||||
locations?: EventTypeLocation_2024_04_15[];
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
disableGuests?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
slotInterval?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
minimumBookingNotice?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
beforeEventBuffer?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
afterEventBuffer?: number;
|
||||
|
||||
// @ApiHideProperty()
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiHideProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validator";
|
||||
|
||||
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||
@@ -6,7 +6,7 @@ import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validato
|
||||
|
||||
export class EventTypeLocation_2024_04_15 {
|
||||
@IsString()
|
||||
@DocsProperty({ example: "link" })
|
||||
@ApiProperty({ example: "link" })
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@@ -16,7 +16,7 @@ export class EventTypeLocation_2024_04_15 {
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@DocsProperty({ example: "https://masterchief.com/argentina/flan/video/9129412" })
|
||||
@ApiPropertyOptional({ example: "https://masterchief.com/argentina/flan/video/9129412" })
|
||||
link?: string;
|
||||
|
||||
@IsOptional()
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsBoolean, IsOptional, IsString } from "class-validator";
|
||||
|
||||
@@ -6,11 +6,11 @@ export class GetPublicEventTypeQueryParams_2024_04_15 {
|
||||
@Transform(({ value }: { value: string }) => value === "true")
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiProperty({ required: false })
|
||||
@ApiPropertyOptional()
|
||||
isTeamEvent?: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
org?: string | null;
|
||||
}
|
||||
|
||||
+66
@@ -2,6 +2,7 @@ import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/e
|
||||
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 { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -21,206 +22,259 @@ import {
|
||||
|
||||
class Option {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
value!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
}
|
||||
|
||||
class Source {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
editUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
fieldRequired?: boolean;
|
||||
}
|
||||
|
||||
class View {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
class OptionsInput {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Type of the field, can be one of "address", "text", or "phone".',
|
||||
enum: ["address", "text", "phone"],
|
||||
example: "text",
|
||||
})
|
||||
type!: "address" | "text" | "phone";
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
class VariantField {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: BaseField;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
label?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
labelAsSafeHtml?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
class Variant {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => VariantField)
|
||||
@ApiProperty({ type: [VariantField] })
|
||||
fields!: VariantField[];
|
||||
}
|
||||
|
||||
class VariantsConfig {
|
||||
@ApiProperty({ type: Object })
|
||||
variants!: Record<string, Variant>;
|
||||
}
|
||||
|
||||
export class BookingField_2024_04_15 {
|
||||
@IsEnum(BaseField)
|
||||
@ApiProperty({ enum: BaseField })
|
||||
type!: BaseField;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Option)
|
||||
@ApiPropertyOptional({ type: [Option] })
|
||||
options?: Option[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
label?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
labelAsSafeHtml?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
defaultLabel?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
getOptionsAt?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => OptionsInput)
|
||||
@ApiPropertyOptional()
|
||||
optionsInputs?: Record<string, OptionsInput>;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
variant?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => VariantsConfig)
|
||||
@ApiPropertyOptional({ type: VariantsConfig })
|
||||
variantsConfig?: VariantsConfig;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => View)
|
||||
@ApiPropertyOptional({ type: [View] })
|
||||
views?: View[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
hideWhenJustOneOption?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
hidden?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(Editable)
|
||||
@ApiPropertyOptional()
|
||||
editable?: Editable;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Source)
|
||||
@ApiPropertyOptional({ type: [Source] })
|
||||
sources?: Source[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
disableOnPrefill?: boolean;
|
||||
}
|
||||
|
||||
export class RecurringEvent_2024_04_15 {
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Date })
|
||||
dtstart?: Date;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
interval!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
count!: number;
|
||||
|
||||
@IsEnum(Frequency)
|
||||
@ApiProperty({ enum: Frequency })
|
||||
freq!: Frequency;
|
||||
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Date })
|
||||
until?: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
tzid?: string;
|
||||
}
|
||||
|
||||
export class IntervalLimits_2024_04_15 {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
PER_DAY?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
PER_WEEK?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
PER_MONTH?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
PER_YEAR?: number;
|
||||
}
|
||||
|
||||
@@ -228,27 +282,33 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
length?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
slug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
description?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
hidden?: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation_2024_04_15)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [EventTypeLocation_2024_04_15] })
|
||||
locations?: EventTypeLocation_2024_04_15[];
|
||||
|
||||
// @IsInt()
|
||||
@@ -283,6 +343,7 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BookingField_2024_04_15)
|
||||
@ApiPropertyOptional({ type: [BookingField_2024_04_15] })
|
||||
bookingFields?: BookingField_2024_04_15[];
|
||||
|
||||
// @IsString()
|
||||
@@ -328,6 +389,7 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
disableGuests?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
@@ -337,16 +399,19 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
minimumBookingNotice?: number;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
beforeEventBuffer?: number;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
afterEventBuffer?: number;
|
||||
|
||||
// @IsInt()
|
||||
@@ -384,6 +449,7 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
slotInterval?: number;
|
||||
|
||||
// @IsString()
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
IntervalLimits_2024_04_15,
|
||||
RecurringEvent_2024_04_15,
|
||||
} from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { ApiHideProperty, ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
@@ -29,23 +29,23 @@ import {
|
||||
|
||||
export class EventTypeOutput {
|
||||
@IsInt()
|
||||
@DocsProperty({ example: 1 })
|
||||
@ApiProperty({ example: 1 })
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
@ApiProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
@ApiProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
@ApiProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
|
||||
@ApiProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE, nullable: true })
|
||||
description!: string | null;
|
||||
|
||||
@IsBoolean()
|
||||
@@ -55,6 +55,7 @@ export class EventTypeOutput {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation_2024_04_15)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [EventTypeLocation_2024_04_15], nullable: true })
|
||||
locations!: EventTypeLocation_2024_04_15[] | null;
|
||||
|
||||
@IsInt()
|
||||
|
||||
+94
-3
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
@@ -17,330 +17,421 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
}
|
||||
|
||||
class Source {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
}
|
||||
|
||||
class OptionInput {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
class BookingField {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
defaultLabel?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
label?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
getOptionsAt?: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
optionsInputs?: { [key: string]: OptionInput };
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
hideWhenJustOneOption?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
editable?: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Source)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Source] })
|
||||
sources?: Source[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
disableOnPrefill?: boolean;
|
||||
}
|
||||
|
||||
class Organization {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
slug?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
metadata!: Record<string, any>;
|
||||
@ApiPropertyOptional({ type: Object })
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
class Profile {
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
id!: number | null;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
userId?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
uid?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
name?: string;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
organizationId!: number | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Organization)
|
||||
@ApiPropertyOptional({ type: Organization, nullable: true })
|
||||
organization?: Organization | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
upId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
image?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
brandColor?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
darkBrandColor?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
bookerLayouts?: any;
|
||||
}
|
||||
|
||||
class Owner {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
avatarUrl?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
weekStart!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
brandColor?: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
darkBrandColor?: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
theme?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
metadata!: any;
|
||||
@ApiPropertyOptional()
|
||||
metadata?: any;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
defaultScheduleId?: number | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
nonProfileUsername!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Profile)
|
||||
@ApiProperty({ type: Profile })
|
||||
profile!: Profile;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
weekStart!: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
organizationId?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
avatarUrl?: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@ApiProperty({ type: Profile })
|
||||
profile!: Profile;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
bookerUrl!: string;
|
||||
}
|
||||
|
||||
class Schedule {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
timeZone!: string | null;
|
||||
}
|
||||
|
||||
class PublicEventTypeOutput {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
description!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
eventName?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
slug!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isInstantEvent!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
aiPhoneCallConfig?: any;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
schedulingType?: any;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
length!: number;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Location)
|
||||
@ApiProperty({ type: [Location] })
|
||||
locations!: Location[];
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [Object] })
|
||||
customInputs!: any[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
disableGuests!: boolean;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object, nullable: true })
|
||||
metadata!: object | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
lockTimeZoneToggleOnBookingPage!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
requiresConfirmation!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
requiresBookerEmailVerification!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
recurringEvent?: any;
|
||||
|
||||
@IsNumber()
|
||||
@ApiProperty()
|
||||
price!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
currency!: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
seatsPerTimeSlot?: number | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: Boolean, nullable: true })
|
||||
seatsShowAvailabilityCount!: boolean | null;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BookingField)
|
||||
@ApiProperty({ type: [BookingField] })
|
||||
bookingFields!: BookingField[];
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
team?: any;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
successRedirectUrl?: string | null;
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
workflows!: any[];
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
hosts!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Owner)
|
||||
@ApiProperty({ type: Owner, nullable: true })
|
||||
owner!: Owner | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Schedule)
|
||||
@ApiProperty({ type: Schedule, nullable: true })
|
||||
schedule!: Schedule | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
hidden!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
assignAllTeamMembers!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
bookerLayouts?: any;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => User)
|
||||
@ApiProperty({ type: [User] })
|
||||
users!: User[];
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object })
|
||||
entity!: object;
|
||||
|
||||
@IsBoolean()
|
||||
|
||||
+392
-64
@@ -10,10 +10,12 @@ import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User, Schedule } from "@prisma/client";
|
||||
import { PlatformOAuthClient, Team, User, Schedule, EventType } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
@@ -31,11 +33,18 @@ import {
|
||||
ApiSuccessResponse,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
NameFieldInput_2024_06_14,
|
||||
NameDefaultFieldInput_2024_06_14,
|
||||
NotesDefaultFieldInput_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
|
||||
if (a.slug < b.slug) return -1;
|
||||
if (a.slug > b.slug) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
describe("Event types Endpoints", () => {
|
||||
describe("Not authenticated", () => {
|
||||
let app: INestApplication;
|
||||
@@ -75,17 +84,83 @@ describe("Event types Endpoints", () => {
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let schedulesRepostoryFixture: SchedulesRepositoryFixture;
|
||||
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
const userEmail = "event-types-test-e2e@api.com";
|
||||
const falseTestEmail = "false-event-types@api.com";
|
||||
const name = "bob-the-builder";
|
||||
const username = name;
|
||||
let eventType: EventTypeOutput_2024_06_14;
|
||||
let user: User;
|
||||
let orgUser: User;
|
||||
let falseTestUser: User;
|
||||
let firstSchedule: Schedule;
|
||||
let secondSchedule: Schedule;
|
||||
let falseTestSchedule: Schedule;
|
||||
let orgUserEventType1: EventType;
|
||||
let orgUserEventType2: EventType;
|
||||
let orgUserEventType3: EventType;
|
||||
|
||||
const defaultResponseBookingFieldName = {
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldEmail = {
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldLocation = {
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
hidden: false,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldTitle = {
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
hidden: true,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldNotes = {
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
hidden: false,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldGuests = {
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
hidden: false,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldRescheduleReason = {
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
hidden: false,
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
@@ -109,8 +184,12 @@ describe("Event types Endpoints", () => {
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
schedulesRepostoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: "organization",
|
||||
slug: "event-type-2024-06-14-org-slug",
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
@@ -118,6 +197,49 @@ describe("Event types Endpoints", () => {
|
||||
username,
|
||||
});
|
||||
|
||||
orgUser = await userRepositoryFixture.create({
|
||||
email: "event-types-2024-06-14-org-user@example.com",
|
||||
name: "event-types-2024-06-14-org-user",
|
||||
username: "event-types-2024-06-14-org-user",
|
||||
});
|
||||
|
||||
profileRepositoryFixture.create({
|
||||
uid: `usr-${orgUser.id}`,
|
||||
username: orgUser.username as string,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: orgUser.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
orgUserEventType1 = await eventTypesRepositoryFixture.create(
|
||||
{ title: "orgUserEventType1", slug: "org-event-type-1", length: 60, locations: [] },
|
||||
orgUser.id
|
||||
);
|
||||
|
||||
orgUserEventType2 = await eventTypesRepositoryFixture.create(
|
||||
{ title: "orgUserEventType2", slug: "org-event-type-2", length: 60, locations: [] },
|
||||
orgUser.id
|
||||
);
|
||||
|
||||
orgUserEventType3 = await eventTypesRepositoryFixture.create(
|
||||
{ title: "orgUserEventType3", slug: "org-event-type-3", length: 60, locations: [] },
|
||||
orgUser.id
|
||||
);
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: orgUser.id } },
|
||||
team: { connect: { id: organization.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
falseTestUser = await userRepositoryFixture.create({
|
||||
email: falseTestEmail,
|
||||
name: "false-test",
|
||||
@@ -200,7 +322,7 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
|
||||
it("should create an event type", async () => {
|
||||
const nameBookingField: NameFieldInput_2024_06_14 = {
|
||||
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
|
||||
type: "name",
|
||||
label: "Your name sir / madam",
|
||||
placeholder: "john doe",
|
||||
@@ -238,6 +360,7 @@ describe("Event types Endpoints", () => {
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: true,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
scheduleId: firstSchedule.id,
|
||||
@@ -325,16 +448,25 @@ describe("Event types Endpoints", () => {
|
||||
);
|
||||
expect(createdEventType.color).toEqual(body.color);
|
||||
|
||||
const requestBookingFields = body.bookingFields || [];
|
||||
const expectedBookingFields = [
|
||||
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||
// note(Lauris): location booking field is added if multiple locations are passed
|
||||
{ isDefault: true, required: false, slug: "location", type: "radioInput" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
...requestBookingFields
|
||||
.filter((field) => field.type !== "name" && field.type !== "email")
|
||||
.map((field) => ({ isDefault: false, ...field })),
|
||||
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
||||
{ ...defaultResponseBookingFieldEmail },
|
||||
{ ...defaultResponseBookingFieldLocation },
|
||||
{ ...defaultResponseBookingFieldTitle },
|
||||
{ ...defaultResponseBookingFieldGuests },
|
||||
{ ...defaultResponseBookingFieldRescheduleReason },
|
||||
{
|
||||
type: "select",
|
||||
label: "select which language you want to learn",
|
||||
slug: "select-language",
|
||||
required: true,
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: true,
|
||||
hidden: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{ ...defaultResponseBookingFieldNotes },
|
||||
];
|
||||
|
||||
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
|
||||
@@ -342,7 +474,7 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET/even-types by username`, async () => {
|
||||
it(`/GET/event-types by username`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${username}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
@@ -364,7 +496,9 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
|
||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
|
||||
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
|
||||
);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||
@@ -386,6 +520,60 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType.color).toEqual(eventType.color);
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and orgSlug`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(3);
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined();
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType2.id)?.id).toBeDefined();
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType3.id)?.id).toBeDefined();
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and orgSlug and eventSlug`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}&eventSlug=${orgUserEventType1.slug}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined();
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and orgId`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${orgUser.username}&orgId=${organization.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(3);
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined();
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType2.id)?.id).toBeDefined();
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType3.id)?.id).toBeDefined();
|
||||
});
|
||||
|
||||
it("should return an error when creating an event type with seats enabled and multiple locations", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class 2",
|
||||
@@ -513,7 +701,6 @@ describe("Event types Endpoints", () => {
|
||||
.expect(201);
|
||||
|
||||
const createdEventType = createResponse.body.data;
|
||||
console.log("createdEventType: ", createdEventType);
|
||||
|
||||
expect(createdEventType).toMatchObject({
|
||||
id: expect.any(Number),
|
||||
@@ -656,13 +843,20 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
|
||||
it("should update event type", async () => {
|
||||
const nameBookingField: NameFieldInput_2024_06_14 = {
|
||||
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
|
||||
type: "name",
|
||||
label: "Your name sir / madam",
|
||||
placeholder: "john doe",
|
||||
disableOnPrefill: true,
|
||||
};
|
||||
|
||||
const notesBookingField: NotesDefaultFieldInput_2024_06_14 = {
|
||||
slug: "notes",
|
||||
label: "lemme take some notes",
|
||||
placeholder: "write your notes here",
|
||||
required: true,
|
||||
};
|
||||
|
||||
const newTitle = "Coding class in Italian!";
|
||||
|
||||
const body: UpdateEventTypeInput_2024_06_14 = {
|
||||
@@ -679,7 +873,9 @@ describe("Event types Endpoints", () => {
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
notesBookingField,
|
||||
],
|
||||
bookingLimitsCount: {
|
||||
day: 4,
|
||||
@@ -742,14 +938,25 @@ describe("Event types Endpoints", () => {
|
||||
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(updatedEventType.locations).toEqual(eventType.locations);
|
||||
|
||||
const requestBookingFields = body.bookingFields || [];
|
||||
const expectedBookingFields = [
|
||||
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
...requestBookingFields
|
||||
.filter((field) => field.type !== "name" && field.type !== "email")
|
||||
.map((field) => ({ isDefault: false, ...field })),
|
||||
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
||||
{ ...defaultResponseBookingFieldEmail },
|
||||
{ ...defaultResponseBookingFieldLocation },
|
||||
{ ...defaultResponseBookingFieldTitle },
|
||||
{ ...defaultResponseBookingFieldGuests },
|
||||
{ ...defaultResponseBookingFieldRescheduleReason },
|
||||
{
|
||||
type: "select",
|
||||
label: "select which language you want to learn",
|
||||
slug: "select-language",
|
||||
required: true,
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{ ...defaultResponseBookingFieldNotes, ...notesBookingField },
|
||||
];
|
||||
|
||||
expect(updatedEventType.bookingFields).toEqual(expectedBookingFields);
|
||||
@@ -866,7 +1073,9 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
|
||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
|
||||
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
|
||||
);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||
@@ -919,6 +1128,12 @@ describe("Event types Endpoints", () => {
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
|
||||
try {
|
||||
await userRepositoryFixture.delete(orgUser.id);
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -941,14 +1156,44 @@ describe("Event types Endpoints", () => {
|
||||
let legacyEventTypeId2: number;
|
||||
|
||||
const expectedReturnSystemFields = [
|
||||
{ isDefault: true, required: true, slug: "name", type: "name" },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||
{ isDefault: true, type: "radioInput", slug: "location", required: false },
|
||||
{ isDefault: true, required: true, slug: "title", type: "text" },
|
||||
{ isDefault: true, required: false, slug: "notes", type: "textarea" },
|
||||
{ isDefault: true, required: false, slug: "guests", type: "multiemail" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false },
|
||||
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
label: "",
|
||||
placeholder: "",
|
||||
},
|
||||
{ isDefault: true, required: true, slug: "title", type: "text", disableOnPrefill: false, hidden: true },
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "guests",
|
||||
type: "multiemail",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "rescheduleReason",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false, hidden: true },
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "notes",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -1072,15 +1317,6 @@ describe("Event types Endpoints", () => {
|
||||
defaultLabel: "what_is_this_meeting_about",
|
||||
defaultPlaceholder: "",
|
||||
},
|
||||
{
|
||||
name: "notes",
|
||||
type: "textarea",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_notes",
|
||||
defaultPlaceholder: "share_additional_notes",
|
||||
},
|
||||
{
|
||||
name: "guests",
|
||||
type: "multiemail",
|
||||
@@ -1116,6 +1352,15 @@ describe("Event types Endpoints", () => {
|
||||
required: false,
|
||||
defaultLabel: "phone_number",
|
||||
},
|
||||
{
|
||||
name: "notes",
|
||||
type: "textarea",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_notes",
|
||||
defaultPlaceholder: "share_additional_notes",
|
||||
},
|
||||
],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
|
||||
@@ -1202,15 +1447,6 @@ describe("Event types Endpoints", () => {
|
||||
defaultLabel: "what_is_this_meeting_about",
|
||||
defaultPlaceholder: "",
|
||||
},
|
||||
{
|
||||
name: "notes",
|
||||
type: "textarea",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_notes",
|
||||
defaultPlaceholder: "share_additional_notes",
|
||||
},
|
||||
{
|
||||
name: "guests",
|
||||
type: "multiemail",
|
||||
@@ -1246,6 +1482,15 @@ describe("Event types Endpoints", () => {
|
||||
required: false,
|
||||
defaultLabel: "phone_number",
|
||||
},
|
||||
{
|
||||
name: "notes",
|
||||
type: "textarea",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_notes",
|
||||
defaultPlaceholder: "share_additional_notes",
|
||||
},
|
||||
],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
|
||||
@@ -1259,17 +1504,21 @@ describe("Event types Endpoints", () => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const fetchedEventType = responseBody.data;
|
||||
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
...expectedReturnSystemFields,
|
||||
{
|
||||
isDefault: false,
|
||||
type: userDefinedBookingField.type,
|
||||
slug: userDefinedBookingField.name,
|
||||
label: userDefinedBookingField.label,
|
||||
required: userDefinedBookingField.required,
|
||||
placeholder: userDefinedBookingField.placeholder,
|
||||
},
|
||||
]);
|
||||
expect(fetchedEventType.bookingFields.sort(orderBySlug)).toEqual(
|
||||
[
|
||||
{
|
||||
isDefault: false,
|
||||
type: userDefinedBookingField.type,
|
||||
slug: userDefinedBookingField.name,
|
||||
label: userDefinedBookingField.label,
|
||||
required: userDefinedBookingField.required,
|
||||
placeholder: userDefinedBookingField.placeholder,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
...expectedReturnSystemFields,
|
||||
].sort(orderBySlug)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1321,6 +1570,85 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return event type with default bookingFields if they are not defined", async () => {
|
||||
const eventTypeInput = {
|
||||
title: "undefined booking fields",
|
||||
description: "undefined booking fields",
|
||||
length: 40,
|
||||
hidden: false,
|
||||
slug: "undefined-booking-fields",
|
||||
locations: [],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
};
|
||||
const eventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${eventType.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const fetchedEventType = responseBody.data;
|
||||
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
{
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
+18
-1
@@ -34,6 +34,7 @@ import {
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
GetEventTypesQuery_2024_06_14,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
@@ -113,10 +114,26 @@ export class EventTypesController_2024_06_14 {
|
||||
@Query() queryParams: GetEventTypesQuery_2024_06_14
|
||||
): Promise<GetEventTypesOutput_2024_06_14> {
|
||||
const eventTypes = await this.eventTypesService.getEventTypes(queryParams);
|
||||
const eventTypesFormatted = this.eventTypeResponseTransformPipe.transform(eventTypes);
|
||||
const eventTypesWithoutHiddenFields = eventTypesFormatted.map((eventType) => {
|
||||
return {
|
||||
...eventType,
|
||||
bookingFields: Array.isArray(eventType?.bookingFields)
|
||||
? eventType?.bookingFields
|
||||
.map((field) => {
|
||||
if ("hidden" in field) {
|
||||
return field.hidden !== true ? field : null;
|
||||
}
|
||||
return field;
|
||||
})
|
||||
.filter((f) => f)
|
||||
: [],
|
||||
};
|
||||
}) as EventTypeOutput_2024_06_14[];
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: this.eventTypeResponseTransformPipe.transform(eventTypes),
|
||||
data: eventTypesWithoutHiddenFields,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsString, IsBoolean, IsOptional, IsEnum, Matches } from "class-validator";
|
||||
|
||||
@@ -38,34 +38,34 @@ export class CreatePhoneCallInput {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ description: "Scheduler name" })
|
||||
@ApiPropertyOptional({ description: "Scheduler name" })
|
||||
schedulerName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => (value ? value : undefined))
|
||||
@DocsProperty({ description: "Guest name" })
|
||||
@ApiPropertyOptional({ description: "Guest name" })
|
||||
guestName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => (value ? value : undefined))
|
||||
@DocsProperty({ description: "Guest email" })
|
||||
@ApiPropertyOptional({ description: "Guest email" })
|
||||
guestEmail?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => (value ? value : undefined))
|
||||
@DocsProperty({ description: "Guest company" })
|
||||
@ApiPropertyOptional({ description: "Guest company" })
|
||||
guestCompany?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ description: "Begin message" })
|
||||
@ApiPropertyOptional({ description: "Begin message" })
|
||||
beginMessage?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ description: "General prompt" })
|
||||
@ApiPropertyOptional({ description: "General prompt" })
|
||||
generalPrompt?: string;
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ export class EventTypeResponseTransformPipe implements PipeTransform {
|
||||
private transformEventType(eventType: EventTypeResponse): EventTypeOutput_2024_06_14 {
|
||||
return plainToClass(
|
||||
EventTypeOutput_2024_06_14,
|
||||
this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType),
|
||||
this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType, false),
|
||||
{ strategy: "exposeAll" }
|
||||
);
|
||||
}
|
||||
|
||||
+15
-11
@@ -74,8 +74,13 @@ export class EventTypesService_2024_06_14 {
|
||||
await this.checkUserOwnsSchedule(userId, body.scheduleId);
|
||||
}
|
||||
|
||||
async getEventTypeByUsernameAndSlug(username: string, eventTypeSlug: string) {
|
||||
const user = await this.usersRepository.findByUsername(username);
|
||||
async getEventTypeByUsernameAndSlug(
|
||||
username: string,
|
||||
eventTypeSlug: string,
|
||||
orgSlug?: string,
|
||||
orgId?: number
|
||||
) {
|
||||
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
@@ -92,8 +97,8 @@ export class EventTypesService_2024_06_14 {
|
||||
};
|
||||
}
|
||||
|
||||
async getEventTypesByUsername(username: string) {
|
||||
const user = await this.usersRepository.findByUsername(username);
|
||||
async getEventTypesByUsername(username: string, orgSlug?: string, orgId?: number) {
|
||||
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
@@ -152,27 +157,26 @@ export class EventTypesService_2024_06_14 {
|
||||
}
|
||||
|
||||
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14) {
|
||||
const { username, eventSlug, usernames } = queryParams;
|
||||
|
||||
const { username, eventSlug, usernames, orgSlug, orgId } = queryParams;
|
||||
if (username && eventSlug) {
|
||||
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug);
|
||||
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug, orgSlug, orgId);
|
||||
return eventType ? [eventType] : [];
|
||||
}
|
||||
|
||||
if (username) {
|
||||
return await this.getEventTypesByUsername(username);
|
||||
return await this.getEventTypesByUsername(username, orgSlug, orgId);
|
||||
}
|
||||
|
||||
if (usernames) {
|
||||
const dynamicEventType = await this.getDynamicEventType(usernames);
|
||||
const dynamicEventType = await this.getDynamicEventType(usernames, orgSlug, orgId);
|
||||
return [dynamicEventType];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async getDynamicEventType(usernames: string[]) {
|
||||
const users = await this.usersService.getByUsernames(usernames);
|
||||
async getDynamicEventType(usernames: string[], orgSlug?: string, orgId?: number) {
|
||||
const users = await this.usersService.getByUsernames(usernames, orgSlug, orgId);
|
||||
const usersFiltered: UserWithProfile[] = [];
|
||||
for (const user of users) {
|
||||
if (user) {
|
||||
|
||||
+36
-14
@@ -12,9 +12,12 @@ import {
|
||||
transformRecurrenceApiToInternal,
|
||||
systemBeforeFieldName,
|
||||
systemBeforeFieldEmail,
|
||||
systemBeforeFieldLocation,
|
||||
systemAfterFieldTitle,
|
||||
systemAfterFieldNotes,
|
||||
systemAfterFieldGuests,
|
||||
systemAfterFieldRescheduleReason,
|
||||
EventTypeMetaDataSchema,
|
||||
systemBeforeFieldLocation,
|
||||
transformBookerLayoutsApiToInternal,
|
||||
transformConfirmationPolicyApiToInternal,
|
||||
transformEventColorsApiToInternal,
|
||||
@@ -217,27 +220,46 @@ export class InputEventTypesService_2024_06_14 {
|
||||
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
|
||||
hasMultipleLocations: boolean
|
||||
) {
|
||||
const customFields: (SystemField | CustomField)[] = inputBookingFields
|
||||
const internalFields: (SystemField | CustomField)[] = inputBookingFields
|
||||
? transformBookingFieldsApiToInternal(inputBookingFields)
|
||||
: [];
|
||||
const customFieldsWithoutNameEmail = customFields.filter(
|
||||
(field) => field.type !== "name" && field.type !== "email"
|
||||
const systemCustomFields = internalFields.filter((field) => !this.isUserCustomField(field));
|
||||
const userCustomFields = internalFields.filter((field) => this.isUserCustomField(field));
|
||||
|
||||
const systemCustomNameField = systemCustomFields?.find((field) => field.type === "name");
|
||||
const systemCustomEmailField = systemCustomFields?.find((field) => field.type === "email");
|
||||
const systemCustomTitleField = systemCustomFields?.find((field) => field.name === "title");
|
||||
const systemCustomNotesField = systemCustomFields?.find((field) => field.name === "notes");
|
||||
const systemCustomGuestsField = systemCustomFields?.find((field) => field.name === "guests");
|
||||
const systemCustomRescheduleReasonField = systemCustomFields?.find(
|
||||
(field) => field.name === "rescheduleReason"
|
||||
);
|
||||
const customNameField = customFields?.find((field) => field.type === "name");
|
||||
const customEmailField = customFields?.find((field) => field.type === "email");
|
||||
|
||||
const defaultFieldsBefore: (SystemField | CustomField)[] = [
|
||||
customNameField || systemBeforeFieldName,
|
||||
customEmailField || systemBeforeFieldEmail,
|
||||
systemCustomNameField || systemBeforeFieldName,
|
||||
systemCustomEmailField || systemBeforeFieldEmail,
|
||||
systemBeforeFieldLocation,
|
||||
];
|
||||
// note(Lauris): if event type has multiple locations then a radio button booking field has to be displayed to allow booker to pick location
|
||||
if (hasMultipleLocations) {
|
||||
defaultFieldsBefore.push(systemBeforeFieldLocation);
|
||||
}
|
||||
|
||||
const defaultFieldsAfter = [systemAfterFieldRescheduleReason];
|
||||
const defaultFieldsAfter = [
|
||||
systemCustomTitleField || systemAfterFieldTitle,
|
||||
systemCustomNotesField || systemAfterFieldNotes,
|
||||
systemCustomGuestsField || systemAfterFieldGuests,
|
||||
systemCustomRescheduleReasonField || systemAfterFieldRescheduleReason,
|
||||
];
|
||||
|
||||
return [...defaultFieldsBefore, ...customFieldsWithoutNameEmail, ...defaultFieldsAfter];
|
||||
return [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
|
||||
}
|
||||
|
||||
isUserCustomField(field: SystemField | CustomField): field is CustomField {
|
||||
return (
|
||||
field.type !== "name" &&
|
||||
field.type !== "email" &&
|
||||
field.name !== "title" &&
|
||||
field.name !== "notes" &&
|
||||
field.name !== "guests" &&
|
||||
field.name !== "rescheduleReason"
|
||||
);
|
||||
}
|
||||
|
||||
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user