Compare commits
641
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b3fbb672b | ||
|
|
2d5a86774e | ||
|
|
0865104a77 | ||
|
|
1914b37ee4 | ||
|
|
58ef0abb99 | ||
|
|
13ba680a73 | ||
|
|
f40c6da93b | ||
|
|
fc2e81e89a | ||
|
|
293dd1a140 | ||
|
|
b7fda31678 | ||
|
|
94a04f0dc2 | ||
|
|
2338f85dcf | ||
|
|
fbaccc8e5f | ||
|
|
750db38d38 | ||
|
|
8829e1ca3f | ||
|
|
ab703a4346 | ||
|
|
8d5a7b4e10 | ||
|
|
6ba3049314 | ||
|
|
abe92cd693 | ||
|
|
6c6cf0433a | ||
|
|
211b958c58 | ||
|
|
01a0d43943 | ||
|
|
3133eb5aeb | ||
|
|
76b68bebd7 | ||
|
|
c20e723e34 | ||
|
|
7b7478c0a9 | ||
|
+3 |
ebeb008f9b | ||
|
|
a90d765fca | ||
|
|
af51df2b4d | ||
|
|
076e93ca56 | ||
|
|
47f45409d2 | ||
|
|
759d5b8663 | ||
|
|
c554396572 | ||
|
|
fafd3fb181 | ||
|
|
eb05d547de | ||
|
|
1fa8a5ef25 | ||
|
|
5a350efb40 | ||
|
|
242f86891a | ||
|
|
ce6969d058 | ||
|
|
26f8246b91 | ||
|
|
4baa9b6bc1 | ||
|
|
02c86d0268 | ||
|
|
49f99d69a8 | ||
|
|
659d9cf767 | ||
|
|
4d662d31a1 | ||
|
|
6fe6540bc6 | ||
|
|
ee1904bb2c | ||
|
|
aaa59f1455 | ||
|
|
e29b7e83b8 | ||
|
|
6af473a486 | ||
|
|
bf8f966811 | ||
|
|
c64b663514 | ||
|
|
29314cde75 | ||
|
|
3ea73e75d6 | ||
|
|
d18e23378b | ||
|
|
736f1f594c | ||
|
|
09b1643478 | ||
|
|
86f6e8bac3 | ||
|
|
9c65d02e84 | ||
|
|
be9ab02a7e | ||
|
|
cfefba6d4d | ||
|
|
815e13303b | ||
|
|
7c8dbb3ea3 | ||
|
|
cabd459682 | ||
|
|
33aedbae20 | ||
|
|
d3fbc73a39 | ||
|
|
cfda4757e4 | ||
|
|
895ed48bd6 | ||
|
|
eda328d196 | ||
|
|
1c501d1501 | ||
|
|
08a57f32c5 | ||
|
|
b5a26bc227 | ||
|
|
737b019199 | ||
|
|
b41031626f | ||
|
|
a1fc4bccb9 | ||
|
|
d764b6a32d | ||
|
|
87cb8b170a | ||
|
|
badd0de33c | ||
|
|
124fd8ebca | ||
|
|
5d0dc3f546 | ||
|
|
e4baf15b53 | ||
|
|
a1bd8b6259 | ||
|
|
7987c9f45d | ||
|
|
781d0d92df | ||
|
|
b71d8baccc | ||
|
|
a71d949a0c | ||
|
|
e55cee443b | ||
|
|
c3634b3aba | ||
|
|
cd576f5378 | ||
|
|
2965450929 | ||
|
|
db9b9c45dc | ||
|
|
37f500526c | ||
|
|
006aecb066 | ||
|
|
0829929e98 | ||
|
|
a61148cb51 | ||
|
|
593c48c8b4 | ||
|
|
fb364971fa | ||
|
|
a2205d497d | ||
|
|
cf4e7a5d2c | ||
|
|
0e2ee3adb2 | ||
|
|
dbe8a85cf5 | ||
|
|
84210dc1b4 | ||
|
|
4061bfb3fe | ||
|
|
18059e50fa | ||
|
|
852eb346ed | ||
|
|
b7e11dc112 | ||
|
|
35a373e70a | ||
|
|
02c0b721b9 | ||
|
|
29349845f8 | ||
|
|
28cb2cff64 | ||
|
|
9cb2eb3b8c | ||
|
|
6ff20b98f4 | ||
|
|
aa618dbaa2 | ||
|
|
6e3de84067 | ||
|
|
c6b19200a9 | ||
|
|
91932212e3 | ||
|
|
f81bb92beb | ||
|
|
1d1a242a72 | ||
|
|
ad4af64036 | ||
|
+5 |
2cc7827b0e | ||
|
|
ef6618743d | ||
|
|
3601a9adf1 | ||
|
|
0cc2367556 | ||
|
|
f7b201b0b5 | ||
|
|
82063cc9a1 | ||
|
|
4b2f89d6e8 | ||
|
|
5360cb511d | ||
|
|
704bbf2757 | ||
|
|
7fef683abf | ||
|
|
63df3d9c14 | ||
|
|
32d2fd51e5 | ||
|
|
016a966057 | ||
|
|
dc967cffbe | ||
|
|
4489cb8fde | ||
|
|
babd514c64 | ||
|
|
2e5cc11f29 | ||
|
|
84bc330fa8 | ||
|
|
4cb7e4a91a | ||
|
|
d1bd05a791 | ||
|
|
96a6350143 | ||
|
|
c8bea5b143 | ||
|
|
1cdd8ebc53 | ||
|
|
d78ed21a9f | ||
|
|
4e92b6edeb | ||
|
|
7b2f811bfe | ||
|
|
7b6d5d6de3 | ||
|
|
12b418b532 | ||
|
|
3e299f9511 | ||
|
|
1b9691ddaf | ||
|
|
a11ea3dd1e | ||
|
|
b21400c613 | ||
|
|
6c936472d2 | ||
|
|
3b823670ea | ||
|
|
6b10127d97 | ||
|
|
9cd806ff61 | ||
|
|
628b169f53 | ||
|
|
19d7764e9d | ||
|
|
30061c0d58 | ||
|
|
ee073cdf35 | ||
|
|
4ff5f5f3fa | ||
|
|
f905ae7502 | ||
|
|
a1c0daa1b5 | ||
|
|
b155d736d8 | ||
|
|
e42013bee1 | ||
|
|
fde1d671fb | ||
|
|
9ccf5f3723 | ||
|
|
91ee225f14 | ||
|
|
e6a0a0fc69 | ||
|
|
e98024b08e | ||
|
|
8d54d80dee | ||
|
|
751aa62935 | ||
|
|
f149650911 | ||
|
|
f901fba0fc | ||
|
|
cd50719b03 | ||
|
|
000324c02d | ||
|
|
cc6beedfb0 | ||
|
|
1d28215c5e | ||
|
|
045ea34580 | ||
|
|
3228d8d869 | ||
|
|
6b9115dfde | ||
|
|
6259b0b3cb | ||
|
|
f991669b8d | ||
|
|
b93568359a | ||
|
|
66b52bb19f | ||
|
|
b9c49b4567 | ||
|
|
29e1dcb43c | ||
|
|
3ab676794f | ||
|
|
877fdbf46a | ||
|
|
b05e1b4589 | ||
|
|
611f76bf68 | ||
|
|
9b886036ad | ||
|
|
c88ed8cae7 | ||
|
|
098b520129 | ||
|
|
eb4eebe6f9 | ||
|
|
16a7bb7bc5 | ||
|
|
ee9699e267 | ||
|
|
7813d3739f | ||
|
|
fa47456d5f | ||
|
|
dbaa0727ae | ||
|
|
42a4a38939 | ||
|
|
97bde083ca | ||
|
|
48bd12be32 | ||
|
|
0cbc8a3816 | ||
|
|
4e1048b221 | ||
|
|
cb78692347 | ||
|
|
3784421f02 | ||
|
|
e7535b9db3 | ||
|
|
bb4260c98b | ||
|
|
9d65b6f794 | ||
|
|
4874c6c2ee | ||
|
|
30a92a4d66 | ||
|
|
04ce054cf7 | ||
|
|
e901e8d189 | ||
|
|
0aa595ddac | ||
|
|
2115a045ec | ||
|
|
f67522dbbd | ||
|
|
dc5aef0dd4 | ||
|
|
e1d7631215 | ||
|
|
05e1ad0c91 | ||
|
|
cf392562da | ||
|
|
c8de8ca681 | ||
|
|
2c91c0e309 | ||
|
|
cef007d852 | ||
|
|
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 |
@@ -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,36 @@
|
|||||||
|
# Review Instructions
|
||||||
|
|
||||||
|
## File Naming Conventions
|
||||||
|
|
||||||
|
- **Repository Files**: Must include `Repository` suffix, prefix with technology if applicable (e.g., `PrismaAppRepository.ts`), use PascalCase matching exported class
|
||||||
|
- **Service Files**: Must include `Service` suffix, use PascalCase matching exported class, avoid generic names (e.g., `MembershipService.ts`)
|
||||||
|
- **New files**: Avoid dot-suffixes like `.service.ts` or `.repository.ts`; reserve `.test.ts`, `.spec.ts`, `.types.ts` for their specific purposes
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
- 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
|
||||||
|
- For large pull requests (>500 lines changed or >10 files touched), advise splitting into smaller, focused PRs:
|
||||||
|
- Split by feature boundaries: separate different features or user stories
|
||||||
|
- Split by layer/component: frontend changes, backend changes, database migrations, and tests in separate PRs
|
||||||
|
- Split by dependency chain: create PRs that can be merged sequentially
|
||||||
|
- Split by file/module: group related file changes together
|
||||||
|
- Suggested pattern: Database migrations → Backend logic → Frontend components → Integration tests
|
||||||
|
- Benefits: easier review, faster feedback, reduced conflicts, easier rollback, better history
|
||||||
@@ -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=
|
||||||
|
|||||||
+5
-1
@@ -1 +1,5 @@
|
|||||||
module.exports = require("./packages/config/eslint-preset");
|
// This configuration only applies to the package manager root.
|
||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
extends: ["./packages/config/eslint-preset.js"],
|
||||||
|
};
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
name: Atoms E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'packages/platform/atoms/**'
|
||||||
|
- 'packages/platform/examples/base/**'
|
||||||
|
- 'apps/api/v2/**'
|
||||||
|
- '.github/workflows/atoms-e2e.yml'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --max-old-space-size=8096
|
||||||
|
ATOMS_E2E_OAUTH_CLIENT_ID: ${{ secrets.ATOMS_E2E_OAUTH_CLIENT_ID }}
|
||||||
|
ATOMS_E2E_OAUTH_CLIENT_SECRET: ${{ secrets.ATOMS_E2E_OAUTH_CLIENT_SECRET }}
|
||||||
|
ATOMS_E2E_API_URL: ${{ secrets.ATOMS_E2E_API_URL }}
|
||||||
|
ATOMS_E2E_ORG_ID: ${{ secrets.ATOMS_E2E_ORG_ID }}
|
||||||
|
ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED: ${{ secrets.ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED }}
|
||||||
|
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
atoms-e2e:
|
||||||
|
timeout-minutes: 15
|
||||||
|
name: Atoms E2E Tests
|
||||||
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
|
steps:
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: ./.github/actions/yarn-install
|
||||||
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
|
- name: Run Atoms E2E Tests
|
||||||
|
working-directory: packages/platform/examples/base
|
||||||
|
run: yarn test:e2e
|
||||||
|
- name: Upload Test Results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: atoms-e2e-test-results
|
||||||
|
path: test-results
|
||||||
|
retention-days: 7
|
||||||
|
- name: Upload Playwright Report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: atoms-e2e-playwright-report
|
||||||
|
path: playwright-report
|
||||||
|
retention-days: 7
|
||||||
@@ -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
|
||||||
@@ -34,4 +34,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
VITE_BOOKER_EMBED_OAUTH_CLIENT_ID: ${{ secrets.VITE_BOOKER_EMBED_OAUTH_CLIENT_ID }}
|
||||||
|
VITE_BOOKER_EMBED_API_URL: ${{ secrets.VITE_BOOKER_EMBED_API_URL }}
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL: ${{ secrets.NEXT_PUBLIC_WEBAPP_URL }}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ env:
|
|||||||
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||||
|
SLOTS_CACHE_TTL: ${{ secrets.CI_SLOTS_CACHE_TTL }}
|
||||||
jobs:
|
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
|
||||||
|
|||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
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
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
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
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
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
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
@@ -95,6 +95,57 @@ Write with the future in mind. If there are trade-offs, edge cases, or temporary
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
## File Naming Conventions
|
||||||
|
|
||||||
|
To ensure consistency and make files easy to fuzzy-find, we follow the naming conventions below for **services**, **repositories**, and other class-based files.
|
||||||
|
|
||||||
|
### Repository Files
|
||||||
|
|
||||||
|
- Repository class files must include the `Repository` suffix.
|
||||||
|
- If the repository is backed by a specific technology (e.g. Prisma), prefix the filename and class name with it.
|
||||||
|
- File name must match the exported class exactly (PascalCase).
|
||||||
|
|
||||||
|
**Pattern:**
|
||||||
|
|
||||||
|
`Prisma<Entity>Repository.ts`
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// File: PrismaAppRepository.ts
|
||||||
|
export class PrismaAppRepository { ... }
|
||||||
|
|
||||||
|
// File: PrismaMembershipRepository.ts
|
||||||
|
export class PrismaMembershipRepository { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
This avoids ambiguous filenames like app.ts and improves discoverability in editors.
|
||||||
|
|
||||||
|
### Service Files
|
||||||
|
|
||||||
|
- Service class files must include the Service suffix.
|
||||||
|
- File name should be in PascalCase, matching the exported class.
|
||||||
|
- Keep naming specific — avoid generic names like AppService.ts.
|
||||||
|
|
||||||
|
**Pattern:**
|
||||||
|
|
||||||
|
`<Entity>Service.ts`
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// File: MembershipService.ts
|
||||||
|
export class MembershipService { ... }
|
||||||
|
|
||||||
|
// File: HashedLinkService.ts
|
||||||
|
export class HashedLinkService { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:**
|
||||||
|
|
||||||
|
- New files must avoid dot-suffixes like .service.ts or .repository.ts; these will be migrated from the existing codebase progressively.
|
||||||
|
- We still reserve suffixes such as .test.ts, .spec.ts, and .types.ts for their respective use cases.
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
[See README](https://github.com/calcom/cal.com#development)
|
[See README](https://github.com/calcom/cal.com#development)
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
|
|||||||
|
|
||||||
<a href="https://producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
This project is tested with browserstack
|
||||||
|
|
||||||
### Built With
|
### Built With
|
||||||
|
|
||||||
- [Next.js](https://nextjs.org/?ref=cal.com)
|
- [Next.js](https://nextjs.org/?ref=cal.com)
|
||||||
@@ -488,13 +490,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
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# Checkly Tests
|
# Checkly Tests
|
||||||
|
|
||||||
Run as `yarn checkly test`
|
Run as `yarn checkly test`
|
||||||
|
Optionally, run as `yarn checkly test --record` to be able to get detailed debugging view in Checkly UI
|
||||||
|
Run the tests for a particylar file as `yarn checkly test {filePattern}`
|
||||||
Deploy the tests as `yarn checkly deploy`
|
Deploy the tests as `yarn checkly deploy`
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("CSP Headers", () => {
|
||||||
|
test("Login page should have CSP header with nonce", async ({ page }) => {
|
||||||
|
const targetUrl = process.env.ENVIRONMENT_URL || "https://app.cal.com";
|
||||||
|
|
||||||
|
const response = await page.goto(`${targetUrl}/auth/login`);
|
||||||
|
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
|
||||||
|
const cspHeader = response?.headers()["content-security-policy"];
|
||||||
|
expect(cspHeader).toBeTruthy();
|
||||||
|
|
||||||
|
// Verify nonce is present in CSP header
|
||||||
|
const nonceMatch = cspHeader?.match(/'nonce-([^']+)'/);
|
||||||
|
expect(nonceMatch).toBeTruthy();
|
||||||
|
expect(nonceMatch![1]).toHaveLength(24);
|
||||||
|
expect(nonceMatch![1]).toMatch(/==$/);
|
||||||
|
|
||||||
|
await page.screenshot({ path: "screenshot.jpg" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
+19
-4
@@ -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,25 +7,40 @@ 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";
|
vi.mock("@calcom/lib/crypto", () => ({
|
||||||
|
symmetricDecrypt: vi.fn().mockReturnValue("mocked-decrypted-value"),
|
||||||
|
symmetricEncrypt: vi.fn().mockReturnValue("mocked-encrypted-value"),
|
||||||
|
}));
|
||||||
|
|
||||||
type CustomNextApiRequest = NextApiRequest & Request;
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
type CustomNextApiResponse = NextApiResponse & Response;
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubEnv("CALENDSO_ENCRYPTION_KEY", "22gfxhWUlcKliUeXcu8xNah2+HP/29ZX");
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockDeploymentRepository: IDeploymentRepository = {
|
||||||
|
getLicenseKeyWithId: vi.fn().mockResolvedValue("mockLicenseKey"), // Mocked return value
|
||||||
|
getSignatureToken: vi.fn().mockResolvedValue("mockSignatureToken"),
|
||||||
|
};
|
||||||
|
|
||||||
describe("Verify API key", () => {
|
describe("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(),
|
||||||
|
|||||||
@@ -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": "^5.8.3",
|
"typescript": "^5.9.0-beta",
|
||||||
"tzdata": "^1.0.30",
|
"tzdata": "^1.0.30",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getUserAvailability } from "@calcom/lib/getUserAvailability";
|
import { getUserAvailabilityService } from "@calcom/lib/di/containers/GetUserAvailability";
|
||||||
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 prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
@@ -191,8 +191,9 @@ const availabilitySchema = z
|
|||||||
async function handler(req: NextApiRequest) {
|
async function handler(req: NextApiRequest) {
|
||||||
const { isSystemWideAdmin, userId: reqUserId } = req;
|
const { isSystemWideAdmin, userId: reqUserId } = req;
|
||||||
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
||||||
|
const userAvailabilityService = getUserAvailabilityService();
|
||||||
if (!teamId)
|
if (!teamId)
|
||||||
return getUserAvailability({
|
return userAvailabilityService.getUserAvailability({
|
||||||
username,
|
username,
|
||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
@@ -230,7 +231,7 @@ async function handler(req: NextApiRequest) {
|
|||||||
const availabilities = members.map(async (user) => {
|
const availabilities = members.map(async (user) => {
|
||||||
return {
|
return {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
availability: await getUserAvailability({
|
availability: await userAvailabilityService.getUserAvailability({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||||
|
import { ErrorWithCode } from "@calcom/lib/errors";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
@@ -106,6 +108,11 @@ export async function getHandler(req: NextApiRequest) {
|
|||||||
eventType: expand.includes("team") ? { include: { team: true } } : false,
|
eventType: expand.includes("team") ? { include: { team: true } } : false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
throw new ErrorWithCode(ErrorCode.BookingNotFound, "Booking not found");
|
||||||
|
}
|
||||||
|
|
||||||
return { booking: schemaBookingReadPublic.parse(booking) };
|
return { booking: schemaBookingReadPublic.parse(booking) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -247,6 +254,14 @@ async function handler(req: NextApiRequest) {
|
|||||||
throw new HttpError({ statusCode: 400, message: knownError.message });
|
throw new HttpError({ statusCode: 400, message: knownError.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (knownError?.message === ErrorCode.RequestBodyInvalid) {
|
||||||
|
throw new HttpError({ statusCode: 400, message: knownError.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (knownError?.message === ErrorCode.EventTypeNotFound) {
|
||||||
|
throw new HttpError({ statusCode: 400, message: knownError.message });
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/AvailableSlots";
|
||||||
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) => {
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import prismaMock from "../../../../../../../tests/libs/__mocks__/prismaMock";
|
||||||
|
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, expect, test, vi, afterEach, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
import { buildBooking, buildEventType } from "@calcom/lib/test/builder";
|
||||||
|
|
||||||
|
import handler from "../../../../pages/api/bookings/[id]/_delete";
|
||||||
|
|
||||||
|
vi.mock("@calcom/features/bookings/lib/handleCancelBooking", () => ({
|
||||||
|
default: vi.fn().mockResolvedValue({ success: true }),
|
||||||
|
handleCancelBooking: vi.fn().mockResolvedValue({ success: true }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
const userId = 1;
|
||||||
|
const bookingId = 123;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
prismaMock.user.findUnique.mockResolvedValue({
|
||||||
|
id: userId,
|
||||||
|
email: "test@example.com",
|
||||||
|
name: "Test User",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DELETE /api/bookings/[id]", () => {
|
||||||
|
describe("Success", () => {
|
||||||
|
test("should cancel booking successfully", async () => {
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId: userId,
|
||||||
|
eventTypeId: buildEventType().id,
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "DELETE",
|
||||||
|
query: {
|
||||||
|
id: bookingId.toString(),
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
reason: "User requested cancellation",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const responseData = JSON.parse(res._getData());
|
||||||
|
expect(responseData.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should allow system-wide admin to cancel any booking", async () => {
|
||||||
|
const adminUserId = 999;
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId: userId,
|
||||||
|
eventTypeId: buildEventType().id,
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "DELETE",
|
||||||
|
query: {
|
||||||
|
id: bookingId.toString(),
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
reason: "Admin cancellation",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = adminUserId;
|
||||||
|
req.isSystemWideAdmin = true;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Errors", () => {
|
||||||
|
test("should return 404 when booking not found", async () => {
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "DELETE",
|
||||||
|
query: {
|
||||||
|
id: "999",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
reason: "Test cancellation",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 400 for invalid booking ID", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "DELETE",
|
||||||
|
query: {
|
||||||
|
id: "invalid",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
reason: "Test cancellation",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 403 when user doesn't have permission to cancel booking", async () => {
|
||||||
|
const otherUserId = 999;
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId: otherUserId,
|
||||||
|
eventTypeId: buildEventType().id,
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "DELETE",
|
||||||
|
query: {
|
||||||
|
id: bookingId.toString(),
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
reason: "Unauthorized cancellation",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 400 when required cancellation reason is missing", async () => {
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId: userId,
|
||||||
|
eventTypeId: buildEventType().id,
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "DELETE",
|
||||||
|
query: {
|
||||||
|
id: bookingId.toString(),
|
||||||
|
},
|
||||||
|
body: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import prismaMock from "../../../../../../../tests/libs/__mocks__/prismaMock";
|
||||||
|
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, expect, test, vi, afterEach, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
import { buildBooking, buildEventType } from "@calcom/lib/test/builder";
|
||||||
|
|
||||||
|
import handler from "../../../../pages/api/bookings/[id]/_patch";
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
const userId = 1;
|
||||||
|
const bookingId = 123;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
prismaMock.user.findUnique.mockResolvedValue({
|
||||||
|
id: userId,
|
||||||
|
email: "test@example.com",
|
||||||
|
name: "Test User",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("PATCH /api/bookings/[id]", () => {
|
||||||
|
describe("Success", () => {
|
||||||
|
test("should update booking successfully", async () => {
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId: userId,
|
||||||
|
title: "Original Title",
|
||||||
|
eventTypeId: buildEventType().id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedBooking = {
|
||||||
|
...mockBooking,
|
||||||
|
title: "Updated Title",
|
||||||
|
};
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
|
||||||
|
prismaMock.booking.update.mockResolvedValue(updatedBooking);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "PATCH",
|
||||||
|
query: {
|
||||||
|
id: bookingId.toString(),
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
title: "Updated Title",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const responseData = JSON.parse(res._getData());
|
||||||
|
expect(responseData.booking.title).toBe("Updated Title");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should allow system-wide admin to update any booking", async () => {
|
||||||
|
const adminUserId = 999;
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId: userId,
|
||||||
|
title: "Original Title",
|
||||||
|
eventTypeId: buildEventType().id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedBooking = {
|
||||||
|
...mockBooking,
|
||||||
|
title: "Admin Updated Title",
|
||||||
|
};
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
|
||||||
|
prismaMock.booking.update.mockResolvedValue(updatedBooking);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "PATCH",
|
||||||
|
query: {
|
||||||
|
id: bookingId.toString(),
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
title: "Admin Updated Title",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = adminUserId;
|
||||||
|
req.isSystemWideAdmin = true;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should update booking times successfully", async () => {
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId: userId,
|
||||||
|
startTime: new Date("2024-01-01T10:00:00Z"),
|
||||||
|
endTime: new Date("2024-01-01T11:00:00Z"),
|
||||||
|
eventTypeId: buildEventType().id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newStartTime = new Date("2024-01-01T14:00:00Z");
|
||||||
|
const newEndTime = new Date("2024-01-01T15:00:00Z");
|
||||||
|
|
||||||
|
const updatedBooking = {
|
||||||
|
...mockBooking,
|
||||||
|
startTime: newStartTime,
|
||||||
|
endTime: newEndTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
|
||||||
|
prismaMock.booking.update.mockResolvedValue(updatedBooking);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "PATCH",
|
||||||
|
query: {
|
||||||
|
id: bookingId.toString(),
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
startTime: newStartTime.toISOString(),
|
||||||
|
endTime: newEndTime.toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const responseData = JSON.parse(res._getData());
|
||||||
|
expect(new Date(responseData.booking.startTime)).toEqual(newStartTime);
|
||||||
|
expect(new Date(responseData.booking.endTime)).toEqual(newEndTime);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Errors", () => {
|
||||||
|
test("should return 404 when booking not found", async () => {
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "PATCH",
|
||||||
|
query: {
|
||||||
|
id: "999",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
title: "Updated Title",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 400 for invalid booking ID", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "PATCH",
|
||||||
|
query: {
|
||||||
|
id: "invalid",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
title: "Updated Title",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 403 when user doesn't have permission to update booking", async () => {
|
||||||
|
const otherUserId = 999;
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId: otherUserId,
|
||||||
|
eventTypeId: buildEventType().id,
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "PATCH",
|
||||||
|
query: {
|
||||||
|
id: bookingId.toString(),
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
title: "Unauthorized Update",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 401 when non-admin tries to change userId", async () => {
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId: userId,
|
||||||
|
eventTypeId: buildEventType().id,
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "PATCH",
|
||||||
|
query: {
|
||||||
|
id: bookingId.toString(),
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
userId: 999,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 400 for invalid date format", async () => {
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: bookingId,
|
||||||
|
userId: userId,
|
||||||
|
eventTypeId: buildEventType().id,
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue(mockBooking);
|
||||||
|
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "PATCH",
|
||||||
|
query: {
|
||||||
|
id: bookingId.toString(),
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
startTime: "invalid-date",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock";
|
||||||
|
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, expect, test, vi, afterEach, beforeEach } from "vitest";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { buildBooking } from "@calcom/lib/test/builder";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAccessibleUsers,
|
||||||
|
retrieveOrgScopedAccessibleUsers,
|
||||||
|
} from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
|
|
||||||
|
import { handler } from "../../../pages/api/bookings/_get";
|
||||||
|
|
||||||
|
vi.mock("~/lib/utils/retrieveScopedAccessibleUsers", () => ({
|
||||||
|
getAccessibleUsers: vi.fn(),
|
||||||
|
retrieveOrgScopedAccessibleUsers: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
const userId = 1;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
prismaMock.user.findUnique.mockResolvedValue({
|
||||||
|
id: userId,
|
||||||
|
email: "test@example.com",
|
||||||
|
name: "Test User",
|
||||||
|
});
|
||||||
|
(getAccessibleUsers as any).mockResolvedValue([userId]);
|
||||||
|
(retrieveOrgScopedAccessibleUsers as any).mockResolvedValue([userId]);
|
||||||
|
|
||||||
|
prismaMock.membership.findMany.mockResolvedValue([
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
id: 1,
|
||||||
|
isOrganization: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
prismaMock.user.findMany.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: userId,
|
||||||
|
email: "test@example.com",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /api/bookings", () => {
|
||||||
|
describe("Query parameter validation", () => {
|
||||||
|
test("should validate status parameter correctly", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
status: "invalid_status",
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await expect(handler(req)).rejects.toThrow(ZodError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept valid status parameter", async () => {
|
||||||
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||||
|
prismaMock.booking.count.mockResolvedValue(0);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
status: "upcoming",
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
const result = await handler(req);
|
||||||
|
expect(result.bookings).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should validate dateFrom parameter format", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
dateFrom: "invalid-date",
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await expect(handler(req)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should validate dateTo parameter format", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
dateTo: "invalid-date",
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await expect(handler(req)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should validate sortBy parameter", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
sortBy: "invalid_field",
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await expect(handler(req)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should validate order parameter", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
order: "invalid_order",
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await expect(handler(req)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Permission logic", () => {
|
||||||
|
test("should only return user's own bookings for regular user", async () => {
|
||||||
|
const mockBookings = [buildBooking({ id: 1, userId: userId }), buildBooking({ id: 2, userId: userId })];
|
||||||
|
|
||||||
|
prismaMock.booking.findMany.mockResolvedValue(mockBookings);
|
||||||
|
prismaMock.booking.count.mockResolvedValue(2);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
const result = await handler(req);
|
||||||
|
expect(result.bookings).toHaveLength(2);
|
||||||
|
expect(result.bookings.every((b) => b.userId === userId)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should allow system-wide admin to access all bookings", async () => {
|
||||||
|
const adminUserId = 999;
|
||||||
|
const mockBookings = [
|
||||||
|
buildBooking({ id: 1, userId: 1 }),
|
||||||
|
buildBooking({ id: 2, userId: 2 }),
|
||||||
|
buildBooking({ id: 3, userId: 3 }),
|
||||||
|
];
|
||||||
|
|
||||||
|
prismaMock.booking.findMany.mockResolvedValue(mockBookings);
|
||||||
|
prismaMock.booking.count.mockResolvedValue(3);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = adminUserId;
|
||||||
|
req.isSystemWideAdmin = true;
|
||||||
|
|
||||||
|
const result = await handler(req);
|
||||||
|
expect(result.bookings).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should allow org admin to access org bookings", async () => {
|
||||||
|
const orgAdminUserId = 999;
|
||||||
|
const mockBookings = [buildBooking({ id: 1, userId: 1 }), buildBooking({ id: 2, userId: 2 })];
|
||||||
|
|
||||||
|
prismaMock.booking.findMany.mockResolvedValue(mockBookings);
|
||||||
|
prismaMock.booking.count.mockResolvedValue(2);
|
||||||
|
|
||||||
|
(retrieveOrgScopedAccessibleUsers as any).mockResolvedValue([1, 2]);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = orgAdminUserId;
|
||||||
|
req.isOrganizationOwnerOrAdmin = true;
|
||||||
|
|
||||||
|
const result = await handler(req);
|
||||||
|
expect(result.bookings).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Filtering edge cases", () => {
|
||||||
|
test("should handle empty attendeeEmail array", async () => {
|
||||||
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||||
|
prismaMock.booking.count.mockResolvedValue(0);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
attendeeEmail: [],
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
const result = await handler(req);
|
||||||
|
expect(result.bookings).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle single attendeeEmail string", async () => {
|
||||||
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||||
|
prismaMock.booking.count.mockResolvedValue(0);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
attendeeEmail: "test@example.com",
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
const result = await handler(req);
|
||||||
|
expect(result.bookings).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle pagination edge cases", async () => {
|
||||||
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||||
|
prismaMock.booking.count.mockResolvedValue(0);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
pagination: {
|
||||||
|
take: 0,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
const result = await handler(req);
|
||||||
|
expect(result.bookings).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle large skip values", async () => {
|
||||||
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||||
|
prismaMock.booking.count.mockResolvedValue(5);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 1000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
const result = await handler(req);
|
||||||
|
expect(result.bookings).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Expand parameter functionality", () => {
|
||||||
|
test("should handle invalid expand parameter", async () => {
|
||||||
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||||
|
prismaMock.booking.count.mockResolvedValue(0);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
expand: "invalid_field",
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
await expect(handler(req)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle valid expand parameter", async () => {
|
||||||
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||||
|
prismaMock.booking.count.mockResolvedValue(0);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
expand: "team",
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = userId;
|
||||||
|
|
||||||
|
const result = await handler(req);
|
||||||
|
expect(result.bookings).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,30 +4,245 @@ import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock";
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
|
||||||
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
import { buildBooking, buildEventType, buildWebhook, buildUser } from "@calcom/lib/test/builder";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { Booking } from "@calcom/prisma/client";
|
import type { Booking } from "@calcom/prisma/client";
|
||||||
import { CreationSource } from "@calcom/prisma/enums";
|
import { CreationSource } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import handler from "../../../pages/api/bookings/_post";
|
import handler from "../../../pages/api/bookings/_post";
|
||||||
|
|
||||||
|
vi.mock("@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB", () => ({
|
||||||
|
getEventTypesFromDB: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockEventTypeData = {
|
||||||
|
eventType: {
|
||||||
|
id: 1,
|
||||||
|
title: "Test Event",
|
||||||
|
profile: { organizationId: null },
|
||||||
|
users: [{ id: 1, email: "test@example.com", credentials: [] }],
|
||||||
|
hosts: [],
|
||||||
|
customInputs: [],
|
||||||
|
recurringEvent: null,
|
||||||
|
length: 15,
|
||||||
|
slug: "test-event",
|
||||||
|
price: 0,
|
||||||
|
currency: "USD",
|
||||||
|
requiresConfirmation: false,
|
||||||
|
disableGuests: false,
|
||||||
|
minimumBookingNotice: 120,
|
||||||
|
beforeEventBuffer: 0,
|
||||||
|
afterEventBuffer: 0,
|
||||||
|
seatsPerTimeSlot: null,
|
||||||
|
seatsShowAttendees: false,
|
||||||
|
schedulingType: null,
|
||||||
|
periodType: "UNLIMITED",
|
||||||
|
periodStartDate: null,
|
||||||
|
periodEndDate: null,
|
||||||
|
periodDays: null,
|
||||||
|
periodCountCalendarDays: false,
|
||||||
|
locations: [],
|
||||||
|
metadata: {},
|
||||||
|
successRedirectUrl: null,
|
||||||
|
description: null,
|
||||||
|
team: null,
|
||||||
|
owner: { id: 1, email: "test@example.com", credentials: [] },
|
||||||
|
},
|
||||||
|
users: [{ id: 1, email: "test@example.com", credentials: [] }],
|
||||||
|
allCredentials: [],
|
||||||
|
destinationCalendar: null,
|
||||||
|
};
|
||||||
|
|
||||||
type CustomNextApiRequest = NextApiRequest & Request;
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
type CustomNextApiResponse = NextApiResponse & Response;
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
vi.mock("@calcom/features/webhooks/lib/sendPayload");
|
vi.mock("@calcom/features/webhooks/lib/sendOrSchedulePayload", () => ({
|
||||||
|
default: vi.fn().mockResolvedValue({}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockFindOriginalRescheduledBooking = vi.fn();
|
||||||
|
vi.mock("@calcom/lib/server/repository/booking", () => ({
|
||||||
|
BookingRepository: vi.fn().mockImplementation(() => ({
|
||||||
|
findOriginalRescheduledBooking: mockFindOriginalRescheduledBooking,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@calcom/features/watchlist/operations/check-if-users-are-blocked.controller", () => ({
|
||||||
|
checkIfUsersAreBlocked: vi.fn().mockResolvedValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@calcom/lib/di/containers/QualifiedHosts", () => ({
|
||||||
|
getQualifiedHostsService: vi.fn().mockReturnValue({
|
||||||
|
findQualifiedHostsWithDelegationCredentials: vi.fn().mockResolvedValue({
|
||||||
|
qualifiedRRHosts: [],
|
||||||
|
allFallbackRRHosts: [],
|
||||||
|
fixedHosts: [],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@calcom/lib/EventManager", () => ({
|
||||||
|
default: vi.fn().mockImplementation(() => ({
|
||||||
|
reschedule: vi.fn().mockResolvedValue({
|
||||||
|
results: [],
|
||||||
|
referencesToCreate: [],
|
||||||
|
}),
|
||||||
|
create: vi.fn().mockResolvedValue({
|
||||||
|
results: [],
|
||||||
|
referencesToCreate: [],
|
||||||
|
}),
|
||||||
|
update: vi.fn().mockResolvedValue({
|
||||||
|
results: [],
|
||||||
|
referencesToCreate: [],
|
||||||
|
}),
|
||||||
|
createAllCalendarEvents: vi.fn().mockResolvedValue([]),
|
||||||
|
updateAllCalendarEvents: vi.fn().mockResolvedValue([]),
|
||||||
|
deleteEventsAndMeetings: vi.fn().mockResolvedValue([]),
|
||||||
|
})),
|
||||||
|
placeholderCreatedEvent: {
|
||||||
|
results: [],
|
||||||
|
referencesToCreate: [],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@calcom/lib/availability", () => ({
|
||||||
|
getUserAvailability: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
start: new Date("1970-01-01T09:00:00.000Z"),
|
||||||
|
end: new Date("1970-01-01T17:00:00.000Z"),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
getAvailableSlots: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
attendees: 1,
|
||||||
|
bookingUid: null,
|
||||||
|
users: [2],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@calcom/features/bookings/lib/handleNewBooking/ensureAvailableUsers", () => ({
|
||||||
|
ensureAvailableUsers: vi.fn().mockImplementation(async (eventType) => {
|
||||||
|
return eventType.users || [{ id: 2, email: "test@example.com", name: "Test User", isFixed: false }];
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@calcom/lib/server/repository/profile", () => ({
|
||||||
|
ProfileRepository: {
|
||||||
|
findManyForUser: vi.fn().mockResolvedValue([]),
|
||||||
|
buildPersonalProfileFromUser: vi.fn().mockReturnValue({
|
||||||
|
id: null,
|
||||||
|
upId: "usr-2",
|
||||||
|
username: "test-user",
|
||||||
|
organizationId: null,
|
||||||
|
organization: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mock("@calcom/features/flags/features.repository", () => ({
|
||||||
|
FeaturesRepository: vi.fn().mockImplementation(() => ({
|
||||||
|
checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(false),
|
||||||
|
checkIfTeamHasFeature: vi.fn().mockResolvedValue(false),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@calcom/features/webhooks/lib/getWebhooks", () => ({
|
||||||
|
default: vi.fn().mockResolvedValue([]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@calcom/features/ee/workflows/lib/getAllWorkflows", () => ({
|
||||||
|
getAllWorkflows: vi.fn().mockResolvedValue([]),
|
||||||
|
workflowSelect: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@calcom/trpc/server/routers/viewer/workflows/util", () => ({
|
||||||
|
getAllWorkflowsFromEventType: vi.fn().mockResolvedValue([]),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@calcom/lib/server/i18n", () => {
|
vi.mock("@calcom/lib/server/i18n", () => {
|
||||||
|
const mockT = (key: string, options?: any) => {
|
||||||
|
if (key === "event_between_users") {
|
||||||
|
return `${options?.eventName} between ${options?.host} and ${options?.attendeeName}`;
|
||||||
|
}
|
||||||
|
if (key === "scheduler") {
|
||||||
|
return "Scheduler";
|
||||||
|
}
|
||||||
|
if (key === "google_meet_warning") {
|
||||||
|
return "Google Meet warning";
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getTranslation: (key: string) => key,
|
getTranslation: vi.fn().mockResolvedValue(mockT),
|
||||||
|
t: mockT,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skipIf(true)("POST /api/bookings", () => {
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue(
|
||||||
|
buildEventType({
|
||||||
|
id: 123,
|
||||||
|
profileId: null,
|
||||||
|
locations: [{ type: "integrations:daily" }],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
const statusCode = res._getStatusCode();
|
||||||
|
const responseData = JSON.parse(res._getData());
|
||||||
|
|
||||||
|
if (statusCode === 400) {
|
||||||
|
expect(responseData.message).not.toBe("Bad request, eventTypeId must be a number");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("POST /api/bookings", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockFindOriginalRescheduledBooking.mockResolvedValue(null);
|
||||||
|
|
||||||
|
(getEventTypesFromDB as any).mockResolvedValue(mockEventTypeData.eventType);
|
||||||
|
});
|
||||||
|
|
||||||
describe("Errors", () => {
|
describe("Errors", () => {
|
||||||
test("Missing required data", async () => {
|
test("Missing required data", async () => {
|
||||||
|
(getEventTypesFromDB as any).mockRejectedValue(new Error(ErrorCode.RequestBodyInvalid));
|
||||||
|
|
||||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {},
|
body: {},
|
||||||
@@ -35,102 +250,45 @@ describe.skipIf(true)("POST /api/bookings", () => {
|
|||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
|
||||||
expect(res.statusCode).toBe(400);
|
expect(res._getStatusCode()).toBe(400);
|
||||||
expect(JSON.parse(res._getData())).toEqual(
|
expect(JSON.parse(res._getData())).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
message:
|
message: ErrorCode.RequestBodyInvalid,
|
||||||
"invalid_type in 'eventTypeId': Required; invalid_type in 'title': Required; invalid_type in 'startTime': Required; invalid_type in 'startTime': Required; invalid_type in 'endTime': Required; invalid_type in 'endTime': Required",
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Invalid eventTypeId", async () => {
|
test("Invalid eventTypeId", async () => {
|
||||||
|
(getEventTypesFromDB as any).mockRejectedValue(new Error(ErrorCode.EventTypeNotFound));
|
||||||
|
|
||||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
title: "test",
|
title: "test",
|
||||||
eventTypeId: 2,
|
eventTypeId: 999,
|
||||||
startTime: dayjs().toDate(),
|
startTime: dayjs().add(1, "day").toDate(),
|
||||||
endTime: dayjs().add(1, "day").toDate(),
|
endTime: dayjs().add(1, "day").add(15, "minutes").toDate(),
|
||||||
},
|
},
|
||||||
prisma,
|
prisma,
|
||||||
});
|
});
|
||||||
|
|
||||||
prismaMock.eventType.findUnique.mockResolvedValue(null);
|
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
|
||||||
expect(res._getStatusCode()).toBe(400);
|
expect(res._getStatusCode()).toBe(400);
|
||||||
expect(JSON.parse(res._getData())).toEqual(
|
expect(JSON.parse(res._getData())).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
message:
|
message: ErrorCode.EventTypeNotFound,
|
||||||
"'invalid_type' in 'email': Required; 'invalid_type' in 'end': Required; 'invalid_type' in 'location': Required; 'invalid_type' in 'name': Required; 'invalid_type' in 'start': Required; 'invalid_type' in 'timeZone': Required; 'invalid_type' in 'language': Required; 'invalid_type' in 'customInputs': Required; 'invalid_type' in 'metadata': Required",
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Missing recurringCount", async () => {
|
test.skip("Missing recurringCount", async () => {
|
||||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
title: "test",
|
|
||||||
eventTypeId: 2,
|
|
||||||
startTime: dayjs().toDate(),
|
|
||||||
endTime: dayjs().add(1, "day").toDate(),
|
|
||||||
},
|
|
||||||
prisma,
|
|
||||||
});
|
|
||||||
|
|
||||||
prismaMock.eventType.findUnique.mockResolvedValue(
|
|
||||||
buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } })
|
|
||||||
);
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res._getStatusCode()).toBe(400);
|
|
||||||
expect(JSON.parse(res._getData())).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
message:
|
|
||||||
"'invalid_type' in 'email': Required; 'invalid_type' in 'end': Required; 'invalid_type' in 'location': Required; 'invalid_type' in 'name': Required; 'invalid_type' in 'start': Required; 'invalid_type' in 'timeZone': Required; 'invalid_type' in 'language': Required; 'invalid_type' in 'customInputs': Required; 'invalid_type' in 'metadata': Required",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Invalid recurringCount", async () => {
|
|
||||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
title: "test",
|
|
||||||
eventTypeId: 2,
|
|
||||||
startTime: dayjs().toDate(),
|
|
||||||
endTime: dayjs().add(1, "day").toDate(),
|
|
||||||
recurringCount: 15,
|
|
||||||
},
|
|
||||||
prisma,
|
|
||||||
});
|
|
||||||
|
|
||||||
prismaMock.eventType.findUnique.mockResolvedValue(
|
|
||||||
buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } })
|
|
||||||
);
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res._getStatusCode()).toBe(400);
|
|
||||||
expect(JSON.parse(res._getData())).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
message:
|
|
||||||
"'invalid_type' in 'email': Required; 'invalid_type' in 'end': Required; 'invalid_type' in 'location': Required; 'invalid_type' in 'name': Required; 'invalid_type' in 'start': Required; 'invalid_type' in 'timeZone': Required; 'invalid_type' in 'language': Required; 'invalid_type' in 'customInputs': Required; 'invalid_type' in 'metadata': Required",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("No available users", async () => {
|
|
||||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
name: "test",
|
name: "test",
|
||||||
start: dayjs().format(),
|
start: dayjs().add(1, "day").format(),
|
||||||
end: dayjs().add(1, "day").format(),
|
end: dayjs().add(1, "day").add(15, "minutes").format(),
|
||||||
eventTypeId: 2,
|
eventTypeId: 2,
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
location: "Cal.com Video",
|
location: "Cal.com Video",
|
||||||
@@ -143,15 +301,93 @@ describe.skipIf(true)("POST /api/bookings", () => {
|
|||||||
prisma,
|
prisma,
|
||||||
});
|
});
|
||||||
|
|
||||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue(buildEventType());
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
|
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
|
||||||
|
profile: { organizationId: null },
|
||||||
|
hosts: [],
|
||||||
|
users: [buildUser()],
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res._getStatusCode()).toBe(400);
|
||||||
|
expect(JSON.parse(res._getData())).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
message: expect.stringContaining("recurringCount"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("Invalid recurringCount", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
name: "test",
|
||||||
|
start: dayjs().add(1, "day").format(),
|
||||||
|
end: dayjs().add(1, "day").add(15, "minutes").format(),
|
||||||
|
eventTypeId: 2,
|
||||||
|
email: "test@example.com",
|
||||||
|
location: "Cal.com Video",
|
||||||
|
timeZone: "America/Montevideo",
|
||||||
|
language: "en",
|
||||||
|
customInputs: [],
|
||||||
|
metadata: {},
|
||||||
|
userId: 4,
|
||||||
|
recurringCount: 15,
|
||||||
|
},
|
||||||
|
prisma,
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
|
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
|
||||||
|
profile: { organizationId: null },
|
||||||
|
hosts: [],
|
||||||
|
users: [buildUser()],
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res._getStatusCode()).toBe(400);
|
||||||
|
expect(JSON.parse(res._getData())).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
message: expect.stringContaining("recurringCount"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("No available users", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
name: "test",
|
||||||
|
start: dayjs().add(1, "day").format(),
|
||||||
|
end: dayjs().add(1, "day").add(1, "hour").format(),
|
||||||
|
eventTypeId: 2,
|
||||||
|
email: "test@example.com",
|
||||||
|
location: "Cal.com Video",
|
||||||
|
timeZone: "America/Montevideo",
|
||||||
|
language: "en",
|
||||||
|
customInputs: [],
|
||||||
|
metadata: {},
|
||||||
|
userId: 4,
|
||||||
|
},
|
||||||
|
prisma,
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
|
...buildEventType({ profileId: null }),
|
||||||
|
profile: { organizationId: null },
|
||||||
|
hosts: [],
|
||||||
|
users: [buildUser()],
|
||||||
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
|
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
|
||||||
|
|
||||||
expect(res._getStatusCode()).toBe(500);
|
expect(res._getStatusCode()).toBe(400);
|
||||||
expect(JSON.parse(res._getData())).toEqual(
|
expect(JSON.parse(res._getData())).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
message: ErrorCode.NoAvailableUsersFound,
|
message: "Invalid event length",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -165,8 +401,8 @@ describe.skipIf(true)("POST /api/bookings", () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
name: "test",
|
name: "test",
|
||||||
start: dayjs().format(),
|
start: dayjs().add(1, "day").format(),
|
||||||
end: dayjs().add(1, "day").format(),
|
end: dayjs().add(1, "day").add(15, "minutes").format(),
|
||||||
eventTypeId: 2,
|
eventTypeId: 2,
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
location: "Cal.com Video",
|
location: "Cal.com Video",
|
||||||
@@ -179,8 +415,38 @@ describe.skipIf(true)("POST /api/bookings", () => {
|
|||||||
prisma,
|
prisma,
|
||||||
});
|
});
|
||||||
|
|
||||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue(buildEventType());
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
|
...buildEventType({ profileId: null, length: 15 }),
|
||||||
|
profile: { organizationId: null },
|
||||||
|
hosts: [],
|
||||||
|
users: [buildUser()],
|
||||||
|
});
|
||||||
prismaMock.booking.findMany.mockResolvedValue([]);
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: 1,
|
||||||
|
uid: "test-booking-uid",
|
||||||
|
title: "Test Event",
|
||||||
|
startTime: new Date(dayjs().add(1, "day").format()),
|
||||||
|
endTime: new Date(dayjs().add(1, "day").add(15, "minutes").format()),
|
||||||
|
eventTypeId: buildEventType({
|
||||||
|
id: 2,
|
||||||
|
profileId: null,
|
||||||
|
}).id,
|
||||||
|
oneTimePassword: null,
|
||||||
|
creationSource: "API_V1",
|
||||||
|
});
|
||||||
|
prismaMock.$transaction.mockImplementation(async (callback) => {
|
||||||
|
const mockTx = {
|
||||||
|
booking: {
|
||||||
|
create: prismaMock.booking.create.mockResolvedValue(mockBooking),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
app_RoutingForms_FormResponse: {
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return await callback(prismaMock);
|
||||||
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
|
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
|
||||||
@@ -189,12 +455,28 @@ describe.skipIf(true)("POST /api/bookings", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Reschedule created booking", async () => {
|
test("Reschedule created booking", async () => {
|
||||||
|
const originalBooking = buildBooking({
|
||||||
|
uid: "original-booking-uid",
|
||||||
|
userId: 4,
|
||||||
|
eventTypeId: buildEventType({
|
||||||
|
id: 2,
|
||||||
|
locations: [{ type: "integrations:daily" }],
|
||||||
|
}).id,
|
||||||
|
references: [],
|
||||||
|
});
|
||||||
|
mockFindOriginalRescheduledBooking.mockResolvedValue(originalBooking);
|
||||||
|
|
||||||
|
prismaMock.booking.findUnique.mockResolvedValue({
|
||||||
|
...originalBooking,
|
||||||
|
status: "cancelled",
|
||||||
|
});
|
||||||
|
|
||||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
name: "testReschedule",
|
name: "testReschedule",
|
||||||
start: dayjs().add(2, "day").format(),
|
start: dayjs().add(2, "day").format(),
|
||||||
end: dayjs().add(3, "day").format(),
|
end: dayjs().add(2, "day").add(15, "minutes").format(),
|
||||||
eventTypeId: 2,
|
eventTypeId: 2,
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
location: "Cal.com Video",
|
location: "Cal.com Video",
|
||||||
@@ -203,21 +485,52 @@ describe.skipIf(true)("POST /api/bookings", () => {
|
|||||||
customInputs: [],
|
customInputs: [],
|
||||||
metadata: {},
|
metadata: {},
|
||||||
userId: 4,
|
userId: 4,
|
||||||
rescheduleUid: createdBooking.uid,
|
rescheduleUid: "original-booking-uid",
|
||||||
},
|
},
|
||||||
prisma,
|
prisma,
|
||||||
});
|
});
|
||||||
|
|
||||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue(buildEventType());
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
|
...buildEventType({ profileId: null, length: 15 }),
|
||||||
|
profile: { organizationId: null },
|
||||||
|
hosts: [],
|
||||||
|
users: [buildUser()],
|
||||||
|
});
|
||||||
prismaMock.booking.findMany.mockResolvedValue([]);
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: 2,
|
||||||
|
uid: "test-reschedule-uid",
|
||||||
|
title: "Test Reschedule Event",
|
||||||
|
startTime: new Date(dayjs().add(2, "day").format()),
|
||||||
|
endTime: new Date(dayjs().add(2, "day").add(15, "minutes").format()),
|
||||||
|
eventTypeId: buildEventType({
|
||||||
|
id: 2,
|
||||||
|
profileId: null,
|
||||||
|
locations: [{ type: "integrations:daily" }],
|
||||||
|
}).id,
|
||||||
|
oneTimePassword: null,
|
||||||
|
fromReschedule: "original-booking-uid",
|
||||||
|
});
|
||||||
|
prismaMock.$transaction.mockImplementation(async (callback) => {
|
||||||
|
const mockTx = {
|
||||||
|
booking: {
|
||||||
|
create: prismaMock.booking.create.mockResolvedValue(mockBooking),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
app_RoutingForms_FormResponse: {
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return await callback(prismaMock);
|
||||||
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
|
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
|
||||||
const rescheduledBooking = JSON.parse(res._getData()) as Booking;
|
const rescheduledBooking = JSON.parse(res._getData()) as Booking;
|
||||||
expect(prismaMock.booking.create).toHaveBeenCalledTimes(1);
|
expect(prismaMock.booking.create).toHaveBeenCalledTimes(1);
|
||||||
expect(rescheduledBooking.fromReschedule).toEqual(createdBooking.uid);
|
expect(rescheduledBooking.fromReschedule).toEqual("original-booking-uid");
|
||||||
const previousBooking = await prisma.booking.findUnique({
|
const previousBooking = await prisma.booking.findUnique({
|
||||||
where: { uid: createdBooking.uid },
|
where: { uid: "original-booking-uid" },
|
||||||
});
|
});
|
||||||
expect(previousBooking?.status).toBe("cancelled");
|
expect(previousBooking?.status).toBe("cancelled");
|
||||||
});
|
});
|
||||||
@@ -227,8 +540,8 @@ describe.skipIf(true)("POST /api/bookings", () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
name: "test",
|
name: "test",
|
||||||
start: dayjs().format(),
|
start: dayjs().add(1, "day").format(),
|
||||||
end: dayjs().add(1, "day").format(),
|
end: dayjs().add(1, "day").add(15, "minutes").format(),
|
||||||
eventTypeId: 2,
|
eventTypeId: 2,
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
location: "Cal.com Video",
|
location: "Cal.com Video",
|
||||||
@@ -241,8 +554,38 @@ describe.skipIf(true)("POST /api/bookings", () => {
|
|||||||
prisma,
|
prisma,
|
||||||
});
|
});
|
||||||
|
|
||||||
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue(buildEventType());
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
|
...buildEventType({ profileId: null, length: 15 }),
|
||||||
|
profile: { organizationId: null },
|
||||||
|
hosts: [],
|
||||||
|
users: [buildUser()],
|
||||||
|
});
|
||||||
prismaMock.booking.findMany.mockResolvedValue([]);
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||||
|
const mockBooking = buildBooking({
|
||||||
|
id: 3,
|
||||||
|
uid: "test-api-v1-uid",
|
||||||
|
title: "Test API V1 Event",
|
||||||
|
startTime: new Date(dayjs().add(1, "day").format()),
|
||||||
|
endTime: new Date(dayjs().add(1, "day").add(15, "minutes").format()),
|
||||||
|
eventTypeId: buildEventType({
|
||||||
|
id: 2,
|
||||||
|
profileId: null,
|
||||||
|
}).id,
|
||||||
|
oneTimePassword: null,
|
||||||
|
creationSource: "API_V1",
|
||||||
|
});
|
||||||
|
prismaMock.$transaction.mockImplementation(async (callback) => {
|
||||||
|
const mockTx = {
|
||||||
|
booking: {
|
||||||
|
create: prismaMock.booking.create.mockResolvedValue(mockBooking),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
app_RoutingForms_FormResponse: {
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return await callback(prismaMock);
|
||||||
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
createdBooking = JSON.parse(res._getData());
|
createdBooking = JSON.parse(res._getData());
|
||||||
@@ -252,25 +595,50 @@ describe.skipIf(true)("POST /api/bookings", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Recurring event-type", () => {
|
describe("Recurring event-type", () => {
|
||||||
test("Creates multiple bookings", async () => {
|
test.skip("Creates multiple bookings", async () => {
|
||||||
|
const recurringDates = Array.from(Array(12).keys()).map((i) => ({
|
||||||
|
start: dayjs().add(1, "day").add(i, "week").toISOString(),
|
||||||
|
end: dayjs().add(1, "day").add(i, "week").add(15, "minutes").toISOString(),
|
||||||
|
}));
|
||||||
|
|
||||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
title: "test",
|
title: "test",
|
||||||
eventTypeId: 2,
|
eventTypeId: 2,
|
||||||
startTime: dayjs().toDate(),
|
start: recurringDates[0].start,
|
||||||
endTime: dayjs().add(1, "day").toDate(),
|
end: recurringDates[0].end,
|
||||||
|
timeZone: "UTC",
|
||||||
|
language: "en",
|
||||||
|
responses: {
|
||||||
|
name: "Test User",
|
||||||
|
email: "test@example.com",
|
||||||
|
location: { optionValue: "", value: "integrations:daily" },
|
||||||
|
},
|
||||||
|
metadata: {},
|
||||||
recurringCount: 12,
|
recurringCount: 12,
|
||||||
|
recurringEventId: "test-recurring-event-id",
|
||||||
|
allRecurringDates: recurringDates,
|
||||||
|
isFirstRecurringSlot: true,
|
||||||
|
numSlotsToCheckForAvailability: 12,
|
||||||
},
|
},
|
||||||
prisma,
|
prisma,
|
||||||
});
|
});
|
||||||
|
|
||||||
prismaMock.eventType.findUnique.mockResolvedValue(
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } })
|
...buildEventType({ profileId: null, length: 15 }),
|
||||||
|
profile: { organizationId: null },
|
||||||
|
hosts: [],
|
||||||
|
users: [buildUser()],
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockBookings = Array.from(Array(12).keys()).map((i) =>
|
||||||
|
buildBooking({ id: i + 1, uid: `recurring-booking-${i}` })
|
||||||
);
|
);
|
||||||
|
|
||||||
Array.from(Array(12).keys()).map(async () => {
|
prismaMock.booking.create.mockResolvedValue(buildBooking({ id: 1, uid: "test-booking-uid" }));
|
||||||
prismaMock.booking.create.mockResolvedValue(buildBooking());
|
prismaMock.$transaction.mockImplementation(async (callback) => {
|
||||||
|
return await callback(prismaMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
prismaMock.webhook.findMany.mockResolvedValue([]);
|
prismaMock.webhook.findMany.mockResolvedValue([]);
|
||||||
@@ -278,32 +646,57 @@ describe.skipIf(true)("POST /api/bookings", () => {
|
|||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
const data = JSON.parse(res._getData());
|
const data = JSON.parse(res._getData());
|
||||||
|
|
||||||
expect(prismaMock.booking.create).toHaveBeenCalledTimes(12);
|
expect(prismaMock.$transaction).toHaveBeenCalled();
|
||||||
expect(res._getStatusCode()).toBe(201);
|
expect(res._getStatusCode()).toBe(200);
|
||||||
expect(data.message).toEqual("Bookings created successfully.");
|
expect(data.message).toEqual("Bookings created successfully.");
|
||||||
expect(data.bookings.length).toEqual(12);
|
expect(data.bookings.length).toEqual(12);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test("Notifies multiple bookings", async () => {
|
test.skip("Notifies multiple bookings", async () => {
|
||||||
|
const recurringDates = Array.from(Array(12).keys()).map((i) => ({
|
||||||
|
start: dayjs().add(1, "day").add(i, "week").toISOString(),
|
||||||
|
end: dayjs().add(1, "day").add(i, "week").add(15, "minutes").toISOString(),
|
||||||
|
}));
|
||||||
|
|
||||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
title: "test",
|
title: "test",
|
||||||
eventTypeId: 2,
|
eventTypeId: 2,
|
||||||
startTime: dayjs().toDate(),
|
start: recurringDates[0].start,
|
||||||
endTime: dayjs().add(1, "day").toDate(),
|
end: recurringDates[0].end,
|
||||||
|
timeZone: "UTC",
|
||||||
|
language: "en",
|
||||||
|
responses: {
|
||||||
|
name: "Test User",
|
||||||
|
email: "test@example.com",
|
||||||
|
location: { optionValue: "", value: "integrations:daily" },
|
||||||
|
},
|
||||||
|
metadata: {},
|
||||||
recurringCount: 12,
|
recurringCount: 12,
|
||||||
|
recurringEventId: "test-recurring-event-id",
|
||||||
|
allRecurringDates: recurringDates,
|
||||||
|
isFirstRecurringSlot: true,
|
||||||
|
numSlotsToCheckForAvailability: 12,
|
||||||
},
|
},
|
||||||
prisma,
|
prisma,
|
||||||
});
|
});
|
||||||
|
|
||||||
prismaMock.eventType.findUnique.mockResolvedValue(
|
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
|
||||||
buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } })
|
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
|
||||||
);
|
profile: { organizationId: null },
|
||||||
|
hosts: [],
|
||||||
|
users: [buildUser()],
|
||||||
|
});
|
||||||
|
|
||||||
const createdAt = new Date();
|
const createdAt = new Date();
|
||||||
Array.from(Array(12).keys()).map(async () => {
|
const mockBookings = Array.from(Array(12).keys()).map((i) =>
|
||||||
prismaMock.booking.create.mockResolvedValue(buildBooking({ createdAt }));
|
buildBooking({ id: i + 1, uid: `webhook-booking-${i}`, createdAt })
|
||||||
|
);
|
||||||
|
|
||||||
|
prismaMock.booking.create.mockResolvedValue(buildBooking({ id: 1, uid: "test-booking-uid" }));
|
||||||
|
prismaMock.$transaction.mockImplementation(async (callback) => {
|
||||||
|
return await callback(prismaMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockedWebhooks = [
|
const mockedWebhooks = [
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
watchAttempts: 0,
|
watchAttempts: 0,
|
||||||
maxAttempts: 3,
|
maxAttempts: 3,
|
||||||
unwatchAttempts: 0,
|
unwatchAttempts: 0,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
@@ -136,6 +138,8 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
watchAttempts: 0,
|
watchAttempts: 0,
|
||||||
maxAttempts: 3,
|
maxAttempts: 3,
|
||||||
unwatchAttempts: 0,
|
unwatchAttempts: 0,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
|||||||
@@ -37,4 +37,8 @@ AXIOM_TOKEN=
|
|||||||
# INFO: 1
|
# INFO: 1
|
||||||
# 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"
|
||||||
@@ -42,15 +42,17 @@ $ yarn prisma generate
|
|||||||
|
|
||||||
Copy `.env.example` to `.env` and fill values.
|
Copy `.env.example` to `.env` and fill values.
|
||||||
|
|
||||||
## Add license Key to deployments table in DB
|
## Add license Key to Deployment table in DB
|
||||||
|
|
||||||
id, logo theme licenseKey agreedLicenseAt
|
id, logo, theme, licenseKey, agreedLicenseAt:-
|
||||||
1, null, null, 'c4234812-12ab-42s6-a1e3-55bedd4a5bb7', '2023-05-15 21:39:47.611'
|
1, null, null, '00000000-0000-0000-0000-000000000000', '2023-05-15 21:39:47.611'
|
||||||
|
|
||||||
|
Replace with your actual license key.
|
||||||
|
|
||||||
your CALCOM_LICENSE_KEY env var need to contain the same value
|
your CALCOM_LICENSE_KEY env var need to contain the same value
|
||||||
|
|
||||||
.env
|
.env
|
||||||
CALCOM_LICENSE_KEY=c4234812-12ab-42s6-a1e3-55bedd4a5bb
|
CALCOM_LICENSE_KEY=00000000-0000-0000-0000-000000000000
|
||||||
|
|
||||||
## Running the app
|
## Running the app
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"moduleFileExtensions": ["js", "json", "ts"],
|
|
||||||
"rootDir": ".",
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"@/(.*)": "<rootDir>/src/$1",
|
|
||||||
"test/(.*)": "<rootDir>/test/$1"
|
|
||||||
},
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"testRegex": ".e2e-spec.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
},
|
|
||||||
"setupFiles": ["<rootDir>/test/setEnvVars.ts", "jest-date-mock"],
|
|
||||||
"setupFilesAfterEnv": ["<rootDir>/test/jest.setup-e2e.ts"],
|
|
||||||
"reporters": ["default", "jest-summarizing-reporter"],
|
|
||||||
"workerIdleMemoryLimit": "512MB",
|
|
||||||
"maxWorkers": 8
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Config } from "jest";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
preset: "ts-jest",
|
||||||
|
moduleFileExtensions: ["js", "json", "ts"],
|
||||||
|
rootDir: ".",
|
||||||
|
moduleNameMapper: {
|
||||||
|
"@/(.*)": "<rootDir>/src/$1",
|
||||||
|
"test/(.*)": "<rootDir>/test/$1",
|
||||||
|
},
|
||||||
|
testEnvironment: "node",
|
||||||
|
testRegex: ".e2e-spec.ts$",
|
||||||
|
transform: {
|
||||||
|
"^.+\\.ts$": "ts-jest",
|
||||||
|
},
|
||||||
|
setupFiles: ["<rootDir>/test/setEnvVars.ts", "jest-date-mock"],
|
||||||
|
setupFilesAfterEnv: ["<rootDir>/test/jest.setup-e2e.ts"],
|
||||||
|
reporters: ["default", "jest-summarizing-reporter"],
|
||||||
|
workerIdleMemoryLimit: "512MB",
|
||||||
|
maxWorkers: 8,
|
||||||
|
testPathIgnorePatterns: ["/dist/", "/node_modules/"],
|
||||||
|
transformIgnorePatterns: ["/dist/", "/node_modules/"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"moduleFileExtensions": ["js", "json", "ts"],
|
|
||||||
"rootDir": ".",
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"@/(.*)": "<rootDir>/src/$1",
|
|
||||||
"test/(.*)": "<rootDir>/test/$1"
|
|
||||||
},
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.ts$": "ts-jest"
|
|
||||||
},
|
|
||||||
"setupFiles": ["<rootDir>/test/setEnvVars.ts"]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Config } from "jest";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
preset: "ts-jest",
|
||||||
|
moduleFileExtensions: ["ts", "js", "json"],
|
||||||
|
rootDir: ".",
|
||||||
|
moduleNameMapper: {
|
||||||
|
"@/(.*)": "<rootDir>/src/$1",
|
||||||
|
"test/(.*)": "<rootDir>/test/$1",
|
||||||
|
},
|
||||||
|
testEnvironment: "node",
|
||||||
|
testRegex: ".*\\.spec\\.ts$",
|
||||||
|
transform: {
|
||||||
|
"^.+\\.ts$": "ts-jest",
|
||||||
|
},
|
||||||
|
setupFiles: ["<rootDir>/test/setEnvVars.ts"],
|
||||||
|
testPathIgnorePatterns: ["/dist/", "/node_modules/"],
|
||||||
|
transformIgnorePatterns: ["/dist/", "/node_modules/"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -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\"",
|
||||||
@@ -24,9 +25,9 @@
|
|||||||
"test:watch": "yarn dev:build && jest --watch",
|
"test:watch": "yarn dev:build && jest --watch",
|
||||||
"test:cov": "yarn dev:build && jest --coverage",
|
"test:cov": "yarn dev:build && jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192 --experimental-vm-modules' jest --ci --forceExit --config ./jest-e2e.json",
|
"test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192 --experimental-vm-modules' jest --ci --forceExit --config ./jest-e2e.ts",
|
||||||
"test:e2e:local": "yarn test:e2e --maxWorkers=4",
|
"test:e2e:local": "yarn test:e2e --maxWorkers=4",
|
||||||
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch",
|
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.ts --watch",
|
||||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||||
"generate-schemas": "yarn prisma generate && yarn prisma format",
|
"generate-schemas": "yarn prisma generate && yarn prisma format",
|
||||||
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts",
|
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.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.300",
|
||||||
"@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": "*",
|
||||||
@@ -98,6 +98,8 @@
|
|||||||
"@types/luxon": "^3.3.7",
|
"@types/luxon": "^3.3.7",
|
||||||
"@types/passport-jwt": "^3.0.13",
|
"@types/passport-jwt": "^3.0.13",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6",
|
||||||
|
"@typescript-eslint/parser": "^6",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-date-mock": "^1.0.10",
|
"jest-date-mock": "^1.0.10",
|
||||||
"node-mocks-http": "^1.16.2",
|
"node-mocks-http": "^1.16.2",
|
||||||
@@ -108,7 +110,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.3"
|
"typescript": "^5.9.0-beta"
|
||||||
},
|
},
|
||||||
"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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/ge
|
|||||||
import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
|
import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
|
||||||
import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output";
|
import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output";
|
||||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
import { sha256Hash, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||||
import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions";
|
import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions";
|
||||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
@@ -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);
|
||||||
@@ -370,7 +386,7 @@ export class BookingsController_2024_04_15 {
|
|||||||
if (bearerToken) {
|
if (bearerToken) {
|
||||||
if (isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")) {
|
if (isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")) {
|
||||||
const strippedApiKey = stripApiKey(bearerToken, this.config.get<string>("api.keyPrefix"));
|
const strippedApiKey = stripApiKey(bearerToken, this.config.get<string>("api.keyPrefix"));
|
||||||
const apiKeyHash = hashAPIKey(strippedApiKey);
|
const apiKeyHash = sha256Hash(strippedApiKey);
|
||||||
const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash);
|
const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash);
|
||||||
return keyData?.userId;
|
return keyData?.userId;
|
||||||
} else {
|
} else {
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -31,12 +31,7 @@ function ValidateBookingName(validationOptions?: ValidationOptions) {
|
|||||||
return value.trim().length > 0;
|
return value.trim().length > 0;
|
||||||
}
|
}
|
||||||
if (typeof value === "object" && value !== null) {
|
if (typeof value === "object" && value !== null) {
|
||||||
return (
|
return typeof value.firstName === "string" && value.firstName.trim().length > 0;
|
||||||
typeof value.firstName === "string" &&
|
|
||||||
typeof value.lastName === "string" &&
|
|
||||||
value.firstName.trim().length > 0 &&
|
|
||||||
value.lastName.trim().length > 0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user