Compare commits
165
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eae2711f21 | ||
|
|
fac956b761 | ||
|
|
b286244aab | ||
|
|
c46f824e3f | ||
|
|
dbe55f0804 | ||
|
|
3bcce02b92 | ||
|
|
2a23911a5b | ||
|
|
5aa24c8472 | ||
|
|
8277ce934b | ||
|
|
bb68cd73ef | ||
|
|
445b307972 | ||
|
|
d2d86a4079 | ||
|
|
7601cc2194 | ||
|
|
aa48e72c12 | ||
|
|
bec49ef417 | ||
|
|
c13aa69565 | ||
|
|
1470adb159 | ||
|
|
bf89991eef | ||
|
|
4dfab874e0 | ||
|
|
4303fb8726 | ||
|
|
64f811ed0b | ||
|
|
4d51873140 | ||
|
|
3c0fc2e88b | ||
|
|
c702da0334 | ||
|
|
92169eb0d2 | ||
|
|
3d74357e96 | ||
|
|
6593291d00 | ||
|
|
c48fb44a8d | ||
|
|
e7c908bc80 | ||
|
|
ad35e19f8e | ||
|
|
3fc8c6c84e | ||
|
|
da7aeb4a21 | ||
|
|
6197a2a5c5 | ||
|
|
de186b95c1 | ||
|
|
6b24513073 | ||
|
|
55ea18128a | ||
|
|
d5557fa512 | ||
|
|
9c97b2aa6d | ||
|
|
1e226a40d1 | ||
|
|
bbe46eaa7d | ||
|
|
7799b191ec | ||
|
|
f3269a3ddf | ||
|
|
a4be822636 | ||
|
|
7cd8dbf7d8 | ||
|
|
654a865007 | ||
|
|
409a0277fb | ||
|
|
7adab1f65f | ||
|
|
3b92c86582 | ||
|
|
de195a1c0f | ||
|
|
d7d2487f6d | ||
|
|
3e31945157 | ||
|
|
c11814c342 | ||
|
|
60e3244088 | ||
|
|
a1ac5f36f0 | ||
|
|
6f38d40613 | ||
|
|
6b7d22de3f | ||
|
|
2f763dceda | ||
|
|
96468c4083 | ||
|
|
858d52563f | ||
|
|
7505b3b271 | ||
|
|
758dc88b55 | ||
|
|
9be418dc65 | ||
|
|
2ac310456e | ||
|
|
ff264d6f7a | ||
|
|
8019c414e1 | ||
|
|
a3fbe3731e | ||
|
|
e3742dbc66 | ||
|
|
83bf717d6d | ||
|
|
7fcdaa1c46 | ||
|
|
a8154f9405 | ||
|
|
52a5afeba5 | ||
|
|
43cc40d7fe | ||
|
|
42a98f000e | ||
|
|
f003ef935b | ||
|
|
e2b2a184c8 | ||
|
|
094595c435 | ||
|
|
4118a86c0d | ||
|
|
928d6f3821 | ||
|
|
b2ade568c8 | ||
|
|
27820ce897 | ||
|
|
fea7e66996 | ||
|
|
474d1571ae | ||
|
|
2ef627b87e | ||
|
|
2dfb82b623 | ||
|
|
ccd7fdfd52 | ||
|
|
f92e59ed34 | ||
|
|
d29477349a | ||
|
|
259e480baa | ||
|
|
0055dd1142 | ||
|
|
61c44780f0 | ||
|
|
9710d3b15c | ||
|
|
04cefebd75 | ||
|
|
dbd927c002 | ||
|
|
e6b2116a2b | ||
|
|
855a02091f | ||
|
|
358434429e | ||
|
|
b00061d85e | ||
|
|
022f08c05e | ||
|
|
6ff5c8ce5f | ||
|
|
3be7fb9dd8 | ||
|
|
72dd970b6e | ||
|
|
f4d09ece43 | ||
|
|
239e6085ba | ||
|
|
b17eab3aeb | ||
|
|
752f7fcde0 | ||
|
|
f6b061f787 | ||
|
|
cc8a1290ee | ||
|
|
b7cf893169 | ||
|
|
1458cacdbb | ||
|
|
a48a1bd5d8 | ||
|
|
1acd0a14ba | ||
|
|
6c9ac7636b | ||
|
|
d261a23dbc | ||
|
|
8b7947a172 | ||
|
|
89789b1e3b | ||
|
|
43dd24c480 | ||
|
|
ae6b1d402a | ||
|
|
3054ad33b2 | ||
|
|
a4dbb1a90b | ||
|
|
689150d78a | ||
|
|
fba096f23d | ||
|
|
7d5a3391f1 | ||
|
|
dcef99bf49 | ||
|
|
805fcc22a5 | ||
|
|
c5b7da8ffd | ||
|
|
97f1f7b130 | ||
|
|
0ac460fce3 | ||
|
|
82dfca8272 | ||
|
|
d1c5576d4f | ||
|
|
7090e3ed60 | ||
|
|
9926fd2e79 | ||
|
|
7793cb20d1 | ||
|
|
8ed0fb6a07 | ||
|
|
f5a1222fc5 | ||
|
|
2b892296b0 | ||
|
|
f796802ed3 | ||
|
|
b2ac227877 | ||
|
|
f0aec4efdc | ||
|
|
8d7cdf9d3b | ||
|
|
2ed0380e4f | ||
|
|
752cfa6bf9 | ||
|
|
f11c4dd7ec | ||
|
|
386f60ca6d | ||
|
|
e98f279f64 | ||
|
|
fe190f20f3 | ||
|
|
96855ea391 | ||
|
|
57150d992e | ||
|
|
8b75bf347f | ||
|
|
788fbdbe5e | ||
|
|
b2239374bc | ||
|
|
77c61abef8 | ||
|
|
c2472cc05a | ||
|
|
608fe1c586 | ||
|
|
4413295b27 | ||
|
|
bcc433ba9a | ||
|
|
2ba571d230 | ||
|
|
4a0a1ef053 | ||
|
|
7e882eedf8 | ||
|
|
224e606439 | ||
|
|
14b3344a41 | ||
|
|
a538ba64d2 | ||
|
|
b3bfad7fea | ||
|
|
f80b45e36a | ||
|
|
f3d60b141f | ||
|
|
5463dc261c |
@@ -144,6 +144,12 @@ GOOGLE_WEBHOOK_TOKEN=
|
|||||||
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
|
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
|
||||||
GOOGLE_WEBHOOK_URL=
|
GOOGLE_WEBHOOK_URL=
|
||||||
|
|
||||||
|
# Token to verify incoming webhooks from Microsoft Calendar
|
||||||
|
MICROSOFT_WEBHOOK_TOKEN=
|
||||||
|
|
||||||
|
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
|
||||||
|
MICROSOFT_WEBHOOK_URL=
|
||||||
|
|
||||||
# Inbox to send user feedback
|
# Inbox to send user feedback
|
||||||
SEND_FEEDBACK_EMAIL=
|
SEND_FEEDBACK_EMAIL=
|
||||||
|
|
||||||
@@ -261,6 +267,9 @@ NEXT_PUBLIC_CLOUDFLARE_SITEKEY=
|
|||||||
NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER=
|
NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER=
|
||||||
CLOUDFLARE_TURNSTILE_SECRET=
|
CLOUDFLARE_TURNSTILE_SECRET=
|
||||||
|
|
||||||
|
# 0 = false, 1=true
|
||||||
|
NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER=
|
||||||
|
|
||||||
# Set the following value to true if you wish to enable Team Impersonation
|
# Set the following value to true if you wish to enable Team Impersonation
|
||||||
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
||||||
|
|
||||||
@@ -473,3 +482,7 @@ DATABASE_CHUNK_SIZE=
|
|||||||
# Service Account Encryption Key for encrypting/decrypting service account keys
|
# Service Account Encryption Key for encrypting/decrypting service account keys
|
||||||
# You can use: `openssl rand -base64 24` to generate one
|
# You can use: `openssl rand -base64 24` to generate one
|
||||||
CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY=
|
CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY=
|
||||||
|
|
||||||
|
SALESFORCE_GRAPHQL_DELAY_MS=500
|
||||||
|
SALESFORCE_GRAPHQL_MAX_DELAY_MS=2000
|
||||||
|
SALESFORCE_GRAPHQL_MAX_RETRIES=3
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
node_modules
|
|
||||||
apps/api/v2/dist
|
|
||||||
packages/platform/**/dist/*
|
|
||||||
**/**/node_modules
|
|
||||||
**/**/.next
|
|
||||||
**/**/public
|
|
||||||
packages/prisma/zod
|
|
||||||
apps/web/public/embed
|
|
||||||
packages/ui/components/icon/dynamicIconImports.tsx
|
|
||||||
-115
@@ -1,115 +0,0 @@
|
|||||||
/** @type {import("eslint").Linter.Config} */
|
|
||||||
module.exports = {
|
|
||||||
extends: ["./packages/config/eslint-preset.js"],
|
|
||||||
plugins: ["import"],
|
|
||||||
rules: {
|
|
||||||
"import/no-cycle": ["warn", { maxDepth: Infinity }],
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
// WARN: features must not be imported by app-store or lib
|
|
||||||
{
|
|
||||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}", "packages/lib/**/*.{ts,tsx,js,jsx}"],
|
|
||||||
rules: {
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: [
|
|
||||||
// Catch all relative paths into features
|
|
||||||
"**/features",
|
|
||||||
"**/features/*",
|
|
||||||
// Catch all alias imports
|
|
||||||
"@calcom/features",
|
|
||||||
"@calcom/features/*",
|
|
||||||
],
|
|
||||||
message: "Avoid importing @calcom/features from @calcom/app-store or @calcom/lib.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// WARN: lib must not import app-store or features
|
|
||||||
{
|
|
||||||
files: ["packages/lib/**/*.{ts,tsx,js,jsx}"],
|
|
||||||
rules: {
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: [
|
|
||||||
// Catch all relative paths into app-store
|
|
||||||
"**/app-store",
|
|
||||||
"**/app-store/*",
|
|
||||||
// Catch all relative paths into features
|
|
||||||
"**/features",
|
|
||||||
"**/features/*",
|
|
||||||
// Catch alias imports
|
|
||||||
"@calcom/app-store",
|
|
||||||
"@calcom/app-store/*",
|
|
||||||
"@calcom/features",
|
|
||||||
"@calcom/features/*",
|
|
||||||
],
|
|
||||||
message: "@calcom/lib should not import @calcom/app-store or @calcom/features.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// ERROR: app-store must not import trpc
|
|
||||||
{
|
|
||||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
|
|
||||||
rules: {
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: [
|
|
||||||
// Catch all relative paths into trpc
|
|
||||||
"**/trpc",
|
|
||||||
"**/trpc/*",
|
|
||||||
// Catch alias imports
|
|
||||||
"@calcom/trpc",
|
|
||||||
"@calcom/trpc/*",
|
|
||||||
"@trpc",
|
|
||||||
"@trpc/*",
|
|
||||||
],
|
|
||||||
message:
|
|
||||||
"@calcom/app-store must not import trpc. Move UI to apps/web/components/apps or introduce an API boundary.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// ERROR: prisma must not import `features` package
|
|
||||||
{
|
|
||||||
files: ["packages/prisma/**/*.{ts,tsx,js,jsx}"],
|
|
||||||
rules: {
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: [
|
|
||||||
// Catch all relative paths into features
|
|
||||||
"**/features",
|
|
||||||
"**/features/*",
|
|
||||||
// Catch all alias imports
|
|
||||||
"@calcom/features",
|
|
||||||
"@calcom/features/*",
|
|
||||||
],
|
|
||||||
message: "Avoid importing @calcom/features from @calcom/prisma.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
+2
-1
@@ -1,5 +1,5 @@
|
|||||||
/**/package.json @calcom/Foundation
|
/**/package.json @calcom/Foundation
|
||||||
/apps/api/**/* @calcom/Platform @calcom/Foundation
|
/apps/api/**/* @calcom/API
|
||||||
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/lib/csp.ts @calcom/Foundation
|
/apps/web/lib/csp.ts @calcom/Foundation
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
/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/embeds/**/* @calcom/Foundation
|
||||||
|
/packages/features/auth/lib/next-auth-options.ts @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
|
||||||
|
|||||||
@@ -74,14 +74,8 @@ jobs:
|
|||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e-atoms:
|
|
||||||
name: Tests
|
|
||||||
needs: [lint, build, build-atoms, build-api-v2]
|
|
||||||
uses: ./.github/workflows/e2e-atoms.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
required:
|
required:
|
||||||
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store, e2e-atoms]
|
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: E2E
|
name: Check breaking changes and run E2E
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
env:
|
env:
|
||||||
@@ -23,6 +23,10 @@ env:
|
|||||||
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 }}
|
SLOTS_CACHE_TTL: ${{ secrets.CI_SLOTS_CACHE_TTL }}
|
||||||
|
NEXT_PUBLIC_VAPID_PUBLIC_KEY: ${{ secrets.NEXT_PUBLIC_VAPID_PUBLIC_KEY }}
|
||||||
|
VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
|
||||||
|
JWT_SECRET: ${{ secrets.CI_JWT_SECRET }}
|
||||||
|
NODE_ENV: ${{ vars.CI_NODE_ENV }}
|
||||||
jobs:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
@@ -68,6 +72,18 @@ jobs:
|
|||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
|
|
||||||
|
- name: Generate Swagger
|
||||||
|
working-directory: apps/api/v2
|
||||||
|
run: yarn generate-swagger
|
||||||
|
|
||||||
|
- name: Check breaking changes
|
||||||
|
uses: oasdiff/oasdiff-action/breaking@main
|
||||||
|
with:
|
||||||
|
base: https://raw.githubusercontent.com/calcom/cal.com/refs/heads/main/docs/api-reference/v2/openapi.json
|
||||||
|
revision: docs/api-reference/v2/openapi.json
|
||||||
|
fail-on: ERR
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
working-directory: apps/api/v2
|
working-directory: apps/api/v2
|
||||||
run: |
|
run: |
|
||||||
@@ -75,6 +91,7 @@ jobs:
|
|||||||
EXIT_CODE=$?
|
EXIT_CODE=$?
|
||||||
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
|
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
|
||||||
exit $EXIT_CODE
|
exit $EXIT_CODE
|
||||||
|
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -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, 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, self-hosted, teams, webhooks, workflows, zapier"
|
ignore-labels: "admin, app-store, ai, api, 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, self-hosted, sre, teams, webhooks, workflows, zapier"
|
||||||
apply-labels-from-issue:
|
apply-labels-from-issue:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|||||||
@@ -195,13 +195,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e-atoms:
|
|
||||||
name: Tests
|
|
||||||
needs: [changes, check-label, build, build-atoms, build-api-v2]
|
|
||||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
|
||||||
uses: ./.github/workflows/e2e-atoms.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze Build
|
name: Analyze Build
|
||||||
needs: [build]
|
needs: [build]
|
||||||
@@ -211,7 +204,7 @@ jobs:
|
|||||||
merge-reports:
|
merge-reports:
|
||||||
name: Merge reports
|
name: Merge reports
|
||||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store, e2e-atoms]
|
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||||
uses: ./.github/workflows/merge-reports.yml
|
uses: ./.github/workflows/merge-reports.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
@@ -255,8 +248,7 @@ jobs:
|
|||||||
e2e-api-v2,
|
e2e-api-v2,
|
||||||
e2e-embed,
|
e2e-embed,
|
||||||
e2e-embed-react,
|
e2e-embed-react,
|
||||||
e2e-app-store,
|
e2e-app-store
|
||||||
e2e-atoms,
|
|
||||||
]
|
]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
|
|||||||
@@ -16,5 +16,4 @@ public
|
|||||||
packages/prisma/zod
|
packages/prisma/zod
|
||||||
packages/prisma/enums
|
packages/prisma/enums
|
||||||
apps/web/public/embed
|
apps/web/public/embed
|
||||||
apps/api/v2/swagger/documentation.json
|
|
||||||
packages/ui/components/icon/dynamicIconImports.tsx
|
packages/ui/components/icon/dynamicIconImports.tsx
|
||||||
|
|||||||
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
Vendored
@@ -13,5 +13,8 @@
|
|||||||
"connect": "^3.7.0",
|
"connect": "^3.7.0",
|
||||||
"http": "^0.0.1-security",
|
"http": "^0.0.1-security",
|
||||||
"http-proxy-middleware": "^2.0.6"
|
"http-proxy-middleware": "^2.0.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@calcom/eslint-config": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { nextJsConfig } from "@calcom/eslint-config/next-js";
|
||||||
|
|
||||||
|
export default nextJsConfig;
|
||||||
+11
-10
@@ -10,7 +10,7 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||||
"dev": "PORT=3003 next dev",
|
"dev": "PORT=3003 next dev",
|
||||||
"lint": "eslint . --ignore-path .gitignore",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"start": "PORT=3003 next start",
|
"start": "PORT=3003 next start",
|
||||||
"docker-start-api": "PORT=80 next start",
|
"docker-start-api": "PORT=80 next start",
|
||||||
@@ -18,18 +18,19 @@
|
|||||||
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/tsconfig": "*",
|
"@calcom/eslint-config": "workspace:*",
|
||||||
"@calcom/types": "*",
|
"@calcom/tsconfig": "workspace:*",
|
||||||
|
"@calcom/types": "workspace:*",
|
||||||
"node-mocks-http": "^1.11.0"
|
"node-mocks-http": "^1.11.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/app-store": "*",
|
"@calcom/app-store": "workspace:*",
|
||||||
"@calcom/dayjs": "*",
|
"@calcom/dayjs": "workspace:*",
|
||||||
"@calcom/emails": "*",
|
"@calcom/emails": "workspace:*",
|
||||||
"@calcom/features": "*",
|
"@calcom/features": "workspace:*",
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "workspace:*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "workspace:*",
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "workspace:*",
|
||||||
"@prisma/nextjs-monorepo-workaround-plugin": "^6.16.1",
|
"@prisma/nextjs-monorepo-workaround-plugin": "^6.16.1",
|
||||||
"@sentry/nextjs": "^9.15.0",
|
"@sentry/nextjs": "^9.15.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getUserAvailabilityService } from "@calcom/lib/di/containers/GetUserAvailability";
|
import { getUserAvailabilityService } from "@calcom/features/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";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { NextApiRequest } from "next";
|
|||||||
import {
|
import {
|
||||||
getRecordingsOfCalVideoByRoomName,
|
getRecordingsOfCalVideoByRoomName,
|
||||||
getDownloadLinkOfCalVideoByRecordingId,
|
getDownloadLinkOfCalVideoByRecordingId,
|
||||||
} from "@calcom/app-store/videoClient";
|
} from "@calcom/features/conferencing/lib/videoClient";
|
||||||
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";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { NextApiRequest } from "next";
|
|||||||
import {
|
import {
|
||||||
getTranscriptsAccessLinkFromRecordingId,
|
getTranscriptsAccessLinkFromRecordingId,
|
||||||
checkIfRoomNameMatchesInRecording,
|
checkIfRoomNameMatchesInRecording,
|
||||||
} from "@calcom/app-store/videoClient";
|
} from "@calcom/features/conferencing/lib/videoClient";
|
||||||
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";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/app-store/videoClient";
|
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/features/conferencing/lib/videoClient";
|
||||||
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";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { getRegularBookingService } from "@calcom/features/bookings/di/RegularBookingService.container";
|
||||||
import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
|
import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
|
||||||
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
|
||||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||||
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";
|
||||||
@@ -239,15 +239,17 @@ async function handler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await handleNewBooking(
|
const regularBookingService = getRegularBookingService();
|
||||||
{
|
|
||||||
bookingData: req.body,
|
return await regularBookingService.createBookingForApiV1({
|
||||||
|
bookingData: req.body,
|
||||||
|
bookingMeta: {
|
||||||
userId,
|
userId,
|
||||||
hostname: req.headers.host || "",
|
hostname: req.headers.host || "",
|
||||||
forcedSlug: req.headers["x-cal-force-slug"] as string | undefined,
|
forcedSlug: req.headers["x-cal-force-slug"] as string | undefined,
|
||||||
},
|
},
|
||||||
getBookingDataSchemaForApi
|
bookingDataSchemaGetter: getBookingDataSchemaForApi,
|
||||||
);
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const knownError = error as Error;
|
const knownError = error as Error;
|
||||||
if (knownError?.message === ErrorCode.NoAvailableUsersFound) {
|
if (knownError?.message === ErrorCode.NoAvailableUsersFound) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import type { UserWithCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
|
import type { UserWithCalendars } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars";
|
||||||
import { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/lib/getConnectedDestinationCalendars";
|
import { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars";
|
||||||
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 { UserRepository } from "@calcom/lib/server/repository/user";
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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 { getAvailableSlotsService } from "@calcom/features/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";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/pay
|
|||||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||||
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 { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@@ -61,24 +62,24 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
const { teamId } = schemaQueryTeamId.parse(req.query);
|
||||||
|
|
||||||
/** Only OWNERS and ADMINS can edit teams */
|
/** Only OWNERS and ADMINS can edit teams */
|
||||||
const _team = await prisma.team.findFirst({
|
const team = await prisma.team.findFirst({
|
||||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||||
include: { members: true },
|
include: { members: true },
|
||||||
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||||
});
|
});
|
||||||
if (!_team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" });
|
if (!team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" });
|
||||||
|
|
||||||
const slugAlreadyExists = await prisma.team.findFirst({
|
if (data.slug) {
|
||||||
where: {
|
const teamRepository = new TeamRepository(prisma);
|
||||||
slug: {
|
const isSlugAvailable = await teamRepository.isSlugAvailableForUpdate({
|
||||||
mode: "insensitive",
|
slug: data.slug,
|
||||||
equals: data.slug,
|
teamId: team.id,
|
||||||
},
|
parentId: team.parentId,
|
||||||
},
|
});
|
||||||
});
|
if (!isSlugAvailable) {
|
||||||
|
throw new HttpError({ statusCode: 409, message: "Team slug already exists" });
|
||||||
if (slugAlreadyExists && data.slug !== _team.slug)
|
}
|
||||||
throw new HttpError({ statusCode: 409, message: "Team slug already exists" });
|
}
|
||||||
|
|
||||||
// Check if parentId is related to this user
|
// Check if parentId is related to this user
|
||||||
if (data.parentId && data.parentId === teamId) {
|
if (data.parentId && data.parentId === teamId) {
|
||||||
@@ -99,16 +100,16 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let paymentUrl;
|
let paymentUrl;
|
||||||
if (_team.slug === null && data.slug) {
|
if (team.slug === null && data.slug) {
|
||||||
data.metadata = {
|
data.metadata = {
|
||||||
...(_team.metadata as Prisma.JsonObject),
|
...(team.metadata as Prisma.JsonObject),
|
||||||
requestedSlug: data.slug,
|
requestedSlug: data.slug,
|
||||||
};
|
};
|
||||||
delete data.slug;
|
delete data.slug;
|
||||||
if (IS_TEAM_BILLING_ENABLED) {
|
if (IS_TEAM_BILLING_ENABLED) {
|
||||||
const checkoutSession = await purchaseTeamOrOrgSubscription({
|
const checkoutSession = await purchaseTeamOrOrgSubscription({
|
||||||
teamId: _team.id,
|
teamId: team.id,
|
||||||
seatsUsed: _team.members.length,
|
seatsUsed: team.members.length,
|
||||||
userId,
|
userId,
|
||||||
pricePerSeat: null,
|
pricePerSeat: null,
|
||||||
});
|
});
|
||||||
@@ -131,9 +132,9 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
bookingLimits: data.bookingLimits === null ? {} : data.bookingLimits,
|
bookingLimits: data.bookingLimits === null ? {} : data.bookingLimits,
|
||||||
metadata: data.metadata === null ? {} : data.metadata || undefined,
|
metadata: data.metadata === null ? {} : data.metadata || undefined,
|
||||||
};
|
};
|
||||||
const team = await prisma.team.update({ where: { id: teamId }, data: cloneData });
|
const updatedTeam = await prisma.team.update({ where: { id: teamId }, data: cloneData });
|
||||||
const result = {
|
const result = {
|
||||||
team: schemaTeamReadPublic.parse(team),
|
team: schemaTeamReadPublic.parse(updatedTeam),
|
||||||
paymentUrl,
|
paymentUrl,
|
||||||
};
|
};
|
||||||
if (!paymentUrl) {
|
if (!paymentUrl) {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export async function getHandler(req: NextApiRequest) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const [total, data] = await prisma.$transaction([
|
const [total, data] = await Promise.all([
|
||||||
prisma.user.count({ where }),
|
prisma.user.count({ where }),
|
||||||
prisma.user.findMany({ where, take, skip }),
|
prisma.user.findMany({ where, take, skip }),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { buildBooking } from "@calcom/lib/test/builder";
|
|||||||
import {
|
import {
|
||||||
getRecordingsOfCalVideoByRoomName,
|
getRecordingsOfCalVideoByRoomName,
|
||||||
getDownloadLinkOfCalVideoByRecordingId,
|
getDownloadLinkOfCalVideoByRecordingId,
|
||||||
} from "@calcom/app-store/videoClient";
|
} from "@calcom/features/conferencing/lib/videoClient";
|
||||||
|
|
||||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ type CustomNextApiResponse = NextApiResponse & Response;
|
|||||||
const adminUserId = 1;
|
const adminUserId = 1;
|
||||||
const memberUserId = 10;
|
const memberUserId = 10;
|
||||||
|
|
||||||
vi.mock("@calcom/app-store/videoClient", () => {
|
vi.mock("@calcom/features/conferencing/lib/videoClient", () => {
|
||||||
return {
|
return {
|
||||||
getRecordingsOfCalVideoByRoomName: vi.fn(),
|
getRecordingsOfCalVideoByRoomName: vi.fn(),
|
||||||
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
|
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { describe, expect, test, vi, afterEach } from "vitest";
|
|||||||
import {
|
import {
|
||||||
getTranscriptsAccessLinkFromRecordingId,
|
getTranscriptsAccessLinkFromRecordingId,
|
||||||
checkIfRoomNameMatchesInRecording,
|
checkIfRoomNameMatchesInRecording,
|
||||||
} from "@calcom/app-store/videoClient";
|
} from "@calcom/features/conferencing/lib/videoClient";
|
||||||
import { buildBooking } from "@calcom/lib/test/builder";
|
import { buildBooking } from "@calcom/lib/test/builder";
|
||||||
|
|
||||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
@@ -19,7 +19,7 @@ import handler from "../../../../../../pages/api/bookings/[id]/transcripts/[reco
|
|||||||
type CustomNextApiRequest = NextApiRequest & Request;
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
type CustomNextApiResponse = NextApiResponse & Response;
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
vi.mock("@calcom/app-store/videoClient", () => {
|
vi.mock("@calcom/features/conferencing/lib/videoClient", () => {
|
||||||
return {
|
return {
|
||||||
getTranscriptsAccessLinkFromRecordingId: vi.fn(),
|
getTranscriptsAccessLinkFromRecordingId: vi.fn(),
|
||||||
checkIfRoomNameMatchesInRecording: vi.fn(),
|
checkIfRoomNameMatchesInRecording: vi.fn(),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { createMocks } from "node-mocks-http";
|
|||||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||||
|
|
||||||
import { buildBooking } from "@calcom/lib/test/builder";
|
import { buildBooking } from "@calcom/lib/test/builder";
|
||||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/app-store/videoClient";
|
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/features/conferencing/lib/videoClient";
|
||||||
|
|
||||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ import handler from "../../../../../pages/api/bookings/[id]/transcripts/_get";
|
|||||||
type CustomNextApiRequest = NextApiRequest & Request;
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
type CustomNextApiResponse = NextApiResponse & Response;
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
vi.mock("@calcom/app-store/videoClient", () => {
|
vi.mock("@calcom/features/conferencing/lib/videoClient", () => {
|
||||||
return {
|
return {
|
||||||
getAllTranscriptsAccessLinkFromRoomName: vi.fn(),
|
getAllTranscriptsAccessLinkFromRoomName: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ vi.mock("@calcom/features/watchlist/operations/check-if-users-are-blocked.contro
|
|||||||
checkIfUsersAreBlocked: vi.fn().mockResolvedValue(false),
|
checkIfUsersAreBlocked: vi.fn().mockResolvedValue(false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@calcom/lib/di/containers/QualifiedHosts", () => ({
|
vi.mock("@calcom/features/di/containers/QualifiedHosts", () => ({
|
||||||
getQualifiedHostsService: vi.fn().mockReturnValue({
|
getQualifiedHostsService: vi.fn().mockReturnValue({
|
||||||
findQualifiedHostsWithDelegationCredentials: vi.fn().mockResolvedValue({
|
findQualifiedHostsWithDelegationCredentials: vi.fn().mockResolvedValue({
|
||||||
qualifiedRRHosts: [],
|
qualifiedRRHosts: [],
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createMocks } from "node-mocks-http";
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import type { User } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import handler from "../../../pages/api/selected-calendars/_post";
|
import handler from "../../../pages/api/selected-calendars/_post";
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
|
|
||||||
prismaMock.user.findFirstOrThrow.mockResolvedValue({
|
prismaMock.user.findFirstOrThrow.mockResolvedValue({
|
||||||
id: 444444,
|
id: 444444,
|
||||||
} as any);
|
} as User);
|
||||||
|
|
||||||
prismaMock.selectedCalendar.create.mockResolvedValue({
|
prismaMock.selectedCalendar.create.mockResolvedValue({
|
||||||
credentialId: 1,
|
credentialId: 1,
|
||||||
@@ -95,6 +96,16 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
unwatchAttempts: 0,
|
unwatchAttempts: 0,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
channelId: null,
|
||||||
|
channelKind: null,
|
||||||
|
channelResourceId: null,
|
||||||
|
channelResourceUri: null,
|
||||||
|
channelExpiration: null,
|
||||||
|
syncSubscribedAt: null,
|
||||||
|
syncToken: null,
|
||||||
|
syncedAt: null,
|
||||||
|
syncErrorAt: null,
|
||||||
|
syncErrorCount: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
@@ -140,6 +151,16 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
unwatchAttempts: 0,
|
unwatchAttempts: 0,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
channelId: null,
|
||||||
|
channelKind: null,
|
||||||
|
channelResourceId: null,
|
||||||
|
channelResourceUri: null,
|
||||||
|
channelExpiration: null,
|
||||||
|
syncSubscribedAt: null,
|
||||||
|
syncToken: null,
|
||||||
|
syncedAt: null,
|
||||||
|
syncErrorAt: null,
|
||||||
|
syncErrorCount: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
|
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||||
|
|
||||||
|
import type { NextApiRequest } from "next";
|
||||||
import { describe, expect, it, beforeEach } from "vitest";
|
import { describe, expect, it, beforeEach } from "vitest";
|
||||||
|
|
||||||
import { WatchlistSeverity } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked";
|
import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked";
|
||||||
|
|
||||||
describe("isLockedOrBlocked", () => {
|
describe("isLockedOrBlocked", () => {
|
||||||
@@ -18,7 +17,6 @@ describe("isLockedOrBlocked", () => {
|
|||||||
{
|
{
|
||||||
type: "DOMAIN",
|
type: "DOMAIN",
|
||||||
value: "blocked.com",
|
value: "blocked.com",
|
||||||
severity: WatchlistSeverity.CRITICAL,
|
|
||||||
createdById: 1,
|
createdById: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -26,13 +24,13 @@ describe("isLockedOrBlocked", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return false if no user in request", async () => {
|
it("should return false if no user in request", async () => {
|
||||||
const req = { userId: null, user: null } as any;
|
const req = { userId: null, user: null } as unknown as NextApiRequest;
|
||||||
const result = await isLockedOrBlocked(req);
|
const result = await isLockedOrBlocked(req);
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false if user has no email", async () => {
|
it("should return false if user has no email", async () => {
|
||||||
const req = { userId: 123, user: { email: null } } as any;
|
const req = { userId: 123, user: { email: null } } as unknown as NextApiRequest;
|
||||||
const result = await isLockedOrBlocked(req);
|
const result = await isLockedOrBlocked(req);
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -44,7 +42,7 @@ describe("isLockedOrBlocked", () => {
|
|||||||
locked: true,
|
locked: true,
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
},
|
},
|
||||||
} as any;
|
} as unknown as NextApiRequest;
|
||||||
|
|
||||||
const result = await isLockedOrBlocked(req);
|
const result = await isLockedOrBlocked(req);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
@@ -57,7 +55,7 @@ describe("isLockedOrBlocked", () => {
|
|||||||
locked: false,
|
locked: false,
|
||||||
email: "test@blocked.com",
|
email: "test@blocked.com",
|
||||||
},
|
},
|
||||||
} as any;
|
} as unknown as NextApiRequest;
|
||||||
|
|
||||||
const result = await isLockedOrBlocked(req);
|
const result = await isLockedOrBlocked(req);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
@@ -70,7 +68,7 @@ describe("isLockedOrBlocked", () => {
|
|||||||
locked: false,
|
locked: false,
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
},
|
},
|
||||||
} as any;
|
} as unknown as NextApiRequest;
|
||||||
|
|
||||||
const result = await isLockedOrBlocked(req);
|
const result = await isLockedOrBlocked(req);
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
@@ -83,7 +81,7 @@ describe("isLockedOrBlocked", () => {
|
|||||||
locked: false,
|
locked: false,
|
||||||
email: "test@BLOCKED.COM",
|
email: "test@BLOCKED.COM",
|
||||||
},
|
},
|
||||||
} as any;
|
} as unknown as NextApiRequest;
|
||||||
|
|
||||||
const result = await isLockedOrBlocked(req);
|
const result = await isLockedOrBlocked(req);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
parserOptions: {
|
|
||||||
project: "tsconfig.json",
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
sourceType: "module",
|
|
||||||
},
|
|
||||||
plugins: ["@typescript-eslint/eslint-plugin"],
|
|
||||||
extends: ["plugin:@typescript-eslint/recommended"],
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
jest: true,
|
|
||||||
},
|
|
||||||
ignorePatterns: [".eslintrc.js", "next-i18next.config.js"],
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/interface-name-prefix": "off",
|
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ["./src/**/*.controller.ts"],
|
|
||||||
excludedFiles: "*.spec.js",
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/explicit-function-return-type": "error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"_dev:build:watch:enums": "yarn workspace @calcom/platform-enums build:watch",
|
"_dev:build:watch:enums": "yarn workspace @calcom/platform-enums build:watch",
|
||||||
"_dev:build:watch:utils": "yarn workspace @calcom/platform-utils build:watch",
|
"_dev:build:watch:utils": "yarn workspace @calcom/platform-utils build:watch",
|
||||||
"_dev:build:watch:types": "yarn workspace @calcom/platform-types build:watch",
|
"_dev:build:watch:types": "yarn workspace @calcom/platform-types build:watch",
|
||||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build",
|
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build",
|
||||||
"dev": "yarn dev:build && ts-node scripts/docker-start.ts && yarn copy-swagger-module && yarn start --watch",
|
"dev": "yarn dev:build && ts-node scripts/docker-start.ts && yarn copy-swagger-module && yarn start --watch",
|
||||||
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
|
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
@@ -30,18 +30,19 @@
|
|||||||
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.ts --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 src/swagger/copy-swagger-module.ts",
|
||||||
|
"generate-swagger": "yarn copy-swagger-module && yarn build && node ./dist/apps/api/v2/src/swagger/generate-swagger-script.js",
|
||||||
"prepare": "yarn run snyk-protect",
|
"prepare": "yarn run snyk-protect",
|
||||||
"snyk-protect": "snyk-protect"
|
"snyk-protect": "snyk-protect"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "workspace:*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "workspace:*",
|
||||||
"@calcom/platform-libraries": "*",
|
"@calcom/platform-libraries": "workspace:*",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "workspace:*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "workspace:*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "workspace:*",
|
||||||
"@golevelup/ts-jest": "^0.4.0",
|
"@golevelup/ts-jest": "^0.4.0",
|
||||||
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
|
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
|
||||||
"@nest-lab/throttler-storage-redis": "1.0.0",
|
"@nest-lab/throttler-storage-redis": "1.0.0",
|
||||||
@@ -88,6 +89,7 @@
|
|||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@calcom/eslint-config": "workspace:*",
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
|||||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
|
import { InstantBookingModule } from "@/lib/modules/instant-booking.module";
|
||||||
|
import { RecurringBookingModule } from "@/lib/modules/recurring-booking.module";
|
||||||
|
import { RegularBookingModule } from "@/lib/modules/regular-booking.module";
|
||||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import { BillingModule } from "@/modules/billing/billing.module";
|
import { BillingModule } from "@/modules/billing/billing.module";
|
||||||
@@ -35,6 +38,9 @@ import { Module } from "@nestjs/common";
|
|||||||
SchedulesModule_2024_04_15,
|
SchedulesModule_2024_04_15,
|
||||||
EventTypesModule_2024_06_14,
|
EventTypesModule_2024_06_14,
|
||||||
ProfilesModule,
|
ProfilesModule,
|
||||||
|
RegularBookingModule,
|
||||||
|
RecurringBookingModule,
|
||||||
|
InstantBookingModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
TokensRepository,
|
TokensRepository,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { randomString } from "test/utils/randomString";
|
|||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { handleNewBooking } from "@calcom/platform-libraries";
|
import { type RegularBookingCreateResult } from "@calcom/platform-libraries/bookings";
|
||||||
import type { ApiSuccessResponse, ApiErrorResponse } from "@calcom/platform-types";
|
import type { ApiSuccessResponse, ApiErrorResponse } from "@calcom/platform-types";
|
||||||
import type { User } from "@calcom/prisma/client";
|
import type { User } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
|||||||
|
|
||||||
let eventTypeId: number;
|
let eventTypeId: number;
|
||||||
|
|
||||||
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
|
let createdBooking: RegularBookingCreateResult;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await withApiAuth(
|
||||||
@@ -131,8 +131,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
|||||||
.send(body)
|
.send(body)
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
|
const responseBody: ApiSuccessResponse<RegularBookingCreateResult> = response.body;
|
||||||
response.body;
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
||||||
@@ -231,8 +230,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
|||||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
|
const responseBody: ApiSuccessResponse<RegularBookingCreateResult> = response.body;
|
||||||
response.body;
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
||||||
@@ -289,8 +287,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
|||||||
.send(body)
|
.send(body)
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
|
const responseBody: ApiSuccessResponse<RegularBookingCreateResult> = response.body;
|
||||||
response.body;
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
||||||
@@ -387,8 +384,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
|||||||
.send(body)
|
.send(body)
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
|
const responseBody: ApiSuccessResponse<RegularBookingCreateResult> = response.body;
|
||||||
response.body;
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/ma
|
|||||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||||
import { sha256Hash, 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 { InstantBookingCreateService } from "@/lib/services/instant-booking-create.service";
|
||||||
|
import { RecurringBookingService } from "@/lib/services/recurring-booking.service";
|
||||||
|
import { RegularBookingService } from "@/lib/services/regular-booking.service";
|
||||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
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";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
@@ -45,11 +48,8 @@ import { v4 as uuidv4 } from "uuid";
|
|||||||
import { X_CAL_CLIENT_ID, X_CAL_PLATFORM_EMBED } from "@calcom/platform-constants";
|
import { X_CAL_CLIENT_ID, X_CAL_PLATFORM_EMBED } from "@calcom/platform-constants";
|
||||||
import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants";
|
import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants";
|
||||||
import {
|
import {
|
||||||
handleNewRecurringBooking,
|
|
||||||
handleNewBooking,
|
|
||||||
BookingResponse,
|
BookingResponse,
|
||||||
HttpError,
|
HttpError,
|
||||||
handleInstantMeeting,
|
|
||||||
handleMarkNoShow,
|
handleMarkNoShow,
|
||||||
getAllUserBookings,
|
getAllUserBookings,
|
||||||
getBookingInfo,
|
getBookingInfo,
|
||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
ErrorCode,
|
ErrorCode,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries";
|
||||||
import { CreationSource } from "@calcom/platform-libraries";
|
import { CreationSource } from "@calcom/platform-libraries";
|
||||||
|
import { type InstantBookingCreateResult } from "@calcom/platform-libraries/bookings";
|
||||||
import {
|
import {
|
||||||
GetBookingsInput_2024_04_15,
|
GetBookingsInput_2024_04_15,
|
||||||
CancelBookingInput_2024_04_15,
|
CancelBookingInput_2024_04_15,
|
||||||
@@ -109,7 +110,10 @@ export class BookingsController_2024_04_15 {
|
|||||||
private readonly apiKeyRepository: ApiKeysRepository,
|
private readonly apiKeyRepository: ApiKeysRepository,
|
||||||
private readonly platformBookingsService: PlatformBookingsService,
|
private readonly platformBookingsService: PlatformBookingsService,
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
private readonly usersService: UsersService
|
private readonly usersService: UsersService,
|
||||||
|
private readonly regularBookingService: RegularBookingService,
|
||||||
|
private readonly recurringBookingService: RecurringBookingService,
|
||||||
|
private readonly instantBookingCreateService: InstantBookingCreateService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@@ -187,17 +191,19 @@ export class BookingsController_2024_04_15 {
|
|||||||
const { orgSlug, locationUrl } = body;
|
const { orgSlug, locationUrl } = body;
|
||||||
try {
|
try {
|
||||||
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed);
|
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed);
|
||||||
const booking = await handleNewBooking({
|
const booking = await this.regularBookingService.createBooking({
|
||||||
bookingData: bookingRequest.body,
|
bookingData: bookingRequest.body,
|
||||||
userId: bookingRequest.userId,
|
bookingMeta: {
|
||||||
hostname: bookingRequest.headers?.host || "",
|
userId: bookingRequest.userId,
|
||||||
forcedSlug: orgSlug,
|
hostname: bookingRequest.headers?.host || "",
|
||||||
platformClientId: bookingRequest.platformClientId,
|
forcedSlug: orgSlug,
|
||||||
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
platformClientId: bookingRequest.platformClientId,
|
||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
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, {
|
||||||
@@ -315,15 +321,17 @@ export class BookingsController_2024_04_15 {
|
|||||||
|
|
||||||
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
|
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
|
||||||
|
|
||||||
const createdBookings: BookingResponse[] = await handleNewRecurringBooking({
|
const createdBookings: BookingResponse[] = await this.recurringBookingService.createBooking({
|
||||||
bookingData: bookingRequest.body,
|
bookingData: bookingRequest.body,
|
||||||
userId: bookingRequest.userId,
|
bookingMeta: {
|
||||||
hostname: bookingRequest.headers?.host || "",
|
userId: bookingRequest.userId,
|
||||||
platformClientId: bookingRequest.platformClientId,
|
hostname: bookingRequest.headers?.host || "",
|
||||||
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
platformClientId: bookingRequest.platformClientId,
|
||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
createdBookings.forEach(async (booking) => {
|
createdBookings.forEach(async (booking) => {
|
||||||
@@ -351,14 +359,15 @@ export class BookingsController_2024_04_15 {
|
|||||||
@Body() body: CreateBookingInput_2024_04_15,
|
@Body() body: CreateBookingInput_2024_04_15,
|
||||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||||
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
|
): Promise<ApiResponse<InstantBookingCreateResult>> {
|
||||||
const oAuthClientId =
|
const oAuthClientId =
|
||||||
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
|
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
|
||||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||||
try {
|
try {
|
||||||
const instantMeeting = await handleInstantMeeting(
|
const bookingReq = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
|
||||||
await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed)
|
const instantMeeting = await this.instantBookingCreateService.createBooking({
|
||||||
);
|
bookingData: bookingReq.body,
|
||||||
|
});
|
||||||
|
|
||||||
if (instantMeeting.userId && instantMeeting.bookingUid) {
|
if (instantMeeting.userId && instantMeeting.bookingUid) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_0
|
|||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||||
|
import { InstantBookingModule } from "@/lib/modules/instant-booking.module";
|
||||||
|
import { RecurringBookingModule } from "@/lib/modules/recurring-booking.module";
|
||||||
|
import { RegularBookingModule } from "@/lib/modules/regular-booking.module";
|
||||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import { BillingModule } from "@/modules/billing/billing.module";
|
import { BillingModule } from "@/modules/billing/billing.module";
|
||||||
@@ -58,6 +61,9 @@ import { Module } from "@nestjs/common";
|
|||||||
TeamsEventTypesModule,
|
TeamsEventTypesModule,
|
||||||
MembershipsModule,
|
MembershipsModule,
|
||||||
ProfilesModule,
|
ProfilesModule,
|
||||||
|
RegularBookingModule,
|
||||||
|
RecurringBookingModule,
|
||||||
|
InstantBookingModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
TokensRepository,
|
TokensRepository,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ import {
|
|||||||
@DocsTags("Bookings")
|
@DocsTags("Bookings")
|
||||||
@ApiHeader({
|
@ApiHeader({
|
||||||
name: "cal-api-version",
|
name: "cal-api-version",
|
||||||
description: `Must be set to ${VERSION_2024_08_13}`,
|
description: `Must be set to ${VERSION_2024_08_13}. If not set to this value, the endpoint will default to an older version.`,
|
||||||
example: VERSION_2024_08_13,
|
example: VERSION_2024_08_13,
|
||||||
required: true,
|
required: true,
|
||||||
schema: {
|
schema: {
|
||||||
@@ -133,6 +133,8 @@ export class BookingsController_2024_08_13 {
|
|||||||
|
|
||||||
If you are creating a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
If you are creating a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
@@ -188,6 +190,8 @@ export class BookingsController_2024_08_13 {
|
|||||||
|
|
||||||
If you are fetching a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
If you are fetching a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
async getBooking(
|
async getBooking(
|
||||||
@@ -206,7 +210,10 @@ export class BookingsController_2024_08_13 {
|
|||||||
@UseGuards(BookingUidGuard)
|
@UseGuards(BookingUidGuard)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get all the recordings for the booking",
|
summary: "Get all the recordings for the booking",
|
||||||
description: `Fetches all the recordings for the booking \`:bookingUid\``,
|
description: `Fetches all the recordings for the booking \`:bookingUid\`
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
async getBookingRecordings(@Param("bookingUid") bookingUid: string): Promise<GetBookingRecordingsOutput> {
|
async getBookingRecordings(@Param("bookingUid") bookingUid: string): Promise<GetBookingRecordingsOutput> {
|
||||||
const recordings = await this.calVideoService.getRecordings(bookingUid);
|
const recordings = await this.calVideoService.getRecordings(bookingUid);
|
||||||
@@ -221,7 +228,10 @@ export class BookingsController_2024_08_13 {
|
|||||||
@UseGuards(BookingUidGuard)
|
@UseGuards(BookingUidGuard)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get all the transcripts download links for the booking",
|
summary: "Get all the transcripts download links for the booking",
|
||||||
description: `Fetches all the transcripts download links for the booking \`:bookingUid\``,
|
description: `Fetches all the transcripts download links for the booking \`:bookingUid\`
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
async getBookingTranscripts(@Param("bookingUid") bookingUid: string): Promise<GetBookingTranscriptsOutput> {
|
async getBookingTranscripts(@Param("bookingUid") bookingUid: string): Promise<GetBookingTranscriptsOutput> {
|
||||||
const transcripts = await this.calVideoService.getTranscripts(bookingUid);
|
const transcripts = await this.calVideoService.getTranscripts(bookingUid);
|
||||||
@@ -236,7 +246,10 @@ export class BookingsController_2024_08_13 {
|
|||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@Permissions([BOOKING_READ])
|
@Permissions([BOOKING_READ])
|
||||||
@ApiOperation({ summary: "Get all bookings" })
|
@ApiOperation({
|
||||||
|
summary: "Get all bookings",
|
||||||
|
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
|
||||||
|
})
|
||||||
async getBookings(
|
async getBookings(
|
||||||
@Query() queryParams: GetBookingsInput_2024_08_13,
|
@Query() queryParams: GetBookingsInput_2024_08_13,
|
||||||
@GetUser() user: ApiAuthGuardUser
|
@GetUser() user: ApiAuthGuardUser
|
||||||
@@ -263,7 +276,10 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Reschedule a booking",
|
summary: "Reschedule a booking",
|
||||||
description: "Reschedule a booking or seated booking",
|
description: `Reschedule a booking or seated booking
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
schema: {
|
schema: {
|
||||||
@@ -315,6 +331,8 @@ export class BookingsController_2024_08_13 {
|
|||||||
|
|
||||||
If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
@@ -350,7 +368,10 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Mark a booking absence",
|
summary: "Mark a booking absence",
|
||||||
description: "The provided authorization header refers to the owner of the booking.",
|
description: `The provided authorization header refers to the owner of the booking.
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
async markNoShow(
|
async markNoShow(
|
||||||
@Param("bookingUid") bookingUid: string,
|
@Param("bookingUid") bookingUid: string,
|
||||||
@@ -372,8 +393,10 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Reassign a booking to auto-selected host",
|
summary: "Reassign a booking to auto-selected host",
|
||||||
description:
|
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
|
||||||
"Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.",
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
async reassignBooking(
|
async reassignBooking(
|
||||||
@Param("bookingUid") bookingUid: string,
|
@Param("bookingUid") bookingUid: string,
|
||||||
@@ -394,8 +417,10 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Reassign a booking to a specific host",
|
summary: "Reassign a booking to a specific host",
|
||||||
description:
|
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
|
||||||
"Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.",
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
async reassignBookingToUser(
|
async reassignBookingToUser(
|
||||||
@Param("bookingUid") bookingUid: string,
|
@Param("bookingUid") bookingUid: string,
|
||||||
@@ -423,7 +448,10 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Confirm a booking",
|
summary: "Confirm a booking",
|
||||||
description: "The provided authorization header refers to the owner of the booking.",
|
description: `The provided authorization header refers to the owner of the booking.
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
async confirmBooking(
|
async confirmBooking(
|
||||||
@Param("bookingUid") bookingUid: string,
|
@Param("bookingUid") bookingUid: string,
|
||||||
@@ -444,7 +472,10 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Decline a booking",
|
summary: "Decline a booking",
|
||||||
description: "The provided authorization header refers to the owner of the booking.",
|
description: `The provided authorization header refers to the owner of the booking.
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
async declineBooking(
|
async declineBooking(
|
||||||
@Param("bookingUid") bookingUid: string,
|
@Param("bookingUid") bookingUid: string,
|
||||||
@@ -465,8 +496,10 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get 'Add to Calendar' links for a booking",
|
summary: "Get 'Add to Calendar' links for a booking",
|
||||||
description:
|
description: `Retrieve calendar links for a booking that can be used to add the event to various calendar services. Returns links for Google Calendar, Microsoft Office, Microsoft Outlook, and a downloadable ICS file.
|
||||||
"Retrieve calendar links for a booking that can be used to add the event to various calendar services. Returns links for Google Calendar, Microsoft Office, Microsoft Outlook, and a downloadable ICS file.",
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async getCalendarLinks(@Param("bookingUid") bookingUid: string): Promise<CalendarLinksOutput_2024_08_13> {
|
async getCalendarLinks(@Param("bookingUid") bookingUid: string): Promise<CalendarLinksOutput_2024_08_13> {
|
||||||
@@ -485,6 +518,7 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get booking references",
|
summary: "Get booking references",
|
||||||
|
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
|
||||||
})
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async getBookingReferences(
|
async getBookingReferences(
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
let team1EventTypeId: number;
|
let team1EventTypeId: number;
|
||||||
let team2EventTypeId: number;
|
let team2EventTypeId: number;
|
||||||
let phoneOnlyEventTypeId: number;
|
let phoneOnlyEventTypeId: number;
|
||||||
|
let collectiveEventWithoutHostsId: number;
|
||||||
|
let roundRobinEventWithoutHostsId: number;
|
||||||
|
|
||||||
const team1EventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
const team1EventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
||||||
const team2EventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
const team2EventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
||||||
@@ -223,6 +225,28 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
|
|
||||||
team1EventTypeId = team1EventType.id;
|
team1EventTypeId = team1EventType.id;
|
||||||
|
|
||||||
|
const team1CollectiveEventTypeWithoutHosts = await eventTypesRepositoryFixture.createTeamEventType({
|
||||||
|
schedulingType: "COLLECTIVE",
|
||||||
|
team: {
|
||||||
|
connect: { id: team1.id },
|
||||||
|
},
|
||||||
|
title: `team-bookings-2024-08-13-collective-event-type-without-hosts-${randomString()}`,
|
||||||
|
slug: `team-bookings-2024-08-13-collective-event-type-without-hosts-${randomString()}`,
|
||||||
|
length: 60,
|
||||||
|
});
|
||||||
|
collectiveEventWithoutHostsId = team1CollectiveEventTypeWithoutHosts.id;
|
||||||
|
|
||||||
|
const team1RoundRobinEventTypeWithoutHosts = await eventTypesRepositoryFixture.createTeamEventType({
|
||||||
|
schedulingType: "ROUND_ROBIN",
|
||||||
|
team: {
|
||||||
|
connect: { id: team1.id },
|
||||||
|
},
|
||||||
|
title: `team-bookings-2024-08-13-round-robin-event-type-without-hosts-${randomString()}`,
|
||||||
|
slug: `team-bookings-2024-08-13-round-robin-event-type-without-hosts-${randomString()}`,
|
||||||
|
length: 60,
|
||||||
|
});
|
||||||
|
roundRobinEventWithoutHostsId = team1RoundRobinEventTypeWithoutHosts.id;
|
||||||
|
|
||||||
const phoneOnlyEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
const phoneOnlyEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||||
schedulingType: "ROUND_ROBIN",
|
schedulingType: "ROUND_ROBIN",
|
||||||
team: {
|
team: {
|
||||||
@@ -363,6 +387,52 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("cant book event types without hosts", () => {
|
||||||
|
it("should fail to book a collective event type without hosts", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: collectiveEventWithoutHostsId,
|
||||||
|
attendee: {
|
||||||
|
name: "alice",
|
||||||
|
timeZone: "Europe/Madrid",
|
||||||
|
email: "alice@gmail.com",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||||
|
|
||||||
|
expect(response.status).toBe(422);
|
||||||
|
expect(response.body.error.message).toBe(
|
||||||
|
`Can't book this team event type because it has no hosts. Please, add at least 1 host to event type with id=${collectiveEventWithoutHostsId} belonging to team with id=${team1.id} and try again.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail to book a round robin event type without hosts", async () => {
|
||||||
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: roundRobinEventWithoutHostsId,
|
||||||
|
attendee: {
|
||||||
|
name: "alice",
|
||||||
|
timeZone: "Europe/Madrid",
|
||||||
|
email: "alice@gmail.com",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||||
|
|
||||||
|
expect(response.status).toBe(422);
|
||||||
|
expect(response.body.error.message).toBe(
|
||||||
|
`Can't book this team event type because it has no hosts. Please, add at least 1 host to event type with id=${roundRobinEventWithoutHostsId} belonging to team with id=${team1.id} and try again.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("create team bookings", () => {
|
describe("create team bookings", () => {
|
||||||
it("should create a team 1 booking", async () => {
|
it("should create a team 1 booking", async () => {
|
||||||
const body: CreateBookingInput_2024_08_13 = {
|
const body: CreateBookingInput_2024_08_13 = {
|
||||||
@@ -765,8 +835,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
|
||||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
return !Array.isArray(data) && typeof data === "object" && data !== null && data && "id" in data;
|
||||||
}
|
}
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, ValidateNested } from "class-validator";
|
import { IsEnum, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
import { ReassignBookingOutput_2024_08_13 as ReassignBookingOutputData_2024_08_13 } from "@calcom/platform-types";
|
import { ReassignBookingOutput_2024_08_13 as ReassignBookingOutputData_2024_08_13 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@ApiExtraModels(ReassignBookingOutputData_2024_08_13)
|
||||||
export class ReassignBookingOutput_2024_08_13 {
|
export class ReassignBookingOutput_2024_08_13 {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import {
|
|||||||
BookingOutput_2024_08_13,
|
BookingOutput_2024_08_13,
|
||||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||||
CreateSeatedBookingOutput_2024_08_13,
|
CreateSeatedBookingOutput_2024_08_13,
|
||||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
|
||||||
GetSeatedBookingOutput_2024_08_13,
|
|
||||||
RecurringBookingOutput_2024_08_13,
|
RecurringBookingOutput_2024_08_13,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servi
|
|||||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
import { getPagination } from "@/lib/pagination/pagination";
|
import { getPagination } from "@/lib/pagination/pagination";
|
||||||
|
import { InstantBookingCreateService } from "@/lib/services/instant-booking-create.service";
|
||||||
|
import { RecurringBookingService } from "@/lib/services/recurring-booking.service";
|
||||||
|
import { RegularBookingService } from "@/lib/services/regular-booking.service";
|
||||||
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||||
@@ -29,6 +32,7 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
|
UnprocessableEntityException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { BadRequestException } from "@nestjs/common";
|
import { BadRequestException } from "@nestjs/common";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
@@ -36,10 +40,8 @@ import { DateTime } from "luxon";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
handleNewRecurringBooking,
|
|
||||||
getTranslation,
|
getTranslation,
|
||||||
getAllUserBookings,
|
getAllUserBookings,
|
||||||
handleInstantMeeting,
|
|
||||||
handleCancelBooking,
|
handleCancelBooking,
|
||||||
roundRobinReassignment,
|
roundRobinReassignment,
|
||||||
roundRobinManualReassignment,
|
roundRobinManualReassignment,
|
||||||
@@ -47,7 +49,6 @@ import {
|
|||||||
confirmBookingHandler,
|
confirmBookingHandler,
|
||||||
getCalendarLinks,
|
getCalendarLinks,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries";
|
||||||
import { handleNewBooking } from "@calcom/platform-libraries";
|
|
||||||
import {
|
import {
|
||||||
CreateBookingInput_2024_08_13,
|
CreateBookingInput_2024_08_13,
|
||||||
CreateBookingInput,
|
CreateBookingInput,
|
||||||
@@ -108,7 +109,10 @@ export class BookingsService_2024_08_13 {
|
|||||||
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
|
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
|
||||||
private readonly membershipsRepository: MembershipsRepository,
|
private readonly membershipsRepository: MembershipsRepository,
|
||||||
private readonly membershipsService: MembershipsService,
|
private readonly membershipsService: MembershipsService,
|
||||||
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13
|
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13,
|
||||||
|
private readonly regularBookingService: RegularBookingService,
|
||||||
|
private readonly recurringBookingService: RecurringBookingService,
|
||||||
|
private readonly instantBookingCreateService: InstantBookingCreateService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) {
|
async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) {
|
||||||
@@ -132,6 +136,10 @@ export class BookingsService_2024_08_13 {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventType.schedulingType === "COLLECTIVE" || eventType.schedulingType === "ROUND_ROBIN") {
|
||||||
|
await this.checkEventTypeHasHosts(eventType.id);
|
||||||
|
}
|
||||||
|
|
||||||
body.eventTypeId = eventType.id;
|
body.eventTypeId = eventType.id;
|
||||||
|
|
||||||
if ("instant" in body && body.instant) {
|
if ("instant" in body && body.instant) {
|
||||||
@@ -159,6 +167,15 @@ export class BookingsService_2024_08_13 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkEventTypeHasHosts(eventTypeId: number) {
|
||||||
|
const eventType = await this.eventTypesRepository.getEventTypeWithHosts(eventTypeId);
|
||||||
|
if (!eventType?.hosts?.length) {
|
||||||
|
throw new UnprocessableEntityException(
|
||||||
|
`Can't book this team event type because it has no hosts. Please, add at least 1 host to event type with id=${eventTypeId} belonging to team with id=${eventType?.teamId} and try again.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async checkBookingRequiresAuthenticationSetting(
|
async checkBookingRequiresAuthenticationSetting(
|
||||||
eventType: EventTypeWithOwnerAndTeam,
|
eventType: EventTypeWithOwnerAndTeam,
|
||||||
authUser: AuthOptionalUser,
|
authUser: AuthOptionalUser,
|
||||||
@@ -450,7 +467,9 @@ export class BookingsService_2024_08_13 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
|
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
|
||||||
const booking = await handleInstantMeeting(bookingRequest);
|
const booking = await this.instantBookingCreateService.createBooking({
|
||||||
|
bookingData: bookingRequest.body,
|
||||||
|
});
|
||||||
|
|
||||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(
|
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(
|
||||||
booking.bookingId
|
booking.bookingId
|
||||||
@@ -468,17 +487,19 @@ export class BookingsService_2024_08_13 {
|
|||||||
eventType: EventTypeWithOwnerAndTeam
|
eventType: EventTypeWithOwnerAndTeam
|
||||||
) {
|
) {
|
||||||
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body, eventType);
|
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body, eventType);
|
||||||
const bookings = await handleNewRecurringBooking({
|
const bookings = await this.recurringBookingService.createBooking({
|
||||||
bookingData: bookingRequest.body,
|
bookingData: bookingRequest.body,
|
||||||
userId: bookingRequest.userId,
|
bookingMeta: {
|
||||||
hostname: bookingRequest.headers?.host || "",
|
userId: bookingRequest.userId,
|
||||||
platformClientId: bookingRequest.platformClientId,
|
hostname: bookingRequest.headers?.host || "",
|
||||||
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
platformClientId: bookingRequest.platformClientId,
|
||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
noEmail: bookingRequest.noEmail,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
noEmail: bookingRequest.noEmail,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const ids = bookings.map((booking) => booking.id || 0);
|
const ids = bookings.map((booking) => booking.id || 0);
|
||||||
return this.outputService.getOutputRecurringBookings(ids);
|
return this.outputService.getOutputRecurringBookings(ids);
|
||||||
@@ -491,16 +512,18 @@ export class BookingsService_2024_08_13 {
|
|||||||
userIsEventTypeAdminOrOwner: boolean
|
userIsEventTypeAdminOrOwner: boolean
|
||||||
) {
|
) {
|
||||||
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body, eventType);
|
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body, eventType);
|
||||||
const bookings = await handleNewRecurringBooking({
|
const bookings = await this.recurringBookingService.createBooking({
|
||||||
bookingData: bookingRequest.body,
|
bookingData: bookingRequest.body,
|
||||||
userId: bookingRequest.userId,
|
bookingMeta: {
|
||||||
hostname: bookingRequest.headers?.host || "",
|
userId: bookingRequest.userId,
|
||||||
platformClientId: bookingRequest.platformClientId,
|
hostname: bookingRequest.headers?.host || "",
|
||||||
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
platformClientId: bookingRequest.platformClientId,
|
||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return this.outputService.getOutputCreateRecurringSeatedBookings(
|
return this.outputService.getOutputCreateRecurringSeatedBookings(
|
||||||
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" })),
|
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" })),
|
||||||
@@ -514,16 +537,18 @@ export class BookingsService_2024_08_13 {
|
|||||||
eventType: EventTypeWithOwnerAndTeam
|
eventType: EventTypeWithOwnerAndTeam
|
||||||
) {
|
) {
|
||||||
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
|
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
|
||||||
const booking = await handleNewBooking({
|
const booking = await this.regularBookingService.createBooking({
|
||||||
bookingData: bookingRequest.body,
|
bookingData: bookingRequest.body,
|
||||||
userId: bookingRequest.userId,
|
bookingMeta: {
|
||||||
hostname: bookingRequest.headers?.host || "",
|
userId: bookingRequest.userId,
|
||||||
platformClientId: bookingRequest.platformClientId,
|
hostname: bookingRequest.headers?.host || "",
|
||||||
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
platformClientId: bookingRequest.platformClientId,
|
||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!booking.uid) {
|
if (!booking.uid) {
|
||||||
@@ -546,16 +571,18 @@ export class BookingsService_2024_08_13 {
|
|||||||
) {
|
) {
|
||||||
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
|
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
|
||||||
try {
|
try {
|
||||||
const booking = await handleNewBooking({
|
const booking = await this.regularBookingService.createBooking({
|
||||||
bookingData: bookingRequest.body,
|
bookingData: bookingRequest.body,
|
||||||
userId: bookingRequest.userId,
|
bookingMeta: {
|
||||||
hostname: bookingRequest.headers?.host || "",
|
userId: bookingRequest.userId,
|
||||||
platformClientId: bookingRequest.platformClientId,
|
hostname: bookingRequest.headers?.host || "",
|
||||||
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
platformClientId: bookingRequest.platformClientId,
|
||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!booking.uid) {
|
if (!booking.uid) {
|
||||||
@@ -741,16 +768,18 @@ export class BookingsService_2024_08_13 {
|
|||||||
bookingUid,
|
bookingUid,
|
||||||
body
|
body
|
||||||
);
|
);
|
||||||
const booking = await handleNewBooking({
|
const booking = await this.regularBookingService.createBooking({
|
||||||
bookingData: bookingRequest.body,
|
bookingData: bookingRequest.body,
|
||||||
userId: bookingRequest.userId,
|
bookingMeta: {
|
||||||
hostname: bookingRequest.headers?.host || "",
|
userId: bookingRequest.userId,
|
||||||
platformClientId: bookingRequest.platformClientId,
|
hostname: bookingRequest.headers?.host || "",
|
||||||
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
platformClientId: bookingRequest.platformClientId,
|
||||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
||||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||||
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||||
|
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!booking.uid) {
|
if (!booking.uid) {
|
||||||
throw new Error("Booking missing uid");
|
throw new Error("Booking missing uid");
|
||||||
|
|||||||
+51
-9
@@ -38,6 +38,7 @@ import type {
|
|||||||
GuestsDefaultFieldOutput_2024_06_14,
|
GuestsDefaultFieldOutput_2024_06_14,
|
||||||
NameDefaultFieldInput_2024_06_14,
|
NameDefaultFieldInput_2024_06_14,
|
||||||
NotesDefaultFieldInput_2024_06_14,
|
NotesDefaultFieldInput_2024_06_14,
|
||||||
|
SplitNameDefaultFieldOutput_2024_06_14,
|
||||||
UpdateEventTypeInput_2024_06_14,
|
UpdateEventTypeInput_2024_06_14,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import type { PlatformOAuthClient, Team, User, Schedule, EventType } from "@calcom/prisma/client";
|
import type { PlatformOAuthClient, Team, User, Schedule, EventType } from "@calcom/prisma/client";
|
||||||
@@ -1447,23 +1448,23 @@ describe("Event types Endpoints", () => {
|
|||||||
try {
|
try {
|
||||||
await eventTypesRepositoryFixture.delete(eventType.id);
|
await eventTypesRepositoryFixture.delete(eventType.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Event type might have been deleted by the test
|
console.log(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await userRepositoryFixture.delete(user.id);
|
await userRepositoryFixture.delete(user.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// User might have been deleted by the test
|
console.log(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await userRepositoryFixture.delete(falseTestUser.id);
|
await userRepositoryFixture.delete(falseTestUser.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// User might have been deleted by the test
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await userRepositoryFixture.delete(orgUser.id);
|
await userRepositoryFixture.delete(orgUser.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// User might have been deleted by the test
|
console.log(e);
|
||||||
}
|
}
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
@@ -2187,6 +2188,47 @@ describe("Event types Endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("split name booking field", () => {
|
||||||
|
it("should create an event type with split name booking field", async () => {
|
||||||
|
const splitNameBookingField: SplitNameDefaultFieldOutput_2024_06_14 = {
|
||||||
|
isDefault: true,
|
||||||
|
type: "splitName",
|
||||||
|
slug: "splitName",
|
||||||
|
firstNameLabel: "First name",
|
||||||
|
firstNamePlaceholder: "",
|
||||||
|
lastNameLabel: "last name",
|
||||||
|
lastNamePlaceholder: "",
|
||||||
|
lastNameRequired: false,
|
||||||
|
disableOnPrefill: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const body: CreateEventTypeInput_2024_06_14 = {
|
||||||
|
title: "Coding class 9",
|
||||||
|
slug: "coding-class-9",
|
||||||
|
description: "Let's learn how to code like a pro.",
|
||||||
|
lengthInMinutes: 60,
|
||||||
|
bookingFields: [splitNameBookingField],
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/api/v2/event-types")
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||||
|
.send(body)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
|
const createdEventType = responseBody.data;
|
||||||
|
|
||||||
|
const splitNameBookingFieldResponse = createdEventType.bookingFields.find(
|
||||||
|
(field) => field.type === "splitName"
|
||||||
|
) as SplitNameDefaultFieldOutput_2024_06_14 | undefined;
|
||||||
|
expect(splitNameBookingFieldResponse).toEqual(splitNameBookingField);
|
||||||
|
|
||||||
|
await eventTypesRepositoryFixture.delete(createdEventType.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
@@ -2194,12 +2236,12 @@ describe("Event types Endpoints", () => {
|
|||||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
|
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
|
||||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
|
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Event type might have been deleted by the test
|
console.log(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await userRepositoryFixture.delete(user.id);
|
await userRepositoryFixture.delete(user.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// User might have been deleted by the test
|
console.log(e);
|
||||||
}
|
}
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
@@ -2379,7 +2421,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||||
const updatedEventType = responseBody.data;
|
const updatedEventType = responseBody.data;
|
||||||
+expect(updatedEventType.hidden).toBe(false);
|
expect(updatedEventType.hidden).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2389,12 +2431,12 @@ describe("Event types Endpoints", () => {
|
|||||||
try {
|
try {
|
||||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
|
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Event type might have been deleted by the test
|
console.log(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await userRepositoryFixture.delete(user.id);
|
await userRepositoryFixture.delete(user.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// User might have been deleted by the test
|
console.log(e);
|
||||||
}
|
}
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
|
|||||||
+21
-7
@@ -61,7 +61,7 @@ import {
|
|||||||
@DocsTags("Event Types")
|
@DocsTags("Event Types")
|
||||||
@ApiHeader({
|
@ApiHeader({
|
||||||
name: "cal-api-version",
|
name: "cal-api-version",
|
||||||
description: `Must be set to ${VERSION_2024_06_14}`,
|
description: `Must be set to ${VERSION_2024_06_14}. If not set to this value, the endpoint will default to an older version.`,
|
||||||
example: VERSION_2024_06_14,
|
example: VERSION_2024_06_14,
|
||||||
required: true,
|
required: true,
|
||||||
schema: {
|
schema: {
|
||||||
@@ -80,7 +80,10 @@ export class EventTypesController_2024_06_14 {
|
|||||||
@Permissions([EVENT_TYPE_WRITE])
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({ summary: "Create an event type" })
|
@ApiOperation({
|
||||||
|
summary: "Create an event type",
|
||||||
|
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
|
||||||
|
})
|
||||||
async createEventType(
|
async createEventType(
|
||||||
@Body() body: CreateEventTypeInput_2024_06_14,
|
@Body() body: CreateEventTypeInput_2024_06_14,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
@@ -102,7 +105,10 @@ export class EventTypesController_2024_06_14 {
|
|||||||
@Permissions([EVENT_TYPE_READ])
|
@Permissions([EVENT_TYPE_READ])
|
||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({ summary: "Get an event type" })
|
@ApiOperation({
|
||||||
|
summary: "Get an event type",
|
||||||
|
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
|
||||||
|
})
|
||||||
async getEventTypeById(
|
async getEventTypeById(
|
||||||
@Param("eventTypeId") eventTypeId: string,
|
@Param("eventTypeId") eventTypeId: string,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
@@ -122,8 +128,10 @@ export class EventTypesController_2024_06_14 {
|
|||||||
@Get("/")
|
@Get("/")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get all event types",
|
summary: "Get all event types",
|
||||||
description:
|
description: `Hidden event types are returned only if authentication is provided and it belongs to the event type owner.
|
||||||
"Hidden event types are returned only if authentication is provided and it belongs to the event type owner.",
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
@UseGuards(OptionalApiAuthGuard)
|
@UseGuards(OptionalApiAuthGuard)
|
||||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||||
@@ -149,7 +157,10 @@ export class EventTypesController_2024_06_14 {
|
|||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: "Update an event type" })
|
@ApiOperation({
|
||||||
|
summary: "Update an event type",
|
||||||
|
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
|
||||||
|
})
|
||||||
async updateEventType(
|
async updateEventType(
|
||||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
@Body() body: UpdateEventTypeInput_2024_06_14,
|
@Body() body: UpdateEventTypeInput_2024_06_14,
|
||||||
@@ -173,7 +184,10 @@ export class EventTypesController_2024_06_14 {
|
|||||||
@Permissions([EVENT_TYPE_WRITE])
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({ summary: "Delete an event type" })
|
@ApiOperation({
|
||||||
|
summary: "Delete an event type",
|
||||||
|
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
|
||||||
|
})
|
||||||
async deleteEventType(
|
async deleteEventType(
|
||||||
@Param("eventTypeId") eventTypeId: number,
|
@Param("eventTypeId") eventTypeId: number,
|
||||||
@GetUser("id") userId: number
|
@GetUser("id") userId: number
|
||||||
|
|||||||
+23
-6
@@ -40,7 +40,7 @@ import {
|
|||||||
@DocsTags("Schedules")
|
@DocsTags("Schedules")
|
||||||
@ApiHeader({
|
@ApiHeader({
|
||||||
name: "cal-api-version",
|
name: "cal-api-version",
|
||||||
description: `Must be set to ${VERSION_2024_06_11}`,
|
description: `Must be set to ${VERSION_2024_06_11}. If not set to this value, the endpoint will default to an older version.`,
|
||||||
example: VERSION_2024_06_11,
|
example: VERSION_2024_06_11,
|
||||||
required: true,
|
required: true,
|
||||||
schema: {
|
schema: {
|
||||||
@@ -69,6 +69,8 @@ export class SchedulesController_2024_06_11 {
|
|||||||
After creating a non-default schedule, you can update an event type to point to that schedule via the PATCH \`event-types/{eventTypeId}\` endpoint.
|
After creating a non-default schedule, you can update an event type to point to that schedule via the PATCH \`event-types/{eventTypeId}\` endpoint.
|
||||||
|
|
||||||
When specifying start time and end time for each day use the 24 hour format e.g. 08:00, 15:00 etc.
|
When specifying start time and end time for each day use the 24 hour format e.g. 08:00, 15:00 etc.
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
async createSchedule(
|
async createSchedule(
|
||||||
@@ -91,7 +93,10 @@ export class SchedulesController_2024_06_11 {
|
|||||||
})
|
})
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get default schedule",
|
summary: "Get default schedule",
|
||||||
description: "Get the default schedule of the authenticated user.",
|
description: `Get the default schedule of the authenticated user.
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetScheduleOutput_2024_06_11> {
|
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetScheduleOutput_2024_06_11> {
|
||||||
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
||||||
@@ -104,7 +109,10 @@ export class SchedulesController_2024_06_11 {
|
|||||||
|
|
||||||
@Get("/:scheduleId")
|
@Get("/:scheduleId")
|
||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
@ApiOperation({ summary: "Get a schedule" })
|
@ApiOperation({
|
||||||
|
summary: "Get a schedule",
|
||||||
|
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
|
||||||
|
})
|
||||||
async getSchedule(
|
async getSchedule(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Param("scheduleId") scheduleId: number
|
@Param("scheduleId") scheduleId: number
|
||||||
@@ -121,7 +129,10 @@ export class SchedulesController_2024_06_11 {
|
|||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get all schedules",
|
summary: "Get all schedules",
|
||||||
description: "Get all schedules of the authenticated user.",
|
description: `Get all schedules of the authenticated user.
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_06_11> {
|
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_06_11> {
|
||||||
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
||||||
@@ -134,7 +145,10 @@ export class SchedulesController_2024_06_11 {
|
|||||||
|
|
||||||
@Patch("/:scheduleId")
|
@Patch("/:scheduleId")
|
||||||
@Permissions([SCHEDULE_WRITE])
|
@Permissions([SCHEDULE_WRITE])
|
||||||
@ApiOperation({ summary: "Update a schedule" })
|
@ApiOperation({
|
||||||
|
summary: "Update a schedule",
|
||||||
|
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
|
||||||
|
})
|
||||||
async updateSchedule(
|
async updateSchedule(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() bodySchedule: UpdateScheduleInput_2024_06_11,
|
@Body() bodySchedule: UpdateScheduleInput_2024_06_11,
|
||||||
@@ -155,7 +169,10 @@ export class SchedulesController_2024_06_11 {
|
|||||||
@Delete("/:scheduleId")
|
@Delete("/:scheduleId")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Permissions([SCHEDULE_WRITE])
|
@Permissions([SCHEDULE_WRITE])
|
||||||
@ApiOperation({ summary: "Delete a schedule" })
|
@ApiOperation({
|
||||||
|
summary: "Delete a schedule",
|
||||||
|
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
|
||||||
|
})
|
||||||
async deleteSchedule(
|
async deleteSchedule(
|
||||||
@GetUser("id") userId: number,
|
@GetUser("id") userId: number,
|
||||||
@Param("scheduleId") scheduleId: number
|
@Param("scheduleId") scheduleId: number
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { InstantBookingCreateService } from "@/lib/services/instant-booking-create.service";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [InstantBookingCreateService],
|
||||||
|
exports: [InstantBookingCreateService],
|
||||||
|
})
|
||||||
|
export class InstantBookingModule {}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { RegularBookingModule } from "@/lib/modules/regular-booking.module";
|
||||||
|
import { RecurringBookingService } from "@/lib/services/recurring-booking.service";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [RegularBookingModule],
|
||||||
|
providers: [RecurringBookingService],
|
||||||
|
exports: [RecurringBookingService],
|
||||||
|
})
|
||||||
|
export class RecurringBookingModule {}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { PrismaAttributeRepository } from "@/lib/repositories/prisma-attribute.repository";
|
||||||
|
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||||
|
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||||
|
import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository";
|
||||||
|
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||||
|
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||||
|
import { CacheService } from "@/lib/services/cache.service";
|
||||||
|
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
|
||||||
|
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||||
|
import { LuckyUserService } from "@/lib/services/lucky-user.service";
|
||||||
|
import { RegularBookingService } from "@/lib/services/regular-booking.service";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [
|
||||||
|
PrismaAttributeRepository,
|
||||||
|
PrismaBookingRepository,
|
||||||
|
PrismaFeaturesRepository,
|
||||||
|
PrismaHostRepository,
|
||||||
|
PrismaOOORepository,
|
||||||
|
PrismaUserRepository,
|
||||||
|
CacheService,
|
||||||
|
CheckBookingAndDurationLimitsService,
|
||||||
|
CheckBookingLimitsService,
|
||||||
|
LuckyUserService,
|
||||||
|
RegularBookingService,
|
||||||
|
],
|
||||||
|
exports: [RegularBookingService],
|
||||||
|
})
|
||||||
|
export class RegularBookingModule {}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { InstantBookingCreateService as BaseInstantBookingCreateService } from "@calcom/platform-libraries/bookings";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class InstantBookingCreateService extends BaseInstantBookingCreateService {}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { RegularBookingService } from "@/lib/services/regular-booking.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { RecurringBookingService as BaseRecurringBookingService } from "@calcom/platform-libraries/bookings";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RecurringBookingService extends BaseRecurringBookingService {
|
||||||
|
constructor(regularBookingService: RegularBookingService) {
|
||||||
|
super({
|
||||||
|
regularBookingService,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { PrismaAttributeRepository } from "@/lib/repositories/prisma-attribute.repository";
|
||||||
|
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||||
|
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||||
|
import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository";
|
||||||
|
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||||
|
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||||
|
import { CacheService } from "@/lib/services/cache.service";
|
||||||
|
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
|
||||||
|
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||||
|
import { LuckyUserService } from "@/lib/services/lucky-user.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { RegularBookingService as BaseRegularBookingService } from "@calcom/platform-libraries/bookings";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RegularBookingService extends BaseRegularBookingService {
|
||||||
|
constructor(
|
||||||
|
cacheService: CacheService,
|
||||||
|
checkBookingAndDurationLimitsService: CheckBookingAndDurationLimitsService,
|
||||||
|
prismaWriteService: PrismaWriteService,
|
||||||
|
bookingRepository: PrismaBookingRepository,
|
||||||
|
featuresRepository: PrismaFeaturesRepository,
|
||||||
|
checkBookingLimitsService: CheckBookingLimitsService,
|
||||||
|
luckyUserService: LuckyUserService,
|
||||||
|
hostRepository: PrismaHostRepository,
|
||||||
|
oooRepository: PrismaOOORepository,
|
||||||
|
userRepository: PrismaUserRepository,
|
||||||
|
attributeRepository: PrismaAttributeRepository
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
cacheService,
|
||||||
|
checkBookingAndDurationLimitsService,
|
||||||
|
prismaClient: prismaWriteService.prisma as unknown as PrismaClient,
|
||||||
|
bookingRepository,
|
||||||
|
featuresRepository,
|
||||||
|
checkBookingLimitsService,
|
||||||
|
luckyUserService,
|
||||||
|
hostRepository,
|
||||||
|
oooRepository,
|
||||||
|
userRepository,
|
||||||
|
attributeRepository,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-108
@@ -1,39 +1,29 @@
|
|||||||
import type { AppConfig } from "@/config/type";
|
import type { AppConfig } from "@/config/type";
|
||||||
import { getEnv } from "@/env";
|
|
||||||
import { Logger } from "@nestjs/common";
|
import { Logger } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { NestFactory } from "@nestjs/core";
|
import { NestFactory } from "@nestjs/core";
|
||||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
|
||||||
import {
|
|
||||||
PathItemObject,
|
|
||||||
PathsObject,
|
|
||||||
OperationObject,
|
|
||||||
TagObject,
|
|
||||||
} from "@nestjs/swagger/dist/interfaces/open-api-spec.interface";
|
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import * as fs from "fs";
|
|
||||||
import { Server } from "http";
|
|
||||||
import { WinstonModule } from "nest-winston";
|
import { WinstonModule } from "nest-winston";
|
||||||
|
|
||||||
import { bootstrap } from "./app";
|
import { bootstrap } from "./app";
|
||||||
import { AppModule } from "./app.module";
|
import { AppModule } from "./app.module";
|
||||||
import { loggerConfig } from "./lib/logger";
|
import { loggerConfig } from "./lib/logger";
|
||||||
|
import { generateSwaggerForApp } from "./swagger/generate-swagger";
|
||||||
|
|
||||||
const HttpMethods: (keyof PathItemObject)[] = ["get", "post", "put", "delete", "patch", "options", "head"];
|
run().catch((error: Error) => {
|
||||||
|
console.error("Failed to start Cal Platform API", { error: error.stack });
|
||||||
const run = async () => {
|
process.exit(1);
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
});
|
||||||
logger: WinstonModule.createLogger(loggerConfig()),
|
|
||||||
bodyParser: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const app = await createNestApp();
|
||||||
const logger = new Logger("App");
|
const logger = new Logger("App");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bootstrap(app);
|
bootstrap(app);
|
||||||
const port = app.get(ConfigService<AppConfig, true>).get("api.port", { infer: true });
|
const port = app.get(ConfigService<AppConfig, true>).get("api.port", { infer: true });
|
||||||
void generateSwagger(app);
|
generateSwaggerForApp(app);
|
||||||
await app.listen(port);
|
await app.listen(port);
|
||||||
logger.log(`Application started on port: ${port}`);
|
logger.log(`Application started on port: ${port}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -42,97 +32,11 @@ const run = async () => {
|
|||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
function customTagSort(a: string, b: string): number {
|
|
||||||
const platformPrefix = "Platform";
|
|
||||||
const orgsPrefix = "Orgs";
|
|
||||||
|
|
||||||
if (a.startsWith(platformPrefix) && !b.startsWith(platformPrefix)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (!a.startsWith(platformPrefix) && b.startsWith(platformPrefix)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a.startsWith(orgsPrefix) && !b.startsWith(orgsPrefix)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (!a.startsWith(orgsPrefix) && b.startsWith(orgsPrefix)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.localeCompare(b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOperationObject(obj: any): obj is OperationObject {
|
export async function createNestApp() {
|
||||||
return obj && typeof obj === "object" && "tags" in obj;
|
return NestFactory.create<NestExpressApplication>(AppModule, {
|
||||||
}
|
logger: WinstonModule.createLogger(loggerConfig()),
|
||||||
|
bodyParser: false,
|
||||||
function groupAndSortPathsByFirstTag(paths: PathsObject): PathsObject {
|
|
||||||
const groupedPaths: { [key: string]: PathsObject } = {};
|
|
||||||
|
|
||||||
Object.keys(paths).forEach((pathKey) => {
|
|
||||||
const pathItem = paths[pathKey];
|
|
||||||
|
|
||||||
HttpMethods.forEach((method) => {
|
|
||||||
const operation = pathItem[method];
|
|
||||||
|
|
||||||
if (isOperationObject(operation) && operation.tags && operation.tags.length > 0) {
|
|
||||||
const firstTag = operation.tags[0];
|
|
||||||
|
|
||||||
if (!groupedPaths[firstTag]) {
|
|
||||||
groupedPaths[firstTag] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
groupedPaths[firstTag][pathKey] = pathItem;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedTags = Object.keys(groupedPaths).sort(customTagSort);
|
|
||||||
const sortedPaths: PathsObject = {};
|
|
||||||
|
|
||||||
sortedTags.forEach((tag) => {
|
|
||||||
Object.assign(sortedPaths, groupedPaths[tag]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return sortedPaths;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateSwagger(app: NestExpressApplication<Server>) {
|
|
||||||
const logger = new Logger("App");
|
|
||||||
logger.log(`Generating Swagger documentation...\n`);
|
|
||||||
|
|
||||||
const config = new DocumentBuilder().setTitle("Cal.com API v2").build();
|
|
||||||
const document = SwaggerModule.createDocument(app, config);
|
|
||||||
document.paths = groupAndSortPathsByFirstTag(document.paths);
|
|
||||||
|
|
||||||
const swaggerOutputFile = "./swagger/documentation.json";
|
|
||||||
const docsOutputFile = "../../../docs/api-reference/v2/openapi.json";
|
|
||||||
const stringifiedContents = JSON.stringify(document, null, 2);
|
|
||||||
|
|
||||||
if (fs.existsSync(swaggerOutputFile)) {
|
|
||||||
fs.unlinkSync(swaggerOutputFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(swaggerOutputFile, stringifiedContents, { encoding: "utf8" });
|
|
||||||
|
|
||||||
if (fs.existsSync(docsOutputFile) && getEnv("NODE_ENV") === "development") {
|
|
||||||
fs.unlinkSync(docsOutputFile);
|
|
||||||
fs.writeFileSync(docsOutputFile, stringifiedContents, { encoding: "utf8" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.DOCS_URL) {
|
|
||||||
SwaggerModule.setup("docs", app, document, {
|
|
||||||
customCss: ".swagger-ui .topbar { display: none }",
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log(`Swagger documentation available in the "/docs" endpoint\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run().catch((error: Error) => {
|
|
||||||
console.error("Failed to start Cal Platform API", { error: error.stack });
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AtomsSecondaryEmailsRepository {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async getSecondaryEmailsVerified(userId: number) {
|
||||||
|
return await this.dbRead.prisma.secondaryEmail.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
emailVerified: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExistingSecondaryEmailByUserAndEmail(userId: number, email: string) {
|
||||||
|
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_email: { userId, email },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return existingSecondaryEmailRecord?.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExistingSecondaryEmail(email: string) {
|
||||||
|
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return existingSecondaryEmailRecord?.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addSecondaryEmailVerified(userId: number, email: string) {
|
||||||
|
const existingSecondaryEmailRecord = await this.dbWrite.prisma.secondaryEmail.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
emailVerified: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return existingSecondaryEmailRecord?.email;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
|
import { AtomsSecondaryEmailsRepository } from "@/modules/atoms/atoms-secondary-emails.repository";
|
||||||
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
||||||
import { AtomsConferencingAppsController } from "@/modules/atoms/controllers/atoms.conferencing-apps.controller";
|
import { AtomsConferencingAppsController } from "@/modules/atoms/controllers/atoms.conferencing-apps.controller";
|
||||||
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
||||||
@@ -33,6 +34,7 @@ import { Module } from "@nestjs/common";
|
|||||||
CredentialsRepository,
|
CredentialsRepository,
|
||||||
UsersRepository,
|
UsersRepository,
|
||||||
AtomsRepository,
|
AtomsRepository,
|
||||||
|
AtomsSecondaryEmailsRepository,
|
||||||
UsersService,
|
UsersService,
|
||||||
SchedulesAtomsService,
|
SchedulesAtomsService,
|
||||||
VerificationAtomsService,
|
VerificationAtomsService,
|
||||||
|
|||||||
@@ -64,47 +64,4 @@ export class AtomsRepository {
|
|||||||
|
|
||||||
return userTeams;
|
return userTeams;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSecondaryEmails(userId: number) {
|
|
||||||
return await this.dbRead.prisma.secondaryEmail.findMany({
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
emailVerified: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getExistingSecondaryEmailByUserAndEmail(userId: number, email: string) {
|
|
||||||
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
|
|
||||||
where: {
|
|
||||||
userId_email: { userId, email },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return existingSecondaryEmailRecord?.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getExistingSecondaryEmail(email: string) {
|
|
||||||
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
|
|
||||||
where: {
|
|
||||||
email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return existingSecondaryEmailRecord?.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addSecondaryEmail(userId: number, email: string) {
|
|
||||||
const existingSecondaryEmailRecord = await this.dbWrite.prisma.secondaryEmail.create({
|
|
||||||
data: {
|
|
||||||
userId,
|
|
||||||
email,
|
|
||||||
emailVerified: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return existingSecondaryEmailRecord?.email;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
import { AtomsSecondaryEmailsRepository } from "@/modules/atoms/atoms-secondary-emails.repository";
|
||||||
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||||
import { GetVerifiedEmailsInput } from "@/modules/atoms/inputs/get-verified-emails-params";
|
import { GetVerifiedEmailsInput } from "@/modules/atoms/inputs/get-verified-emails-params";
|
||||||
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||||
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Injectable, BadRequestException, UnauthorizedException } from "@nestjs/common";
|
import { Injectable, BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -17,8 +17,8 @@ import {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class VerificationAtomsService {
|
export class VerificationAtomsService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly atomsRepository: AtomsRepository,
|
private readonly atomsSecondaryEmailsRepository: AtomsSecondaryEmailsRepository,
|
||||||
private readonly teamsRepository: TeamsRepository
|
private readonly usersRepository: UsersRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async checkEmailVerificationRequired(input: CheckEmailVerificationRequiredParams) {
|
async checkEmailVerificationRequired(input: CheckEmailVerificationRequiredParams) {
|
||||||
@@ -76,7 +76,8 @@ export class VerificationAtomsService {
|
|||||||
|
|
||||||
if (teamId) {
|
if (teamId) {
|
||||||
const verifiedEmails: string[] = [];
|
const verifiedEmails: string[] = [];
|
||||||
const teamMembers = await this.teamsRepository.getTeamMemberEmails(teamId);
|
|
||||||
|
const teamMembers = await this.usersRepository.getUserEmailsVerifiedForTeam(teamId);
|
||||||
|
|
||||||
if (teamMembers.length === 0) {
|
if (teamMembers.length === 0) {
|
||||||
return verifiedEmails;
|
return verifiedEmails;
|
||||||
@@ -96,7 +97,7 @@ export class VerificationAtomsService {
|
|||||||
|
|
||||||
let verifiedEmails = [userEmailWithoutOauthClientId];
|
let verifiedEmails = [userEmailWithoutOauthClientId];
|
||||||
|
|
||||||
const secondaryEmails = await this.atomsRepository.getSecondaryEmails(userId);
|
const secondaryEmails = await this.atomsSecondaryEmailsRepository.getSecondaryEmailsVerified(userId);
|
||||||
verifiedEmails = verifiedEmails.concat(
|
verifiedEmails = verifiedEmails.concat(
|
||||||
secondaryEmails.map((secondaryEmail) => this.removeClientIdFromEmail(secondaryEmail.email))
|
secondaryEmails.map((secondaryEmail) => this.removeClientIdFromEmail(secondaryEmail.email))
|
||||||
);
|
);
|
||||||
@@ -113,11 +114,9 @@ export class VerificationAtomsService {
|
|||||||
existingPrimaryEmail: string;
|
existingPrimaryEmail: string;
|
||||||
email: string;
|
email: string;
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
const existingSecondaryEmail = await this.atomsRepository.getExistingSecondaryEmailByUserAndEmail(
|
const existingSecondaryEmail =
|
||||||
userId,
|
await this.atomsSecondaryEmailsRepository.getExistingSecondaryEmailByUserAndEmail(userId, email);
|
||||||
email
|
const alreadyExistingEmail = await this.atomsSecondaryEmailsRepository.getExistingSecondaryEmail(email);
|
||||||
);
|
|
||||||
const alreadyExistingEmail = await this.atomsRepository.getExistingSecondaryEmail(email);
|
|
||||||
|
|
||||||
if (alreadyExistingEmail) {
|
if (alreadyExistingEmail) {
|
||||||
throw new BadRequestException("Email already exists");
|
throw new BadRequestException("Email already exists");
|
||||||
@@ -127,7 +126,7 @@ export class VerificationAtomsService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.atomsRepository.addSecondaryEmail(userId, email);
|
await this.atomsSecondaryEmailsRepository.addSecondaryEmailVerified(userId, email);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
|||||||
import {
|
import {
|
||||||
NO_AUTH_PROVIDED_MESSAGE,
|
NO_AUTH_PROVIDED_MESSAGE,
|
||||||
ONLY_CLIENT_ID_PROVIDED_MESSAGE,
|
ONLY_CLIENT_ID_PROVIDED_MESSAGE,
|
||||||
|
ONLY_CLIENT_SECRET_PROVIDED_MESSAGE,
|
||||||
} from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
} from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||||
|
|
||||||
export class OptionalApiAuthGuard extends ApiAuthGuard {
|
export class OptionalApiAuthGuard extends ApiAuthGuard {
|
||||||
@@ -9,11 +10,16 @@ export class OptionalApiAuthGuard extends ApiAuthGuard {
|
|||||||
// note(Lauris): optional means that auth is not required but if it is invalid then still throw error.
|
// note(Lauris): optional means that auth is not required but if it is invalid then still throw error.
|
||||||
const noAuthProvided = error && error.message.includes(NO_AUTH_PROVIDED_MESSAGE);
|
const noAuthProvided = error && error.message.includes(NO_AUTH_PROVIDED_MESSAGE);
|
||||||
const onlyClientIdProvided = error && error.message.includes(ONLY_CLIENT_ID_PROVIDED_MESSAGE);
|
const onlyClientIdProvided = error && error.message.includes(ONLY_CLIENT_ID_PROVIDED_MESSAGE);
|
||||||
|
const onlyClientSecretProvided = error && error.message.includes(ONLY_CLIENT_SECRET_PROVIDED_MESSAGE);
|
||||||
|
|
||||||
if (onlyClientIdProvided) {
|
if (onlyClientIdProvided) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onlyClientSecretProvided) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (user || noAuthProvided || !error) {
|
if (user || noAuthProvided || !error) {
|
||||||
return user || null;
|
return user || null;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ import { randomString } from "test/utils/randomString";
|
|||||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||||
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
|
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { ApiAuthGuardRequest, ONLY_CLIENT_ID_PROVIDED_MESSAGE } from "./api-auth.strategy";
|
import {
|
||||||
|
ApiAuthGuardRequest,
|
||||||
|
ONLY_CLIENT_ID_PROVIDED_MESSAGE,
|
||||||
|
ONLY_CLIENT_SECRET_PROVIDED_MESSAGE,
|
||||||
|
} from "./api-auth.strategy";
|
||||||
import { ApiAuthStrategy } from "./api-auth.strategy";
|
import { ApiAuthStrategy } from "./api-auth.strategy";
|
||||||
|
|
||||||
describe("ApiAuthStrategy", () => {
|
describe("ApiAuthStrategy", () => {
|
||||||
@@ -267,6 +271,29 @@ describe("ApiAuthStrategy", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should throw 401 if only OAuth Client Secret is provided", async () => {
|
||||||
|
const context: ExecutionContext = {
|
||||||
|
switchToHttp: () => ({
|
||||||
|
getRequest: () => ({
|
||||||
|
headers: {
|
||||||
|
[X_CAL_SECRET_KEY]: `${oAuthClient.secret}gibberish`,
|
||||||
|
},
|
||||||
|
get: (key: string) => ({ origin: "http://localhost:3000" }[key]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as ExecutionContext;
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await strategy.authenticate(request);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) {
|
||||||
|
expect(error.getStatus()).toEqual(401);
|
||||||
|
expect(error.message).toContain(ONLY_CLIENT_SECRET_PROVIDED_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("should throw 401 if OAuth ID is invalid", async () => {
|
it("should throw 401 if OAuth ID is invalid", async () => {
|
||||||
const context: ExecutionContext = {
|
const context: ExecutionContext = {
|
||||||
switchToHttp: () => ({
|
switchToHttp: () => ({
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ export type ApiAuthGuardRequest = Request & {
|
|||||||
user: ApiAuthGuardUser;
|
user: ApiAuthGuardUser;
|
||||||
allowedAuthMethods?: AllowedAuthMethod[];
|
allowedAuthMethods?: AllowedAuthMethod[];
|
||||||
};
|
};
|
||||||
export const NO_AUTH_PROVIDED_MESSAGE =
|
export const NO_AUTH_PROVIDED_MESSAGE = `No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as '${X_CAL_SECRET_KEY}' and '${X_CAL_CLIENT_ID}' headers`;
|
||||||
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers";
|
|
||||||
|
|
||||||
export const ONLY_CLIENT_ID_PROVIDED_MESSAGE =
|
export const ONLY_CLIENT_ID_PROVIDED_MESSAGE = `Only '${X_CAL_CLIENT_ID}' header provided. Please also provide '${X_CAL_SECRET_KEY}' header or Auth bearer token as 'Authentication' header`;
|
||||||
"Only 'x-cal-client-id' header provided. Please also provide 'x-cal-secret-key' header or Auth bearer token as 'Authentication' header";
|
|
||||||
|
export const ONLY_CLIENT_SECRET_PROVIDED_MESSAGE = `Only '${X_CAL_SECRET_KEY}' header provided. Please also provide '${X_CAL_CLIENT_ID}' header or Auth bearer token as 'Authentication' header`;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
||||||
@@ -121,6 +121,9 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
|
|
||||||
const noAuthProvided = !oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken;
|
const noAuthProvided = !oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken;
|
||||||
const onlyClientIdProvided = !!oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken;
|
const onlyClientIdProvided = !!oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken;
|
||||||
|
const onlyClientSecretProvided =
|
||||||
|
!oAuthClientId && !!oAuthClientSecret && !bearerToken && !nextAuthToken;
|
||||||
|
|
||||||
if (noAuthProvided) {
|
if (noAuthProvided) {
|
||||||
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
|
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
|
||||||
}
|
}
|
||||||
@@ -129,6 +132,10 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
throw new UnauthorizedException(`ApiAuthStrategy - ${ONLY_CLIENT_ID_PROVIDED_MESSAGE}`);
|
throw new UnauthorizedException(`ApiAuthStrategy - ${ONLY_CLIENT_ID_PROVIDED_MESSAGE}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onlyClientSecretProvided) {
|
||||||
|
throw new UnauthorizedException(`ApiAuthStrategy - ${ONLY_CLIENT_SECRET_PROVIDED_MESSAGE}`);
|
||||||
|
}
|
||||||
|
|
||||||
throw new UnauthorizedException(
|
throw new UnauthorizedException(
|
||||||
`ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${
|
`ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${
|
||||||
allowedMethods && allowedMethods.length > 0 ? allowedMethods.join(", ") : "Any supported method"
|
allowedMethods && allowedMethods.length > 0 ? allowedMethods.join(", ") : "Any supported method"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings
|
|||||||
import { BillingProcessor } from "@/modules/billing/billing.processor";
|
import { BillingProcessor } from "@/modules/billing/billing.processor";
|
||||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||||
import { BillingController } from "@/modules/billing/controllers/billing.controller";
|
import { BillingController } from "@/modules/billing/controllers/billing.controller";
|
||||||
|
import { BillingServiceCachingProxy } from "@/modules/billing/services/billing-service-caching-proxy";
|
||||||
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||||
import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.billing.service";
|
import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.billing.service";
|
||||||
@@ -9,6 +10,7 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
|||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { BullModule } from "@nestjs/bull";
|
import { BullModule } from "@nestjs/bull";
|
||||||
@@ -18,6 +20,7 @@ import { Module } from "@nestjs/common";
|
|||||||
imports: [
|
imports: [
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
StripeModule,
|
StripeModule,
|
||||||
|
RedisModule,
|
||||||
MembershipsModule,
|
MembershipsModule,
|
||||||
OrganizationsModule,
|
OrganizationsModule,
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
@@ -32,6 +35,10 @@ import { Module } from "@nestjs/common";
|
|||||||
providers: [
|
providers: [
|
||||||
BillingConfigService,
|
BillingConfigService,
|
||||||
BillingService,
|
BillingService,
|
||||||
|
{
|
||||||
|
provide: "IBillingService",
|
||||||
|
useClass: BillingServiceCachingProxy,
|
||||||
|
},
|
||||||
BillingRepository,
|
BillingRepository,
|
||||||
BillingProcessor,
|
BillingProcessor,
|
||||||
ManagedOrganizationsBillingService,
|
ManagedOrganizationsBillingService,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
|
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
@@ -16,7 +17,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
|
|||||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { randomString } from "test/utils/randomString";
|
import { randomString } from "test/utils/randomString";
|
||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withNextAuth } from "test/utils/withNextAuth";
|
||||||
|
|
||||||
import type { Team, PlatformBilling } from "@calcom/prisma/client";
|
import type { Team, PlatformBilling } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@@ -31,9 +32,8 @@ describe("Platform Billing Controller (e2e)", () => {
|
|||||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
|
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
|
||||||
let organization: Team;
|
let organization: Team;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await withNextAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
@@ -44,8 +44,10 @@ describe("Platform Billing Controller (e2e)", () => {
|
|||||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
|
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
organization = await organizationsRepositoryFixture.create({
|
organization = await organizationsRepositoryFixture.create({
|
||||||
name: `billing-organization-${randomString()}`,
|
name: `billing-organization-${randomString()}`,
|
||||||
|
isPlatform: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
@@ -87,6 +89,17 @@ describe("Platform Billing Controller (e2e)", () => {
|
|||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("/billing/webhook (GET) should not get billing plan for org since it's not set yet", () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/billing/${organization.id}/check`)
|
||||||
|
|
||||||
|
.expect(200)
|
||||||
|
.then(async (res) => {
|
||||||
|
const data = res.body.data as CheckPlatformBillingResponseDto;
|
||||||
|
expect(data?.plan).toEqual("FREE");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("/billing/webhook (POST) should set billing free plan for org", () => {
|
it("/billing/webhook (POST) should set billing free plan for org", () => {
|
||||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
||||||
() =>
|
() =>
|
||||||
@@ -119,6 +132,17 @@ describe("Platform Billing Controller (e2e)", () => {
|
|||||||
expect(billing?.plan).toEqual("FREE");
|
expect(billing?.plan).toEqual("FREE");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("/billing/webhook (GET) should get billing plan for org", () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/billing/${organization.id}/check`)
|
||||||
|
.expect(200)
|
||||||
|
.then(async (res) => {
|
||||||
|
const data = res.body.data as CheckPlatformBillingResponseDto;
|
||||||
|
expect(data?.plan).toEqual("FREE");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("/billing/webhook (POST) failed payment should set billing free plan to overdue", () => {
|
it("/billing/webhook (POST) failed payment should set billing free plan to overdue", () => {
|
||||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles
|
|||||||
import { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subscribe-to-plan.input";
|
import { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subscribe-to-plan.input";
|
||||||
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
|
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
|
||||||
import { SubscribeTeamToBillingResponseDto } from "@/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto";
|
import { SubscribeTeamToBillingResponseDto } from "@/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto";
|
||||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface";
|
||||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Headers,
|
Headers,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
Inject,
|
||||||
Logger,
|
Logger,
|
||||||
Delete,
|
Delete,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
@@ -40,7 +41,7 @@ export class BillingController {
|
|||||||
private logger = new Logger("Billing Controller");
|
private logger = new Logger("Billing Controller");
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly billingService: BillingService,
|
@Inject("IBillingService") private readonly billingService: IBillingService,
|
||||||
public readonly stripeService: StripeService,
|
public readonly stripeService: StripeService,
|
||||||
private readonly configService: ConfigService<AppConfig>
|
private readonly configService: ConfigService<AppConfig>
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { PlatformPlan } from "@/modules/billing/types";
|
||||||
|
import type { StripeService } from "@/modules/stripe/stripe.service";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
import { PlatformBilling, Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
export type BillingData = {
|
||||||
|
team: (Team & { platformBilling: PlatformBilling | null }) | null;
|
||||||
|
status: "valid" | "no_subscription" | "no_billing";
|
||||||
|
plan: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IBillingService {
|
||||||
|
getBillingData(teamId: number): Promise<BillingData>;
|
||||||
|
createTeamBilling(teamId: number): Promise<string>;
|
||||||
|
redirectToSubscribeCheckout(teamId: number, plan: PlatformPlan, customerId?: string): Promise<string>;
|
||||||
|
updateSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise<string>;
|
||||||
|
cancelTeamSubscription(teamId: number): Promise<void>;
|
||||||
|
handleStripeSubscriptionDeleted(event: Stripe.Event): Promise<void>;
|
||||||
|
handleStripePaymentSuccess(event: Stripe.Event): Promise<void>;
|
||||||
|
handleStripePaymentFailed(event: Stripe.Event): Promise<void>;
|
||||||
|
handleStripePaymentPastDue(event: Stripe.Event): Promise<void>;
|
||||||
|
handleStripeCheckoutEvents(event: Stripe.Event): Promise<void>;
|
||||||
|
handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event): Promise<void>;
|
||||||
|
getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null;
|
||||||
|
getCustomerIdFromInvoice(invoice: Stripe.Invoice): string | null;
|
||||||
|
stripeService: StripeService;
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { IBillingService, BillingData } from "@/modules/billing/interfaces/billing-service.interface";
|
||||||
|
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||||
|
import { PlatformPlan } from "@/modules/billing/types";
|
||||||
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
|
export const REDIS_BILLING_CACHE_KEY = (teamId: number) => `apiv2:team:${teamId}:billing`;
|
||||||
|
export const BILLING_CACHE_TTL_MS = 3_600_000; // 1 hour
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BillingServiceCachingProxy implements IBillingService {
|
||||||
|
constructor(private readonly billingService: BillingService, private readonly redisService: RedisService) {}
|
||||||
|
|
||||||
|
async getBillingData(teamId: number) {
|
||||||
|
const cachedBillingData = await this.getBillingCache(teamId);
|
||||||
|
if (cachedBillingData) {
|
||||||
|
return cachedBillingData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const billingData = await this.billingService.getBillingData(teamId);
|
||||||
|
await this.setBillingCache(teamId, billingData);
|
||||||
|
return billingData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteBillingCache(teamId: number) {
|
||||||
|
await this.redisService.del(REDIS_BILLING_CACHE_KEY(teamId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getBillingCache(teamId: number) {
|
||||||
|
const cachedResult = await this.redisService.get<BillingData>(REDIS_BILLING_CACHE_KEY(teamId));
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setBillingCache(teamId: number, billingData: BillingData): Promise<void> {
|
||||||
|
await this.redisService.set<BillingData>(REDIS_BILLING_CACHE_KEY(teamId), billingData, {
|
||||||
|
ttl: BILLING_CACHE_TTL_MS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTeamBilling(teamId: number): Promise<string> {
|
||||||
|
return this.billingService.createTeamBilling(teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async redirectToSubscribeCheckout(
|
||||||
|
teamId: number,
|
||||||
|
plan: PlatformPlan,
|
||||||
|
customerId?: string
|
||||||
|
): Promise<string> {
|
||||||
|
return this.billingService.redirectToSubscribeCheckout(teamId, plan, customerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise<string> {
|
||||||
|
return this.billingService.updateSubscriptionForTeam(teamId, plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelTeamSubscription(teamId: number): Promise<void> {
|
||||||
|
await this.billingService.cancelTeamSubscription(teamId);
|
||||||
|
await this.deleteBillingCache(teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStripeSubscriptionDeleted(event: Stripe.Event): Promise<void> {
|
||||||
|
await this.billingService.handleStripeSubscriptionDeleted(event);
|
||||||
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
|
const teamId = subscription?.metadata?.teamId;
|
||||||
|
if (teamId) {
|
||||||
|
await this.deleteBillingCache(Number.parseInt(teamId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStripePaymentSuccess(event: Stripe.Event): Promise<void> {
|
||||||
|
await this.billingService.handleStripePaymentSuccess(event);
|
||||||
|
const invoice = event.data.object as Stripe.Invoice;
|
||||||
|
const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
|
||||||
|
if (subscriptionId) {
|
||||||
|
const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId(
|
||||||
|
subscriptionId
|
||||||
|
);
|
||||||
|
if (teamBilling?.id) {
|
||||||
|
await this.deleteBillingCache(teamBilling.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStripePaymentFailed(event: Stripe.Event): Promise<void> {
|
||||||
|
await this.billingService.handleStripePaymentFailed(event);
|
||||||
|
const invoice = event.data.object as Stripe.Invoice;
|
||||||
|
const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
|
||||||
|
if (subscriptionId) {
|
||||||
|
const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId(
|
||||||
|
subscriptionId
|
||||||
|
);
|
||||||
|
if (teamBilling?.id) {
|
||||||
|
await this.deleteBillingCache(teamBilling.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStripePaymentPastDue(event: Stripe.Event): Promise<void> {
|
||||||
|
await this.billingService.handleStripePaymentPastDue(event);
|
||||||
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
|
const subscriptionId = subscription.id;
|
||||||
|
if (subscriptionId) {
|
||||||
|
const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId(
|
||||||
|
subscriptionId
|
||||||
|
);
|
||||||
|
if (teamBilling?.id) {
|
||||||
|
await this.deleteBillingCache(teamBilling.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStripeCheckoutEvents(event: Stripe.Event): Promise<void> {
|
||||||
|
await this.billingService.handleStripeCheckoutEvents(event);
|
||||||
|
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
||||||
|
const teamId = checkoutSession.metadata?.teamId;
|
||||||
|
if (teamId) {
|
||||||
|
await this.deleteBillingCache(Number.parseInt(teamId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event): Promise<void> {
|
||||||
|
return this.billingService.handleStripeSubscriptionForActiveManagedUsers(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null {
|
||||||
|
return this.billingService.getSubscriptionIdFromInvoice(invoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCustomerIdFromInvoice(invoice: Stripe.Invoice): string | null {
|
||||||
|
return this.billingService.getCustomerIdFromInvoice(invoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
get stripeService() {
|
||||||
|
return this.billingService.stripeService;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { AppConfig } from "@/config/type";
|
|||||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor";
|
import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor";
|
||||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||||
|
import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface";
|
||||||
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||||
import { PlatformPlan } from "@/modules/billing/types";
|
import { PlatformPlan } from "@/modules/billing/types";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
@@ -23,14 +24,14 @@ import { DateTime } from "luxon";
|
|||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillingService implements OnModuleDestroy {
|
export class BillingService implements IBillingService, OnModuleDestroy {
|
||||||
private logger = new Logger("BillingService");
|
private logger = new Logger("BillingService");
|
||||||
private readonly webAppUrl: string;
|
private readonly webAppUrl: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly teamsRepository: OrganizationsRepository,
|
private readonly teamsRepository: OrganizationsRepository,
|
||||||
public readonly stripeService: StripeService,
|
public readonly stripeService: StripeService,
|
||||||
private readonly billingRepository: BillingRepository,
|
public readonly billingRepository: BillingRepository,
|
||||||
private readonly configService: ConfigService<AppConfig>,
|
private readonly configService: ConfigService<AppConfig>,
|
||||||
private readonly billingConfigService: BillingConfigService,
|
private readonly billingConfigService: BillingConfigService,
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
@@ -43,18 +44,23 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
|
|
||||||
async getBillingData(teamId: number) {
|
async getBillingData(teamId: number) {
|
||||||
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||||
|
|
||||||
if (teamWithBilling?.platformBilling) {
|
if (teamWithBilling?.platformBilling) {
|
||||||
if (!teamWithBilling?.platformBilling.subscriptionId) {
|
if (!teamWithBilling?.platformBilling.subscriptionId) {
|
||||||
return { team: teamWithBilling, status: "no_subscription", plan: "none" };
|
return { team: teamWithBilling, status: "no_subscription" as const, plan: "none" };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
team: teamWithBilling,
|
||||||
|
status: "valid" as const,
|
||||||
|
plan: teamWithBilling.platformBilling.plan,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { team: teamWithBilling, status: "valid", plan: teamWithBilling.platformBilling.plan };
|
|
||||||
} else {
|
} else {
|
||||||
return { team: teamWithBilling, status: "no_billing", plan: "none" };
|
return { team: teamWithBilling, status: "no_billing" as const, plan: "none" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createTeamBilling(teamId: number) {
|
async createTeamBilling(teamId: number): Promise<string> {
|
||||||
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||||
let customerId = teamWithBilling?.platformBilling?.customerId;
|
let customerId = teamWithBilling?.platformBilling?.customerId;
|
||||||
|
|
||||||
@@ -67,7 +73,7 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return customerId;
|
return customerId!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async redirectToSubscribeCheckout(teamId: number, plan: PlatformPlan, customerId?: string) {
|
async redirectToSubscribeCheckout(teamId: number, plan: PlatformPlan, customerId?: string) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { PlatformPlan } from "@/modules/billing/types";
|
import { hasMinimumPlan } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
|
import { orderedPlans, PlatformPlan } from "@/modules/billing/types";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ManagedOrganizationsBillingService {
|
export class ManagedOrganizationsBillingService {
|
||||||
@@ -14,8 +15,18 @@ export class ManagedOrganizationsBillingService {
|
|||||||
if (!managerOrgBilling) {
|
if (!managerOrgBilling) {
|
||||||
throw new NotFoundException("Manager organization billing not found.");
|
throw new NotFoundException("Manager organization billing not found.");
|
||||||
}
|
}
|
||||||
if (managerOrgBilling.plan !== PlatformPlan.SCALE) {
|
if (
|
||||||
throw new NotFoundException("Manager organization billing plan is not SCALE.");
|
!hasMinimumPlan({
|
||||||
|
currentPlan: managerOrgBilling.plan as PlatformPlan,
|
||||||
|
minimumPlan: PlatformPlan.SCALE,
|
||||||
|
plans: orderedPlans,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
`organization with id=${managerOrgId} does not have required plan for this operation. Minimum plan is ${
|
||||||
|
PlatformPlan.SCALE
|
||||||
|
} while the organization has ${managerOrgBilling.plan || "undefined"}.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.dbWrite.prisma.platformBilling.create({
|
return this.dbWrite.prisma.platformBilling.create({
|
||||||
|
|||||||
+7
-1
@@ -1,4 +1,4 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsISO8601, IsOptional, IsString, ValidateNested } from "class-validator";
|
import { IsEnum, IsISO8601, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
@@ -297,6 +297,12 @@ export class DateTimeWithZone {
|
|||||||
timeZone!: string;
|
timeZone!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiExtraModels(
|
||||||
|
CalendarEventVideoLocation,
|
||||||
|
CalendarEventPhoneLocation,
|
||||||
|
CalendarEventSipLocation,
|
||||||
|
CalendarEventMoreLocation
|
||||||
|
)
|
||||||
export class UnifiedCalendarEventOutput {
|
export class UnifiedCalendarEventOutput {
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => DateTimeWithZone)
|
@Type(() => DateTimeWithZone)
|
||||||
|
|||||||
+109
-4
@@ -705,6 +705,111 @@ describe("OAuth Client Users Endpoints", () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("managed user time zone", () => {
|
||||||
|
describe("negative tests", () => {
|
||||||
|
it("should not allow '' time zone", async () => {
|
||||||
|
const requestBody = {
|
||||||
|
email: "whatever2@gmail.com",
|
||||||
|
timeZone: "",
|
||||||
|
name: "Bob Smithson",
|
||||||
|
};
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.send(requestBody)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow 'invalid-timezone' time zone", async () => {
|
||||||
|
const requestBody = {
|
||||||
|
email: "whatever2@gmail.com",
|
||||||
|
timeZone: "invalid-timezone",
|
||||||
|
name: "Bob Smithson",
|
||||||
|
};
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.send(requestBody)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("positive tests", () => {
|
||||||
|
it("should allow null timezone", async () => {
|
||||||
|
const requestBody = {
|
||||||
|
email: "whatever1@gmail.com",
|
||||||
|
timeZone: null,
|
||||||
|
name: "Bob Smithson",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.send(requestBody)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const responseBody: CreateManagedUserOutput = response.body;
|
||||||
|
expect(responseBody.data.user.timeZone).toEqual("Europe/London");
|
||||||
|
await userRepositoryFixture.delete(responseBody.data.user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow undefined time zone", async () => {
|
||||||
|
const requestBody = {
|
||||||
|
email: "whatever3@gmail.com",
|
||||||
|
timeZone: undefined,
|
||||||
|
name: "Bob Smithson",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.send(requestBody)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const responseBody: CreateManagedUserOutput = response.body;
|
||||||
|
expect(responseBody.data.user.timeZone).toEqual("Europe/London");
|
||||||
|
await userRepositoryFixture.delete(responseBody.data.user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow valid time zone", async () => {
|
||||||
|
const requestBody = {
|
||||||
|
email: "whatever4@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
name: "Bob Smithson",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.send(requestBody)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const responseBody: CreateManagedUserOutput = response.body;
|
||||||
|
expect(responseBody.data.user.timeZone).toBe("Europe/Rome");
|
||||||
|
await userRepositoryFixture.delete(responseBody.data.user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow without any time zone", async () => {
|
||||||
|
const requestBody = {
|
||||||
|
email: "whatever5@gmail.com",
|
||||||
|
name: "Bob Smithson",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||||
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
|
.send(requestBody)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const responseBody: CreateManagedUserOutput = response.body;
|
||||||
|
expect(responseBody.data.user.timeZone).toEqual("Europe/London");
|
||||||
|
await userRepositoryFixture.delete(responseBody.data.user.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClientEventTypesDisabled.id);
|
await oauthClientRepositoryFixture.delete(oAuthClientEventTypesDisabled.id);
|
||||||
@@ -712,17 +817,17 @@ describe("OAuth Client Users Endpoints", () => {
|
|||||||
try {
|
try {
|
||||||
await userRepositoryFixture.delete(postResponseData.user.id);
|
await userRepositoryFixture.delete(postResponseData.user.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// User might have been deleted by the test
|
console.log(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await userRepositoryFixture.delete(postResponseData2.user.id);
|
await userRepositoryFixture.delete(postResponseData2.user.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// User might have been deleted by the test
|
console.log(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await userRepositoryFixture.delete(platformAdmin.id);
|
await userRepositoryFixture.delete(platformAdmin.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// User might have been deleted by the test
|
console.log(e);
|
||||||
}
|
}
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
@@ -931,7 +1036,7 @@ describe("OAuth Client Users Endpoints", () => {
|
|||||||
try {
|
try {
|
||||||
await userRepositoryFixture.delete(postResponseData.user.id);
|
await userRepositoryFixture.delete(postResponseData.user.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// User might have been deleted by the test
|
console.log(e);
|
||||||
}
|
}
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-1
@@ -4,9 +4,11 @@ import {
|
|||||||
MicrosoftServiceAccountKeyInput,
|
MicrosoftServiceAccountKeyInput,
|
||||||
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
||||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||||
|
import { ApiExtraModels } from "@nestjs/swagger";
|
||||||
import { Expose, Type } from "class-transformer";
|
import { Expose, Type } from "class-transformer";
|
||||||
import { IsString, IsNotEmpty, ValidateNested, Validate } from "class-validator";
|
import { IsString, IsNotEmpty, Validate } from "class-validator";
|
||||||
|
|
||||||
|
@ApiExtraModels(GoogleServiceAccountKeyInput, MicrosoftServiceAccountKeyInput)
|
||||||
export class CreateDelegationCredentialInput {
|
export class CreateDelegationCredentialInput {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
+4
-3
@@ -3,10 +3,11 @@ import {
|
|||||||
MicrosoftServiceAccountKeyInput,
|
MicrosoftServiceAccountKeyInput,
|
||||||
ServiceAccountKeyValidator,
|
ServiceAccountKeyValidator,
|
||||||
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
||||||
import { ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
import { ApiExtraModels, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||||
import { Expose, plainToClass, Transform, Type } from "class-transformer";
|
import { Expose, Type } from "class-transformer";
|
||||||
import { IsBoolean, IsOptional, Validate, ValidateNested } from "class-validator";
|
import { IsBoolean, IsOptional, Validate } from "class-validator";
|
||||||
|
|
||||||
|
@ApiExtraModels(GoogleServiceAccountKeyInput, MicrosoftServiceAccountKeyInput)
|
||||||
export class UpdateDelegationCredentialInput {
|
export class UpdateDelegationCredentialInput {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -174,4 +174,14 @@ export class OrganizationsRepository {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findTeamByPlatformBillingId(billingId: number) {
|
||||||
|
return this.dbRead.prisma.team.findFirst({
|
||||||
|
where: {
|
||||||
|
platformBilling: {
|
||||||
|
id: billingId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { OrganizationsTeamsController } from "@/modules/organizations/teams/inde
|
|||||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||||
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.controller";
|
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.controller";
|
||||||
|
import { OrganizationsTeamsInviteController } from "@/modules/organizations/teams/invite/organizations-teams-invite.controller";
|
||||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
||||||
import { OrganizationsTeamsRoutingFormsModule } from "@/modules/organizations/teams/routing-forms/organizations-teams-routing-forms.module";
|
import { OrganizationsTeamsRoutingFormsModule } from "@/modules/organizations/teams/routing-forms/organizations-teams-routing-forms.module";
|
||||||
@@ -183,6 +184,7 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsMembershipsController,
|
OrganizationsMembershipsController,
|
||||||
OrganizationsEventTypesController,
|
OrganizationsEventTypesController,
|
||||||
OrganizationsTeamsMembershipsController,
|
OrganizationsTeamsMembershipsController,
|
||||||
|
OrganizationsTeamsInviteController,
|
||||||
OrganizationsAttributesController,
|
OrganizationsAttributesController,
|
||||||
OrganizationsAttributesOptionsController,
|
OrganizationsAttributesOptionsController,
|
||||||
OrganizationsWebhooksController,
|
OrganizationsWebhooksController,
|
||||||
|
|||||||
+1
-1
@@ -68,7 +68,7 @@ export class ManagedOrganizationsRepository {
|
|||||||
managedOrganization: managedOrganizationFilter,
|
managedOrganization: managedOrganizationFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
const [totalItems, linkRows] = await this.dbRead.prisma.$transaction([
|
const [totalItems, linkRows] = await Promise.all([
|
||||||
this.dbRead.prisma.managedOrganization.count({ where }),
|
this.dbRead.prisma.managedOrganization.count({ where }),
|
||||||
this.dbRead.prisma.managedOrganization.findMany({
|
this.dbRead.prisma.managedOrganization.findMany({
|
||||||
where,
|
where,
|
||||||
|
|||||||
+132
-45
@@ -61,11 +61,24 @@ describe("Organizations Organizations Endpoints", () => {
|
|||||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
|
||||||
let managerOrg: Team;
|
let managerOrg: Team;
|
||||||
|
let payPerUserPlanManagerOrg: Team;
|
||||||
|
let essentialsPlanManagerOrg: Team;
|
||||||
|
|
||||||
let managedOrg: ManagedOrganizationWithApiKeyOutput;
|
let managedOrg: ManagedOrganizationWithApiKeyOutput;
|
||||||
let managedOrg2: ManagedOrganizationWithApiKeyOutput;
|
let managedOrg2: ManagedOrganizationWithApiKeyOutput;
|
||||||
|
|
||||||
const managerOrgAdminEmail = `organizations-organizations-admin-${randomString()}@api.com`;
|
const managerOrgAdminEmail = `organizations-organizations-admin-${randomString()}@api.com`;
|
||||||
|
const payPerUserPlanManagerOrgAdminEmail = `organizations-organizations-admin-${randomString()}@api.com`;
|
||||||
|
const essentialsPlanManagerOrgAdminEmail = `organizations-organizations-admin-${randomString()}@api.com`;
|
||||||
|
|
||||||
let managerOrgAdmin: User;
|
let managerOrgAdmin: User;
|
||||||
|
let payPerUserPlanManagerOrgAdmin: User;
|
||||||
|
let essentialsPlanManagerOrgAdmin: User;
|
||||||
|
|
||||||
let managerOrgAdminApiKey: string;
|
let managerOrgAdminApiKey: string;
|
||||||
|
let payPerUserPlanManagerOrgAdminApiKey: string;
|
||||||
|
let essentialsPlanManagerOrgAdminApiKey: string;
|
||||||
|
|
||||||
let managerOrgBilling: PlatformBilling;
|
let managerOrgBilling: PlatformBilling;
|
||||||
|
|
||||||
let managedOrgApiKey: string;
|
let managedOrgApiKey: string;
|
||||||
@@ -93,40 +106,36 @@ describe("Organizations Organizations Endpoints", () => {
|
|||||||
oAuthClientsRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
oAuthClientsRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
managerOrgAdmin = await userRepositoryFixture.create({
|
// Setup manager organization with SCALE plan
|
||||||
email: managerOrgAdminEmail,
|
const managerSetup = await setupTestOrganization(
|
||||||
username: managerOrgAdminEmail,
|
managerOrgAdminEmail,
|
||||||
});
|
`organizations-organizations-organization-${randomString()}`,
|
||||||
|
"SCALE"
|
||||||
managerOrg = await organizationsRepositoryFixture.create({
|
|
||||||
name: `organizations-organizations-organization-${randomString()}`,
|
|
||||||
isOrganization: true,
|
|
||||||
isPlatform: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await profilesRepositoryFixture.create({
|
|
||||||
uid: "asd-asd",
|
|
||||||
username: managerOrgAdminEmail,
|
|
||||||
user: { connect: { id: managerOrgAdmin.id } },
|
|
||||||
organization: { connect: { id: managerOrg.id } },
|
|
||||||
movedFromUser: { connect: { id: managerOrgAdmin.id } },
|
|
||||||
});
|
|
||||||
|
|
||||||
managerOrgBilling = await platformBillingRepositoryFixture.create(managerOrg.id, "SCALE");
|
|
||||||
|
|
||||||
await membershipsRepositoryFixture.create({
|
|
||||||
role: "ADMIN",
|
|
||||||
user: { connect: { id: managerOrgAdmin.id } },
|
|
||||||
team: { connect: { id: managerOrg.id } },
|
|
||||||
accepted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(
|
|
||||||
managerOrgAdmin.id,
|
|
||||||
null,
|
|
||||||
managerOrg.id
|
|
||||||
);
|
);
|
||||||
managerOrgAdminApiKey = `cal_test_${keyString}`;
|
managerOrgAdmin = managerSetup.admin;
|
||||||
|
managerOrg = managerSetup.org;
|
||||||
|
managerOrgBilling = managerSetup.billing;
|
||||||
|
managerOrgAdminApiKey = managerSetup.apiKey;
|
||||||
|
|
||||||
|
// Setup pay-per-user organization
|
||||||
|
const payPerUserSetup = await setupTestOrganization(
|
||||||
|
payPerUserPlanManagerOrgAdminEmail,
|
||||||
|
`pay-per-user-plan-organization-${randomString()}`,
|
||||||
|
"PER_ACTIVE_USER"
|
||||||
|
);
|
||||||
|
payPerUserPlanManagerOrgAdmin = payPerUserSetup.admin;
|
||||||
|
payPerUserPlanManagerOrg = payPerUserSetup.org;
|
||||||
|
payPerUserPlanManagerOrgAdminApiKey = payPerUserSetup.apiKey;
|
||||||
|
|
||||||
|
// Setup essentials organization
|
||||||
|
const essentialsSetup = await setupTestOrganization(
|
||||||
|
essentialsPlanManagerOrgAdminEmail,
|
||||||
|
`essentials-per-user-plan-organization-${randomString()}`,
|
||||||
|
"ESSENTIALS"
|
||||||
|
);
|
||||||
|
essentialsPlanManagerOrgAdmin = essentialsSetup.admin;
|
||||||
|
essentialsPlanManagerOrg = essentialsSetup.org;
|
||||||
|
essentialsPlanManagerOrgAdminApiKey = essentialsSetup.apiKey;
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
bootstrap(app as NestExpressApplication);
|
bootstrap(app as NestExpressApplication);
|
||||||
@@ -136,6 +145,57 @@ describe("Organizations Organizations Endpoints", () => {
|
|||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function setupTestOrganization(
|
||||||
|
adminEmail: string,
|
||||||
|
orgName: string,
|
||||||
|
plan: "SCALE" | "PER_ACTIVE_USER" | "ESSENTIALS"
|
||||||
|
) {
|
||||||
|
const admin = await userRepositoryFixture.create({
|
||||||
|
email: adminEmail,
|
||||||
|
username: adminEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
const org = await organizationsRepositoryFixture.create({
|
||||||
|
name: orgName,
|
||||||
|
isOrganization: true,
|
||||||
|
isPlatform: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await profilesRepositoryFixture.create({
|
||||||
|
uid: `${randomString()}-uid`,
|
||||||
|
username: adminEmail,
|
||||||
|
user: { connect: { id: admin.id } },
|
||||||
|
organization: { connect: { id: org.id } },
|
||||||
|
movedFromUser: { connect: { id: admin.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const billing = await platformBillingRepositoryFixture.create(org.id, plan);
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "ADMIN",
|
||||||
|
user: { connect: { id: admin.id } },
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { keyString } = await apiKeysRepositoryFixture.createApiKey(admin.id, null, org.id);
|
||||||
|
const apiKey = `cal_test_${keyString}`;
|
||||||
|
|
||||||
|
return { admin, org, billing, apiKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createManagedOrgInput(
|
||||||
|
namePrefix: string,
|
||||||
|
metadata?: Record<string, string | number | boolean>
|
||||||
|
): CreateOrganizationInput {
|
||||||
|
const suffix = randomString(5);
|
||||||
|
return {
|
||||||
|
name: `${namePrefix} ${suffix}`,
|
||||||
|
slug: `${namePrefix.toLowerCase().replace(/\s+/g, "-")}-${suffix}`,
|
||||||
|
metadata: metadata || { key: "value" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
clear();
|
clear();
|
||||||
});
|
});
|
||||||
@@ -155,21 +215,44 @@ describe("Organizations Organizations Endpoints", () => {
|
|||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
const suffix = randomString(5);
|
|
||||||
const metadataKey = "first-org-metadata-key";
|
const metadataKey = "first-org-metadata-key";
|
||||||
const metadataValue = "first-org-metadata-value";
|
const metadataValue = "first-org-metadata-value";
|
||||||
const createManagedOrganizationBody: CreateOrganizationInput = {
|
const createManagedOrganizationBody: CreateOrganizationInput = createManagedOrgInput("org", {
|
||||||
name: `org ${suffix}`,
|
[metadataKey]: metadataValue,
|
||||||
slug: `org-${suffix}`,
|
});
|
||||||
metadata: { [metadataKey]: metadataValue },
|
|
||||||
};
|
|
||||||
|
|
||||||
const suffix2 = randomString(5);
|
const createManagedOrganizationBodySecond: CreateOrganizationInput = createManagedOrgInput("org2");
|
||||||
const createManagedOrganizationBodySecond: CreateOrganizationInput = {
|
|
||||||
name: `org2 ${suffix2}`,
|
const createManagedOrganizationBodyThird: CreateOrganizationInput = createManagedOrgInput("org3");
|
||||||
slug: `org2-${suffix2}`,
|
|
||||||
metadata: { key: "value" },
|
it("should not allow to create managed organization if plan is below SCALE", async () => {
|
||||||
};
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${essentialsPlanManagerOrg.id}/organizations`)
|
||||||
|
.set("Authorization", `Bearer ${essentialsPlanManagerOrgAdminApiKey}`)
|
||||||
|
.send(createManagedOrganizationBody)
|
||||||
|
.expect(403);
|
||||||
|
expect(response.body.error.message).toBe(
|
||||||
|
`PlatformPlanGuard - organization with id=${essentialsPlanManagerOrg.id} does not have required plan for this operation. Minimum plan is SCALE while the organization has ESSENTIALS.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow to create managed organization if plan is SCALE or above", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${payPerUserPlanManagerOrg.id}/organizations`)
|
||||||
|
.set("Authorization", `Bearer ${payPerUserPlanManagerOrgAdminApiKey}`)
|
||||||
|
.send(createManagedOrganizationBodyThird)
|
||||||
|
.expect(201)
|
||||||
|
.then(async (response) => {
|
||||||
|
const responseBody: ApiSuccessResponse<ManagedOrganizationWithApiKeyOutput> = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const managedOrg3 = responseBody.data;
|
||||||
|
expect(managedOrg3?.id).toBeDefined();
|
||||||
|
expect(managedOrg3?.name).toEqual(createManagedOrganizationBodyThird.name);
|
||||||
|
expect(managedOrg3?.slug).toEqual(createManagedOrganizationBodyThird.slug);
|
||||||
|
expect(managedOrg3?.metadata).toEqual(createManagedOrganizationBodyThird.metadata);
|
||||||
|
expect(managedOrg3?.apiKey).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should create managed organization", async () => {
|
it("should create managed organization", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
@@ -625,7 +708,11 @@ describe("Organizations Organizations Endpoints", () => {
|
|||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await userRepositoryFixture.deleteByEmail(managerOrgAdmin.email);
|
await userRepositoryFixture.deleteByEmail(managerOrgAdmin.email);
|
||||||
|
await userRepositoryFixture.deleteByEmail(payPerUserPlanManagerOrgAdmin.email);
|
||||||
|
await userRepositoryFixture.deleteByEmail(essentialsPlanManagerOrgAdmin.email);
|
||||||
await organizationsRepositoryFixture.delete(managerOrg.id);
|
await organizationsRepositoryFixture.delete(managerOrg.id);
|
||||||
|
await organizationsRepositoryFixture.delete(payPerUserPlanManagerOrg.id);
|
||||||
|
await organizationsRepositoryFixture.delete(essentialsPlanManagerOrg.id);
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsArray, IsBoolean, IsInt, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
import { IsArray, IsBoolean, IsInt, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@ class Routing {
|
|||||||
crmOwnerRecordType?: string;
|
crmOwnerRecordType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiExtraModels(SlotsOutput_2024_09_04, RangeSlotsOutput_2024_09_04)
|
||||||
export class CreateRoutingFormResponseOutputData {
|
export class CreateRoutingFormResponseOutputData {
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
type: Number,
|
type: Number,
|
||||||
|
|||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { randomString } from "test/utils/randomString";
|
||||||
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import type { Team, User } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
describe("Organizations Teams Invite Endpoints", () => {
|
||||||
|
describe("User Authentication - User is Org Team Admin", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
|
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
|
let org: Team;
|
||||||
|
let orgTeam: Team;
|
||||||
|
let nonOrgTeam: Team;
|
||||||
|
|
||||||
|
const userEmail = `organizations-teams-invite-admin-${randomString()}@api.com`;
|
||||||
|
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await withApiAuth(
|
||||||
|
userEmail,
|
||||||
|
Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
|
})
|
||||||
|
).compile();
|
||||||
|
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: userEmail,
|
||||||
|
username: userEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
org = await organizationsRepositoryFixture.create({
|
||||||
|
name: `organizations-teams-invite-organization-${randomString()}`,
|
||||||
|
isOrganization: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
orgTeam = await teamsRepositoryFixture.create({
|
||||||
|
name: `organizations-teams-invite-team-${randomString()}`,
|
||||||
|
isOrganization: false,
|
||||||
|
parent: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
nonOrgTeam = await teamsRepositoryFixture.create({
|
||||||
|
name: `organizations-teams-invite-non-org-team-${randomString()}`,
|
||||||
|
isOrganization: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin of the org team
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "ADMIN",
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: orgTeam.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also a member of the organization
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "ADMIN",
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a team invite", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/invite`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(response.body.data.token.length).toBeGreaterThan(0);
|
||||||
|
expect(response.body.data.inviteLink).toEqual(expect.any(String));
|
||||||
|
expect(response.body.data.inviteLink).toContain(response.body.data.token);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a new invite on each request", async () => {
|
||||||
|
const first = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/invite`)
|
||||||
|
.expect(200);
|
||||||
|
const firstToken = first.body.data.token as string;
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/invite`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(response.body.data.token).not.toEqual(firstToken);
|
||||||
|
expect(response.body.data.inviteLink).toEqual(expect.any(String));
|
||||||
|
expect(response.body.data.inviteLink).toContain(response.body.data.token);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail for team not in organization", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(`/v2/organizations/${org.id}/teams/${nonOrgTeam.id}/invite`)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await organizationsRepositoryFixture.delete(org.id);
|
||||||
|
await teamsRepositoryFixture.delete(nonOrgTeam.id);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { OPTIONAL_API_KEY_HEADER, OPTIONAL_X_CAL_CLIENT_ID_HEADER, OPTIONAL_X_CAL_SECRET_KEY_HEADER } from "@/lib/docs/headers";
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
|
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||||
|
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||||
|
import { Controller, UseGuards, Post, Param, ParseIntPipe, HttpCode, HttpStatus } from "@nestjs/common";
|
||||||
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
|
|
||||||
|
import { CreateInviteOutputDto } from "./outputs/invite.output";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/organizations/:orgId/teams/:teamId",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@DocsTags("Orgs / Teams / Invite")
|
||||||
|
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||||
|
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||||
|
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||||
|
export class OrganizationsTeamsInviteController {
|
||||||
|
@Post("/invite")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@ApiOperation({ summary: "Create team invite link" })
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async createInvite(
|
||||||
|
@Param("orgId", ParseIntPipe) _orgId: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<CreateInviteOutputDto> {
|
||||||
|
const result = await TeamService.createInvite(teamId);
|
||||||
|
return { status: SUCCESS_STATUS, data: result };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
export class InviteDataDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description:
|
||||||
|
"Unique invitation token for this team. Share this token with prospective members to allow them to join the team/organization.",
|
||||||
|
example: "f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2",
|
||||||
|
})
|
||||||
|
token!: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description:
|
||||||
|
"Complete invitation URL that can be shared with prospective members. Opens the signup page with the token and redirects to getting started after signup.",
|
||||||
|
example: "http://app.cal.com/signup?token=f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2&callbackUrl=/getting-started",
|
||||||
|
})
|
||||||
|
inviteLink!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateInviteOutputDto {
|
||||||
|
@ApiProperty({ example: "success" })
|
||||||
|
status!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: InviteDataDto })
|
||||||
|
data!: InviteDataDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+2
@@ -351,6 +351,7 @@ describe("Organizations Users Endpoints", () => {
|
|||||||
email: `organizations-users-new-member-${randomString()}@api.com`,
|
email: `organizations-users-new-member-${randomString()}@api.com`,
|
||||||
bio,
|
bio,
|
||||||
metadata,
|
metadata,
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
};
|
};
|
||||||
|
|
||||||
const emailSpy = jest
|
const emailSpy = jest
|
||||||
@@ -367,6 +368,7 @@ describe("Organizations Users Endpoints", () => {
|
|||||||
expect(userData.email).toBe(newOrgUser.email);
|
expect(userData.email).toBe(newOrgUser.email);
|
||||||
expect(userData.bio).toBe(newOrgUser.bio);
|
expect(userData.bio).toBe(newOrgUser.bio);
|
||||||
expect(userData.metadata).toEqual(newOrgUser.metadata);
|
expect(userData.metadata).toEqual(newOrgUser.metadata);
|
||||||
|
expect(userData.timeZone).toBe(newOrgUser.timeZone);
|
||||||
expect(emailSpy).toHaveBeenCalledWith({
|
expect(emailSpy).toHaveBeenCalledWith({
|
||||||
usernameOrEmail: newOrgUser.email,
|
usernameOrEmail: newOrgUser.email,
|
||||||
orgName: org.name,
|
orgName: org.name,
|
||||||
|
|||||||
+205
-1
@@ -25,7 +25,12 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.
|
|||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { randomString } from "test/utils/randomString";
|
import { randomString } from "test/utils/randomString";
|
||||||
|
|
||||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_09_04 } from "@calcom/platform-constants";
|
import {
|
||||||
|
CAL_API_VERSION_HEADER,
|
||||||
|
SUCCESS_STATUS,
|
||||||
|
VERSION_2024_09_04,
|
||||||
|
ERROR_STATUS,
|
||||||
|
} from "@calcom/platform-constants";
|
||||||
import type {
|
import type {
|
||||||
CreateScheduleInput_2024_06_11,
|
CreateScheduleInput_2024_06_11,
|
||||||
ReserveSlotOutput_2024_09_04 as ReserveSlotOutputData_2024_09_04,
|
ReserveSlotOutput_2024_09_04 as ReserveSlotOutputData_2024_09_04,
|
||||||
@@ -48,6 +53,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
const teammateEmailOne = `slots-2024-09-04-user-1-team-slots-${randomString()}`;
|
const teammateEmailOne = `slots-2024-09-04-user-1-team-slots-${randomString()}`;
|
||||||
let teammateApiKeyString: string;
|
let teammateApiKeyString: string;
|
||||||
const teammateEmailTwo = `slots-2024-09-04-user-2-team-slots-${randomString()}`;
|
const teammateEmailTwo = `slots-2024-09-04-user-2-team-slots-${randomString()}`;
|
||||||
|
let teammateTwoApiKeyString: string;
|
||||||
|
|
||||||
const outsiderEmail = `slots-2024-09-04-unrelated-team-slots-${randomString()}`;
|
const outsiderEmail = `slots-2024-09-04-unrelated-team-slots-${randomString()}`;
|
||||||
let outsider: User;
|
let outsider: User;
|
||||||
@@ -61,6 +67,8 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
let collectiveEventTypeSlug: string;
|
let collectiveEventTypeSlug: string;
|
||||||
let collectiveEventTypeWithoutHostsId: number;
|
let collectiveEventTypeWithoutHostsId: number;
|
||||||
let roundRobinEventTypeId: number;
|
let roundRobinEventTypeId: number;
|
||||||
|
let roundRobinEventTypeWithoutFixedHostsId: number;
|
||||||
|
let roundRobinEventTypeWithFixedAndNonFixedHostsId: number;
|
||||||
let collectiveBookingId: number;
|
let collectiveBookingId: number;
|
||||||
let roundRobinBookingId: number;
|
let roundRobinBookingId: number;
|
||||||
let fullyBookedRoundRobinBookingIdOne: number;
|
let fullyBookedRoundRobinBookingIdOne: number;
|
||||||
@@ -115,6 +123,12 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(teammateOne.id, null);
|
const { keyString } = await apiKeysRepositoryFixture.createApiKey(teammateOne.id, null);
|
||||||
teammateApiKeyString = keyString;
|
teammateApiKeyString = keyString;
|
||||||
|
|
||||||
|
const { keyString: keyStringForTeammateTwo } = await apiKeysRepositoryFixture.createApiKey(
|
||||||
|
teammateTwo.id,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
teammateTwoApiKeyString = keyStringForTeammateTwo;
|
||||||
|
|
||||||
const { keyString: unrelatedUserKeyString } = await apiKeysRepositoryFixture.createApiKey(
|
const { keyString: unrelatedUserKeyString } = await apiKeysRepositoryFixture.createApiKey(
|
||||||
outsider.id,
|
outsider.id,
|
||||||
null
|
null
|
||||||
@@ -217,6 +231,67 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
});
|
});
|
||||||
roundRobinEventTypeId = roundRobinEventType.id;
|
roundRobinEventTypeId = roundRobinEventType.id;
|
||||||
|
|
||||||
|
const roundRobinEventTypeWithoutFixedHosts = await eventTypesRepositoryFixture.createTeamEventType({
|
||||||
|
schedulingType: "ROUND_ROBIN",
|
||||||
|
team: {
|
||||||
|
connect: { id: team.id },
|
||||||
|
},
|
||||||
|
title: "RR Event Type Without Fixed Hosts",
|
||||||
|
slug: `slots-2024-09-04-round-robin-event-type-${randomString()}`,
|
||||||
|
length: 60,
|
||||||
|
assignAllTeamMembers: true,
|
||||||
|
bookingFields: [],
|
||||||
|
locations: [],
|
||||||
|
users: {
|
||||||
|
connect: [{ id: teammateOne.id }, { id: teammateTwo.id }],
|
||||||
|
},
|
||||||
|
hosts: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
userId: teammateOne.id,
|
||||||
|
isFixed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: teammateTwo.id,
|
||||||
|
isFixed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
roundRobinEventTypeWithoutFixedHostsId = roundRobinEventTypeWithoutFixedHosts.id;
|
||||||
|
|
||||||
|
const roundRobinEventTypeWithFixedAndNonFixedHosts =
|
||||||
|
await eventTypesRepositoryFixture.createTeamEventType({
|
||||||
|
schedulingType: "ROUND_ROBIN",
|
||||||
|
team: {
|
||||||
|
connect: { id: team.id },
|
||||||
|
},
|
||||||
|
title: "RR Event Type With Fixed and Non-Fixed Hosts",
|
||||||
|
slug: `slots-2024-09-04-round-robin-event-type-${randomString()}`,
|
||||||
|
length: 60,
|
||||||
|
assignAllTeamMembers: true,
|
||||||
|
bookingFields: [],
|
||||||
|
locations: [],
|
||||||
|
users: {
|
||||||
|
connect: [{ id: teammateOne.id }, { id: teammateTwo.id }],
|
||||||
|
},
|
||||||
|
hosts: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
userId: teammateOne.id,
|
||||||
|
isFixed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: teammateTwo.id,
|
||||||
|
isFixed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
roundRobinEventTypeWithFixedAndNonFixedHostsId = roundRobinEventTypeWithFixedAndNonFixedHosts.id;
|
||||||
|
|
||||||
const userSchedule: CreateScheduleInput_2024_06_11 = {
|
const userSchedule: CreateScheduleInput_2024_06_11 = {
|
||||||
name: "working time",
|
name: "working time",
|
||||||
timeZone: "Europe/Rome",
|
timeZone: "Europe/Rome",
|
||||||
@@ -541,6 +616,135 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
bookingsRepositoryFixture.deleteById(bookingTwo.id);
|
bookingsRepositoryFixture.deleteById(bookingTwo.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should reserve all available slots for round robin event type with non-fixed hosts", async () => {
|
||||||
|
const now = "2049-09-05T12:00:00.000Z";
|
||||||
|
const newDate = DateTime.fromISO(now, { zone: "UTC" }).toJSDate();
|
||||||
|
advanceTo(newDate);
|
||||||
|
|
||||||
|
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
||||||
|
|
||||||
|
const reserveResponseOne = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${teammateApiKeyString}` })
|
||||||
|
.send({
|
||||||
|
eventTypeId: roundRobinEventTypeWithoutFixedHostsId,
|
||||||
|
slotStart: slotStartTime,
|
||||||
|
reservationDuration: 10,
|
||||||
|
})
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const reserveResponseOneBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponseOne.body;
|
||||||
|
expect(reserveResponseOneBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const responseReservedSlotOne: ReserveSlotOutputData_2024_09_04 = reserveResponseOneBody.data;
|
||||||
|
expect(responseReservedSlotOne.reservationUid).toBeDefined();
|
||||||
|
if (!responseReservedSlotOne.reservationUid) {
|
||||||
|
throw new Error("Reserved slot one uid is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbSlotOne = await selectedSlotRepositoryFixture.getByUid(responseReservedSlotOne.reservationUid);
|
||||||
|
expect(dbSlotOne).toBeDefined();
|
||||||
|
if (dbSlotOne) {
|
||||||
|
const dbReleaseAt = DateTime.fromJSDate(dbSlotOne.releaseAt, { zone: "UTC" }).toISO();
|
||||||
|
const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 10 }).toISO();
|
||||||
|
expect(dbReleaseAt).toEqual(expectedReleaseAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reserveResponseTwo = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${teammateTwoApiKeyString}` })
|
||||||
|
.send({
|
||||||
|
eventTypeId: roundRobinEventTypeWithoutFixedHostsId,
|
||||||
|
slotStart: slotStartTime,
|
||||||
|
reservationDuration: 10,
|
||||||
|
})
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const reserveResponseTwoBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponseTwo.body;
|
||||||
|
expect(reserveResponseTwoBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const responseReservedSlotTwo: ReserveSlotOutputData_2024_09_04 = reserveResponseTwoBody.data;
|
||||||
|
expect(responseReservedSlotTwo.reservationUid).toBeDefined();
|
||||||
|
if (!responseReservedSlotTwo.reservationUid) {
|
||||||
|
throw new Error("Reserved slot two uid is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbSlotTwo = await selectedSlotRepositoryFixture.getByUid(responseReservedSlotTwo.reservationUid);
|
||||||
|
expect(dbSlotTwo).toBeDefined();
|
||||||
|
if (dbSlotTwo) {
|
||||||
|
const dbReleaseAt = DateTime.fromJSDate(dbSlotTwo.releaseAt, { zone: "UTC" }).toISO();
|
||||||
|
const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 10 }).toISO();
|
||||||
|
expect(dbReleaseAt).toEqual(expectedReleaseAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reserveResponseThree = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${outsiderApiKeyString}` })
|
||||||
|
.send({
|
||||||
|
eventTypeId: roundRobinEventTypeWithoutFixedHostsId,
|
||||||
|
slotStart: slotStartTime,
|
||||||
|
reservationDuration: 10,
|
||||||
|
})
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04);
|
||||||
|
|
||||||
|
expect(reserveResponseThree.status).toEqual(403);
|
||||||
|
expect(reserveResponseThree.body.status).toEqual(ERROR_STATUS);
|
||||||
|
|
||||||
|
await selectedSlotRepositoryFixture.deleteByUId(responseReservedSlotOne.reservationUid);
|
||||||
|
await selectedSlotRepositoryFixture.deleteByUId(responseReservedSlotTwo.reservationUid);
|
||||||
|
clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reserve available slot for round robin event type with fixed and non-fixed hosts and should not be able to reserve another slot", async () => {
|
||||||
|
const now = "2049-09-05T12:00:00.000Z";
|
||||||
|
const newDate = DateTime.fromISO(now, { zone: "UTC" }).toJSDate();
|
||||||
|
advanceTo(newDate);
|
||||||
|
|
||||||
|
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
||||||
|
|
||||||
|
const reserveResponseOne = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${teammateApiKeyString}` })
|
||||||
|
.send({
|
||||||
|
eventTypeId: roundRobinEventTypeWithFixedAndNonFixedHostsId,
|
||||||
|
slotStart: slotStartTime,
|
||||||
|
reservationDuration: 10,
|
||||||
|
})
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const reserveResponseBodyOne: ReserveSlotOutputResponse_2024_09_04 = reserveResponseOne.body;
|
||||||
|
expect(reserveResponseBodyOne.status).toEqual(SUCCESS_STATUS);
|
||||||
|
const responseReservedSlotOne: ReserveSlotOutputData_2024_09_04 = reserveResponseBodyOne.data;
|
||||||
|
expect(responseReservedSlotOne.reservationUid).toBeDefined();
|
||||||
|
if (!responseReservedSlotOne.reservationUid) {
|
||||||
|
throw new Error("Reserved slot uid is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbSlotOne = await selectedSlotRepositoryFixture.getByUid(responseReservedSlotOne.reservationUid);
|
||||||
|
expect(dbSlotOne).toBeDefined();
|
||||||
|
if (dbSlotOne) {
|
||||||
|
const dbReleaseAt = DateTime.fromJSDate(dbSlotOne.releaseAt, { zone: "UTC" }).toISO();
|
||||||
|
const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 10 }).toISO();
|
||||||
|
expect(dbReleaseAt).toEqual(expectedReleaseAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reserveResponseTwo = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.send({
|
||||||
|
eventTypeId: roundRobinEventTypeWithFixedAndNonFixedHostsId,
|
||||||
|
slotStart: slotStartTime,
|
||||||
|
})
|
||||||
|
.set({ Authorization: `Bearer cal_test_${teammateTwoApiKeyString}` })
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04);
|
||||||
|
|
||||||
|
expect(reserveResponseTwo.status).toEqual(422);
|
||||||
|
expect(reserveResponseTwo.body.status).toEqual(ERROR_STATUS);
|
||||||
|
|
||||||
|
await selectedSlotRepositoryFixture.deleteByUId(responseReservedSlotOne.reservationUid);
|
||||||
|
clear();
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await userRepositoryFixture.deleteByEmail(teammateOne.email);
|
await userRepositoryFixture.deleteByEmail(teammateOne.email);
|
||||||
await userRepositoryFixture.deleteByEmail(teammateTwo.email);
|
await userRepositoryFixture.deleteByEmail(teammateTwo.email);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import { ApiResponse } from "@calcom/platform-types";
|
|||||||
@DocsTags("Slots")
|
@DocsTags("Slots")
|
||||||
@ApiHeader({
|
@ApiHeader({
|
||||||
name: "cal-api-version",
|
name: "cal-api-version",
|
||||||
description: `Must be set to ${VERSION_2024_09_04}`,
|
description: `Must be set to ${VERSION_2024_09_04}. If not set to this value, the endpoint will default to an older version.`,
|
||||||
example: VERSION_2024_09_04,
|
example: VERSION_2024_09_04,
|
||||||
required: true,
|
required: true,
|
||||||
schema: {
|
schema: {
|
||||||
@@ -86,6 +86,8 @@ export class SlotsController_2024_09_04 {
|
|||||||
- duration: Only use for event types that allow multiple durations or for dynamic event types. If not passed for multiple duration event types defaults to default duration. For dynamic event types defaults to 30 aka each returned slot is 30 minutes long. So duration=60 means that returned slots will be each 60 minutes long.
|
- duration: Only use for event types that allow multiple durations or for dynamic event types. If not passed for multiple duration event types defaults to default duration. For dynamic event types defaults to 30 aka each returned slot is 30 minutes long. So duration=60 means that returned slots will be each 60 minutes long.
|
||||||
- format: Format of the slots. By default return is an object where each key is date and value is array of slots as string. If you want to get start and end of each slot use "range" as value.
|
- format: Format of the slots. By default return is an object where each key is date and value is array of slots as string. If you want to get start and end of each slot use "range" as value.
|
||||||
- bookingUidToReschedule: When rescheduling an existing booking, provide the booking's unique identifier to exclude its time slot from busy time calculations. This ensures the original booking time appears as available for rescheduling.
|
- bookingUidToReschedule: When rescheduling an existing booking, provide the booking's unique identifier to exclude its time slot from busy time calculations. This ensures the original booking time appears as available for rescheduling.
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
@ApiQuery({
|
@ApiQuery({
|
||||||
@@ -256,7 +258,10 @@ export class SlotsController_2024_09_04 {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Reserve a slot",
|
summary: "Reserve a slot",
|
||||||
description: `Make a slot not available for others to book for a certain period of time. If you authenticate using oAuth credentials, api key or access token
|
description: `Make a slot not available for others to book for a certain period of time. If you authenticate using oAuth credentials, api key or access token
|
||||||
then you can also specify custom duration for how long the slot should be reserved for (defaults to 5 minutes).`,
|
then you can also specify custom duration for how long the slot should be reserved for (defaults to 5 minutes).
|
||||||
|
|
||||||
|
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||||
@@ -278,6 +283,7 @@ export class SlotsController_2024_09_04 {
|
|||||||
@Get("/reservations/:uid")
|
@Get("/reservations/:uid")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get reserved slot",
|
summary: "Get reserved slot",
|
||||||
|
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
|
||||||
})
|
})
|
||||||
async getReservedSlot(@Param("uid") uid: string): Promise<GetReservedSlotOutput_2024_09_04> {
|
async getReservedSlot(@Param("uid") uid: string): Promise<GetReservedSlotOutput_2024_09_04> {
|
||||||
const reservedSlot = await this.slotsService.getReservedSlot(uid);
|
const reservedSlot = await this.slotsService.getReservedSlot(uid);
|
||||||
@@ -293,6 +299,7 @@ export class SlotsController_2024_09_04 {
|
|||||||
@Patch("/reservations/:uid")
|
@Patch("/reservations/:uid")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Update a reserved slot",
|
summary: "Update a reserved slot",
|
||||||
|
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
|
||||||
})
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async updateReservedSlot(
|
async updateReservedSlot(
|
||||||
@@ -312,6 +319,7 @@ export class SlotsController_2024_09_04 {
|
|||||||
@Delete("/reservations/:uid")
|
@Delete("/reservations/:uid")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Delete a reserved slot",
|
summary: "Delete a reserved slot",
|
||||||
|
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
|
||||||
})
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@DocsResponse({
|
@DocsResponse({
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { DateTime } from "luxon";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SlotFormat } from "@calcom/platform-enums";
|
import { SlotFormat } from "@calcom/platform-enums";
|
||||||
|
import { SchedulingType } from "@calcom/platform-libraries";
|
||||||
|
import { validateRoundRobinSlotAvailability } from "@calcom/platform-libraries/slots";
|
||||||
import type {
|
import type {
|
||||||
GetSlotsInput_2024_09_04,
|
GetSlotsInput_2024_09_04,
|
||||||
GetSlotsInputWithRouting_2024_09_04,
|
GetSlotsInputWithRouting_2024_09_04,
|
||||||
@@ -147,13 +149,26 @@ export class SlotsService_2024_09_04 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nonSeatedEventAlreadyBooked = !eventType.seatsPerTimeSlot && booking;
|
const nonSeatedEventAlreadyBooked = !eventType.seatsPerTimeSlot && booking;
|
||||||
if (nonSeatedEventAlreadyBooked) {
|
const isRoundRobinEvent = eventType.schedulingType === SchedulingType.ROUND_ROBIN;
|
||||||
|
|
||||||
|
if (nonSeatedEventAlreadyBooked && !isRoundRobinEvent) {
|
||||||
throw new UnprocessableEntityException(`Can't reserve a slot if the event is already booked.`);
|
throw new UnprocessableEntityException(`Can't reserve a slot if the event is already booked.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const reservationDuration = input.reservationDuration ?? DEFAULT_RESERVATION_DURATION;
|
if (isRoundRobinEvent) {
|
||||||
|
try {
|
||||||
|
await validateRoundRobinSlotAvailability(input.eventTypeId, startDate, endDate, eventType.hosts);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new UnprocessableEntityException(error?.message);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.checkSlotOverlap(input.eventTypeId, startDate.toISO(), endDate.toISO());
|
||||||
|
}
|
||||||
|
|
||||||
await this.checkSlotOverlap(input.eventTypeId, startDate.toISO(), endDate.toISO());
|
const reservationDuration = input.reservationDuration ?? DEFAULT_RESERVATION_DURATION;
|
||||||
|
|
||||||
if (eventType.userId) {
|
if (eventType.userId) {
|
||||||
const slot = await this.slotsRepository.createSlot(
|
const slot = await this.slotsRepository.createSlot(
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@ApiExtraModels(TeamEventTypeOutput_2024_06_14)
|
||||||
export class CreateTeamEventTypeOutput extends ApiResponseWithoutData {
|
export class CreateTeamEventTypeOutput extends ApiResponseWithoutData {
|
||||||
@IsNotEmptyObject()
|
@IsNotEmptyObject()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@ApiExtraModels(TeamEventTypeOutput_2024_06_14)
|
||||||
export class UpdateTeamEventTypeOutput extends ApiResponseWithoutData {
|
export class UpdateTeamEventTypeOutput extends ApiResponseWithoutData {
|
||||||
@IsNotEmptyObject()
|
@IsNotEmptyObject()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
|
|||||||
+125
@@ -328,6 +328,131 @@ describe("Teams Memberships Endpoints", () => {
|
|||||||
return request(app.getHttpServer()).get(`/v2/teams/${team.id}/memberships/123132145`).expect(404);
|
return request(app.getHttpServer()).get(`/v2/teams/${team.id}/memberships/123132145`).expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should filter memberships by single email", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/teams/${team.id}/memberships?emails=${teamAdminEmail}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetTeamMembershipsOutput = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data.length).toEqual(1);
|
||||||
|
expect(responseBody.data[0].user.email).toEqual(teamAdminEmail);
|
||||||
|
expect(responseBody.data[0].userId).toEqual(teamAdmin.id);
|
||||||
|
expect(responseBody.data[0].role).toEqual("ADMIN");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter memberships by multiple emails", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/teams/${team.id}/memberships?emails=${teamAdminEmail},${teamMemberEmail}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetTeamMembershipsOutput = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data.length).toEqual(2);
|
||||||
|
|
||||||
|
const emails = responseBody.data.map((membership) => membership.user.email);
|
||||||
|
expect(emails).toContain(teamAdminEmail);
|
||||||
|
expect(emails).toContain(teamMemberEmail);
|
||||||
|
|
||||||
|
const adminMembership = responseBody.data.find((m) => m.user.email === teamAdminEmail);
|
||||||
|
const memberMembership = responseBody.data.find((m) => m.user.email === teamMemberEmail);
|
||||||
|
|
||||||
|
expect(adminMembership).toBeDefined();
|
||||||
|
expect(memberMembership).toBeDefined();
|
||||||
|
expect(adminMembership?.role).toEqual("ADMIN");
|
||||||
|
expect(memberMembership?.role).toEqual("MEMBER");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when filtering by non-existent email", async () => {
|
||||||
|
const nonExistentEmail = `nonexistent-${randomString()}@test.com`;
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/teams/${team.id}/memberships?emails=${nonExistentEmail}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetTeamMembershipsOutput = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return partial results when filtering by mix of existing and non-existent emails", async () => {
|
||||||
|
const nonExistentEmail = `nonexistent-${randomString()}@test.com`;
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/teams/${team.id}/memberships?emails=${teamAdminEmail},${nonExistentEmail}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetTeamMembershipsOutput = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data.length).toEqual(1);
|
||||||
|
expect(responseBody.data[0].user.email).toEqual(teamAdminEmail);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with pagination and email filtering combined", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/teams/${team.id}/memberships?emails=${teamAdminEmail},${teamMemberEmail}&skip=1&take=1`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetTeamMembershipsOutput = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data.length).toEqual(1);
|
||||||
|
const returnedEmail = responseBody.data[0].user.email;
|
||||||
|
expect([teamAdminEmail, teamMemberEmail]).toContain(returnedEmail);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty emails array gracefully", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/teams/${team.id}/memberships?emails=`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetTeamMembershipsOutput = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data.length).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle URL encoded email addresses in filter", async () => {
|
||||||
|
const encodedEmail = encodeURIComponent(teamAdminEmail);
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/teams/${team.id}/memberships?emails=${encodedEmail}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetTeamMembershipsOutput = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data.length).toEqual(1);
|
||||||
|
expect(responseBody.data[0].user.email).toEqual(teamAdminEmail);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter by email and maintain all user properties", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/teams/${team.id}/memberships?emails=${teamMemberEmail}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetTeamMembershipsOutput = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data.length).toEqual(1);
|
||||||
|
const membership = responseBody.data[0];
|
||||||
|
expect(membership.user.email).toEqual(teamMemberEmail);
|
||||||
|
expect(membership.user.bio).toEqual(teamMember.bio);
|
||||||
|
expect(membership.user.metadata).toEqual(teamMember.metadata);
|
||||||
|
expect(membership.user.username).toEqual(teamMember.username);
|
||||||
|
expect(membership.teamId).toEqual(team.id);
|
||||||
|
expect(membership.userId).toEqual(teamMember.id);
|
||||||
|
expect(membership.role).toEqual("MEMBER");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should validate email array size limits", async () => {
|
||||||
|
const tooManyEmails = Array.from({ length: 21 }, (_, i) => `test${i}@example.com`).join(",");
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/teams/${team.id}/memberships?emails=${tooManyEmails}`)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await userRepositoryFixture.deleteByEmail(teamAdmin.email);
|
await userRepositoryFixture.deleteByEmail(teamAdmin.email);
|
||||||
await userRepositoryFixture.deleteByEmail(teammateInvitedViaApi.email);
|
await userRepositoryFixture.deleteByEmail(teammateInvitedViaApi.email);
|
||||||
|
|||||||
+8
-4
@@ -4,6 +4,7 @@ import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
|||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
||||||
|
import { GetTeamMembershipsInput } from "@/modules/teams/memberships/inputs/get-team-memberships.input";
|
||||||
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
||||||
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
||||||
import { DeleteTeamMembershipOutput } from "@/modules/teams/memberships/outputs/delete-team-membership.output";
|
import { DeleteTeamMembershipOutput } from "@/modules/teams/memberships/outputs/delete-team-membership.output";
|
||||||
@@ -32,7 +33,6 @@ import { plainToClass } from "class-transformer";
|
|||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries/event-types";
|
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries/event-types";
|
||||||
import { SkipTakePagination } from "@calcom/platform-types";
|
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "/v2/teams/:teamId/memberships",
|
path: "/v2/teams/:teamId/memberships",
|
||||||
@@ -85,16 +85,20 @@ export class TeamsMembershipsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@ApiOperation({ summary: "Get all memberships" })
|
@ApiOperation({
|
||||||
|
summary: "Get all memberships",
|
||||||
|
description: "Retrieve team memberships with optional filtering by email addresses. Supports pagination.",
|
||||||
|
})
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async getTeamMemberships(
|
async getTeamMemberships(
|
||||||
@Param("teamId", ParseIntPipe) teamId: number,
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
@Query() queryParams: SkipTakePagination
|
@Query() queryParams: GetTeamMembershipsInput
|
||||||
): Promise<GetTeamMembershipsOutput> {
|
): Promise<GetTeamMembershipsOutput> {
|
||||||
const { skip, take } = queryParams;
|
const { skip, take, emails } = queryParams;
|
||||||
const orgTeamMemberships = await this.teamsMembershipsService.getPaginatedTeamMemberships(
|
const orgTeamMemberships = await this.teamsMembershipsService.getPaginatedTeamMemberships(
|
||||||
teamId,
|
teamId,
|
||||||
|
emails,
|
||||||
skip ?? 0,
|
skip ?? 0,
|
||||||
take ?? 250
|
take ?? 250
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { GetUsersInput } from "@/modules/users/inputs/get-users.input";
|
||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { Transform } from "class-transformer";
|
||||||
|
import { ArrayMaxSize, ArrayNotEmpty, IsEmail, IsOptional } from "class-validator";
|
||||||
|
|
||||||
|
export class GetTeamMembershipsInput extends GetUsersInput {
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (value == null) return undefined;
|
||||||
|
const rawValues = (Array.isArray(value) ? value : [value]).flatMap((entry) =>
|
||||||
|
typeof entry === "string" ? entry.split(",") : []
|
||||||
|
);
|
||||||
|
const normalized = rawValues
|
||||||
|
.map((email) => email.trim())
|
||||||
|
.filter((email) => email.length > 0)
|
||||||
|
.map((email) => email.toLowerCase());
|
||||||
|
const deduplicated = [...new Set(normalized)];
|
||||||
|
return deduplicated.length > 0 ? deduplicated : undefined;
|
||||||
|
})
|
||||||
|
@ArrayNotEmpty({ message: "emails cannot be empty." })
|
||||||
|
@ArrayMaxSize(20, {
|
||||||
|
message: "emails array cannot contain more than 20 email addresses for team membership filtering",
|
||||||
|
})
|
||||||
|
@IsEmail({}, { each: true, message: "Each email must be a valid email address" })
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
type: [String],
|
||||||
|
description:
|
||||||
|
"Filter team memberships by email addresses. If you want to filter by multiple emails, separate them with a comma (max 20 emails for performance).",
|
||||||
|
example: "?emails=user1@example.com,user2@example.com",
|
||||||
|
})
|
||||||
|
emails?: string[];
|
||||||
|
}
|
||||||
@@ -23,13 +23,15 @@ export class TeamsMembershipsService {
|
|||||||
return teamMembership;
|
return teamMembership;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPaginatedTeamMemberships(teamId: number, skip = 0, take = 250) {
|
async getPaginatedTeamMemberships(teamId: number, emails?: string[], skip = 0, take = 250) {
|
||||||
const teamMemberships = await this.teamsMembershipsRepository.findTeamMembershipsPaginated(
|
const emailArray = !emails ? [] : emails;
|
||||||
|
|
||||||
|
return await this.teamsMembershipsRepository.findTeamMembershipsPaginatedWithFilters(
|
||||||
teamId,
|
teamId,
|
||||||
|
{ emails: emailArray },
|
||||||
skip,
|
skip,
|
||||||
take
|
take
|
||||||
);
|
);
|
||||||
return teamMemberships;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTeamMembership(teamId: number, membershipId: number) {
|
async getTeamMembership(teamId: number, membershipId: number) {
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { Injectable } from "@nestjs/common";
|
|||||||
|
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
export interface TeamMembershipFilters {
|
||||||
|
emails?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export const MembershipUserSelect: Prisma.UserSelect = {
|
export const MembershipUserSelect: Prisma.UserSelect = {
|
||||||
username: true,
|
username: true,
|
||||||
email: true,
|
email: true,
|
||||||
@@ -41,6 +45,30 @@ export class TeamsMembershipsRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findTeamMembershipsPaginatedWithFilters(
|
||||||
|
teamId: number,
|
||||||
|
filters: TeamMembershipFilters,
|
||||||
|
skip: number,
|
||||||
|
take: number
|
||||||
|
) {
|
||||||
|
const whereClause: Prisma.MembershipWhereInput = {
|
||||||
|
teamId: teamId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filters.emails && filters.emails.length > 0) {
|
||||||
|
whereClause.user = {
|
||||||
|
email: { in: filters.emails },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.dbRead.prisma.membership.findMany({
|
||||||
|
where: whereClause,
|
||||||
|
include: { user: { select: MembershipUserSelect } },
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async findTeamMembership(teamId: number, membershipId: number) {
|
async findTeamMembership(teamId: number, membershipId: number) {
|
||||||
return this.dbRead.prisma.membership.findUnique({
|
return this.dbRead.prisma.membership.findUnique({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||||
import { Expose } from "class-transformer";
|
import { Expose } from "class-transformer";
|
||||||
import { IsEnum, ValidateNested, IsString, IsUrl } from "class-validator";
|
import { IsEnum, ValidateNested, IsString, IsUrl } from "class-validator";
|
||||||
|
|
||||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { TeamOutputDto } from "@calcom/platform-types";
|
import { TeamOutputDto } from "@calcom/platform-types";
|
||||||
|
|
||||||
class Output {
|
class CreateTeamOutputData {
|
||||||
@Expose()
|
@Expose()
|
||||||
@IsString()
|
@IsString()
|
||||||
message!: string;
|
message!: string;
|
||||||
@@ -19,16 +19,17 @@ class Output {
|
|||||||
pendingTeam!: TeamOutputDto;
|
pendingTeam!: TeamOutputDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiExtraModels(TeamOutputDto, CreateTeamOutputData)
|
||||||
export class CreateTeamOutput {
|
export class CreateTeamOutput {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
oneOf: [{ $ref: getSchemaPath(Output) }, { $ref: getSchemaPath(TeamOutputDto) }],
|
oneOf: [{ $ref: getSchemaPath(CreateTeamOutputData) }, { $ref: getSchemaPath(TeamOutputDto) }],
|
||||||
description: "Either an Output object or a TeamOutputDto.",
|
description: "Either an Output object or a TeamOutputDto.",
|
||||||
})
|
})
|
||||||
@Expose()
|
@Expose()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
data!: Output | TeamOutputDto;
|
data!: CreateTeamOutputData | TeamOutputDto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,30 +120,4 @@ export class TeamsRepository {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTeamMemberEmails(teamId: number) {
|
|
||||||
return this.dbRead.prisma.user.findMany({
|
|
||||||
where: {
|
|
||||||
teams: {
|
|
||||||
some: {
|
|
||||||
teamId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
email: true,
|
|
||||||
secondaryEmails: {
|
|
||||||
where: {
|
|
||||||
emailVerified: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Locales } from "@/lib/enums/locales";
|
import { Locales } from "@/lib/enums/locales";
|
||||||
import { CapitalizeTimeZone } from "@/lib/inputs/capitalize-timezone";
|
import { CapitalizeTimeZone } from "@/lib/inputs/capitalize-timezone";
|
||||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { Transform } from "class-transformer";
|
||||||
import { IsOptional, IsTimeZone, IsString, IsEnum, IsIn, IsUrl, IsObject } from "class-validator";
|
import { IsOptional, IsTimeZone, IsString, IsEnum, IsIn, IsUrl, IsObject } from "class-validator";
|
||||||
|
|
||||||
import { ValidateMetadata } from "@calcom/platform-types";
|
import { ValidateMetadata } from "@calcom/platform-types";
|
||||||
@@ -29,6 +30,7 @@ export class CreateManagedUserInput {
|
|||||||
})
|
})
|
||||||
weekStart?: WeekDay;
|
weekStart?: WeekDay;
|
||||||
|
|
||||||
|
@Transform(({ value }) => (value === null ? undefined : value))
|
||||||
@IsTimeZone()
|
@IsTimeZone()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@CapitalizeTimeZone()
|
@CapitalizeTimeZone()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user