Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97b81d6f6b | ||
|
|
ac117a14eb | ||
|
|
f85f226982 | ||
|
|
f4b48e863b | ||
|
|
4dc22606d9 | ||
|
|
292073eb4a | ||
|
|
1878802213 | ||
|
|
9cfc74a74f | ||
|
|
c472bce54f | ||
|
|
a498641b61 | ||
|
|
ec58e59865 | ||
|
|
4f95cff70e | ||
|
|
fa66f2531f | ||
|
|
b40d448e07 | ||
|
|
21add18ad8 | ||
|
|
10fca86ddc | ||
|
|
c2fc8774df | ||
|
|
969b58f17d | ||
|
|
a0e25ff270 | ||
|
|
df30f5c703 | ||
|
|
fb59342de4 | ||
|
|
a84e8987cb | ||
|
|
c4eda5f21d | ||
|
|
e6da5c85e0 | ||
|
|
bfc6d6e1e9 | ||
|
|
10bf982028 | ||
|
|
8ee7b12c2b | ||
|
|
afe5fea6eb | ||
|
|
5854de3c23 | ||
|
|
967442892a | ||
|
|
202d248ae4 | ||
|
|
f667417d84 | ||
|
|
750e6315dc | ||
|
|
f117b2df32 | ||
|
|
9631f083b8 | ||
|
|
c8a52b78e1 | ||
|
|
fe4946a269 | ||
|
|
3de9c2bda0 | ||
|
|
cee30c53b8 | ||
|
|
f22b5d9daa | ||
|
|
88f8753329 | ||
|
|
fe02a97fa1 | ||
|
|
a9172fd832 | ||
|
|
96a659ae5a | ||
|
|
13739dabd8 | ||
|
|
22acfbae28 | ||
|
|
adf50b2419 | ||
|
|
35022a0478 | ||
|
|
2838053ba8 | ||
|
|
66397a6626 | ||
|
|
dfe997dce2 | ||
|
|
163c7ff791 | ||
|
|
bd5e14b488 | ||
|
|
8cd16ee3dc | ||
|
|
57564381cb | ||
|
|
0207109612 | ||
|
|
b84405c5eb | ||
|
|
2743b32536 | ||
|
|
a251422f7a | ||
|
|
d7c9bd6ed8 | ||
|
|
8b3ff0b789 | ||
|
|
94a537bfda | ||
|
|
64305ffee1 | ||
|
|
45698db0b4 | ||
|
|
14e14289f0 | ||
|
|
18650b1215 | ||
|
|
d2bbbf3f45 | ||
|
|
13cb99b7fe | ||
|
|
a1571d01bd | ||
|
|
bbd8af074e | ||
|
|
c9a47dd0ce | ||
|
|
1c9778e667 | ||
|
|
758143ef70 | ||
|
|
b5911a9a2d | ||
|
|
260f4e1b51 | ||
|
|
e6c36af3b4 | ||
|
|
2fb2458eee | ||
|
|
b340b0edbe | ||
|
|
f7fd1c4402 | ||
|
|
c316721051 | ||
|
|
80ac118242 | ||
|
|
efd15c5a3e | ||
|
|
7d02d1bb23 | ||
|
|
0d877e7be3 | ||
|
|
e4c47640fc | ||
|
|
89e7fca183 | ||
|
|
33b5bf1db8 | ||
|
|
f69c4a900e | ||
|
|
1da25cc85f | ||
|
|
3a65b372c5 | ||
|
|
9892d521fb | ||
|
|
3285b252c2 | ||
|
|
97af2fc0f9 | ||
|
|
5c508bce6d | ||
|
|
db84343581 | ||
|
|
2b44d09191 | ||
|
|
10d67aee86 | ||
|
|
b33bbb8af0 | ||
|
|
3fe9327724 | ||
|
|
179a547bd0 | ||
|
|
7ada02177d | ||
|
|
59b55e40b5 | ||
|
|
19563aa697 | ||
|
|
6410687fb6 | ||
|
|
4e4c8f5f49 | ||
|
|
416b17cf69 | ||
|
|
27cd7c8496 | ||
|
|
02321c33b2 | ||
|
|
c3dab3e4f8 | ||
|
|
0c182e2813 | ||
|
|
51a076212b | ||
|
|
27dce7374b | ||
|
|
52f2c18800 | ||
|
|
35a8918ea9 | ||
|
|
b39eab72e7 | ||
|
|
928b2477be | ||
|
|
dc13251bcc | ||
|
|
4843658dfb | ||
|
|
076afdc00e | ||
|
|
4404699fa6 | ||
|
|
52940ce254 | ||
|
|
cd6293d220 | ||
|
|
108f95723b | ||
|
|
f611c3d767 | ||
|
|
6091208f8a | ||
|
|
585a12fa48 | ||
|
|
bfc4f7b66c | ||
|
|
9dca13305f | ||
|
|
0ab9823c9a | ||
|
|
b3fd4f307f | ||
|
|
06ed0de456 | ||
|
|
79b6278883 | ||
|
|
673823cb97 | ||
|
|
15b26dff73 | ||
|
|
2303f2e9e1 | ||
|
|
056b821070 | ||
|
|
c90fded6b7 | ||
|
|
aa6f1a7f93 | ||
|
|
0cdd7c9605 | ||
|
|
f40a1c92ef | ||
|
|
739ef153f6 | ||
|
|
850ac8092f | ||
|
|
5b1eb48e6b | ||
|
|
da6fc9f2d5 | ||
|
|
20434856e9 | ||
|
|
7f891adff2 | ||
|
|
eb66fbe054 | ||
|
|
29122934a9 | ||
|
|
0825cce82c | ||
|
|
fd964131d1 | ||
|
|
77d9c15fed | ||
|
|
b21295f268 | ||
|
|
5769553659 | ||
|
|
d23d68bf2d | ||
|
|
d04c7f58a1 | ||
|
|
93e2426760 | ||
|
|
9f6c05f389 | ||
|
|
f8a1bd7b94 | ||
|
|
02967961f3 | ||
|
|
053b8b5c0a | ||
|
|
d59efd3ba0 | ||
|
|
ce56cd1ab0 | ||
|
|
54b6deee19 | ||
|
|
8315641724 | ||
|
|
aa3086cfae | ||
|
|
b599ce4919 | ||
|
|
52f16604c1 | ||
|
|
c98c06062b | ||
|
|
4963d247dc | ||
|
|
9405bc9553 | ||
|
|
8a080b91b9 | ||
|
|
4aa05fe523 | ||
|
|
d74d99dde7 | ||
|
|
0ee137efbb | ||
|
|
90f55670dc | ||
|
|
9c53f392ca | ||
|
|
cc19a81875 | ||
|
|
67032ddaf1 | ||
|
|
2fe71c3695 | ||
|
|
21422fe027 | ||
|
|
95575291ff | ||
|
|
3e61a0633d | ||
|
|
85c9a790f2 | ||
|
|
e17d55457f | ||
|
|
d8d8bc4d00 | ||
|
|
55cc1aea59 | ||
|
|
ae383164bc | ||
|
|
0d8863fe8e | ||
|
|
51f6654635 | ||
|
|
fa11e10bcb | ||
|
|
a8f5bd6fe9 | ||
|
|
a1c725bb35 | ||
|
|
e4eeb11a61 | ||
|
|
3b0bb25251 | ||
|
|
d27490e6f2 | ||
|
|
b3c191b553 | ||
|
|
0b977e615d | ||
|
|
979e1fd28b | ||
|
|
3437c4331d | ||
|
|
616d62094c | ||
|
|
ce7ff1c43c | ||
|
|
e27e41bc16 | ||
|
|
93e183e6c3 | ||
|
|
88a2bd3317 | ||
|
|
45caaa55b0 | ||
|
|
8c9b7a701d | ||
|
|
c47b992125 | ||
|
|
493c4f89dc | ||
|
|
27234dc809 | ||
|
|
5690b8e1d5 | ||
|
|
61274bc7ef | ||
|
|
e66be23996 | ||
|
|
3c43ce4165 | ||
|
|
5060824795 | ||
|
|
c4941f25a2 | ||
|
|
e7c219905f | ||
|
|
499cbd08ca | ||
|
|
0a3b3a69e3 | ||
|
|
e8607845e2 | ||
|
|
02ef4db0eb | ||
|
|
f8d48a341d | ||
|
|
251393101a | ||
|
|
b50430114f | ||
|
|
b0999abc68 | ||
|
|
2bbd92db13 | ||
|
|
64092abdca | ||
|
|
b7b631f0b0 | ||
|
|
c10f15e75b | ||
|
|
6983179b2f | ||
|
|
aea0661303 | ||
|
|
3fd9ef366f | ||
|
|
cf79a3f580 | ||
|
|
c5da78593a | ||
|
|
801b0be414 | ||
|
|
29aad2c535 | ||
|
|
82d3b9d29d | ||
|
|
a542ba486c | ||
|
|
d50e1a0e15 | ||
|
|
79a0071d65 | ||
|
|
2f1f989266 | ||
|
|
2303eb7908 | ||
|
|
c3666dd064 | ||
|
|
6ed197d35c | ||
|
|
164c9e37d7 | ||
|
|
fa0a18b31d | ||
|
|
80e2118e68 | ||
|
|
ed5086df6b | ||
|
|
218ec9140c | ||
|
|
6dad0e7679 | ||
|
|
cb96a56960 | ||
|
|
b35eb77edb | ||
|
|
89408ce12b | ||
|
|
e22d9a551c | ||
|
|
c215dc5864 | ||
|
|
a37f1d9798 | ||
|
|
07c8bbc562 | ||
|
|
b381cfe207 | ||
|
|
a560b9c8f4 | ||
|
|
e5b8cc6912 | ||
|
|
18b5612c0d | ||
|
|
fad488623f | ||
|
|
c490f084dc | ||
|
|
b1caacd643 | ||
|
|
76c8876923 | ||
|
|
a911c9cb8d | ||
|
|
07d5594085 | ||
|
|
a745d8a60a | ||
|
|
8b83d39ac3 | ||
|
|
4edb39616d | ||
|
|
7a2060fe08 | ||
|
|
d009682d4a | ||
|
|
8adb720a80 | ||
|
|
4d6f338f0c | ||
|
|
ffe9d43d09 | ||
|
|
2227b4ff91 | ||
|
|
5dd4937a1a | ||
|
|
866a0e2596 | ||
|
|
64bfda9dbe | ||
|
|
aacbbc1785 | ||
|
|
3714d5faab | ||
|
|
12cb94950a | ||
|
|
795fd788e2 | ||
|
|
956fd6ae2f | ||
|
|
04e60611ed | ||
|
|
bc99e9e504 | ||
|
|
2f35428232 | ||
|
|
00aac89ed7 | ||
|
|
f390e91bd7 | ||
|
|
e413359c8c | ||
|
|
601df7f808 | ||
|
|
e160a4add0 | ||
|
|
3bea03f4b1 | ||
|
|
19dbde8a3c | ||
|
|
a5fe10189f | ||
|
|
7075384313 | ||
|
|
25bf43cddd | ||
|
|
2880a681a8 | ||
|
|
bbcd0b7e78 | ||
|
|
916ed13bea | ||
|
|
11a06324be | ||
|
|
bcb340d97e | ||
|
|
aabef47ff1 | ||
|
|
e27de47d9f | ||
|
|
7900be3a30 | ||
|
|
ddc5838c7b | ||
|
|
6a13d7c5c6 | ||
|
|
4f0b35d9e2 | ||
|
|
3b19ede6a3 | ||
|
|
1a0ce47404 | ||
|
|
22f136d19b | ||
|
|
9a40e54159 | ||
|
|
229f2627c8 | ||
|
|
6f6454df40 | ||
|
|
3da9fc5500 | ||
|
|
540bf868b5 | ||
|
|
1674ecc585 | ||
|
|
4920abdaf7 | ||
|
|
a6b98bd35d | ||
|
|
34d4f394fc | ||
|
|
a4ef8d4200 | ||
|
|
9d9662687e | ||
|
|
2d41c719c4 | ||
|
|
b69bd966a6 | ||
|
|
09a49dd0fd | ||
|
|
3dd3372c35 | ||
|
|
0a60864310 | ||
|
|
cb24e5d0aa | ||
|
|
8c5aebdca1 | ||
|
|
4e2cff0fd5 | ||
|
|
54e518efad | ||
|
|
ad93099171 | ||
|
|
630065ca89 | ||
|
|
ddf5717992 | ||
|
|
4a53ec0b74 | ||
|
|
d7fd055171 | ||
|
|
874973d78f | ||
|
|
8078c5657b | ||
|
|
b39404860f | ||
|
|
860eae8dcb | ||
|
|
b931d3ee13 | ||
|
|
ba8a71bf3a | ||
|
|
8de4897042 | ||
|
|
6de6f8e697 | ||
|
|
e55ecff899 | ||
|
|
3895ef002e | ||
|
|
afc28503a5 | ||
|
|
e6497c87ca | ||
|
|
cbb020911d | ||
|
|
200b9d037c | ||
|
|
69667a7528 | ||
|
|
9f655ed819 | ||
|
|
5a1e1e231b | ||
|
|
6cccb1725f | ||
|
|
5bb2a904ce | ||
|
|
5e2f06cdea | ||
|
|
cf83038a7a | ||
|
|
c7662cad61 | ||
|
|
86912e0cbc | ||
|
|
74234b4ed1 | ||
|
|
2fcb8a750d | ||
|
|
2aaf672b10 | ||
|
|
34beadeef6 | ||
|
|
6de715b884 | ||
|
|
84717ca73e | ||
|
|
86b1db57d5 | ||
|
|
50c15f0d51 | ||
|
|
86d5c43307 | ||
|
|
e82bd45b55 | ||
|
|
13966e0ec8 | ||
|
|
14b9e0c032 | ||
|
|
f0f168e920 | ||
|
|
dd37caf32c | ||
|
|
856d223535 | ||
|
|
ad697365ce | ||
|
|
84171928cb | ||
|
|
e781a78eea | ||
|
|
7d08c6724b | ||
|
|
899da51f06 | ||
|
|
4e085e404a | ||
|
|
ff3aaadbaa | ||
|
|
11bddbb71d | ||
|
|
55a80ba253 | ||
|
|
32a89c30de | ||
|
|
5404eeedc2 | ||
|
|
d54ba90304 | ||
|
|
ca206915bf | ||
|
|
8461b82f0d | ||
|
|
07ffc3fade | ||
|
|
c2240f0bad | ||
|
|
36a712f979 | ||
|
|
842ba09896 | ||
|
|
b3c822abf7 | ||
|
|
1f52ed7290 | ||
|
|
a2c13e576e | ||
|
|
24bb52d2e9 | ||
|
|
d8145bbc78 | ||
|
|
8753a6e4d6 | ||
|
|
b91607b5b4 | ||
|
|
af0e839f27 | ||
|
|
b6de03de96 | ||
|
|
e115095198 | ||
|
|
c3add4bae8 | ||
|
|
3040e14624 | ||
|
|
34993e3f36 | ||
|
|
8d5c6b8877 | ||
|
|
c9eac73fec | ||
|
|
ab475b80f3 | ||
|
|
2b79039b70 | ||
|
|
044df04d3f | ||
|
|
2cbf805ffb | ||
|
|
d208074063 | ||
|
|
30541da1fa | ||
|
|
97af0af0ba | ||
|
|
e9b26921da | ||
|
|
e4434f199d | ||
|
|
f4b0219a0e | ||
|
|
6f1eaaa02e | ||
|
|
e5868837d5 | ||
|
|
71a5d8b261 | ||
|
|
dfc022d890 | ||
|
|
68cb0cf197 | ||
|
|
14592de6c4 | ||
|
|
d41dcc69ad | ||
|
|
0a28b5825b | ||
|
|
3131318b6a | ||
|
|
08e35e4698 | ||
|
|
ed4b3f082a | ||
|
|
a1007398f6 | ||
|
|
2aafa1c163 | ||
|
|
008178480a | ||
|
|
2334c3bef8 | ||
|
|
9542116ec1 | ||
|
|
f9c11c2856 | ||
|
|
30060f03eb | ||
|
|
b8ffcef45e | ||
|
|
999882b795 | ||
|
|
bda963b851 | ||
|
|
8511494d68 | ||
|
|
1c3ad9dad6 | ||
|
|
d43afe7dbd | ||
|
|
a06b2009ab | ||
|
|
2a786b4652 | ||
|
|
ef1844557f | ||
|
|
d1f7b043e8 | ||
|
|
3a8e40d029 | ||
|
|
bca46228e7 | ||
|
|
d15b24b8ea | ||
|
|
d9df9b3d01 | ||
|
|
fc1d6ae402 | ||
|
|
26b4b7a868 | ||
|
|
a839630ca3 | ||
|
|
e21dbb704e | ||
|
|
53a06797a9 | ||
|
|
99eb743772 | ||
|
|
73adfb0cec | ||
|
|
7127c60116 | ||
|
|
a6d37fd167 | ||
|
|
16ea15076d | ||
|
|
041871f1b6 | ||
|
|
8c8ae127fe | ||
|
|
357c82129c | ||
|
|
324ac691eb | ||
|
|
da99001a7a | ||
|
|
bd10d07083 | ||
|
|
0e9b419a07 | ||
|
|
d5bd08d279 | ||
|
|
4cc83530f9 | ||
|
|
4fcf4081d0 | ||
|
|
d84d80eea6 | ||
|
|
f3e1d67de1 | ||
|
|
2ce25f54d4 | ||
|
|
25eac5d29b | ||
|
|
1710e30719 | ||
|
|
6d970d0b57 | ||
|
|
41a6035a12 | ||
|
|
f84c8239e7 | ||
|
|
1dca160db0 | ||
|
|
4ff366cd99 | ||
|
|
21a4711f07 | ||
|
|
f1dd217de7 | ||
|
|
3e8f0b125d | ||
|
|
25246ef8ff | ||
|
|
bb98143e2e | ||
|
|
6258ca14d0 | ||
|
|
d0e17e8611 | ||
|
|
79dc76239b | ||
|
|
95d62354d9 | ||
|
|
b9fadf82a7 | ||
|
|
1740e34c79 | ||
|
|
9e09066e99 | ||
|
|
0a9cfd3a08 | ||
|
|
0be711ad6e | ||
|
|
1c507b8169 | ||
|
|
81bdb3dc91 | ||
|
|
5efc7f3856 | ||
|
|
0afecec607 | ||
|
|
920574de65 |
No files matched your search
@@ -7,7 +7,7 @@
|
|||||||
"access": "public",
|
"access": "public",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"updateInternalDependencies": "patch",
|
"updateInternalDependencies": "patch",
|
||||||
"ignore": [],
|
"ignore": ["@calcom/platform-libraries"],
|
||||||
"privatePackages": {
|
"privatePackages": {
|
||||||
"version": false,
|
"version": false,
|
||||||
"tag": false
|
"tag": false
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Review Instructions
|
||||||
|
|
||||||
|
- Prefer early returns. It is recommended to throw/return early so we can ensure null-checks and prevent further nesting.
|
||||||
|
- Prefer Composition over Prop Drilling. Instead of relying on prop drilling, let's try to take advantage of react children feature.
|
||||||
|
- For Prisma queries:
|
||||||
|
- Only select data you need
|
||||||
|
1) Increased performance overhead: When you select all columns from a table, Prisma retrieves all the data associated with each row, which can result in a significant performance impact. This includes reading unnecessary columns and transferring a large volume of data between your application and the database.
|
||||||
|
2) Unnecessary data exposure: Selecting all columns exposes sensitive data that might not be required by your application. This can pose security risks, especially if the table contains sensitive user information or proprietary data. (In our case app credentials and booking information)
|
||||||
|
- TLDR: Never use Includes - use select
|
||||||
|
- Select selects only the fields you specify explicitly
|
||||||
|
- Include selects the relationship AND all the fields of the table the relationship belongs to
|
||||||
|
- Make sure the credential.key field is never returned back from tRPC endpoints or APIs
|
||||||
|
- Always use t() for text localization in frontend code; direct text embedding should trigger a warning.
|
||||||
|
- Check if there's any O(n^2) logic in backend code; we should aim for O(n log n) or O(n) ideally.
|
||||||
|
- Check if there are circular references introduced. We should never allow these.
|
||||||
|
- Flag excessive Day.js use in performance-critical code. Functions like `.add`, `.diff`, `.isBefore`, and `.isAfter` are slow, especially in timezone mode. Prefer `.utc()` for better performance. Where possible, replace with native Date and direct `.valueOf()` comparisons for faster execution. Recommend using native methods or Day.js `.utc()` consistently in hot paths like loops.
|
||||||
|
- When docs changes are made in /docs/api-reference/v2/openapi.json, ensure the following
|
||||||
|
- "summary" fields are short and concise
|
||||||
|
- "summary" fields do not end with a period
|
||||||
|
- "summary" fields are written in proper American english
|
||||||
|
- When changes to API v2 or v1 are made, ensure there are no breaking changes on existing endpoints. Instead, there needs to be a newly versioned endpoint with the updated functionality but the old one must remain functional
|
||||||
@@ -164,6 +164,7 @@ SENTRY_PROJECT=
|
|||||||
SENTRY_AUTH_TOKEN=
|
SENTRY_AUTH_TOKEN=
|
||||||
# Used for capturing exceptions and logging messages
|
# Used for capturing exceptions and logging messages
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN_CLIENT=
|
||||||
SENTRY_DEBUG=
|
SENTRY_DEBUG=
|
||||||
SENTRY_MAX_SPANS=
|
SENTRY_MAX_SPANS=
|
||||||
SENTRY_SAMPLE_RATE=
|
SENTRY_SAMPLE_RATE=
|
||||||
@@ -188,6 +189,10 @@ TWILIO_TOKEN=
|
|||||||
TWILIO_MESSAGING_SID=
|
TWILIO_MESSAGING_SID=
|
||||||
TWILIO_PHONE_NUMBER=
|
TWILIO_PHONE_NUMBER=
|
||||||
TWILIO_WHATSAPP_PHONE_NUMBER=
|
TWILIO_WHATSAPP_PHONE_NUMBER=
|
||||||
|
TWILIO_WHATSAPP_REMINDER_CONTENT_SID=
|
||||||
|
TWILIO_WHATSAPP_CANCELLED_CONTENT_SID=
|
||||||
|
TWILIO_WHATSAPP_RESCHEDULED_CONTENT_SID=
|
||||||
|
TWILIO_WHATSAPP_COMPLETED_CONTENT_SID=
|
||||||
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
|
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
|
||||||
NEXT_PUBLIC_SENDER_ID=
|
NEXT_PUBLIC_SENDER_ID=
|
||||||
TWILIO_VERIFY_SID=
|
TWILIO_VERIFY_SID=
|
||||||
@@ -441,6 +446,10 @@ NEXT_PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM=0
|
|||||||
# Used to enable quick availability checks for x% of all visitors
|
# Used to enable quick availability checks for x% of all visitors
|
||||||
NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT=10
|
NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT=10
|
||||||
|
|
||||||
|
# Used to generate token for cal video recordings expiry token
|
||||||
|
# You can use: `openssl rand -base64 32` to generate one
|
||||||
|
CAL_VIDEO_RECORDING_TOKEN_SECRET=
|
||||||
|
|
||||||
# Deprecated. Use NEXT_PUBLIC_BODY_SCRIPTS instead. NEXT_PUBLIC_HEAD_SCRIPTS are now injected into the body only.
|
# Deprecated. Use NEXT_PUBLIC_BODY_SCRIPTS instead. NEXT_PUBLIC_HEAD_SCRIPTS are now injected into the body only.
|
||||||
NEXT_PUBLIC_HEAD_SCRIPTS=
|
NEXT_PUBLIC_HEAD_SCRIPTS=
|
||||||
NEXT_PUBLIC_BODY_SCRIPTS=
|
NEXT_PUBLIC_BODY_SCRIPTS=
|
||||||
|
|||||||
+3
-1
@@ -17,13 +17,15 @@
|
|||||||
/packages/app-store/larkcalendar/**/* @calcom/Foundation
|
/packages/app-store/larkcalendar/**/* @calcom/Foundation
|
||||||
/packages/app-store/office365calendar/**/* @calcom/Foundation
|
/packages/app-store/office365calendar/**/* @calcom/Foundation
|
||||||
/packages/app-store/zohocalendar/**/* @calcom/Foundation
|
/packages/app-store/zohocalendar/**/* @calcom/Foundation
|
||||||
|
/packages/embeds/**/* @calcom/Foundation
|
||||||
/packages/features/bookings/lib/**/* @calcom/Foundation
|
/packages/features/bookings/lib/**/* @calcom/Foundation
|
||||||
/packages/lib/getAggregatedAvailability.ts @calcom/Foundation
|
/packages/lib/getAggregatedAvailability.ts @calcom/Foundation
|
||||||
/packages/lib/getUserAvailability.ts @calcom/Foundation
|
/packages/lib/getUserAvailability.ts @calcom/Foundation
|
||||||
/packages/lib/server/getLuckyUser.ts @calcom/Foundation
|
/packages/lib/server/getLuckyUser.ts @calcom/Foundation
|
||||||
/packages/lib/slots.ts @calcom/Foundation
|
/packages/lib/slots.ts @calcom/Foundation
|
||||||
|
/packages/platform/atoms/**/*WebWrapper.tsx @calcom/Consumer
|
||||||
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
||||||
/packages/prisma/**/* @calcom/Foundation
|
/packages/prisma/**/* @calcom/DBA
|
||||||
/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @calcom/Foundation
|
/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @calcom/Foundation
|
||||||
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
|
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
|
||||||
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
contents: write # to create release by changesets/action below
|
contents: write # to create release by changesets/action below
|
||||||
pull-requests: write # to create pull request by changesets/action below
|
pull-requests: write # to create pull request by changesets/action below
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -27,8 +27,24 @@ jobs:
|
|||||||
|
|
||||||
- name: Configure git
|
- name: Configure git
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "github-actions@github.com"
|
# Define authorized users and their emails using organization secret
|
||||||
git config --local user.name "GitHub Actions"
|
# Expected format: JSON object with username -> email mapping
|
||||||
|
# Example: {"zomars": "zomars@cal.com", "peer": "peer@cal.com", "username3": "email3@cal.com"}
|
||||||
|
USER_EMAILS='${{ secrets.RELEASE_USER_EMAILS }}'
|
||||||
|
|
||||||
|
# Extract email for the triggering user
|
||||||
|
USER_EMAIL=$(echo "$USER_EMAILS" | jq -r --arg user "${{ github.actor }}" '.[$user] // empty')
|
||||||
|
|
||||||
|
# Fail if user is not authorized or not in mapping
|
||||||
|
if [ -z "$USER_EMAIL" ] || [ "$USER_EMAIL" = "null" ]; then
|
||||||
|
echo "Error: User '${{ github.actor }}' is not authorized to run the release workflow."
|
||||||
|
echo "Only authorized team members can trigger releases."
|
||||||
|
echo "Contact your administrator to be added to the authorized users list."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config --local user.email "$USER_EMAIL"
|
||||||
|
git config --local user.name "${{ github.actor }}"
|
||||||
|
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E API v2
|
name: E2E API v2
|
||||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
runs-on: buildjet-16vcpu-ubuntu-2204
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13
|
image: postgres:13
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
- uses: lingodotdev/lingo.dev@main
|
- uses: lingodotdev/lingo.dev@main
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||||
organization-name: calcom
|
organization-name: calcom
|
||||||
ignore-labels: "admin, app-store, ai, authentication, automated-testing, devops, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
ignore-labels: "admin, app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, dba, devops, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||||
apply-labels-from-issue:
|
apply-labels-from-issue:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
outputs:
|
outputs:
|
||||||
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' && (github.event.action != 'labeled' || (github.event.action == 'labeled' && github.event.label.name == 'ready-for-e2e')) }}
|
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' && (github.event.action != 'labeled' || github.event.label.name == 'ready-for-e2e') }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check if PR exists with ready-for-e2e label for this SHA
|
- name: Check if PR exists with ready-for-e2e label for this SHA
|
||||||
id: check-if-pr-has-label
|
id: check-if-pr-has-label
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
name: Submodule Sync
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Runs "At minute 15 past every 4th hour." (see https://crontab.guru)
|
|
||||||
- cron: "15 */4 * * *"
|
|
||||||
workflow_dispatch: ~
|
|
||||||
jobs:
|
|
||||||
submodule-sync:
|
|
||||||
name: Submodule update
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
||||||
- name: Commit
|
|
||||||
run: |
|
|
||||||
git config user.email "actions@github.com"
|
|
||||||
git config user.name "github-actions"
|
|
||||||
git commit -am "Auto updated submodule references" && git push || echo "No changes to commit"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
echo "Updating submodules recursively"
|
|
||||||
pwd
|
|
||||||
git submodule update --init --recursive
|
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Whitespace-only changes.
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Whitespace-only changes.
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Whitespace-only changes.
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
Whitespace-only changes.
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
Whitespace-only changes.
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
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
+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/prisma"
|
||||||
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Whitespace-only changes.
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
Whitespace-only changes.
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
# Cal.com Permission Documentation
|
||||||
|
|
||||||
|
This document maps existing role-based permission checks to the new PBAC (Permission-Based Access Control) system's permission strings in the format `resource.action`.
|
||||||
|
|
||||||
|
## Permissions by Resource
|
||||||
|
|
||||||
|
### Team Permissions
|
||||||
|
|
||||||
|
| Permission String | Description | File Path | Line |
|
||||||
|
|------------------|-------------|-----------|------|
|
||||||
|
| team.create | Create teams | [packages/trpc/server/routers/viewer/teams/create.handler.ts](packages/trpc/server/routers/viewer/teams/create.handler.ts) | 55-57 |
|
||||||
|
| team.update | Update team settings | [packages/trpc/server/routers/viewer/teams/update.handler.ts](packages/trpc/server/routers/viewer/teams/update.handler.ts) | 18 |
|
||||||
|
| team.changeMemberRole | Change role of team members | [packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts](packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts) | 18-21 |
|
||||||
|
| team.remove | Remove members from team | [packages/trpc/server/routers/viewer/teams/removeMember.handler.ts](packages/trpc/server/routers/viewer/teams/removeMember.handler.ts) | 35-36, 51-55 |
|
||||||
|
| team.invite | Invite members to team | [packages/trpc/server/routers/viewer/teams/invite.handler.ts](packages/trpc/server/routers/viewer/teams/invite.handler.ts) | - |
|
||||||
|
|
||||||
|
### Event Type Permissions
|
||||||
|
|
||||||
|
| Permission String | Description | File Path | Line |
|
||||||
|
|------------------|-------------|-----------|------|
|
||||||
|
| eventType.create | Create event types | [packages/trpc/server/routers/viewer/eventTypes/create.handler.ts](packages/trpc/server/routers/viewer/eventTypes/create.handler.ts) | - |
|
||||||
|
| eventType.update | Update event types | [packages/trpc/server/routers/viewer/eventTypes/update.handler.ts](packages/trpc/server/routers/viewer/eventTypes/update.handler.ts) | 162-164 |
|
||||||
|
| eventType.delete | Delete event types | [packages/trpc/server/routers/viewer/eventTypes/delete.handler.ts](packages/trpc/server/routers/viewer/eventTypes/delete.handler.ts) | 13-31 |
|
||||||
|
|
||||||
|
### Booking Permissions
|
||||||
|
|
||||||
|
| Permission String | Description | File Path | Line |
|
||||||
|
|------------------|-------------|-----------|------|
|
||||||
|
| booking.read | Read booking details | [packages/trpc/server/routers/viewer/bookings/get.handler.ts](packages/trpc/server/routers/viewer/bookings/get.handler.ts) | 95-107, 132-134 |
|
||||||
|
| booking.readTeamBookings | Read team bookings | [packages/trpc/server/routers/viewer/bookings/get.handler.ts](packages/trpc/server/routers/viewer/bookings/get.handler.ts) | 240-252 |
|
||||||
|
| booking.readOrgBookings | Read organization bookings | [packages/trpc/server/routers/viewer/bookings/get.handler.ts](packages/trpc/server/routers/viewer/bookings/get.handler.ts) | 240-252 |
|
||||||
|
|
||||||
|
### Organization Permissions
|
||||||
|
|
||||||
|
| Permission String | Description | File Path | Line |
|
||||||
|
|------------------|-------------|-----------|------|
|
||||||
|
| organization.read | Read organization details | [packages/trpc/server/routers/viewer/organizations/get.handler.ts](packages/trpc/server/routers/viewer/organizations/get.handler.ts) | - |
|
||||||
|
| organization.listMembers | List organization members | [packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts](packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts) | 68-76 |
|
||||||
|
| organization.create | Create organization | [packages/trpc/server/routers/viewer/organizations/create.handler.ts](packages/trpc/server/routers/viewer/organizations/create.handler.ts) | - |
|
||||||
|
|
||||||
|
### API Key Permissions
|
||||||
|
|
||||||
|
| Permission String | Description | File Path | Line |
|
||||||
|
|------------------|-------------|-----------|------|
|
||||||
|
| apiKey.create | Create API keys | [packages/trpc/server/routers/viewer/apiKeys/create.handler.ts](packages/trpc/server/routers/viewer/apiKeys/create.handler.ts) | 25-26 |
|
||||||
|
| apiKey.findKeyOfType | Find API keys by type | [packages/trpc/server/routers/viewer/apiKeys/findKeyOfType.handler.ts](packages/trpc/server/routers/viewer/apiKeys/findKeyOfType.handler.ts) | 18-19 |
|
||||||
|
|
||||||
|
## Helper Functions and Utilities
|
||||||
|
|
||||||
|
The following helper functions are commonly used for permission checks:
|
||||||
|
|
||||||
|
| Function | Description | File Path |
|
||||||
|
|----------|-------------|-----------|
|
||||||
|
| isTeamAdmin | Checks if user is admin or owner of a team | [packages/lib/server/queries/teams/index.ts](packages/lib/server/queries/teams/index.ts#L385-L405) |
|
||||||
|
| isTeamOwner | Checks if user is owner of a team | [packages/lib/server/queries/teams/index.ts](packages/lib/server/queries/teams/index.ts#L407-L416) |
|
||||||
|
| isTeamMember | Checks if user is a member of a team | [packages/lib/server/queries/teams/index.ts](packages/lib/server/queries/teams/index.ts#L418-L426) |
|
||||||
|
| canEditEntity | Checks if user can edit an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L13-L22) |
|
||||||
|
| canAccessEntity | Checks if user can access an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L24-L34) |
|
||||||
|
| getEntityPermissionLevel | Gets permission level for an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L36-L73) |
|
||||||
|
| canCreateEntity | Checks if user can create an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L102-L115) |
|
||||||
|
| withRoleCanCreateEntity | Checks if role allows entity creation | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L119-L121) |
|
||||||
|
|
||||||
|
## Permission Level Enums
|
||||||
|
|
||||||
|
The codebase uses several enums to define permission levels:
|
||||||
|
|
||||||
|
### MembershipRole Enum
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum MembershipRole {
|
||||||
|
OWNER = "OWNER",
|
||||||
|
ADMIN = "ADMIN",
|
||||||
|
MEMBER = "MEMBER"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entity Permission Level Enum
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum ENTITY_PERMISSION_LEVEL {
|
||||||
|
NONE,
|
||||||
|
USER_ONLY_WRITE,
|
||||||
|
TEAM_READ_ONLY,
|
||||||
|
TEAM_WRITE
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Permission Patterns
|
||||||
|
|
||||||
|
1. **Team Admin/Owner Check**: Many operations require the user to be a team admin or owner.
|
||||||
|
```typescript
|
||||||
|
if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Team Owner Check**: Some operations (like changing an owner's role) require the user to be a team owner.
|
||||||
|
```typescript
|
||||||
|
if (input.role === MembershipRole.OWNER && !(await isTeamOwner(ctx.user?.id, input.teamId)))
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Organization Admin Check**: Operations within an organization require the user to be an organization admin.
|
||||||
|
```typescript
|
||||||
|
if (user.profile?.organizationId && !user.organization.isOrgAdmin) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN", message: "org_admins_can_create_new_teams" });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Entity Permission Check**: Entity operations use permission level checks.
|
||||||
|
```typescript
|
||||||
|
const permissionLevel = await getEntityPermissionLevel(entity, userId);
|
||||||
|
return permissionLevel === ENTITY_PERMISSION_LEVEL.TEAM_WRITE ||
|
||||||
|
permissionLevel === ENTITY_PERMISSION_LEVEL.USER_ONLY_WRITE;
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Booking Access Control**: Complex permission checks for retrieving bookings based on user roles and team/organization membership.
|
||||||
|
```typescript
|
||||||
|
const membershipIdsWhereUserIsAdminOwner = (
|
||||||
|
await prisma.membership.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
role: {
|
||||||
|
in: ["ADMIN", "OWNER"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).map((membership) => membership.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration to PBAC
|
||||||
|
|
||||||
|
When migrating to the new PBAC system, these existing role-based checks should be replaced with permission string checks using the `permissionMatches` function:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { permissionMatches } from "@calcom/features/pbac/types/permission-registry";
|
||||||
|
|
||||||
|
// Instead of:
|
||||||
|
if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
|
||||||
|
// Use:
|
||||||
|
if (!permissionMatches("team.update", userPermissions)) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Permission String Alternatives for Helper Functions
|
||||||
|
|
||||||
|
The following table provides permission string alternatives for common helper functions:
|
||||||
|
|
||||||
|
| Helper Function | Permission String Alternative | Description |
|
||||||
|
|-----------------|-------------------------------|-------------|
|
||||||
|
| isTeamAdmin | `team.*` | Grants all team permissions |
|
||||||
|
| isTeamAdmin | `team.update` | Update team settings |
|
||||||
|
| isTeamAdmin | `team.invite` | Invite team members |
|
||||||
|
| isTeamAdmin | `team.remove` | Remove team members |
|
||||||
|
| isTeamOwner | `team.changeMemberRole` | Change role of team members |
|
||||||
|
| isTeamOwner | `team.delete` | Delete team |
|
||||||
|
| isTeamMember | `team.read` | Read-only access to team |
|
||||||
|
|
||||||
|
## Entity Permission Functions and Resources
|
||||||
|
|
||||||
|
The following functions are used to check permissions for entities with userId and teamId properties:
|
||||||
|
|
||||||
|
| Function | Description | Permission String Alternative |
|
||||||
|
|----------|-------------|-------------------------------|
|
||||||
|
| canEditEntity | Checks if user can edit an entity | `{resource}.update` |
|
||||||
|
| canAccessEntity | Checks if user can access an entity | `{resource}.read` |
|
||||||
|
| getEntityPermissionLevel | Gets permission level for an entity | N/A - Implementation detail |
|
||||||
|
| canCreateEntity | Checks if user can create an entity | `{resource}.create` |
|
||||||
|
|
||||||
|
### Resources Used with Entity Permission Functions
|
||||||
|
|
||||||
|
| Resource | canEditEntity Usage | canAccessEntity Usage | Permission String |
|
||||||
|
|----------|-------------------|---------------------|------------------|
|
||||||
|
| routingForm | [packages/app-store/routing-forms/trpc/forms.handler.ts](packages/app-store/routing-forms/trpc/forms.handler.ts#L72) | [packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts](packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts#L78) | `routingForm.update`, `routingForm.read` |
|
||||||
|
| routingForm | [packages/app-store/routing-forms/trpc/formMutation.handler.ts](packages/app-store/routing-forms/trpc/formMutation.handler.ts#L315) | | `routingForm.update` |
|
||||||
|
| routingForm | [packages/app-store/routing-forms/api/responses/[formId].ts](packages/app-store/routing-forms/api/responses/[formId].ts#L96) | | `routingForm.update` |
|
||||||
|
|
||||||
|
These functions can be used with any entity that has userId and teamId properties. When migrating to PBAC, replace these checks with appropriate permission string checks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Instead of:
|
||||||
|
if (!(await canEditEntity(form, user.id))) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
|
||||||
|
// Use:
|
||||||
|
if (!permissionMatches("routingForm.update", userPermissions)) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows for more granular permission control and the creation of custom roles with specific permissions.
|
||||||
@@ -488,13 +488,8 @@ Don't code but still want to contribute? Join our [Discussions](https://github.c
|
|||||||
- Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
- 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
|
## 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).
|
Refer to docs [here](./docs/self-hosting/guides/organization/single-organization-setup) for a detailed documentation with screenshots.
|
||||||
- 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
|
## Integrations
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node ./index.js"
|
"dev": "node ./index.js"
|
||||||
},
|
},
|
||||||
|
|||||||
+12
-6
@@ -1,4 +1,4 @@
|
|||||||
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
|
import prismock from "../../../../../tests/libs/__mocks__/prisma";
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
@@ -7,12 +7,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
import type { ILicenseKeyService } from "@calcom/ee/common/server/LicenseKeyService";
|
import type { ILicenseKeyService } from "@calcom/ee/common/server/LicenseKeyService";
|
||||||
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
|
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
|
||||||
|
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
||||||
|
import type { IDeploymentRepository } from "@calcom/lib/server/repository/deployment.interface";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { hashAPIKey } from "~/../../../packages/features/ee/api-keys/lib/apiKeys";
|
import { verifyApiKey } from "./verifyApiKey";
|
||||||
|
|
||||||
import { verifyApiKey } from "../../../lib/helpers/verifyApiKey";
|
|
||||||
|
|
||||||
type CustomNextApiRequest = NextApiRequest & Request;
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
type CustomNextApiResponse = NextApiResponse & Response;
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
@@ -21,11 +21,17 @@ afterEach(() => {
|
|||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Verify API key", () => {
|
const mockDeploymentRepository: IDeploymentRepository = {
|
||||||
|
getLicenseKeyWithId: vi.fn().mockResolvedValue("mockLicenseKey"), // Mocked return value
|
||||||
|
getSignatureToken: vi.fn().mockResolvedValue("mockSignatureToken"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Fix the skip condition for this test suite
|
||||||
|
describe.skip("Verify API key", () => {
|
||||||
let service: ILicenseKeyService;
|
let service: ILicenseKeyService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
service = await LicenseKeyService.create();
|
service = await LicenseKeyService.create(mockDeploymentRepository);
|
||||||
|
|
||||||
vi.spyOn(service, "checkLicense");
|
vi.spyOn(service, "checkLicense");
|
||||||
});
|
});
|
||||||
@@ -3,6 +3,7 @@ import type { NextMiddleware } from "next-api-middleware";
|
|||||||
import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
|
import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
|
||||||
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
||||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
|
import { DeploymentRepository } from "@calcom/lib/server/repository/deployment";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { isAdminGuard } from "../utils/isAdmin";
|
import { isAdminGuard } from "../utils/isAdmin";
|
||||||
@@ -19,11 +20,12 @@ export const dateNotInPast = function (date: Date) {
|
|||||||
|
|
||||||
// This verifies the apiKey and sets the user if it is valid.
|
// This verifies the apiKey and sets the user if it is valid.
|
||||||
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
||||||
const licenseKeyService = await LicenseKeySingleton.getInstance();
|
const deploymentRepo = new DeploymentRepository(prisma);
|
||||||
|
const licenseKeyService = await LicenseKeySingleton.getInstance(deploymentRepo);
|
||||||
const hasValidLicense = await licenseKeyService.checkLicense();
|
const hasValidLicense = await licenseKeyService.checkLicense();
|
||||||
|
|
||||||
if (!hasValidLicense && IS_PRODUCTION) {
|
if (!hasValidLicense && IS_PRODUCTION) {
|
||||||
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
|
return res.status(401).json({ message: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { MAX_SEATS_PER_TIME_SLOT } from "@calcom/lib/constants";
|
import {
|
||||||
|
MAX_SEATS_PER_TIME_SLOT,
|
||||||
|
MAX_EVENT_DURATION_MINUTES,
|
||||||
|
MIN_EVENT_DURATION_MINUTES,
|
||||||
|
} from "@calcom/lib/constants";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import { _EventTypeModel as EventType, _HostModel } from "@calcom/prisma/zod";
|
import { _EventTypeModel as EventType, _HostModel } from "@calcom/prisma/zod";
|
||||||
import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||||
@@ -80,7 +84,7 @@ const schemaEventTypeCreateParams = z
|
|||||||
title: z.string(),
|
title: z.string(),
|
||||||
slug: z.string().transform((s) => slugify(s)),
|
slug: z.string().transform((s) => slugify(s)),
|
||||||
description: z.string().optional().nullable(),
|
description: z.string().optional().nullable(),
|
||||||
length: z.number().int(),
|
length: z.number().int().min(MIN_EVENT_DURATION_MINUTES).max(MAX_EVENT_DURATION_MINUTES),
|
||||||
metadata: z.any().optional(),
|
metadata: z.any().optional(),
|
||||||
recurringEvent: recurringEventInputSchema.optional(),
|
recurringEvent: recurringEventInputSchema.optional(),
|
||||||
seatsPerTimeSlot: z.number().optional(),
|
seatsPerTimeSlot: z.number().optional(),
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyPa
|
|||||||
id: true,
|
id: true,
|
||||||
// No eventTypeId support in API v1
|
// No eventTypeId support in API v1
|
||||||
eventTypeId: true,
|
eventTypeId: true,
|
||||||
|
|
||||||
|
/** No watch related fields support in API v1 */
|
||||||
|
watchAttempts: true,
|
||||||
|
unwatchAttempts: true,
|
||||||
|
maxAttempts: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams
|
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ enum locales {
|
|||||||
SR = "sr",
|
SR = "sr",
|
||||||
SV = "sv",
|
SV = "sv",
|
||||||
VI = "vi",
|
VI = "vi",
|
||||||
|
BN = "bn",
|
||||||
}
|
}
|
||||||
enum theme {
|
enum theme {
|
||||||
DARK = "dark",
|
DARK = "dark",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
|||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||||
@@ -5,9 +5,6 @@ const plugins = [withAxiom];
|
|||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
experimental: {
|
|
||||||
instrumentationHook: true,
|
|
||||||
},
|
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
"@calcom/app-store",
|
"@calcom/app-store",
|
||||||
"@calcom/dayjs",
|
"@calcom/dayjs",
|
||||||
|
|||||||
@@ -33,12 +33,12 @@
|
|||||||
"@sentry/nextjs": "^9.15.0",
|
"@sentry/nextjs": "^9.15.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "^13.5.9",
|
"next": "^15.3.0",
|
||||||
"next-api-middleware": "^1.0.1",
|
"next-api-middleware": "^1.0.1",
|
||||||
"next-axiom": "^0.17.0",
|
"next-axiom": "^0.17.0",
|
||||||
"next-swagger-doc": "^0.3.6",
|
"next-swagger-doc": "^0.3.6",
|
||||||
"next-validations": "^0.2.0",
|
"next-validations": "^0.2.0",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^5.7.2",
|
||||||
"tzdata": "^1.0.30",
|
"tzdata": "^1.0.30",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
|
|||||||
@@ -222,6 +222,13 @@ async function handler(req: NextApiRequest) {
|
|||||||
};
|
};
|
||||||
if (isSystemWideAdmin) userId = req.body.userId || userId;
|
if (isSystemWideAdmin) userId = req.body.userId || userId;
|
||||||
|
|
||||||
|
if (req.body.eventTypeId !== undefined && typeof req.body.eventTypeId !== "number") {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Bad request, eventTypeId must be a number",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (isOrganizationOwnerOrAdmin) {
|
if (isOrganizationOwnerOrAdmin) {
|
||||||
const accessibleUsersIds = await getAccessibleUsers({
|
const accessibleUsersIds = await getAccessibleUsers({
|
||||||
adminUserId: userId,
|
adminUserId: userId,
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ async function getHandler(req: NextApiRequest) {
|
|||||||
|
|
||||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||||
|
|
||||||
const usersWithCalendars = await UserRepository.findManyByIdsIncludeDestinationAndSelectedCalendars({
|
const usersWithCalendars = await new UserRepository(
|
||||||
|
prisma
|
||||||
|
).findManyByIdsIncludeDestinationAndSelectedCalendars({
|
||||||
ids: userIds,
|
ids: userIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -42,11 +42,6 @@ describe("GET /api/slots", () => {
|
|||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
|
||||||
expect(res.statusCode).toBe(400);
|
expect(res.statusCode).toBe(400);
|
||||||
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
|
|
||||||
{
|
|
||||||
"message": "invalid_type in 'startTime': Required; invalid_type in 'endTime': Required",
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
||||||
|
import { getAvailableSlotsService } from "@calcom/lib/di/containers/available-slots";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import { createContext } from "@calcom/trpc/server/createContext";
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
import { getScheduleSchema } from "@calcom/trpc/server/routers/viewer/slots/types";
|
import { getScheduleSchema } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||||
import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util";
|
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||||
@@ -27,7 +27,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
const input = getScheduleSchema.parse({ usernameList: slugs, isTeamEvent: parsedIsTeamEvent, ...rest });
|
const input = getScheduleSchema.parse({ usernameList: slugs, isTeamEvent: parsedIsTeamEvent, ...rest });
|
||||||
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
|
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
|
||||||
const availableSlots = await getAvailableSlots({ ctx: await createContext({ req, res }), input });
|
|
||||||
|
const availableSlotsService = getAvailableSlotsService();
|
||||||
|
const availableSlots = await availableSlotsService.getAvailableSlots({
|
||||||
|
ctx: await createContext({ req, res }),
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
|
||||||
const slotsInProvidedTimeZone = timeZoneSupported
|
const slotsInProvidedTimeZone = timeZoneSupported
|
||||||
? Object.keys(availableSlots.slots).reduce(
|
? Object.keys(availableSlots.slots).reduce(
|
||||||
(acc: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>, date) => {
|
(acc: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>, date) => {
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ describe("GET /api/bookings", async () => {
|
|||||||
|
|
||||||
expect(responseData.bookings.find((b) => b.userId === memberUser.id)).toBeDefined();
|
expect(responseData.bookings.find((b) => b.userId === memberUser.id)).toBeDefined();
|
||||||
expect(groupedUsers.size).toBe(1);
|
expect(groupedUsers.size).toBe(1);
|
||||||
expect(groupedUsers.entries().next().value[0]).toBe(memberUser.id);
|
const firstEntry = groupedUsers.entries().next().value;
|
||||||
|
expect(firstEntry?.[0]).toBe(memberUser.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Returns bookings for regular user", async () => {
|
it("Returns bookings for regular user", async () => {
|
||||||
|
|||||||
@@ -25,6 +25,44 @@ vi.mock("@calcom/lib/server/i18n", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("POST /api/bookings - eventTypeId validation", () => {
|
||||||
|
test("String eventTypeId should return 400", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
eventTypeId: "invalid-string",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res._getStatusCode()).toBe(400);
|
||||||
|
expect(JSON.parse(res._getData())).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
message: "Bad request, eventTypeId must be a number",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Number eventTypeId should not trigger validation error", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
eventTypeId: 123,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
const statusCode = res._getStatusCode();
|
||||||
|
const responseData = JSON.parse(res._getData());
|
||||||
|
|
||||||
|
if (statusCode === 400) {
|
||||||
|
expect(responseData.message).not.toBe("Bad request, eventTypeId must be a number");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe.skipIf(true)("POST /api/bookings", () => {
|
describe.skipIf(true)("POST /api/bookings", () => {
|
||||||
describe("Errors", () => {
|
describe("Errors", () => {
|
||||||
test("Missing required data", async () => {
|
test("Missing required data", async () => {
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
googleChannelResourceUri: null,
|
googleChannelResourceUri: null,
|
||||||
googleChannelExpiration: null,
|
googleChannelExpiration: null,
|
||||||
error: null,
|
error: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
watchAttempts: 0,
|
||||||
|
maxAttempts: 3,
|
||||||
|
unwatchAttempts: 0,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
@@ -128,6 +134,12 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
domainWideDelegationCredentialId: null,
|
domainWideDelegationCredentialId: null,
|
||||||
eventTypeId: null,
|
eventTypeId: null,
|
||||||
error: null,
|
error: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
watchAttempts: 0,
|
||||||
|
maxAttempts: 3,
|
||||||
|
unwatchAttempts: 0,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
|||||||
@@ -38,3 +38,7 @@ AXIOM_TOKEN=
|
|||||||
# WARN: 2
|
# WARN: 2
|
||||||
# ERROR: 3
|
# ERROR: 3
|
||||||
LOGGER_BRIDGE_LOG_LEVEL="1"
|
LOGGER_BRIDGE_LOG_LEVEL="1"
|
||||||
|
|
||||||
|
# 1: Rewrite /api/v2 to /v2
|
||||||
|
# 0: Don't rewrite
|
||||||
|
REWRITE_API_V2_PREFIX="1"
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"description": "Platform API for Cal.com",
|
"description": "Platform API for Cal.com",
|
||||||
"author": "Cal.com Inc.",
|
"author": "Cal.com Inc.",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yarn dev:build && nest build",
|
"build": "yarn dev:build && nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
@@ -37,8 +38,7 @@
|
|||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.208",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.258",
|
||||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
"ts-loader": "^9.4.3",
|
"ts-loader": "^9.4.3",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsconfig-paths": "^4.1.0",
|
"tsconfig-paths": "^4.1.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"schema": "../../../packages/prisma/schema.prisma"
|
"schema": "../../../packages/prisma/schema.prisma"
|
||||||
|
|||||||
@@ -86,10 +86,16 @@ export class AppModule implements NestModule {
|
|||||||
configure(consumer: MiddlewareConsumer): void {
|
configure(consumer: MiddlewareConsumer): void {
|
||||||
consumer
|
consumer
|
||||||
.apply(RawBodyMiddleware)
|
.apply(RawBodyMiddleware)
|
||||||
.forRoutes({
|
.forRoutes(
|
||||||
path: "/api/v2/billing/webhook",
|
{
|
||||||
method: RequestMethod.POST,
|
path: "/api/v2/billing/webhook",
|
||||||
})
|
method: RequestMethod.POST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/v2/billing/webhook",
|
||||||
|
method: RequestMethod.POST,
|
||||||
|
}
|
||||||
|
)
|
||||||
.apply(JsonBodyMiddleware)
|
.apply(JsonBodyMiddleware)
|
||||||
.forRoutes("*")
|
.forRoutes("*")
|
||||||
.apply(RequestIdMiddleware)
|
.apply(RequestIdMiddleware)
|
||||||
|
|||||||
@@ -81,5 +81,9 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
|||||||
|
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
if (process?.env?.API_GLOBAL_PREFIX) {
|
||||||
|
app.setGlobalPrefix(process?.env?.API_GLOBAL_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
@@ -36,7 +36,7 @@ const loadConfig = (): AppConfig => {
|
|||||||
app: {
|
app: {
|
||||||
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
|
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
|
||||||
},
|
},
|
||||||
e2e: getEnv("IS_E2E", false),
|
e2e: getEnv("IS_E2E", "false") === "true" ? true : false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ type OAuthRequestParams = {
|
|||||||
platformBookingUrl: string;
|
platformBookingUrl: string;
|
||||||
platformBookingLocation?: string;
|
platformBookingLocation?: string;
|
||||||
arePlatformEmailsEnabled: boolean;
|
arePlatformEmailsEnabled: boolean;
|
||||||
|
areCalendarEventsEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PLATFORM_PARAMS = {
|
const DEFAULT_PLATFORM_PARAMS = {
|
||||||
@@ -86,6 +87,7 @@ const DEFAULT_PLATFORM_PARAMS = {
|
|||||||
platformBookingUrl: "",
|
platformBookingUrl: "",
|
||||||
arePlatformEmailsEnabled: false,
|
arePlatformEmailsEnabled: false,
|
||||||
platformBookingLocation: undefined,
|
platformBookingLocation: undefined,
|
||||||
|
areCalendarEventsEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
@@ -135,9 +137,13 @@ export class BookingsController_2024_04_15 {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let nextCursor = null;
|
||||||
|
if (bookings.totalCount > (cursor ?? 0) + (limit ?? 10)) {
|
||||||
|
nextCursor = (cursor ?? 0) + (limit ?? 10);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: bookings,
|
data: { ...bookings, nextCursor },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +197,7 @@ export class BookingsController_2024_04_15 {
|
|||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
});
|
});
|
||||||
if (booking.userId && booking.uid && booking.startTime) {
|
if (booking.userId && booking.uid && booking.startTime) {
|
||||||
void (await this.billingService.increaseUsageByUserId(booking.userId, {
|
void (await this.billingService.increaseUsageByUserId(booking.userId, {
|
||||||
@@ -225,6 +232,15 @@ export class BookingsController_2024_04_15 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bookingUid) {
|
if (bookingUid) {
|
||||||
|
const { bookingInfo } = await getBookingInfo(bookingUid);
|
||||||
|
if (!bookingInfo) {
|
||||||
|
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
|
||||||
|
}
|
||||||
|
if (bookingInfo.status === "CANCELLED") {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Can't cancel booking with uid=${bookingUid} because it has been cancelled already. Please provide uid of a booking that is not cancelled.`
|
||||||
|
);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
req.body.uid = bookingUid;
|
req.body.uid = bookingUid;
|
||||||
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
|
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
|
||||||
@@ -424,7 +440,7 @@ export class BookingsController_2024_04_15 {
|
|||||||
|
|
||||||
if (isEmbed) {
|
if (isEmbed) {
|
||||||
// embed should ignore oauth client settings and enable emails by default
|
// embed should ignore oauth client settings and enable emails by default
|
||||||
return { ...res, arePlatformEmailsEnabled: true };
|
return { ...res, arePlatformEmailsEnabled: true, areCalendarEventsEnabled: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -436,6 +452,7 @@ export class BookingsController_2024_04_15 {
|
|||||||
res.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
|
res.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
|
||||||
res.platformBookingUrl = client.bookingRedirectUri ?? "";
|
res.platformBookingUrl = client.bookingRedirectUri ?? "";
|
||||||
res.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
|
res.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
|
||||||
|
res.areCalendarEventsEnabled = client.areCalendarEventsEnabled;
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ import {
|
|||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
enum Status {
|
const Status = {
|
||||||
CANCELLED = "CANCELLED",
|
CANCELLED: "CANCELLED",
|
||||||
REJECTED = "REJECTED",
|
REJECTED: "REJECTED",
|
||||||
ACCEPTED = "ACCEPTED",
|
ACCEPTED: "ACCEPTED",
|
||||||
PENDING = "PENDING",
|
PENDING: "PENDING",
|
||||||
AWAITING_HOST = "AWAITING_HOST",
|
AWAITING_HOST: "AWAITING_HOST",
|
||||||
}
|
} as const;
|
||||||
|
|
||||||
|
export type Status = (typeof Status)[keyof typeof Status];
|
||||||
|
|
||||||
class Attendee {
|
class Attendee {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
|
|||||||
@@ -24,4 +24,26 @@ export class BookingReferencesRepository_2024_08_13 {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBookingReferencesIncludeSensitiveCredentials(eventUid: string) {
|
||||||
|
return this.dbRead.prisma.bookingReference.findFirst({
|
||||||
|
where: {
|
||||||
|
uid: eventUid,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
credential: true,
|
||||||
|
delegationCredential: true,
|
||||||
|
booking: {
|
||||||
|
select: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings
|
|||||||
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
||||||
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||||
|
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||||
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
|
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
|
||||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||||
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
||||||
@@ -77,6 +78,7 @@ import { Module } from "@nestjs/common";
|
|||||||
ErrorsBookingsService_2024_08_13,
|
ErrorsBookingsService_2024_08_13,
|
||||||
BookingReferencesService_2024_08_13,
|
BookingReferencesService_2024_08_13,
|
||||||
BookingReferencesRepository_2024_08_13,
|
BookingReferencesRepository_2024_08_13,
|
||||||
|
CalVideoService,
|
||||||
],
|
],
|
||||||
controllers: [BookingsController_2024_08_13],
|
controllers: [BookingsController_2024_08_13],
|
||||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||||
|
|||||||
@@ -169,4 +169,15 @@ export class BookingsRepository_2024_08_13 {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getByUidWithBookingReference(uid: string) {
|
||||||
|
return this.dbRead.prisma.booking.findUnique({
|
||||||
|
where: {
|
||||||
|
uid,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
references: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outpu
|
|||||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||||
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||||
|
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
@@ -54,6 +55,8 @@ import {
|
|||||||
RescheduleBookingInput_2024_08_13,
|
RescheduleBookingInput_2024_08_13,
|
||||||
RescheduleBookingInputPipe,
|
RescheduleBookingInputPipe,
|
||||||
RescheduleSeatedBookingInput_2024_08_13,
|
RescheduleSeatedBookingInput_2024_08_13,
|
||||||
|
GetBookingRecordingsOutput,
|
||||||
|
GetBookingTranscriptsOutput,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import {
|
import {
|
||||||
CreateBookingInputPipe,
|
CreateBookingInputPipe,
|
||||||
@@ -88,7 +91,8 @@ export class BookingsController_2024_08_13 {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly bookingsService: BookingsService_2024_08_13,
|
private readonly bookingsService: BookingsService_2024_08_13,
|
||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
private readonly bookingReferencesService: BookingReferencesService_2024_08_13
|
private readonly bookingReferencesService: BookingReferencesService_2024_08_13,
|
||||||
|
private readonly calVideoService: CalVideoService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -171,6 +175,36 @@ export class BookingsController_2024_08_13 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("/:bookingUid/recordings")
|
||||||
|
@UseGuards(BookingUidGuard)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get all the recordings for the booking",
|
||||||
|
description: `Fetches all the recordings for the booking \`:bookingUid\``,
|
||||||
|
})
|
||||||
|
async getBookingRecordings(@Param("bookingUid") bookingUid: string): Promise<GetBookingRecordingsOutput> {
|
||||||
|
const recordings = await this.calVideoService.getRecordings(bookingUid);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: recordings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/:bookingUid/transcripts")
|
||||||
|
@UseGuards(BookingUidGuard)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get all the transcripts download links for the booking",
|
||||||
|
description: `Fetches all the transcripts download links for the booking \`:bookingUid\``,
|
||||||
|
})
|
||||||
|
async getBookingTranscripts(@Param("bookingUid") bookingUid: string): Promise<GetBookingTranscriptsOutput> {
|
||||||
|
const transcripts = await this.calVideoService.getTranscripts(bookingUid);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: transcripts ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@@ -233,7 +267,14 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Cancel a booking",
|
summary: "Cancel a booking",
|
||||||
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
|
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
|
||||||
For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call
|
|
||||||
|
\nCancelling seated bookings:
|
||||||
|
It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.
|
||||||
|
\n1. As an attendee - provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and seatUid in the request body \`{"seatUid": "123-123-123"}\` . This will remove this particular attendance from the booking.
|
||||||
|
\n2. As the host - host can cancel booking for all attendees aka for every seat. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.
|
||||||
|
|
||||||
|
\nCancelling recurring seated bookings:
|
||||||
|
For recurring seated bookings it is not possible to cancel all of them with 1 call
|
||||||
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
|
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
|
||||||
})
|
})
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
@@ -290,7 +331,8 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Reassign a booking to auto-selected host",
|
summary: "Reassign a booking to auto-selected host",
|
||||||
description: "The provided authorization header refers to the owner of the booking.",
|
description:
|
||||||
|
"Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.",
|
||||||
})
|
})
|
||||||
async reassignBooking(
|
async reassignBooking(
|
||||||
@Param("bookingUid") bookingUid: string,
|
@Param("bookingUid") bookingUid: string,
|
||||||
@@ -311,7 +353,8 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Reassign a booking to a specific host",
|
summary: "Reassign a booking to a specific host",
|
||||||
description: "The provided authorization header refers to the owner of the booking.",
|
description:
|
||||||
|
"Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.",
|
||||||
})
|
})
|
||||||
async reassignBookingToUser(
|
async reassignBookingToUser(
|
||||||
@Param("bookingUid") bookingUid: string,
|
@Param("bookingUid") bookingUid: string,
|
||||||
@@ -400,7 +443,7 @@ export class BookingsController_2024_08_13 {
|
|||||||
@Permissions([BOOKING_READ])
|
@Permissions([BOOKING_READ])
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get 'Booking References' for a booking",
|
summary: "Get booking references",
|
||||||
})
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async getBookingReferences(
|
async getBookingReferences(
|
||||||
|
|||||||
+1
-2
@@ -874,9 +874,8 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
|||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||||
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||||
|
|
||||||
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
|
||||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||||
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||||
emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid = responseBody.data.uid;
|
emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid = responseBody.data.uid;
|
||||||
|
|||||||
+206
-78
@@ -14,29 +14,27 @@ import { Test } from "@nestjs/testing";
|
|||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||||
|
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
|
||||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { randomString } from "test/utils/randomString";
|
import { randomString } from "test/utils/randomString";
|
||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
|
||||||
|
|
||||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||||
import {
|
import {
|
||||||
|
CancelBookingInput_2024_08_13,
|
||||||
CancelSeatedBookingInput_2024_08_13,
|
CancelSeatedBookingInput_2024_08_13,
|
||||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||||
CreateSeatedBookingOutput_2024_08_13,
|
CreateSeatedBookingOutput_2024_08_13,
|
||||||
GetBookingOutput_2024_08_13,
|
GetBookingOutput_2024_08_13,
|
||||||
GetBookingsOutput_2024_08_13,
|
GetBookingsOutput_2024_08_13,
|
||||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
|
||||||
GetSeatedBookingOutput_2024_08_13,
|
GetSeatedBookingOutput_2024_08_13,
|
||||||
RescheduleSeatedBookingInput_2024_08_13,
|
RescheduleSeatedBookingInput_2024_08_13,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import {
|
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||||
CreateBookingInput_2024_08_13,
|
|
||||||
CreateRecurringBookingInput_2024_08_13,
|
|
||||||
} from "@calcom/platform-types";
|
|
||||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
describe("Bookings Endpoints 2024-08-13", () => {
|
describe("Bookings Endpoints 2024-08-13", () => {
|
||||||
@@ -51,11 +49,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
let schedulesService: SchedulesService_2024_04_15;
|
let schedulesService: SchedulesService_2024_04_15;
|
||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let oAuthClient: PlatformOAuthClient;
|
|
||||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
|
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
|
||||||
|
|
||||||
const userEmail = `seated-bookings-user-${randomString()}@api.com`;
|
const userEmail = `seated-bookings-user-${randomString()}@api.com`;
|
||||||
let user: User;
|
let user: User;
|
||||||
|
let apiKeyString: string;
|
||||||
|
|
||||||
let seatedEventTypeId: number;
|
let seatedEventTypeId: number;
|
||||||
const maxRecurrenceCount = 3;
|
const maxRecurrenceCount = 3;
|
||||||
@@ -63,6 +63,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
||||||
|
|
||||||
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
|
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
|
||||||
|
let createdSeatedBooking2: CreateSeatedBookingOutput_2024_08_13;
|
||||||
|
|
||||||
const emailAttendeeOne = `seated-bookings-attendee1-${randomString()}@api.com`;
|
const emailAttendeeOne = `seated-bookings-attendee1-${randomString()}@api.com`;
|
||||||
const nameAttendeeOne = `Attendee One ${randomString()}`;
|
const nameAttendeeOne = `Attendee One ${randomString()}`;
|
||||||
@@ -70,12 +71,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
const nameAttendeeTwo = `Attendee Two ${randomString()}`;
|
const nameAttendeeTwo = `Attendee Two ${randomString()}`;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await Test.createTestingModule({
|
||||||
userEmail,
|
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||||
Test.createTestingModule({
|
})
|
||||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
.useValue({
|
.useValue({
|
||||||
canActivate: () => true,
|
canActivate: () => true,
|
||||||
@@ -87,22 +85,21 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||||
|
bookingSeatRepositoryFixture = new BookingSeatRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
organization = await teamRepositoryFixture.create({
|
organization = await teamRepositoryFixture.create({
|
||||||
name: `seated-bookings-organization-${randomString()}`,
|
name: `seated-bookings-organization-${randomString()}`,
|
||||||
});
|
});
|
||||||
oAuthClient = await createOAuthClient(organization.id);
|
|
||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
platformOAuthClients: {
|
|
||||||
connect: {
|
|
||||||
id: oAuthClient.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
|
||||||
|
apiKeyString = `cal_test_${keyString}`;
|
||||||
|
|
||||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||||
name: `seated-bookings-2024-08-13-schedule-${randomString()}`,
|
name: `seated-bookings-2024-08-13-schedule-${randomString()}`,
|
||||||
timeZone: "Europe/Rome",
|
timeZone: "Europe/Rome",
|
||||||
@@ -118,6 +115,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
seatsShowAttendees: true,
|
seatsShowAttendees: true,
|
||||||
seatsShowAvailabilityCount: true,
|
seatsShowAvailabilityCount: true,
|
||||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||||
|
metadata: {
|
||||||
|
disableStandardEmails: {
|
||||||
|
all: {
|
||||||
|
attendee: true,
|
||||||
|
host: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
user.id
|
user.id
|
||||||
);
|
);
|
||||||
@@ -129,19 +134,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function createOAuthClient(organizationId: number) {
|
|
||||||
const data = {
|
|
||||||
logo: "logo-url",
|
|
||||||
name: "name",
|
|
||||||
redirectUris: ["http://localhost:5555"],
|
|
||||||
permissions: 32,
|
|
||||||
};
|
|
||||||
const secret = "secret";
|
|
||||||
|
|
||||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
it("should book an event type with seats for the first time", async () => {
|
it("should book an event type with seats for the first time", async () => {
|
||||||
const body: CreateBookingInput_2024_08_13 = {
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||||
@@ -333,6 +325,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.get("/v2/bookings?sortCreated=asc")
|
.get("/v2/bookings?sortCreated=asc")
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
@@ -350,6 +343,22 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not be able to reschedule seated booking if seatUid is not provided", async () => {
|
||||||
|
const body = {
|
||||||
|
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${createdSeatedBooking.uid}/reschedule`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
`Booking with uid=${createdSeatedBooking.uid} is a seated booking which means you have to provide seatUid in the request body to specify which seat specifically you want to reschedule. First, fetch the booking using https://cal.com/docs/api-reference/v2/bookings/get-a-booking and then within the attendees array you will find the seatUid of the booking you want to reschedule. Second, provide the seatUid in the request body to specify which seat specifically you want to reschedule using the reschedule endpoint https://cal.com/docs/api-reference/v2/bookings/reschedule-a-booking#option-2`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("should reschedule seated booking", async () => {
|
it("should reschedule seated booking", async () => {
|
||||||
const body: RescheduleSeatedBookingInput_2024_08_13 = {
|
const body: RescheduleSeatedBookingInput_2024_08_13 = {
|
||||||
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||||
@@ -409,67 +418,186 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should cancel seated booking", async () => {
|
describe("cancel seated booking", () => {
|
||||||
const body: CancelSeatedBookingInput_2024_08_13 = {
|
describe("cancel seated booking as attendee", () => {
|
||||||
seatUid: createdSeatedBooking.seatUid,
|
it("should cancel seated booking", async () => {
|
||||||
};
|
const body: CancelSeatedBookingInput_2024_08_13 = {
|
||||||
|
seatUid: createdSeatedBooking.seatUid,
|
||||||
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/bookings/${createdSeatedBooking.uid}/cancel`)
|
.post(`/v2/bookings/${createdSeatedBooking.uid}/cancel`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||||
expect(data.id).toBeDefined();
|
expect(data.id).toBeDefined();
|
||||||
expect(data.uid).toBeDefined();
|
expect(data.uid).toBeDefined();
|
||||||
expect(data.hosts[0].id).toEqual(user.id);
|
expect(data.hosts[0].id).toEqual(user.id);
|
||||||
expect(data.status).toEqual("cancelled");
|
expect(data.status).toEqual("cancelled");
|
||||||
expect(data.start).toEqual(createdSeatedBooking.start);
|
expect(data.start).toEqual(createdSeatedBooking.start);
|
||||||
expect(data.end).toEqual(createdSeatedBooking.end);
|
expect(data.end).toEqual(createdSeatedBooking.end);
|
||||||
expect(data.duration).toEqual(60);
|
expect(data.duration).toEqual(60);
|
||||||
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||||
expect(data.eventType).toEqual({
|
expect(data.eventType).toEqual({
|
||||||
id: seatedEventTypeId,
|
id: seatedEventTypeId,
|
||||||
slug: seatedEventSlug,
|
slug: seatedEventSlug,
|
||||||
|
});
|
||||||
|
expect(data.attendees.length).toEqual(0);
|
||||||
|
expect(data.location).toBeDefined();
|
||||||
|
expect(data.absentHost).toEqual(false);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid response data - expected booking but received array response");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
expect(data.attendees.length).toEqual(0);
|
|
||||||
expect(data.location).toBeDefined();
|
|
||||||
expect(data.absentHost).toEqual(false);
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid response data - expected booking but received array response");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cancel seated booking as host", () => {
|
||||||
|
it("should book an event type with seats", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: seatedEventTypeId,
|
||||||
|
attendee: {
|
||||||
|
name: nameAttendeeOne,
|
||||||
|
email: emailAttendeeOne,
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
bookingFieldsResponses: {
|
||||||
|
codingLanguage: "TypeScript",
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
userId: "100",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||||
|
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
expect(data.seatUid).toBeDefined();
|
||||||
|
const seatUid = data.seatUid;
|
||||||
|
expect(data.id).toBeDefined();
|
||||||
|
expect(data.uid).toBeDefined();
|
||||||
|
expect(data.hosts[0].id).toEqual(user.id);
|
||||||
|
expect(data.status).toEqual("accepted");
|
||||||
|
expect(data.start).toEqual(body.start);
|
||||||
|
expect(data.end).toEqual(
|
||||||
|
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||||
|
);
|
||||||
|
expect(data.duration).toEqual(60);
|
||||||
|
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||||
|
expect(data.eventType).toEqual({
|
||||||
|
id: seatedEventTypeId,
|
||||||
|
slug: seatedEventSlug,
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
seatUid,
|
||||||
|
bookingFieldsResponses: {
|
||||||
|
name: body.attendee.name,
|
||||||
|
...body.bookingFieldsResponses,
|
||||||
|
},
|
||||||
|
metadata: body.metadata,
|
||||||
|
});
|
||||||
|
expect(data.location).toBeDefined();
|
||||||
|
expect(data.absentHost).toEqual(false);
|
||||||
|
createdSeatedBooking2 = data;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected recurring booking but received non array response"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to cancel without cancellation reason", async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${createdSeatedBooking2.uid}/cancel`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.message).toEqual("Cancellation reason is required when you are the host");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cancel seated booking", async () => {
|
||||||
|
const body: CancelBookingInput_2024_08_13 = {
|
||||||
|
cancellationReason: "I will be travelling without internet",
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${createdSeatedBooking2.uid}/cancel`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||||
|
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||||
|
expect(data.id).toBeDefined();
|
||||||
|
expect(data.uid).toBeDefined();
|
||||||
|
expect(data.hosts[0].id).toEqual(user.id);
|
||||||
|
expect(data.status).toEqual("cancelled");
|
||||||
|
expect(data.start).toEqual(createdSeatedBooking2.start);
|
||||||
|
expect(data.end).toEqual(createdSeatedBooking2.end);
|
||||||
|
expect(data.duration).toEqual(60);
|
||||||
|
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||||
|
expect(data.eventType).toEqual({
|
||||||
|
id: seatedEventTypeId,
|
||||||
|
slug: seatedEventSlug,
|
||||||
|
});
|
||||||
|
expect(data.attendees.length).toEqual(0);
|
||||||
|
expect(data.location).toBeDefined();
|
||||||
|
expect(data.absentHost).toEqual(false);
|
||||||
|
expect(data.cancellationReason).toEqual("I will be travelling without internet");
|
||||||
|
|
||||||
|
const seats = await bookingSeatRepositoryFixture.findAllByBookingId(data.id);
|
||||||
|
expect(seats.length).toEqual(0);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid response data - expected booking but received array response");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function responseDataIsCreateSeatedBooking(data: any): data is CreateSeatedBookingOutput_2024_08_13 {
|
function responseDataIsCreateSeatedBooking(data: any): data is CreateSeatedBookingOutput_2024_08_13 {
|
||||||
return data.hasOwnProperty("seatUid");
|
return data.hasOwnProperty("seatUid");
|
||||||
}
|
}
|
||||||
|
|
||||||
function responseDataIsCreateRecurringSeatedBooking(
|
|
||||||
data: any
|
|
||||||
): data is CreateRecurringSeatedBookingOutput_2024_08_13[] {
|
|
||||||
return Array.isArray(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function responseDataIsGetSeatedBooking(data: any): data is GetSeatedBookingOutput_2024_08_13 {
|
function responseDataIsGetSeatedBooking(data: any): data is GetSeatedBookingOutput_2024_08_13 {
|
||||||
return data?.attendees?.every((attendee: any) => attendee?.hasOwnProperty("seatUid"));
|
return data?.attendees?.every((attendee: any) => attendee?.hasOwnProperty("seatUid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function responseDataIsGetRecurringSeatedBooking(
|
|
||||||
data: any
|
|
||||||
): data is GetRecurringSeatedBookingOutput_2024_08_13[] {
|
|
||||||
return Array.isArray(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
await userRepositoryFixture.deleteByEmail(user.email);
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
X_CAL_CLIENT_ID,
|
X_CAL_CLIENT_ID,
|
||||||
} from "@calcom/platform-constants";
|
} from "@calcom/platform-constants";
|
||||||
import { AttendeeScheduledEmail, OrganizerScheduledEmail } from "@calcom/platform-libraries/emails";
|
import { AttendeeScheduledEmail, OrganizerScheduledEmail } from "@calcom/platform-libraries/emails";
|
||||||
|
import { EventManager } from "@calcom/platform-libraries/event-types";
|
||||||
import {
|
import {
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
FAILED_EVENT_TYPE_IDENTIFICATION_ERROR_MESSAGE,
|
FAILED_EVENT_TYPE_IDENTIFICATION_ERROR_MESSAGE,
|
||||||
@@ -78,6 +79,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
const eventTypeSlug = `user-bookings-event-type-${randomString()}`;
|
const eventTypeSlug = `user-bookings-event-type-${randomString()}`;
|
||||||
let recurringEventTypeId: number;
|
let recurringEventTypeId: number;
|
||||||
const recurringEventTypeSlug = `user-bookings-event-type-${randomString()}`;
|
const recurringEventTypeSlug = `user-bookings-event-type-${randomString()}`;
|
||||||
|
let eventTypeRequiresConfirmationId: number;
|
||||||
|
const eventTypeRequiresConfirmationSlug = `user-bookings-event-type-requires-confirmation-${randomString()}`;
|
||||||
|
|
||||||
let createdBooking: BookingOutput_2024_08_13;
|
let createdBooking: BookingOutput_2024_08_13;
|
||||||
let rescheduledBooking: BookingOutput_2024_08_13;
|
let rescheduledBooking: BookingOutput_2024_08_13;
|
||||||
@@ -153,6 +156,17 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
);
|
);
|
||||||
recurringEventTypeId = recurringEvent.id;
|
recurringEventTypeId = recurringEvent.id;
|
||||||
|
|
||||||
|
const eventTypeRequiresConfirmation = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `user-bookings-2024-08-13-event-type-requires-confirmation-${randomString()}`,
|
||||||
|
slug: eventTypeRequiresConfirmationSlug,
|
||||||
|
length: 60,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
},
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id;
|
||||||
|
|
||||||
bookingInThePast = await bookingsRepositoryFixture.create({
|
bookingInThePast = await bookingsRepositoryFixture.create({
|
||||||
user: {
|
user: {
|
||||||
connect: {
|
connect: {
|
||||||
@@ -1411,8 +1425,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||||
expect(data.status).toEqual("cancelled");
|
expect(data.status).toEqual("cancelled");
|
||||||
|
expect(data.rescheduledToUid).toEqual(rescheduledBooking.uid);
|
||||||
createdBooking = data;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1568,17 +1581,55 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("cancel bookings", () => {
|
describe("cancel bookings", () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||||
|
});
|
||||||
|
|
||||||
it("should cancel booking", async () => {
|
it("should cancel booking", async () => {
|
||||||
|
const createBody: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||||
|
eventTypeId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper Cancel",
|
||||||
|
email: "mr_proper_cancel@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
bookingFieldsResponses: {
|
||||||
|
customField: "customValue",
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
userId: "100",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const createResponse = await request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(createBody)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const createResponseBody: CreateBookingOutput_2024_08_13 = createResponse.body;
|
||||||
|
expect(responseDataIsBooking(createResponseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (!responseDataIsBooking(createResponseBody.data)) {
|
||||||
|
throw new Error("Failed to create booking for test");
|
||||||
|
}
|
||||||
|
|
||||||
|
const testBooking: BookingOutput_2024_08_13 = createResponseBody.data;
|
||||||
|
|
||||||
|
const booking = await bookingsRepositoryFixture.getByUid(testBooking.uid);
|
||||||
|
expect(booking).toBeDefined();
|
||||||
|
expect(booking?.status).toEqual("ACCEPTED");
|
||||||
|
|
||||||
const body: CancelBookingInput_2024_08_13 = {
|
const body: CancelBookingInput_2024_08_13 = {
|
||||||
cancellationReason: "Going on a vacation",
|
cancellationReason: "Going on a vacation",
|
||||||
};
|
};
|
||||||
|
|
||||||
const booking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid);
|
|
||||||
expect(booking).toBeDefined();
|
|
||||||
expect(booking?.status).toEqual("ACCEPTED");
|
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/bookings/${rescheduledBooking.uid}/cancel`)
|
.post(`/v2/bookings/${testBooking.uid}/cancel`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
@@ -1597,27 +1648,61 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
expect(data.hosts[0].email).toEqual(user.email);
|
expect(data.hosts[0].email).toEqual(user.email);
|
||||||
expect(data.status).toEqual("cancelled");
|
expect(data.status).toEqual("cancelled");
|
||||||
expect(data.cancellationReason).toEqual(body.cancellationReason);
|
expect(data.cancellationReason).toEqual(body.cancellationReason);
|
||||||
expect(data.start).toEqual(rescheduledBooking.start);
|
expect(data.start).toEqual(testBooking.start);
|
||||||
expect(data.end).toEqual(rescheduledBooking.end);
|
expect(data.end).toEqual(testBooking.end);
|
||||||
expect(data.duration).toEqual(rescheduledBooking.duration);
|
expect(data.duration).toEqual(testBooking.duration);
|
||||||
expect(data.eventTypeId).toEqual(rescheduledBooking.eventTypeId);
|
expect(data.eventTypeId).toEqual(testBooking.eventTypeId);
|
||||||
expect(data.attendees[0]).toEqual(rescheduledBooking.attendees[0]);
|
expect(data.attendees[0]).toEqual(testBooking.attendees[0]);
|
||||||
expect(data.location).toEqual(rescheduledBooking.location);
|
expect(data.location).toEqual(testBooking.location);
|
||||||
expect(data.absentHost).toEqual(rescheduledBooking.absentHost);
|
expect(data.absentHost).toEqual(testBooking.absentHost);
|
||||||
|
|
||||||
const cancelledBooking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid);
|
const cancelledBooking = await bookingsRepositoryFixture.getByUid(testBooking.uid);
|
||||||
expect(cancelledBooking).toBeDefined();
|
expect(cancelledBooking).toBeDefined();
|
||||||
expect(cancelledBooking?.status).toEqual("CANCELLED");
|
expect(cancelledBooking?.status).toEqual("CANCELLED");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should cancel recurring booking", async () => {
|
it("should cancel recurring booking", async () => {
|
||||||
|
const createBody: CreateRecurringBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: recurringEventTypeId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper Recurring Cancel",
|
||||||
|
email: "mr_proper_recurring_cancel@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
location: "https://meet.google.com/abc-def-ghi",
|
||||||
|
bookingFieldsResponses: {
|
||||||
|
customField: "customValue",
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
userId: "100",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const createResponse = await request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(createBody)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const createResponseBody: CreateBookingOutput_2024_08_13 = createResponse.body;
|
||||||
|
expect(responseDataIsRecurringBooking(createResponseBody.data)).toBe(true);
|
||||||
|
|
||||||
|
if (!responseDataIsRecurringBooking(createResponseBody.data)) {
|
||||||
|
throw new Error("Failed to create recurring booking for test");
|
||||||
|
}
|
||||||
|
|
||||||
|
const testRecurringBooking: RecurringBookingOutput_2024_08_13[] = createResponseBody.data;
|
||||||
|
|
||||||
const body: CancelBookingInput_2024_08_13 = {
|
const body: CancelBookingInput_2024_08_13 = {
|
||||||
cancellationReason: "Going on a vacation",
|
cancellationReason: "Going on a vacation",
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/bookings/${createdRecurringBooking[1].recurringBookingUid}/cancel`)
|
.post(`/v2/bookings/${testRecurringBooking[1].recurringBookingUid}/cancel`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
@@ -1630,7 +1715,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
|
|
||||||
if (responseDataIsRecurringBooking(responseBody.data)) {
|
if (responseDataIsRecurringBooking(responseBody.data)) {
|
||||||
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
|
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
|
||||||
expect(data.length).toEqual(4);
|
expect(data.length).toEqual(3);
|
||||||
|
|
||||||
const firstBooking = data[0];
|
const firstBooking = data[0];
|
||||||
expect(firstBooking.status).toEqual("cancelled");
|
expect(firstBooking.status).toEqual("cancelled");
|
||||||
@@ -1640,9 +1725,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
|
|
||||||
const thirdBooking = data[2];
|
const thirdBooking = data[2];
|
||||||
expect(thirdBooking.status).toEqual("cancelled");
|
expect(thirdBooking.status).toEqual("cancelled");
|
||||||
|
|
||||||
const fourthBooking = data[3];
|
|
||||||
expect(fourthBooking.status).toEqual("cancelled");
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected recurring booking but received non array response"
|
"Invalid response data - expected recurring booking but received non array response"
|
||||||
@@ -2399,6 +2481,478 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("cant't reschedule cancelled booking", () => {
|
||||||
|
it("should not be able to reschedule cancelled booking", async () => {
|
||||||
|
const cancelledBooking = await bookingsRepositoryFixture.create({
|
||||||
|
status: "CANCELLED",
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||||
|
title: "peer coding lets goo",
|
||||||
|
uid: `cancelled-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "integrations:daily",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: "Oldie",
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
name: "Oldie",
|
||||||
|
locale: "lv",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const body: RescheduleBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
|
||||||
|
reschedulingReason: "Flying to mars that day",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${cancelledBooking.uid}/reschedule`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
`Can't reschedule booking with uid=${cancelledBooking.uid} because it has been cancelled. Please provide uid of a booking that is not cancelled.`
|
||||||
|
);
|
||||||
|
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to reschedule rescheduled booking", async () => {
|
||||||
|
const rescheduledBooking = await bookingsRepositoryFixture.create({
|
||||||
|
status: "CANCELLED",
|
||||||
|
rescheduled: true,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||||
|
title: "peer coding lets goo",
|
||||||
|
uid: `cancelled-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "integrations:daily",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: "Oldie",
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
name: "Oldie",
|
||||||
|
locale: "lv",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const newBooking = await bookingsRepositoryFixture.create({
|
||||||
|
status: "ACCEPTED",
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fromReschedule: rescheduledBooking.uid,
|
||||||
|
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||||
|
title: "peer coding lets goo",
|
||||||
|
uid: `new-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "integrations:daily",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: "Oldie",
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
name: "Oldie",
|
||||||
|
locale: "lv",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const body: RescheduleBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
|
||||||
|
reschedulingReason: "Flying to mars that day",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${rescheduledBooking.uid}/reschedule`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
`Can't reschedule booking with uid=${rescheduledBooking.uid} because it has been cancelled and rescheduled already to booking with uid=${newBooking.uid}. You probably want to reschedule ${newBooking.uid} instead by passing it within the request URL.`
|
||||||
|
);
|
||||||
|
await bookingsRepositoryFixture.deleteById(rescheduledBooking.id);
|
||||||
|
await bookingsRepositoryFixture.deleteById(newBooking.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cant't cancel already cancelled booking", () => {
|
||||||
|
it("should not be able to cancel alraedy cancelled booking", async () => {
|
||||||
|
const cancelledBooking = await bookingsRepositoryFixture.create({
|
||||||
|
status: "CANCELLED",
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||||
|
title: "peer coding lets goo",
|
||||||
|
uid: `cancelled-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "integrations:daily",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: "Oldie",
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
name: "Oldie",
|
||||||
|
locale: "lv",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const body: CancelBookingInput_2024_08_13 = {
|
||||||
|
cancellationReason: "Going on a vacation",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${cancelledBooking.uid}/cancel`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
`Can't cancel booking with uid=${cancelledBooking.uid} because it has been cancelled already. Please provide uid of a booking that is not cancelled.`
|
||||||
|
);
|
||||||
|
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("calendar events", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
jest
|
||||||
|
.spyOn(EventManager.prototype, "create")
|
||||||
|
.mockImplementation(() => Promise.resolve({ results: [], referencesToCreate: [] }));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("platform oAuth client has calendar events enabled", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await oauthClientRepositoryFixture.update(oAuthClient.id, {
|
||||||
|
areCalendarEventsEnabled: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create calendar event when booking normal event", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 9, 9, 0, 0)).toISOString(),
|
||||||
|
eventTypeId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper",
|
||||||
|
email: "mr_proper@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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(EventManager.prototype.create).toHaveBeenCalledTimes(1);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create calendar events when booking recurring event", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 10, 9, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: recurringEventTypeId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper",
|
||||||
|
email: "mr_proper@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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(EventManager.prototype.create).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("event type requires confirmation", () => {
|
||||||
|
let bookingThatRequiresConfirmationUid: string;
|
||||||
|
|
||||||
|
it("should not create calendar event when booking event that requires confirmation", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 12, 9, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: eventTypeRequiresConfirmationId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper",
|
||||||
|
email: "mr_proper@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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(EventManager.prototype.create).toHaveBeenCalledTimes(0);
|
||||||
|
bookingThatRequiresConfirmationUid = data.uid;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create calendar event when confirming event that requires confirmation", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${bookingThatRequiresConfirmationUid}/confirm`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(200)
|
||||||
|
.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(EventManager.prototype.create).toHaveBeenCalledTimes(1);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("platform oAuth client has calendar events disabled", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await oauthClientRepositoryFixture.update(oAuthClient.id, {
|
||||||
|
areCalendarEventsEnabled: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create calendar event when booking", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 9, 10, 0, 0)).toISOString(),
|
||||||
|
eventTypeId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper",
|
||||||
|
email: "mr_proper@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeCreate = new Date();
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const afterCreate = new Date();
|
||||||
|
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(EventManager.prototype.create).toHaveBeenCalledTimes(0);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create calendar events when booking recurring event", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 11, 10, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: recurringEventTypeId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper",
|
||||||
|
email: "mr_proper@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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(EventManager.prototype.create).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("event type requires confirmation", () => {
|
||||||
|
let bookingThatRequiresConfirmationUid: string;
|
||||||
|
|
||||||
|
it("should not create calendar event when booking event that requires confirmation", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 12, 10, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: eventTypeRequiresConfirmationId,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Proper",
|
||||||
|
email: "mr_proper@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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(EventManager.prototype.create).toHaveBeenCalledTimes(0);
|
||||||
|
bookingThatRequiresConfirmationUid = data.uid;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create calendar event when confirming event that requires confirmation", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${bookingThatRequiresConfirmationUid}/confirm`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(200)
|
||||||
|
.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(EventManager.prototype.create).toHaveBeenCalledTimes(0);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
|
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
|
||||||
return (
|
return (
|
||||||
!Array.isArray(data) &&
|
!Array.isArray(data) &&
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servic
|
|||||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
|
import { getPagination } from "@/lib/pagination/pagination";
|
||||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||||
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
||||||
@@ -108,6 +109,12 @@ export class BookingsService_2024_08_13 {
|
|||||||
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
|
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventType.schedulingType === "MANAGED") {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Event type with id=${eventType.id} is the parent managed event type that can't be booked. You have to provide the child event type id aka id of event type that has been assigned to one of the users.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
body.eventTypeId = eventType.id;
|
body.eventTypeId = eventType.id;
|
||||||
|
|
||||||
if ("instant" in body && body.instant) {
|
if ("instant" in body && body.instant) {
|
||||||
@@ -397,6 +404,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
noEmail: bookingRequest.noEmail,
|
noEmail: bookingRequest.noEmail,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
});
|
});
|
||||||
const ids = bookings.map((booking) => booking.id || 0);
|
const ids = bookings.map((booking) => booking.id || 0);
|
||||||
return this.outputService.getOutputRecurringBookings(ids);
|
return this.outputService.getOutputRecurringBookings(ids);
|
||||||
@@ -417,6 +425,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
});
|
});
|
||||||
return this.outputService.getOutputCreateRecurringSeatedBookings(
|
return this.outputService.getOutputCreateRecurringSeatedBookings(
|
||||||
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" }))
|
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" }))
|
||||||
@@ -438,6 +447,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!booking.uid) {
|
if (!booking.uid) {
|
||||||
@@ -468,6 +478,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!booking.uid) {
|
if (!booking.uid) {
|
||||||
@@ -532,10 +543,13 @@ export class BookingsService_2024_08_13 {
|
|||||||
queryParams.attendeeEmail = await this.getAttendeeEmail(queryParams.attendeeEmail, user);
|
queryParams.attendeeEmail = await this.getAttendeeEmail(queryParams.attendeeEmail, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const skip = Math.abs(queryParams?.skip ?? 0);
|
||||||
|
const take = Math.abs(queryParams?.take ?? 100);
|
||||||
|
|
||||||
const fetchedBookings: { bookings: { id: number }[]; totalCount: number } = await getAllUserBookings({
|
const fetchedBookings: { bookings: { id: number }[]; totalCount: number } = await getAllUserBookings({
|
||||||
bookingListingByStatus: queryParams.status || [],
|
bookingListingByStatus: queryParams.status || [],
|
||||||
skip: queryParams.skip ?? 0,
|
skip,
|
||||||
take: queryParams.take ?? 100,
|
take,
|
||||||
filters: {
|
filters: {
|
||||||
...this.inputService.transformGetBookingsFilters(queryParams),
|
...this.inputService.transformGetBookingsFilters(queryParams),
|
||||||
...(userIds?.length ? { userIds } : {}),
|
...(userIds?.length ? { userIds } : {}),
|
||||||
@@ -582,34 +596,17 @@ export class BookingsService_2024_08_13 {
|
|||||||
} else if (isRecurring && isSeated) {
|
} else if (isRecurring && isSeated) {
|
||||||
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
|
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
|
||||||
} else if (isSeated) {
|
} else if (isSeated) {
|
||||||
formattedBookings.push(this.outputService.getOutputSeatedBooking(formatted));
|
formattedBookings.push(await this.outputService.getOutputSeatedBooking(formatted));
|
||||||
} else {
|
} else {
|
||||||
formattedBookings.push(this.outputService.getOutputBooking(formatted));
|
formattedBookings.push(await this.outputService.getOutputBooking(formatted));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const skip = Math.abs(queryParams?.skip ?? 0);
|
const pagination = getPagination({ skip, take, totalCount: fetchedBookings.totalCount });
|
||||||
const take = Math.abs(queryParams?.take ?? 100);
|
|
||||||
const itemsPerPage = take;
|
|
||||||
const totalPages = itemsPerPage !== 0 ? Math.ceil(fetchedBookings.totalCount / itemsPerPage) : 0;
|
|
||||||
const currentPage = Math.floor(skip / itemsPerPage) + 1;
|
|
||||||
const hasNextPage = skip + itemsPerPage < fetchedBookings.totalCount;
|
|
||||||
const hasPreviousPage = skip > 0;
|
|
||||||
return {
|
return {
|
||||||
bookings: formattedBookings,
|
bookings: formattedBookings,
|
||||||
pagination: {
|
pagination,
|
||||||
totalItems: fetchedBookings.totalCount,
|
|
||||||
// clamp remainingItems between 0 and totalCount
|
|
||||||
remainingItems: Math.min(
|
|
||||||
Math.max(fetchedBookings.totalCount - (skip + take), 0),
|
|
||||||
fetchedBookings.totalCount
|
|
||||||
),
|
|
||||||
itemsPerPage: itemsPerPage,
|
|
||||||
currentPage: currentPage,
|
|
||||||
totalPages: totalPages,
|
|
||||||
hasNextPage: hasNextPage,
|
|
||||||
hasPreviousPage: hasPreviousPage,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,6 +641,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
|
|
||||||
async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput) {
|
async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput) {
|
||||||
try {
|
try {
|
||||||
|
await this.canRescheduleBooking(bookingUid);
|
||||||
const bookingRequest = await this.inputService.createRescheduleBookingRequest(
|
const bookingRequest = await this.inputService.createRescheduleBookingRequest(
|
||||||
request,
|
request,
|
||||||
bookingUid,
|
bookingUid,
|
||||||
@@ -658,6 +656,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
});
|
});
|
||||||
if (!booking.uid) {
|
if (!booking.uid) {
|
||||||
throw new Error("Booking missing uid");
|
throw new Error("Booking missing uid");
|
||||||
@@ -689,15 +688,29 @@ export class BookingsService_2024_08_13 {
|
|||||||
}
|
}
|
||||||
return this.outputService.getOutputBooking(databaseBooking);
|
return this.outputService.getOutputBooking(databaseBooking);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
this.errorsBookingsService.handleBookingError(error, false);
|
||||||
if (error.message === "no_available_users_found_error") {
|
|
||||||
throw new BadRequestException("User either already has booking at this time or is not available");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async canRescheduleBooking(bookingUid: string) {
|
||||||
|
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||||
|
if (!booking) {
|
||||||
|
throw new Error(`Booking with uid=${bookingUid} was not found in the database`);
|
||||||
|
}
|
||||||
|
if (booking.status === "CANCELLED" && !booking.rescheduled) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Can't reschedule booking with uid=${bookingUid} because it has been cancelled. Please provide uid of a booking that is not cancelled.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (booking.status === "CANCELLED" && booking.rescheduled) {
|
||||||
|
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(bookingUid);
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Can't reschedule booking with uid=${bookingUid} because it has been cancelled and rescheduled already to booking with uid=${rescheduledTo?.uid}. You probably want to reschedule ${rescheduledTo?.uid} instead by passing it within the request URL.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return booking;
|
||||||
|
}
|
||||||
|
|
||||||
async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput) {
|
async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput) {
|
||||||
if (this.inputService.isCancelSeatedBody(body)) {
|
if (this.inputService.isCancelSeatedBody(body)) {
|
||||||
const seat = await this.bookingSeatRepository.getByReferenceUid(body.seatUid);
|
const seat = await this.bookingSeatRepository.getByReferenceUid(body.seatUid);
|
||||||
@@ -922,15 +935,19 @@ export class BookingsService_2024_08_13 {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||||
|
const userCalendars = await this.usersRepository.findByIdWithCalendars(requestUser.id);
|
||||||
|
|
||||||
await confirmBookingHandler({
|
await confirmBookingHandler({
|
||||||
ctx: {
|
ctx: {
|
||||||
user: requestUser,
|
user: {
|
||||||
|
...requestUser,
|
||||||
|
destinationCalendar: userCalendars?.destinationCalendar ?? null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
bookingId: booking.id,
|
bookingId: booking.id,
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
recurringEventId: booking.recurringEventId,
|
recurringEventId: booking.recurringEventId ?? undefined,
|
||||||
emailsEnabled,
|
emailsEnabled,
|
||||||
platformClientParams,
|
platformClientParams,
|
||||||
},
|
},
|
||||||
@@ -950,15 +967,19 @@ export class BookingsService_2024_08_13 {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||||
|
const userCalendars = await this.usersRepository.findByIdWithCalendars(requestUser.id);
|
||||||
|
|
||||||
await confirmBookingHandler({
|
await confirmBookingHandler({
|
||||||
ctx: {
|
ctx: {
|
||||||
user: requestUser,
|
user: {
|
||||||
|
...requestUser,
|
||||||
|
destinationCalendar: userCalendars?.destinationCalendar ?? null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
bookingId: booking.id,
|
bookingId: booking.id,
|
||||||
confirmed: false,
|
confirmed: false,
|
||||||
recurringEventId: booking.recurringEventId,
|
recurringEventId: booking.recurringEventId ?? undefined,
|
||||||
reason,
|
reason,
|
||||||
emailsEnabled,
|
emailsEnabled,
|
||||||
platformClientParams,
|
platformClientParams,
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
|
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||||
|
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getRecordingsOfCalVideoByRoomName,
|
||||||
|
getAllTranscriptsAccessLinkFromRoomName,
|
||||||
|
getDownloadLinkOfCalVideoByRecordingId,
|
||||||
|
} from "@calcom/platform-libraries/conferencing";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CalVideoService {
|
||||||
|
private readonly logger = new Logger("CalVideoService");
|
||||||
|
constructor(
|
||||||
|
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||||
|
private readonly outputService: OutputBookingsService_2024_08_13
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getRecordings(bookingUid: string) {
|
||||||
|
const booking = await this.bookingsRepository.getByUidWithBookingReference(bookingUid);
|
||||||
|
if (!booking) {
|
||||||
|
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomName =
|
||||||
|
booking?.references?.filter((reference) => reference.type === "daily_video")?.pop()?.meetingId ??
|
||||||
|
undefined;
|
||||||
|
if (!roomName) {
|
||||||
|
throw new NotFoundException(`No Cal Video reference found with booking uid ${bookingUid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordings = await getRecordingsOfCalVideoByRoomName(roomName);
|
||||||
|
|
||||||
|
if (!recordings || !("data" in recordings)) return [];
|
||||||
|
|
||||||
|
const recordingWithDownloadLink = recordings.data.map((recording) => {
|
||||||
|
return getDownloadLinkOfCalVideoByRecordingId(recording.id)
|
||||||
|
.then((res: { download_link: string } | undefined) => ({
|
||||||
|
id: recording.id,
|
||||||
|
roomName: recording.room_name,
|
||||||
|
startTs: recording.start_ts,
|
||||||
|
status: recording.status,
|
||||||
|
maxParticipants: recording.max_participants,
|
||||||
|
duration: recording.duration,
|
||||||
|
shareToken: recording.share_token,
|
||||||
|
downloadLink: res?.download_link,
|
||||||
|
}))
|
||||||
|
.catch((err: Error) => ({
|
||||||
|
id: recording.id,
|
||||||
|
roomName: recording.room_name,
|
||||||
|
startTs: recording.start_ts,
|
||||||
|
status: recording.status,
|
||||||
|
maxParticipants: recording.max_participants,
|
||||||
|
duration: recording.duration,
|
||||||
|
shareToken: recording.share_token,
|
||||||
|
downloadLink: null,
|
||||||
|
error: err.message,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
const allRecordingsWithDownloadLink = await Promise.all(recordingWithDownloadLink);
|
||||||
|
|
||||||
|
return allRecordingsWithDownloadLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTranscripts(bookingUid: string) {
|
||||||
|
const booking = await this.bookingsRepository.getByUidWithBookingReference(bookingUid);
|
||||||
|
if (!booking) {
|
||||||
|
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomName =
|
||||||
|
booking?.references?.filter((reference) => reference.type === "daily_video")?.pop()?.meetingId ??
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
if (!roomName) {
|
||||||
|
throw new NotFoundException(`No Cal Video reference found with booking uid ${bookingUid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transcripts = await getAllTranscriptsAccessLinkFromRoomName(roomName);
|
||||||
|
|
||||||
|
return transcripts;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||||
|
import { apiToInternalintegrationsMapping } from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||||
import { defaultBookingResponses } from "@/lib/safe-parse/default-responses-booking";
|
import { defaultBookingResponses } from "@/lib/safe-parse/default-responses-booking";
|
||||||
import { safeParse } from "@/lib/safe-parse/safe-parse";
|
import { safeParse } from "@/lib/safe-parse/safe-parse";
|
||||||
@@ -29,10 +30,7 @@ import { NextApiRequest } from "next/types";
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
|
||||||
EventTypeMetaDataSchema,
|
|
||||||
apiToInternalintegrationsMapping,
|
|
||||||
} from "@calcom/platform-libraries/event-types";
|
|
||||||
import {
|
import {
|
||||||
CancelBookingInput,
|
CancelBookingInput,
|
||||||
CancelBookingInput_2024_08_13,
|
CancelBookingInput_2024_08_13,
|
||||||
@@ -61,6 +59,7 @@ type OAuthRequestParams = {
|
|||||||
platformBookingUrl: string;
|
platformBookingUrl: string;
|
||||||
platformBookingLocation?: string;
|
platformBookingLocation?: string;
|
||||||
arePlatformEmailsEnabled: boolean;
|
arePlatformEmailsEnabled: boolean;
|
||||||
|
areCalendarEventsEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum Frequency {
|
export enum Frequency {
|
||||||
@@ -187,6 +186,31 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
guests,
|
guests,
|
||||||
location,
|
location,
|
||||||
},
|
},
|
||||||
|
...this.getRoutingFormData(inputBooking.routing),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRoutingFormData(
|
||||||
|
routing:
|
||||||
|
| {
|
||||||
|
teamMemberIds?: number[];
|
||||||
|
responseId?: number;
|
||||||
|
teamMemberEmail?: string;
|
||||||
|
skipContactOwner?: boolean;
|
||||||
|
crmAppSlug?: string;
|
||||||
|
crmOwnerRecordType?: string;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
) {
|
||||||
|
if (!routing) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
routedTeamMemberIds: routing.teamMemberIds,
|
||||||
|
routingFormResponseId: routing.responseId,
|
||||||
|
teamMemberEmail: routing.teamMemberEmail,
|
||||||
|
skipContactOwner: routing.skipContactOwner,
|
||||||
|
crmAppSlug: routing.crmAppSlug,
|
||||||
|
crmOwnerRecordType: routing.crmOwnerRecordType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,6 +478,7 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
location,
|
location,
|
||||||
},
|
},
|
||||||
schedulingType: eventType.schedulingType,
|
schedulingType: eventType.schedulingType,
|
||||||
|
...this.getRoutingFormData(inputBooking.routing),
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (timeBetween) {
|
switch (timeBetween) {
|
||||||
@@ -587,6 +612,11 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
if (!eventType) {
|
if (!eventType) {
|
||||||
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
|
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
|
||||||
}
|
}
|
||||||
|
if (eventType.seatsPerTimeSlot) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Booking with uid=${bookingUid} is a seated booking which means you have to provide seatUid in the request body to specify which seat specifically you want to reschedule. First, fetch the booking using https://cal.com/docs/api-reference/v2/bookings/get-a-booking and then within the attendees array you will find the seatUid of the booking you want to reschedule. Second, provide the seatUid in the request body to specify which seat specifically you want to reschedule using the reschedule endpoint https://cal.com/docs/api-reference/v2/bookings/reschedule-a-booking#option-2`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const bookingResponses = safeParse(
|
const bookingResponses = safeParse(
|
||||||
bookingResponsesSchema,
|
bookingResponsesSchema,
|
||||||
@@ -707,6 +737,12 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
|
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (booking.status === "CANCELLED") {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Can't cancel booking with uid=${bookingUid} because it has been cancelled already. Please provide uid of a booking that is not cancelled.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const oAuthClientParams = booking.eventTypeId
|
const oAuthClientParams = booking.eventTypeId
|
||||||
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
|
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
RecurringBookingOutput_2024_08_13,
|
RecurringBookingOutput_2024_08_13,
|
||||||
SeatedAttendee,
|
SeatedAttendee,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import { Booking, BookingReference, BookingSeat } from "@calcom/prisma/client";
|
import { Booking, BookingSeat } from "@calcom/prisma/client";
|
||||||
|
|
||||||
export const bookingResponsesSchema = z
|
export const bookingResponsesSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -90,7 +90,7 @@ type DatabaseMetadata = z.infer<typeof bookingMetadataSchema>;
|
|||||||
export class OutputBookingsService_2024_08_13 {
|
export class OutputBookingsService_2024_08_13 {
|
||||||
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
|
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
|
||||||
|
|
||||||
getOutputBooking(databaseBooking: DatabaseBooking) {
|
async getOutputBooking(databaseBooking: DatabaseBooking) {
|
||||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||||
@@ -101,6 +101,9 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
);
|
);
|
||||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||||
|
const rescheduledToUid = databaseBooking.rescheduled
|
||||||
|
? await this.getRescheduledToUid(databaseBooking.uid)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const booking = {
|
const booking = {
|
||||||
id: databaseBooking.id,
|
id: databaseBooking.id,
|
||||||
@@ -109,8 +112,9 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
description: databaseBooking.description,
|
description: databaseBooking.description,
|
||||||
hosts: [this.getHost(databaseBooking.user)],
|
hosts: [this.getHost(databaseBooking.user)],
|
||||||
status: databaseBooking.status.toLowerCase(),
|
status: databaseBooking.status.toLowerCase(),
|
||||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
cancellationReason:
|
||||||
cancelledByEmail: databaseBooking.cancelledBy || undefined,
|
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||||
|
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
|
||||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||||
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
||||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||||
@@ -137,6 +141,7 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
updatedAt: databaseBooking.updatedAt,
|
updatedAt: databaseBooking.updatedAt,
|
||||||
rating: databaseBooking.rating,
|
rating: databaseBooking.rating,
|
||||||
icsUid: databaseBooking.iCalUID,
|
icsUid: databaseBooking.iCalUID,
|
||||||
|
rescheduledToUid,
|
||||||
};
|
};
|
||||||
|
|
||||||
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||||
@@ -146,6 +151,11 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
return bookingTransformed;
|
return bookingTransformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRescheduledToUid(bookingUid: string) {
|
||||||
|
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(bookingUid);
|
||||||
|
return rescheduledTo?.uid;
|
||||||
|
}
|
||||||
|
|
||||||
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
||||||
if (databaseMetadata === null) return {};
|
if (databaseMetadata === null) return {};
|
||||||
|
|
||||||
@@ -204,8 +214,9 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
description: databaseBooking.description,
|
description: databaseBooking.description,
|
||||||
hosts: [this.getHost(databaseBooking.user)],
|
hosts: [this.getHost(databaseBooking.user)],
|
||||||
status: databaseBooking.status.toLowerCase(),
|
status: databaseBooking.status.toLowerCase(),
|
||||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
cancellationReason:
|
||||||
cancelledByEmail: databaseBooking.cancelledBy || undefined,
|
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||||
|
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
|
||||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||||
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
rescheduledByEmail: databaseBooking.rescheduledBy || undefined,
|
||||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||||
@@ -244,20 +255,23 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
return bookingTransformed;
|
return bookingTransformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutputCreateSeatedBooking(
|
async getOutputCreateSeatedBooking(
|
||||||
databaseBooking: DatabaseBooking,
|
databaseBooking: DatabaseBooking,
|
||||||
seatUid: string
|
seatUid: string
|
||||||
): CreateSeatedBookingOutput_2024_08_13 {
|
): Promise<CreateSeatedBookingOutput_2024_08_13> {
|
||||||
const getSeatedBookingOutput = this.getOutputSeatedBooking(databaseBooking);
|
const getSeatedBookingOutput = await this.getOutputSeatedBooking(databaseBooking);
|
||||||
return { ...getSeatedBookingOutput, seatUid };
|
return { ...getSeatedBookingOutput, seatUid };
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
|
async getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
|
||||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||||
|
const rescheduledToUid = databaseBooking.rescheduled
|
||||||
|
? await this.getRescheduledToUid(databaseBooking.uid)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const booking = {
|
const booking = {
|
||||||
id: databaseBooking.id,
|
id: databaseBooking.id,
|
||||||
@@ -266,6 +280,8 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
description: databaseBooking.description,
|
description: databaseBooking.description,
|
||||||
hosts: [this.getHost(databaseBooking.user)],
|
hosts: [this.getHost(databaseBooking.user)],
|
||||||
status: databaseBooking.status.toLowerCase(),
|
status: databaseBooking.status.toLowerCase(),
|
||||||
|
cancellationReason:
|
||||||
|
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||||
start: databaseBooking.startTime,
|
start: databaseBooking.startTime,
|
||||||
end: databaseBooking.endTime,
|
end: databaseBooking.endTime,
|
||||||
@@ -282,6 +298,7 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
updatedAt: databaseBooking.updatedAt,
|
updatedAt: databaseBooking.updatedAt,
|
||||||
rating: databaseBooking.rating,
|
rating: databaseBooking.rating,
|
||||||
icsUid: databaseBooking.iCalUID,
|
icsUid: databaseBooking.iCalUID,
|
||||||
|
rescheduledToUid,
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||||
@@ -374,7 +391,8 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
description: databaseBooking.description,
|
description: databaseBooking.description,
|
||||||
hosts: [this.getHost(databaseBooking.user)],
|
hosts: [this.getHost(databaseBooking.user)],
|
||||||
status: databaseBooking.status.toLowerCase(),
|
status: databaseBooking.status.toLowerCase(),
|
||||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
cancellationReason:
|
||||||
|
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||||
start: databaseBooking.startTime,
|
start: databaseBooking.startTime,
|
||||||
end: databaseBooking.endTime,
|
end: databaseBooking.endTime,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export class PlatformBookingsService {
|
|||||||
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
|
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
|
||||||
platformBookingUrl: oAuthClient.bookingRedirectUri,
|
platformBookingUrl: oAuthClient.bookingRedirectUri,
|
||||||
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
|
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
|
||||||
|
areCalendarEventsEnabled: oAuthClient.areCalendarEventsEnabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +79,7 @@ export class PlatformBookingsService {
|
|||||||
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
|
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
|
||||||
platformBookingUrl: oAuthClient.bookingRedirectUri,
|
platformBookingUrl: oAuthClient.bookingRedirectUri,
|
||||||
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
|
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
|
||||||
|
areCalendarEventsEnabled: oAuthClient.areCalendarEventsEnabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||||
|
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||||
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
||||||
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
|
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
|
||||||
@@ -25,8 +27,10 @@ import { Module } from "@nestjs/common";
|
|||||||
SelectedCalendarsRepository,
|
SelectedCalendarsRepository,
|
||||||
AppsRepository,
|
AppsRepository,
|
||||||
CalendarsRepository,
|
CalendarsRepository,
|
||||||
|
BookingsRepository_2024_08_13,
|
||||||
|
BookingReferencesRepository_2024_08_13,
|
||||||
],
|
],
|
||||||
controllers: [CalendarsController],
|
controllers: [CalendarsController],
|
||||||
exports: [CalendarsService],
|
exports: [CalendarsService, GoogleCalendarService],
|
||||||
})
|
})
|
||||||
export class CalendarsModule {}
|
export class CalendarsModule {}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
const credentialForCalendarRepositorySelect = Prisma.validator<Prisma.CredentialSelect>()({
|
const credentialForCalendarRepositorySelect = {
|
||||||
id: true,
|
id: true,
|
||||||
appId: true,
|
appId: true,
|
||||||
type: true,
|
type: true,
|
||||||
@@ -16,7 +16,7 @@ const credentialForCalendarRepositorySelect = Prisma.validator<Prisma.Credential
|
|||||||
teamId: true,
|
teamId: true,
|
||||||
key: true,
|
key: true,
|
||||||
invalid: true,
|
invalid: true,
|
||||||
});
|
} satisfies Prisma.CredentialSelect;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CalendarsRepository {
|
export class CalendarsRepository {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { CreateIcsFeedOutput, CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output";
|
import { CreateIcsFeedOutput, CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output";
|
||||||
|
import { ConnectedCalendarsData } from "@/ee/calendars/outputs/connected-calendars.output";
|
||||||
import { DeletedCalendarCredentialsOutputResponseDto } from "@/ee/calendars/outputs/delete-calendar-credentials.output";
|
import { DeletedCalendarCredentialsOutputResponseDto } from "@/ee/calendars/outputs/delete-calendar-credentials.output";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
@@ -30,7 +31,7 @@ import {
|
|||||||
GOOGLE_CALENDAR_ID,
|
GOOGLE_CALENDAR_ID,
|
||||||
} from "@calcom/platform-constants";
|
} from "@calcom/platform-constants";
|
||||||
import { OFFICE_365_CALENDAR_ID, OFFICE_365_CALENDAR_TYPE } from "@calcom/platform-constants";
|
import { OFFICE_365_CALENDAR_ID, OFFICE_365_CALENDAR_TYPE } from "@calcom/platform-constants";
|
||||||
import { ICS_CALENDAR } from "@calcom/platform-constants/apps";
|
import { ICS_CALENDAR, ICS_CALENDAR_TYPE } from "@calcom/platform-constants/apps";
|
||||||
import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
|
import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
|
||||||
|
|
||||||
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
||||||
@@ -247,6 +248,35 @@ describe("Platform Calendars Endpoints", () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`/GET/v2/calendars with access token`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/v2/calendars`)
|
||||||
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const data: ConnectedCalendarsData = response.body.data;
|
||||||
|
expect(data.connectedCalendars).toBeDefined();
|
||||||
|
expect(data.connectedCalendars).toHaveLength(3);
|
||||||
|
|
||||||
|
const googleConnectedCalendar = data.connectedCalendars.find(
|
||||||
|
(calendar) => calendar.integration.type === GOOGLE_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
const office365ConnectedCalendar = data.connectedCalendars.find(
|
||||||
|
(calendar) => calendar.integration.type === OFFICE_365_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
const icsConnectedCalendar = data.connectedCalendars.find(
|
||||||
|
(calendar) => calendar.integration.type === ICS_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(googleConnectedCalendar).toBeDefined();
|
||||||
|
expect(office365ConnectedCalendar).toBeDefined();
|
||||||
|
expect(icsConnectedCalendar).toBeDefined();
|
||||||
|
|
||||||
|
expect(data.destinationCalendar).toBeDefined();
|
||||||
|
expect(data.destinationCalendar.integration).toEqual(GOOGLE_CALENDAR_TYPE);
|
||||||
|
});
|
||||||
|
|
||||||
it.skip(`/POST/v2/calendars/${OFFICE_365_CALENDAR}/disconnect: it should respond with a 201 returning back the user deleted calendar credentials`, async () => {
|
it.skip(`/POST/v2/calendars/${OFFICE_365_CALENDAR}/disconnect: it should respond with a 201 returning back the user deleted calendar credentials`, async () => {
|
||||||
const body = {
|
const body = {
|
||||||
id: 10,
|
id: 10,
|
||||||
|
|||||||
@@ -54,6 +54,23 @@ import {
|
|||||||
} from "@calcom/platform-constants";
|
} from "@calcom/platform-constants";
|
||||||
import { ApiResponse, CalendarBusyTimesInput, CreateCalendarCredentialsInput } from "@calcom/platform-types";
|
import { ApiResponse, CalendarBusyTimesInput, CreateCalendarCredentialsInput } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
export interface CalendarState {
|
||||||
|
accessToken: string;
|
||||||
|
origin: string;
|
||||||
|
redir?: string;
|
||||||
|
isDryRun?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarStateSchema = z.object({
|
||||||
|
accessToken: z.string(),
|
||||||
|
origin: z.string(),
|
||||||
|
redir: z.string().optional(),
|
||||||
|
isDryRun: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => val === "true"),
|
||||||
|
});
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/v2/calendars",
|
path: "/v2/calendars",
|
||||||
version: API_VERSIONS_VALUES,
|
version: API_VERSIONS_VALUES,
|
||||||
@@ -140,7 +157,7 @@ export class CalendarsController {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Get("/:calendar/connect")
|
@Get("/:calendar/connect")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: "Get oAuth connect URL" })
|
@ApiOperation({ summary: "Get OAuth connect URL" })
|
||||||
@ApiQuery({
|
@ApiQuery({
|
||||||
name: "redir",
|
name: "redir",
|
||||||
required: false,
|
required: false,
|
||||||
@@ -181,32 +198,31 @@ export class CalendarsController {
|
|||||||
@Query("code") code: string,
|
@Query("code") code: string,
|
||||||
@Param("calendar") calendar: string
|
@Param("calendar") calendar: string
|
||||||
): Promise<{ url: string }> {
|
): Promise<{ url: string }> {
|
||||||
// state params contains our user access token
|
let stateObj: CalendarState;
|
||||||
const stateParams = new URLSearchParams(state);
|
|
||||||
const { accessToken, origin, redir, isDryRun } = z
|
try {
|
||||||
.object({
|
// First try to parse as JSON
|
||||||
accessToken: z.string(),
|
stateObj = JSON.parse(state) as CalendarState;
|
||||||
origin: z.string(),
|
} catch (e) {
|
||||||
redir: z.string().nullish().optional(),
|
// If JSON parsing fails, try URL params
|
||||||
isDryRun: z.string().nullish().optional(),
|
const stateParams = new URLSearchParams(state);
|
||||||
})
|
|
||||||
.parse({
|
const parsedState = calendarStateSchema.parse({
|
||||||
accessToken: stateParams.get("accessToken"),
|
accessToken: stateParams.get("accessToken"),
|
||||||
origin: stateParams.get("origin"),
|
origin: stateParams.get("origin"),
|
||||||
redir: stateParams.get("redir"),
|
redir: stateParams.get("redir") || undefined,
|
||||||
isDryRun: stateParams.get("isDryRun"),
|
isDryRun: stateParams.get("isDryRun"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stateObj = parsedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessToken, origin, redir, isDryRun } = stateObj;
|
||||||
switch (calendar) {
|
switch (calendar) {
|
||||||
case OFFICE_365_CALENDAR:
|
case OFFICE_365_CALENDAR:
|
||||||
return await this.outlookService.save(code, accessToken, origin, redir ?? "", isDryRun === "true");
|
return await this.outlookService.save(code, accessToken, origin, redir ?? "", !!isDryRun);
|
||||||
case GOOGLE_CALENDAR:
|
case GOOGLE_CALENDAR:
|
||||||
return await this.googleCalendarService.save(
|
return await this.googleCalendarService.save(code, accessToken, origin, redir ?? "", !!isDryRun);
|
||||||
code,
|
|
||||||
accessToken,
|
|
||||||
origin,
|
|
||||||
redir ?? "",
|
|
||||||
isDryRun === "true"
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Invalid calendar type, available calendars are: ",
|
"Invalid calendar type, available calendars are: ",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
|
|||||||
import {
|
import {
|
||||||
getConnectedDestinationCalendarsAndEnsureDefaultsInDb,
|
getConnectedDestinationCalendarsAndEnsureDefaultsInDb,
|
||||||
getBusyCalendarTimes,
|
getBusyCalendarTimes,
|
||||||
|
type EventBusyDate,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries";
|
||||||
import { Calendar } from "@calcom/platform-types";
|
import { Calendar } from "@calcom/platform-types";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
@@ -45,6 +46,7 @@ export class CalendarsService {
|
|||||||
...credential,
|
...credential,
|
||||||
delegatedTo: null,
|
delegatedTo: null,
|
||||||
delegatedToId: null,
|
delegatedToId: null,
|
||||||
|
delegationCredentialId: null,
|
||||||
}))
|
}))
|
||||||
.filter((credential) => !!credential);
|
.filter((credential) => !!credential);
|
||||||
}
|
}
|
||||||
@@ -88,18 +90,19 @@ export class CalendarsService {
|
|||||||
dateTo,
|
dateTo,
|
||||||
composedSelectedCalendars
|
composedSelectedCalendars
|
||||||
);
|
);
|
||||||
// @ts-expect-error Element implicitly has any type
|
const calendarBusyTimesConverted = calendarBusyTimes.map(
|
||||||
const calendarBusyTimesConverted = calendarBusyTimes.map((busyTime) => {
|
(busyTime: EventBusyDate & { timeZone?: string }) => {
|
||||||
const busyTimeStart = DateTime.fromJSDate(new Date(busyTime.start)).setZone(timezone);
|
const busyTimeStart = DateTime.fromJSDate(new Date(busyTime.start)).setZone(timezone);
|
||||||
const busyTimeEnd = DateTime.fromJSDate(new Date(busyTime.end)).setZone(timezone);
|
const busyTimeEnd = DateTime.fromJSDate(new Date(busyTime.end)).setZone(timezone);
|
||||||
const busyTimeStartDate = busyTimeStart.toJSDate();
|
const busyTimeStartDate = busyTimeStart.toJSDate();
|
||||||
const busyTimeEndDate = busyTimeEnd.toJSDate();
|
const busyTimeEndDate = busyTimeEnd.toJSDate();
|
||||||
return {
|
return {
|
||||||
...busyTime,
|
...busyTime,
|
||||||
start: busyTimeStartDate,
|
start: busyTimeStartDate,
|
||||||
end: busyTimeEndDate,
|
end: busyTimeEndDate,
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return calendarBusyTimesConverted;
|
return calendarBusyTimesConverted;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
|
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
|
||||||
|
import type { CalendarState } from "@/ee/calendars/controllers/calendars.controller";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
@@ -23,7 +24,7 @@ const CALENDAR_SCOPES = [
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleCalendarService implements OAuthCalendarApp {
|
export class GoogleCalendarService implements OAuthCalendarApp {
|
||||||
private redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
|
public readonly redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
|
||||||
private gcalResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
|
private gcalResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
|
||||||
private logger = new Logger("GcalService");
|
private logger = new Logger("GcalService");
|
||||||
|
|
||||||
@@ -65,12 +66,18 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
|||||||
|
|
||||||
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string, isDryRun?: boolean) {
|
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string, isDryRun?: boolean) {
|
||||||
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
|
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
|
||||||
|
const state: CalendarState = {
|
||||||
|
accessToken,
|
||||||
|
origin,
|
||||||
|
redir,
|
||||||
|
isDryRun,
|
||||||
|
};
|
||||||
|
|
||||||
const authUrl = oAuth2Client.generateAuthUrl({
|
const authUrl = oAuth2Client.generateAuthUrl({
|
||||||
access_type: "offline",
|
access_type: "offline",
|
||||||
scope: CALENDAR_SCOPES,
|
scope: CALENDAR_SCOPES,
|
||||||
prompt: "consent",
|
prompt: "consent",
|
||||||
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}&isDryRun=${isDryRun}`,
|
state: JSON.stringify(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
return authUrl;
|
return authUrl;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
|
import { OAuthCalendarApp } from "@/ee/calendars/calendars.interface";
|
||||||
|
import { CalendarState } from "@/ee/calendars/controllers/calendars.controller";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
@@ -60,6 +61,13 @@ export class OutlookService implements OAuthCalendarApp {
|
|||||||
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string, isDryRun?: boolean) {
|
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string, isDryRun?: boolean) {
|
||||||
const { client_id } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
|
const { client_id } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
|
||||||
|
|
||||||
|
const state: CalendarState = {
|
||||||
|
accessToken,
|
||||||
|
origin,
|
||||||
|
redir,
|
||||||
|
isDryRun,
|
||||||
|
};
|
||||||
|
|
||||||
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
|
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
|
||||||
const params = {
|
const params = {
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
@@ -67,7 +75,7 @@ export class OutlookService implements OAuthCalendarApp {
|
|||||||
client_id,
|
client_id,
|
||||||
prompt: "select_account",
|
prompt: "select_account",
|
||||||
redirect_uri: this.redirectUri,
|
redirect_uri: this.redirectUri,
|
||||||
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}&isDryRun=${isDryRun}`,
|
state: JSON.stringify(state),
|
||||||
};
|
};
|
||||||
|
|
||||||
const query = stringify(params);
|
const query = stringify(params);
|
||||||
|
|||||||
+26
-5
@@ -18,6 +18,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
|||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
@@ -33,13 +34,19 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
Delete,
|
Delete,
|
||||||
Query,
|
Query,
|
||||||
|
Headers,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import {
|
||||||
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
|
EVENT_TYPE_READ,
|
||||||
|
EVENT_TYPE_WRITE,
|
||||||
|
SUCCESS_STATUS,
|
||||||
|
X_CAL_CLIENT_ID,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
|
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries/event-types";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
@@ -51,7 +58,8 @@ import { PrismaClient } from "@calcom/prisma";
|
|||||||
export class EventTypesController_2024_04_15 {
|
export class EventTypesController_2024_04_15 {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly eventTypesService: EventTypesService_2024_04_15,
|
private readonly eventTypesService: EventTypesService_2024_04_15,
|
||||||
private readonly prismaReadService: PrismaReadService
|
private readonly prismaReadService: PrismaReadService,
|
||||||
|
private readonly organizationsRepository: OrganizationsRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -109,19 +117,32 @@ export class EventTypesController_2024_04_15 {
|
|||||||
async getPublicEventType(
|
async getPublicEventType(
|
||||||
@Param("username") username: string,
|
@Param("username") username: string,
|
||||||
@Param("eventSlug") eventSlug: string,
|
@Param("eventSlug") eventSlug: string,
|
||||||
@Query() queryParams: GetPublicEventTypeQueryParams_2024_04_15
|
@Query() queryParams: GetPublicEventTypeQueryParams_2024_04_15,
|
||||||
|
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||||
): Promise<GetEventTypePublicOutput> {
|
): Promise<GetEventTypePublicOutput> {
|
||||||
try {
|
try {
|
||||||
|
let orgSlug = queryParams.org;
|
||||||
|
|
||||||
|
if (clientId && !orgSlug && username.includes(`-${clientId}`)) {
|
||||||
|
const org = await this.organizationsRepository
|
||||||
|
.findTeamIdAndSlugFromClientId(clientId)
|
||||||
|
.catch(() => null);
|
||||||
|
if (org) {
|
||||||
|
orgSlug = org.slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const event = await getPublicEvent(
|
const event = await getPublicEvent(
|
||||||
username.toLowerCase(),
|
username.toLowerCase(),
|
||||||
eventSlug,
|
eventSlug,
|
||||||
queryParams.isTeamEvent,
|
queryParams.isTeamEvent,
|
||||||
queryParams.org || null,
|
orgSlug ?? null,
|
||||||
this.prismaReadService.prisma as unknown as PrismaClient,
|
this.prismaReadService.prisma as unknown as PrismaClient,
|
||||||
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
|
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
|
||||||
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: event,
|
data: event,
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { EventTypesController_2024_04_15 } from "@/ee/event-types/event-types_20
|
|||||||
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||||
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
|
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
@@ -9,7 +10,14 @@ import { UsersModule } from "@/modules/users/users.module";
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule],
|
imports: [
|
||||||
|
PrismaModule,
|
||||||
|
MembershipsModule,
|
||||||
|
TokensModule,
|
||||||
|
UsersModule,
|
||||||
|
SelectedCalendarsModule,
|
||||||
|
OrganizationsModule,
|
||||||
|
],
|
||||||
providers: [EventTypesRepository_2024_04_15, EventTypesService_2024_04_15],
|
providers: [EventTypesRepository_2024_04_15, EventTypesService_2024_04_15],
|
||||||
controllers: [EventTypesController_2024_04_15],
|
controllers: [EventTypesController_2024_04_15],
|
||||||
exports: [EventTypesService_2024_04_15, EventTypesRepository_2024_04_15],
|
exports: [EventTypesService_2024_04_15, EventTypesRepository_2024_04_15],
|
||||||
|
|||||||
+1
-1
@@ -5,6 +5,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 { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
|
||||||
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
||||||
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
|
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
|
||||||
|
import { systemBeforeFieldEmail } from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
updateEventType,
|
updateEventType,
|
||||||
EventTypesPublic,
|
EventTypesPublic,
|
||||||
getEventTypesPublic,
|
getEventTypesPublic,
|
||||||
systemBeforeFieldEmail,
|
|
||||||
} from "@calcom/platform-libraries/event-types";
|
} from "@calcom/platform-libraries/event-types";
|
||||||
import { EventType } from "@calcom/prisma/client";
|
import { EventType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OrganizerIntegrationLocation } from "@calcom/lib/event-types/transformers/internal/locations";
|
import { OrganizerIntegrationLocation } from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
|
|
||||||
type BaseEventType = {
|
type BaseEventType = {
|
||||||
length: number;
|
length: number;
|
||||||
|
|||||||
+146
-1
@@ -34,6 +34,7 @@ import {
|
|||||||
ApiSuccessResponse,
|
ApiSuccessResponse,
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
EventTypeOutput_2024_06_14,
|
EventTypeOutput_2024_06_14,
|
||||||
|
GuestsDefaultFieldOutput_2024_06_14,
|
||||||
NameDefaultFieldInput_2024_06_14,
|
NameDefaultFieldInput_2024_06_14,
|
||||||
NotesDefaultFieldInput_2024_06_14,
|
NotesDefaultFieldInput_2024_06_14,
|
||||||
UpdateEventTypeInput_2024_06_14,
|
UpdateEventTypeInput_2024_06_14,
|
||||||
@@ -535,6 +536,7 @@ describe("Event types Endpoints", () => {
|
|||||||
body.lockTimeZoneToggleOnBookingPage
|
body.lockTimeZoneToggleOnBookingPage
|
||||||
);
|
);
|
||||||
expect(createdEventType.color).toEqual(body.color);
|
expect(createdEventType.color).toEqual(body.color);
|
||||||
|
expect(createdEventType.disableGuests).toEqual(false);
|
||||||
|
|
||||||
const expectedBookingFields = [
|
const expectedBookingFields = [
|
||||||
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
||||||
@@ -962,6 +964,10 @@ describe("Event types Endpoints", () => {
|
|||||||
title: newTitle,
|
title: newTitle,
|
||||||
scheduleId: secondSchedule.id,
|
scheduleId: secondSchedule.id,
|
||||||
lengthInMinutesOptions: [15, 30],
|
lengthInMinutesOptions: [15, 30],
|
||||||
|
calVideoSettings: {
|
||||||
|
disableRecordingForGuests: true,
|
||||||
|
disableRecordingForOrganizer: true,
|
||||||
|
},
|
||||||
bookingFields: [
|
bookingFields: [
|
||||||
nameBookingField,
|
nameBookingField,
|
||||||
{
|
{
|
||||||
@@ -1037,6 +1043,7 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(updatedEventType.description).toEqual(eventType.description);
|
expect(updatedEventType.description).toEqual(eventType.description);
|
||||||
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||||
expect(updatedEventType.locations).toEqual(eventType.locations);
|
expect(updatedEventType.locations).toEqual(eventType.locations);
|
||||||
|
expect(updatedEventType.disableGuests).toEqual(false);
|
||||||
|
|
||||||
const expectedBookingFields = [
|
const expectedBookingFields = [
|
||||||
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
{ ...defaultResponseBookingFieldName, ...nameBookingField },
|
||||||
@@ -1082,6 +1089,12 @@ describe("Event types Endpoints", () => {
|
|||||||
body.lockTimeZoneToggleOnBookingPage
|
body.lockTimeZoneToggleOnBookingPage
|
||||||
);
|
);
|
||||||
expect(updatedEventType.color).toEqual(body.color);
|
expect(updatedEventType.color).toEqual(body.color);
|
||||||
|
expect(updatedEventType.calVideoSettings?.disableRecordingForGuests).toEqual(
|
||||||
|
body.calVideoSettings?.disableRecordingForGuests
|
||||||
|
);
|
||||||
|
expect(updatedEventType.calVideoSettings?.disableRecordingForOrganizer).toEqual(
|
||||||
|
body.calVideoSettings?.disableRecordingForOrganizer
|
||||||
|
);
|
||||||
|
|
||||||
eventType.title = newTitle;
|
eventType.title = newTitle;
|
||||||
eventType.scheduleId = secondSchedule.id;
|
eventType.scheduleId = secondSchedule.id;
|
||||||
@@ -1708,7 +1721,7 @@ describe("Event types Endpoints", () => {
|
|||||||
description: "undefined booking fields",
|
description: "undefined booking fields",
|
||||||
length: 40,
|
length: 40,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
slug: "undefined-booking-fields",
|
slug: `undefined-booking-fields-${randomString()}`,
|
||||||
locations: [],
|
locations: [],
|
||||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||||
};
|
};
|
||||||
@@ -1738,6 +1751,14 @@ describe("Event types Endpoints", () => {
|
|||||||
disableOnPrefill: false,
|
disableOnPrefill: false,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
disableOnPrefill: false,
|
||||||
|
hidden: true,
|
||||||
|
isDefault: true,
|
||||||
|
required: false,
|
||||||
|
slug: "attendeePhoneNumber",
|
||||||
|
type: "phone",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
type: "radioInput",
|
type: "radioInput",
|
||||||
@@ -1841,6 +1862,130 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(slug2).not.toEqual(slug);
|
expect(slug2).not.toEqual(slug);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should create event type with cal video settings", async () => {
|
||||||
|
const body: CreateEventTypeInput_2024_06_14 = {
|
||||||
|
title: "event type with cal video settings",
|
||||||
|
slug: "event-type-with-cal-video-settings",
|
||||||
|
lengthInMinutes: 60,
|
||||||
|
calVideoSettings: {
|
||||||
|
disableRecordingForGuests: true,
|
||||||
|
disableRecordingForOrganizer: true,
|
||||||
|
},
|
||||||
|
locations: [
|
||||||
|
{
|
||||||
|
type: "integration",
|
||||||
|
integration: "cal-video",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/api/v2/event-types")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(body)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const createdEventType = responseBody.data;
|
||||||
|
expect(createdEventType).toHaveProperty("id");
|
||||||
|
expect(createdEventType.title).toEqual(body.title);
|
||||||
|
expect(createdEventType.locations).toEqual(body.locations);
|
||||||
|
expect(createdEventType.calVideoSettings?.disableRecordingForGuests).toEqual(true);
|
||||||
|
expect(createdEventType.calVideoSettings?.disableRecordingForOrganizer).toEqual(true);
|
||||||
|
firstCreatedEventType = responseBody.data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toggle disable guests", () => {
|
||||||
|
let eventTypeWithGuestsDisabledId: number;
|
||||||
|
|
||||||
|
it("should create an event type with guests disabled", async () => {
|
||||||
|
const body: CreateEventTypeInput_2024_06_14 = {
|
||||||
|
title: "Coding class guests disabled",
|
||||||
|
slug: "coding-class-guests-disabled",
|
||||||
|
description: "Let's learn how to code like a pro.",
|
||||||
|
lengthInMinutes: 60,
|
||||||
|
disableGuests: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/api/v2/event-types")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(body)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const createdEventType = responseBody.data;
|
||||||
|
expect(createdEventType).toHaveProperty("id");
|
||||||
|
expect(createdEventType.title).toEqual(body.title);
|
||||||
|
expect(createdEventType.description).toEqual(body.description);
|
||||||
|
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
|
||||||
|
|
||||||
|
expect(createdEventType.disableGuests).toEqual(true);
|
||||||
|
const guestsBookingField = createdEventType.bookingFields.find(
|
||||||
|
(field) => field.slug === "guests"
|
||||||
|
) as GuestsDefaultFieldOutput_2024_06_14 | undefined;
|
||||||
|
expect(guestsBookingField?.hidden).toEqual(true);
|
||||||
|
|
||||||
|
eventTypeWithGuestsDisabledId = createdEventType.id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update an event type with guests enabled", async () => {
|
||||||
|
const body: UpdateEventTypeInput_2024_06_14 = {
|
||||||
|
disableGuests: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/event-types/${eventTypeWithGuestsDisabledId}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const updatedEventType = responseBody.data;
|
||||||
|
|
||||||
|
expect(updatedEventType.disableGuests).toEqual(false);
|
||||||
|
const guestsBookingField = updatedEventType.bookingFields.find(
|
||||||
|
(field) => field.slug === "guests"
|
||||||
|
) as GuestsDefaultFieldOutput_2024_06_14 | undefined;
|
||||||
|
expect(guestsBookingField?.hidden).toEqual(false);
|
||||||
|
|
||||||
|
eventTypeWithGuestsDisabledId = updatedEventType.id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update an event type with guests disabled", async () => {
|
||||||
|
const body: UpdateEventTypeInput_2024_06_14 = {
|
||||||
|
disableGuests: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/v2/event-types/${eventTypeWithGuestsDisabledId}`)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const updatedEventType = responseBody.data;
|
||||||
|
|
||||||
|
expect(updatedEventType.disableGuests).toEqual(true);
|
||||||
|
const guestsBookingField = updatedEventType.bookingFields.find(
|
||||||
|
(field) => field.slug === "guests"
|
||||||
|
) as GuestsDefaultFieldOutput_2024_06_14 | undefined;
|
||||||
|
expect(guestsBookingField?.hidden).toEqual(true);
|
||||||
|
|
||||||
|
eventTypeWithGuestsDisabledId = updatedEventType.id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (eventTypeWithGuestsDisabledId) {
|
||||||
|
await eventTypesRepositoryFixture.delete(eventTypeWithGuestsDisabledId);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
Loaded 100 of 1472 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user