Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bd17ba365 | ||
|
|
4d5246cf4d | ||
|
|
5d988453fa | ||
|
|
85b9782264 | ||
|
|
c2b091aa67 | ||
|
|
7093e1e4c6 | ||
|
|
ead42c1a9c | ||
|
|
e669996442 | ||
|
|
0e6f365dc2 | ||
|
|
08b501625d | ||
|
|
b89ff43921 | ||
|
|
fadfba8830 | ||
|
|
43be9bd6a8 | ||
|
|
caf184151d | ||
|
|
22e3338709 | ||
|
|
048e56806c | ||
|
|
ade64c0d09 | ||
|
|
5beb2943b9 | ||
|
|
b3d3cb1dee | ||
|
|
79d695236f | ||
|
|
2dbbdc21f2 | ||
|
|
e4254ef7cc | ||
|
|
7f9709bdaa | ||
|
|
77054d92df | ||
|
|
1277f5e9f7 | ||
|
|
109c303b60 | ||
|
|
a52a0e90e7 | ||
|
+25 |
558140fdd6 | ||
|
|
fa09fd66ff | ||
|
|
b1cbeebe48 | ||
|
|
93bad282d7 | ||
|
|
47d941676e | ||
|
|
54c4b0b65e | ||
|
|
0f8356f417 | ||
|
|
cecfcc7c6f | ||
|
|
0b3f9d06a9 | ||
|
|
71e7300830 | ||
|
|
bccffb328b | ||
|
|
80fa172f79 | ||
|
|
50173658f8 | ||
|
|
dddbd7dad4 | ||
|
|
ae2d85d81b | ||
|
|
80c407a4b7 | ||
|
|
6e9ec113fe | ||
|
|
3ee6a7ecde | ||
|
|
8ad62d183c | ||
|
|
f047799299 | ||
|
|
3a9e353443 | ||
|
|
b73a3aeb0c | ||
|
|
b6ff64ed74 | ||
|
|
1b281fe269 | ||
|
|
024913592b | ||
|
|
78a34038d2 | ||
|
|
5f770a9789 | ||
|
|
24a43877ce | ||
|
|
4a96be4175 | ||
|
|
594bb606b8 | ||
|
|
1c088f65c5 | ||
|
|
7d7a429780 | ||
|
|
d3228441aa | ||
|
|
21d6bf1fca | ||
|
|
a4134aa5a3 | ||
|
|
61397e5184 | ||
|
|
6143cd60e5 | ||
|
|
ac821f6b13 | ||
|
|
2bce38fd2a | ||
|
|
390ee79134 | ||
|
|
cfc9f0bfc3 | ||
|
|
58d5f2a17c | ||
|
|
37f271bd74 | ||
|
|
d770cfc65e | ||
|
|
ab8556790e | ||
|
|
3d89b37cc6 | ||
|
|
78b213321c | ||
|
|
276ba1179f | ||
|
|
0109d166a1 | ||
|
|
779c014256 | ||
|
|
073faf9e10 | ||
|
|
d0e39cf2dd | ||
|
|
f9488a1866 | ||
|
|
d4a3894a9e | ||
|
|
91288b41cd | ||
|
|
1aa2f68e73 | ||
|
|
ad707b0ee4 | ||
|
|
49a2f4d4f7 | ||
|
|
f576979345 | ||
|
|
b45399ab78 | ||
|
|
306dc5e4fb | ||
|
|
605304b93b | ||
|
|
d6a17b2b1e | ||
|
|
4bee875a8c | ||
|
|
fe4ae3ae62 | ||
|
|
a245578ff2 | ||
|
|
f5ec696e15 | ||
|
|
ec9384858b | ||
|
|
47613f796e | ||
|
|
446c780d85 | ||
|
|
615968c640 | ||
|
|
fc89436b6a | ||
|
|
3c1015cb11 | ||
|
|
46c513330c | ||
|
|
ef1598faeb | ||
|
|
42a6903057 | ||
|
|
0f0c2b8b92 | ||
|
|
c400c36306 | ||
|
|
6410bd2eaf | ||
|
|
2ddc5317b8 | ||
|
|
6fffe3377d |
No files matched your search
+7
-6
@@ -159,13 +159,16 @@ SENDGRID_EMAIL=
|
|||||||
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
||||||
|
|
||||||
# Sentry
|
# Sentry
|
||||||
|
SENTRY_ORG=
|
||||||
|
SENTRY_PROJECT=
|
||||||
|
SENTRY_AUTH_TOKEN=
|
||||||
# Used for capturing exceptions and logging messages
|
# Used for capturing exceptions and logging messages
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
SENTRY_DEBUG=
|
SENTRY_DEBUG=
|
||||||
SENTRY_DISABLE_CLIENT_SOURCE_MAPS=
|
|
||||||
SENTRY_DISABLE_SERVER_SOURCE_MAPS=
|
|
||||||
SENTRY_MAX_SPANS=
|
SENTRY_MAX_SPANS=
|
||||||
SENTRY_TRACES_SAMPLE_RATE=
|
SENTRY_TRACES_SAMPLE_RATE=
|
||||||
|
SENTRY_REPLAYS_SESSION_SAMPLE_RATE=
|
||||||
|
SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=
|
||||||
|
|
||||||
# Formbricks Experience Management Integration
|
# Formbricks Experience Management Integration
|
||||||
NEXT_PUBLIC_FORMBRICKS_HOST_URL=https://app.formbricks.com
|
NEXT_PUBLIC_FORMBRICKS_HOST_URL=https://app.formbricks.com
|
||||||
@@ -198,6 +201,7 @@ NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
|
|||||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
||||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||||
|
NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID=
|
||||||
STRIPE_TEAM_PRODUCT_ID=
|
STRIPE_TEAM_PRODUCT_ID=
|
||||||
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
|
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
|
||||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||||
@@ -363,9 +367,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
|
|||||||
|
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
# disable setry server source maps
|
|
||||||
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
|
||||||
|
|
||||||
# api v2
|
# api v2
|
||||||
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
|
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
|
||||||
|
|
||||||
@@ -425,7 +426,7 @@ DIRECTORY_IDS_TO_LOG=
|
|||||||
# Read more about it in the README.md
|
# Read more about it in the README.md
|
||||||
NEXT_PUBLIC_SINGLE_ORG_SLUG=
|
NEXT_PUBLIC_SINGLE_ORG_SLUG=
|
||||||
|
|
||||||
AKISMET_API_KEY=
|
IFFY_API_KEY=
|
||||||
|
|
||||||
## Env variables related to avoiding booking failures
|
## Env variables related to avoiding booking failures
|
||||||
# Request for checking reservation would be attempted to send every these seconds if the request is stale at that time
|
# Request for checking reservation would be attempted to send every these seconds if the request is stale at that time
|
||||||
|
|||||||
+3
-1
@@ -3,7 +3,7 @@
|
|||||||
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/components/**/* @calcom/Consumer @calcom/Foundation
|
/apps/web/components/**/* @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/modules/**/* @calcom/Consumer @calcom/Foundation
|
/apps/web/modules/**/* @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/**/layout.tsx @calcom/Foundation
|
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/lib/**/* @calcom/Foundation
|
/apps/web/lib/**/* @calcom/Foundation
|
||||||
/apps/web/middleware.ts @calcom/Foundation
|
/apps/web/middleware.ts @calcom/Foundation
|
||||||
/deploy/**/* @calcom/Foundation
|
/deploy/**/* @calcom/Foundation
|
||||||
@@ -26,5 +26,7 @@
|
|||||||
/packages/lib/slots.ts @calcom/Foundation
|
/packages/lib/slots.ts @calcom/Foundation
|
||||||
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
||||||
/packages/prisma/**/* @calcom/Foundation
|
/packages/prisma/**/* @calcom/Foundation
|
||||||
|
/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @calcom/Foundation
|
||||||
|
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
|
||||||
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
||||||
/packages/ui/**/* @calcom/Consumer @calcom/Foundation
|
/packages/ui/**/* @calcom/Consumer @calcom/Foundation
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: Cron - checkSmsPrices
|
||||||
|
|
||||||
|
on:
|
||||||
|
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||||
|
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||||
|
schedule:
|
||||||
|
# Runs “every minute” (see https://crontab.guru)
|
||||||
|
- cron: "* * * * *"
|
||||||
|
jobs:
|
||||||
|
cron-checkSmsPrices:
|
||||||
|
env:
|
||||||
|
APP_URL: ${{ secrets.APP_URL }}
|
||||||
|
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: cURL request
|
||||||
|
if: ${{ env.APP_URL && env.CRON_API_KEY }}
|
||||||
|
run: |
|
||||||
|
curl ${{ secrets.APP_URL }}/api/cron/checkSmsPrices \
|
||||||
|
-X POST \
|
||||||
|
-H 'content-type: application/json' \
|
||||||
|
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||||
|
--fail
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
|
- "@calcom/prisma"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Constructs the WHERE clause for Prisma booking findMany operation.
|
||||||
|
*
|
||||||
|
* @param userId - The ID of the user making the request. This is used to filter bookings where the user is either the host or an attendee.
|
||||||
|
* @param attendeeEmails - An array of emails provided in the request for filtering bookings by attendee emails, used in case of Admin calls.
|
||||||
|
* @param userIds - An array of user IDs to be included in the filter. Defaults to an empty array, and an array of user IDs in case of Admin call containing it.
|
||||||
|
* @param userEmails - An array of user emails to be included in the filter if it is an Admin call and contains userId in query parameter. Defaults to an empty array.
|
||||||
|
*
|
||||||
|
* @returns An object that represents the WHERE clause for the findMany/findUnique operation.
|
||||||
|
*/
|
||||||
|
export function buildWhereClause(userId: number | null, attendeeEmails: string[], userIds: number[] = []) {
|
||||||
|
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
||||||
|
const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : !!userId ? { userId } : {};
|
||||||
|
|
||||||
|
let whereClause = {};
|
||||||
|
|
||||||
|
if (filterByAttendeeEmails) {
|
||||||
|
whereClause = {
|
||||||
|
AND: [
|
||||||
|
userFilter,
|
||||||
|
{
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: { in: attendeeEmails },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
whereClause = {
|
||||||
|
...userFilter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return whereClause;
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"@calcom/lib": "*",
|
"@calcom/lib": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "*",
|
||||||
"@sentry/nextjs": "^8.8.0",
|
"@sentry/nextjs": "^9.15.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "^13.5.9",
|
"next": "^13.5.9",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma, Booking } from "@prisma/client";
|
||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
@@ -6,6 +6,7 @@ import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
|||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||||
|
import { buildWhereClause } from "~/lib/utils/bookings/get/buildWhereClause";
|
||||||
import {
|
import {
|
||||||
getAccessibleUsers,
|
getAccessibleUsers,
|
||||||
retrieveOrgScopedAccessibleUsers,
|
retrieveOrgScopedAccessibleUsers,
|
||||||
@@ -151,58 +152,6 @@ type GetAdminArgsType = {
|
|||||||
userId: number;
|
userId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs the WHERE clause for Prisma booking findMany operation.
|
|
||||||
*
|
|
||||||
* @param userId - The ID of the user making the request. This is used to filter bookings where the user is either the host or an attendee.
|
|
||||||
* @param attendeeEmails - An array of emails provided in the request for filtering bookings by attendee emails, used in case of Admin calls.
|
|
||||||
* @param userIds - An array of user IDs to be included in the filter. Defaults to an empty array, and an array of user IDs in case of Admin call containing it.
|
|
||||||
* @param userEmails - An array of user emails to be included in the filter if it is an Admin call and contains userId in query parameter. Defaults to an empty array.
|
|
||||||
*
|
|
||||||
* @returns An object that represents the WHERE clause for the findMany/findUnique operation.
|
|
||||||
*/
|
|
||||||
function buildWhereClause(
|
|
||||||
userId: number | null,
|
|
||||||
attendeeEmails: string[],
|
|
||||||
userIds: number[] = [],
|
|
||||||
userEmails: string[] = []
|
|
||||||
) {
|
|
||||||
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
|
||||||
const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : !!userId ? { userId } : {};
|
|
||||||
|
|
||||||
let whereClause = {};
|
|
||||||
|
|
||||||
if (filterByAttendeeEmails) {
|
|
||||||
whereClause = {
|
|
||||||
AND: [
|
|
||||||
userFilter,
|
|
||||||
{
|
|
||||||
attendees: {
|
|
||||||
some: {
|
|
||||||
email: { in: attendeeEmails },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
whereClause = {
|
|
||||||
OR: [
|
|
||||||
userFilter,
|
|
||||||
{
|
|
||||||
attendees: {
|
|
||||||
some: {
|
|
||||||
email: { in: userEmails },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return whereClause;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handler(req: NextApiRequest) {
|
export async function handler(req: NextApiRequest) {
|
||||||
const {
|
const {
|
||||||
userId,
|
userId,
|
||||||
@@ -238,6 +187,7 @@ export async function handler(req: NextApiRequest) {
|
|||||||
? [queryFilterForAttendeeEmails.attendeeEmail]
|
? [queryFilterForAttendeeEmails.attendeeEmail]
|
||||||
: [];
|
: [];
|
||||||
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
||||||
|
let userEmailsToFilterBy: string[] = [];
|
||||||
|
|
||||||
/** Only admins can query other users */
|
/** Only admins can query other users */
|
||||||
if (isSystemWideAdmin) {
|
if (isSystemWideAdmin) {
|
||||||
@@ -251,7 +201,8 @@ export async function handler(req: NextApiRequest) {
|
|||||||
userId,
|
userId,
|
||||||
};
|
};
|
||||||
const { userId: argUserId, userIds, userEmails } = await handleSystemWideAdminArgs(systemWideAdminArgs);
|
const { userId: argUserId, userIds, userEmails } = await handleSystemWideAdminArgs(systemWideAdminArgs);
|
||||||
args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails);
|
userEmailsToFilterBy = userEmails;
|
||||||
|
args.where = buildWhereClause(argUserId, attendeeEmails, userIds);
|
||||||
}
|
}
|
||||||
} else if (isOrganizationOwnerOrAdmin) {
|
} else if (isOrganizationOwnerOrAdmin) {
|
||||||
let requestedUserIds = [userId];
|
let requestedUserIds = [userId];
|
||||||
@@ -265,7 +216,8 @@ export async function handler(req: NextApiRequest) {
|
|||||||
userId,
|
userId,
|
||||||
};
|
};
|
||||||
const { userId: argUserId, userIds, userEmails } = await handleOrgWideAdminArgs(orgWideAdminArgs);
|
const { userId: argUserId, userIds, userEmails } = await handleOrgWideAdminArgs(orgWideAdminArgs);
|
||||||
args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails);
|
userEmailsToFilterBy = userEmails;
|
||||||
|
args.where = buildWhereClause(argUserId, attendeeEmails, userIds);
|
||||||
} else {
|
} else {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
@@ -276,7 +228,7 @@ export async function handler(req: NextApiRequest) {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
throw new HttpError({ message: "User not found", statusCode: 404 });
|
throw new HttpError({ message: "User not found", statusCode: 404 });
|
||||||
}
|
}
|
||||||
args.where = buildWhereClause(userId, attendeeEmails, [], []);
|
args.where = buildWhereClause(userId, attendeeEmails, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dateFrom) {
|
if (dateFrom) {
|
||||||
@@ -317,7 +269,77 @@ export async function handler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await prisma.booking.findMany(args);
|
let data: Booking[] = [];
|
||||||
|
|
||||||
|
if (!filterByAttendeeEmails && userEmailsToFilterBy.length > 0) {
|
||||||
|
const queryOne = prisma.booking.findMany(args);
|
||||||
|
|
||||||
|
const whereClauseForQueryTwo: Prisma.BookingWhereInput = {
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: { in: userEmailsToFilterBy },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dateFrom) {
|
||||||
|
whereClauseForQueryTwo.startTime = { gte: dateFrom };
|
||||||
|
}
|
||||||
|
if (dateTo) {
|
||||||
|
whereClauseForQueryTwo.endTime = { lte: dateTo };
|
||||||
|
}
|
||||||
|
if (status === "upcoming") {
|
||||||
|
whereClauseForQueryTwo.startTime = { gte: new Date().toISOString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
const argsForQueryTwo: Prisma.BookingFindManyArgs = {
|
||||||
|
...args,
|
||||||
|
where: whereClauseForQueryTwo,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryTwo = prisma.booking.findMany(argsForQueryTwo);
|
||||||
|
|
||||||
|
const [resultOne, resultTwo] = await Promise.all([queryOne, queryTwo]);
|
||||||
|
|
||||||
|
const bookingMap = new Map();
|
||||||
|
[...resultOne, ...resultTwo].forEach((booking) => {
|
||||||
|
bookingMap.set(booking.id, booking);
|
||||||
|
});
|
||||||
|
|
||||||
|
const dedupedResults = Array.from(bookingMap.values());
|
||||||
|
|
||||||
|
if (args.orderBy) {
|
||||||
|
let sortField: keyof Booking;
|
||||||
|
let sortDirection: "asc" | "desc" = "asc";
|
||||||
|
|
||||||
|
if (typeof args.orderBy === "object" && !Array.isArray(args.orderBy)) {
|
||||||
|
const orderByKey = Object.keys(args.orderBy)[0] as keyof typeof args.orderBy;
|
||||||
|
sortField = orderByKey as keyof Booking;
|
||||||
|
sortDirection = args.orderBy[orderByKey] as "asc" | "desc";
|
||||||
|
} else {
|
||||||
|
sortField = "id";
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortOrder = sortDirection === "desc" ? -1 : 1;
|
||||||
|
|
||||||
|
dedupedResults.sort((a, b) => {
|
||||||
|
const aValue = a[sortField];
|
||||||
|
const bValue = b[sortField];
|
||||||
|
|
||||||
|
if (aValue < bValue) return -1 * sortOrder;
|
||||||
|
if (aValue > bValue) return 1 * sortOrder;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.take !== undefined && args.skip !== undefined) {
|
||||||
|
data = dedupedResults.slice(args.skip, args.skip + args.take);
|
||||||
|
} else {
|
||||||
|
data = dedupedResults;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data = await prisma.booking.findMany(args);
|
||||||
|
}
|
||||||
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
|
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ import { membershipCreateBodySchema, schemaMembershipPublic } from "~/lib/valida
|
|||||||
*/
|
*/
|
||||||
async function postHandler(req: NextApiRequest) {
|
async function postHandler(req: NextApiRequest) {
|
||||||
const data = membershipCreateBodySchema.parse(req.body);
|
const data = membershipCreateBodySchema.parse(req.body);
|
||||||
const args: Prisma.MembershipCreateArgs = { data };
|
const args: Prisma.MembershipCreateArgs = {
|
||||||
|
data: {
|
||||||
|
createdAt: new Date(),
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
await checkPermissions(req);
|
await checkPermissions(req);
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export {};
|
|
||||||
@@ -2,6 +2,7 @@ import type { Request, Response } from "express";
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
@@ -206,8 +207,155 @@ describe("GET /api/bookings", async () => {
|
|||||||
console.log("bookings=>", responseData.bookings);
|
console.log("bookings=>", responseData.bookings);
|
||||||
responseData.bookings.forEach((booking) => {
|
responseData.bookings.forEach((booking) => {
|
||||||
if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1");
|
if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1");
|
||||||
if (booking.id === 19) expect(booking.eventType?.team).toBe(null);
|
if (booking.id === 19) {
|
||||||
|
// The team field can be either null or undefined due to nullish() in the schema
|
||||||
|
expect(booking.eventType?.team === null || booking.eventType?.team === undefined).toBe(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Date filtering", () => {
|
||||||
|
it("filters bookings by dateFrom", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
dateFrom: "2023-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
pagination: DefaultPagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = proUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
responseData.bookings.forEach((booking) => {
|
||||||
|
expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(
|
||||||
|
new Date("2023-01-01T00:00:00Z").getTime()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters bookings by dateTo", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
dateTo: "2024-12-31T23:59:59Z",
|
||||||
|
},
|
||||||
|
pagination: DefaultPagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = proUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
responseData.bookings.forEach((booking) => {
|
||||||
|
expect(new Date(booking.endTime).getTime()).toBeLessThanOrEqual(
|
||||||
|
new Date("2024-12-31T23:59:59Z").getTime()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Sorting and ordering", () => {
|
||||||
|
it("sorts bookings by createdAt in descending order", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
sortBy: "createdAt",
|
||||||
|
order: "desc",
|
||||||
|
},
|
||||||
|
pagination: DefaultPagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = proUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
const timestamps = responseData.bookings.map((b) => new Date(b.createdAt).getTime());
|
||||||
|
expect(timestamps).toEqual([...timestamps].sort((a, b) => b - a));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Multiple attendee email filtering", () => {
|
||||||
|
it("filters bookings by multiple attendee emails", async () => {
|
||||||
|
const attendeeEmails = ["test1@example.com", "test2@example.com"];
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
attendeeEmail: attendeeEmails,
|
||||||
|
},
|
||||||
|
pagination: DefaultPagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = proUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
responseData.bookings.forEach((booking) => {
|
||||||
|
const bookingAttendeeEmails = booking.attendees?.map((a) => a.email);
|
||||||
|
expect(bookingAttendeeEmails?.some((email) => attendeeEmails.includes(email))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error cases", () => {
|
||||||
|
it("throws error for invalid status parameter", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
status: "invalid_status",
|
||||||
|
},
|
||||||
|
pagination: DefaultPagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = proUser.id;
|
||||||
|
|
||||||
|
await expect(handler(req)).rejects.toThrow(ZodError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Result merging", () => {
|
||||||
|
it("does not return duplicate bookings when merging results from multiple queries", async () => {
|
||||||
|
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
||||||
|
|
||||||
|
const testUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||||
|
|
||||||
|
const testUserBooking = await prisma.booking.findFirstOrThrow({
|
||||||
|
where: { userId: testUser.id },
|
||||||
|
include: { attendees: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
userId: testUser.id, // This will make userEmailsToFilterBy contain the test user's email
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
take: 100,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.isSystemWideAdmin = true;
|
||||||
|
req.userId = adminUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
|
||||||
|
const bookingIds = responseData.bookings.map((booking) => booking.id);
|
||||||
|
|
||||||
|
const uniqueBookingIds = new Set(bookingIds);
|
||||||
|
|
||||||
|
expect(uniqueBookingIds.size).toBe(bookingIds.length);
|
||||||
|
|
||||||
|
if (uniqueBookingIds.size !== bookingIds.length) {
|
||||||
|
const counts = bookingIds.reduce((acc, id) => {
|
||||||
|
acc[id] = (acc[id] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<number, number>);
|
||||||
|
|
||||||
|
const duplicates = Object.entries(counts)
|
||||||
|
.filter(([_, count]) => count > 1)
|
||||||
|
.map(([id]) => id);
|
||||||
|
|
||||||
|
console.log(`Found duplicate booking IDs: ${duplicates.join(", ")}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import { buildWhereClause } from "~/lib/utils/bookings/get/buildWhereClause";
|
||||||
|
|
||||||
|
describe("buildWhereClause", () => {
|
||||||
|
it("should return empty object when no filters are provided", () => {
|
||||||
|
const result = buildWhereClause(null, [], []);
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter by userId when only userId is provided", () => {
|
||||||
|
const userId = 123;
|
||||||
|
const result = buildWhereClause(userId, [], []);
|
||||||
|
expect(result).toEqual({ userId: 123 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter by userIds when userIds array is provided", () => {
|
||||||
|
const userIds = [1, 2, 3];
|
||||||
|
const result = buildWhereClause(null, [], userIds);
|
||||||
|
expect(result).toEqual({ userId: { in: [1, 2, 3] } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter by attendee emails when provided", () => {
|
||||||
|
const attendeeEmails = ["test@example.com", "user@example.com"];
|
||||||
|
const result = buildWhereClause(null, attendeeEmails, []);
|
||||||
|
expect(result).toEqual({
|
||||||
|
AND: [
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: { in: ["test@example.com", "user@example.com"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should combine userId and attendee email filters", () => {
|
||||||
|
const userId = 123;
|
||||||
|
const attendeeEmails = ["test@example.com"];
|
||||||
|
const result = buildWhereClause(userId, attendeeEmails, []);
|
||||||
|
expect(result).toEqual({
|
||||||
|
AND: [
|
||||||
|
{ userId: 123 },
|
||||||
|
{
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: { in: ["test@example.com"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should combine userIds and attendee email filters", () => {
|
||||||
|
const userIds = [1, 2, 3];
|
||||||
|
const attendeeEmails = ["test@example.com"];
|
||||||
|
const result = buildWhereClause(null, attendeeEmails, userIds);
|
||||||
|
expect(result).toEqual({
|
||||||
|
AND: [
|
||||||
|
{ userId: { in: [1, 2, 3] } },
|
||||||
|
{
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: { in: ["test@example.com"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
},
|
},
|
||||||
"setupFiles": ["<rootDir>/test/setEnvVars.ts", "jest-date-mock"],
|
"setupFiles": ["<rootDir>/test/setEnvVars.ts", "jest-date-mock"],
|
||||||
|
"setupFilesAfterEnv": ["<rootDir>/test/jest.setup-e2e.ts"],
|
||||||
"reporters": ["default", "jest-summarizing-reporter"],
|
"reporters": ["default", "jest-summarizing-reporter"],
|
||||||
"workerIdleMemoryLimit": "512MB",
|
"workerIdleMemoryLimit": "512MB",
|
||||||
"maxWorkers": 8
|
"maxWorkers": 8
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.178",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.197",
|
||||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { KyselyModule } from "@/modules/kysely/kysely.module";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
@@ -23,6 +24,7 @@ import { Module } from "@nestjs/common";
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
KyselyModule,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
TokensModule,
|
TokensModule,
|
||||||
BillingModule,
|
BillingModule,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
|
|||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||||
|
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
@@ -99,6 +100,7 @@ export class BookingsController_2024_04_15 {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly oAuthFlowService: OAuthFlowService,
|
private readonly oAuthFlowService: OAuthFlowService,
|
||||||
private readonly prismaReadService: PrismaReadService,
|
private readonly prismaReadService: PrismaReadService,
|
||||||
|
private readonly kyselyReadService: KyselyReadService,
|
||||||
private readonly oAuthClientRepository: OAuthClientRepository,
|
private readonly oAuthClientRepository: OAuthClientRepository,
|
||||||
private readonly billingService: BillingService,
|
private readonly billingService: BillingService,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
@@ -129,6 +131,7 @@ export class BookingsController_2024_04_15 {
|
|||||||
ctx: {
|
ctx: {
|
||||||
user: { email: user.email, id: user.id, orgId: profile?.organizationId },
|
user: { email: user.email, id: user.id, orgId: profile?.organizationId },
|
||||||
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
|
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
|
||||||
|
kysely: this.kyselyReadService.kysely,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
|
||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import type { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BookingReferencesRepository_2024_08_13 {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService) {}
|
||||||
|
|
||||||
|
async getBookingReferences(bookingId: number, filter?: BookingReferencesFilterInput_2024_08_13) {
|
||||||
|
const whereClause: Prisma.BookingReferenceWhereInput = { bookingId };
|
||||||
|
|
||||||
|
if (filter?.type) {
|
||||||
|
whereClause.type = filter.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dbRead.prisma.bookingReference.findMany({
|
||||||
|
where: whereClause,
|
||||||
|
select: {
|
||||||
|
type: true,
|
||||||
|
uid: true,
|
||||||
|
id: true,
|
||||||
|
externalCalendarId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
||||||
|
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||||
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
|
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
|
||||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||||
|
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
||||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||||
@@ -17,6 +20,7 @@ import { BillingModule } from "@/modules/billing/billing.module";
|
|||||||
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
||||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { KyselyModule } from "@/modules/kysely/kysely.module";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
@@ -36,6 +40,7 @@ import { Module } from "@nestjs/common";
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
KyselyModule,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
TokensModule,
|
TokensModule,
|
||||||
BillingModule,
|
BillingModule,
|
||||||
@@ -56,6 +61,7 @@ import { Module } from "@nestjs/common";
|
|||||||
BookingsService_2024_08_13,
|
BookingsService_2024_08_13,
|
||||||
InputBookingsService_2024_08_13,
|
InputBookingsService_2024_08_13,
|
||||||
OutputBookingsService_2024_08_13,
|
OutputBookingsService_2024_08_13,
|
||||||
|
OutputBookingReferencesService_2024_08_13,
|
||||||
BookingsRepository_2024_08_13,
|
BookingsRepository_2024_08_13,
|
||||||
EventTypesRepository_2024_06_14,
|
EventTypesRepository_2024_06_14,
|
||||||
BookingSeatRepository,
|
BookingSeatRepository,
|
||||||
@@ -69,6 +75,8 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsTeamsRepository,
|
OrganizationsTeamsRepository,
|
||||||
OrganizationsRepository,
|
OrganizationsRepository,
|
||||||
ErrorsBookingsService_2024_08_13,
|
ErrorsBookingsService_2024_08_13,
|
||||||
|
BookingReferencesService_2024_08_13,
|
||||||
|
BookingReferencesRepository_2024_08_13,
|
||||||
],
|
],
|
||||||
controllers: [BookingsController_2024_08_13],
|
controllers: [BookingsController_2024_08_13],
|
||||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||||
|
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
|
||||||
|
import { BookingReferencesOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/booking-references.output";
|
||||||
import { CalendarLinksOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
|
import { CalendarLinksOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
|
||||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||||
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
||||||
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
|
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
|
||||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||||
|
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
import { 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";
|
||||||
|
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 { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
@@ -82,7 +87,8 @@ export class BookingsController_2024_08_13 {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly bookingsService: BookingsService_2024_08_13,
|
private readonly bookingsService: BookingsService_2024_08_13,
|
||||||
private readonly usersService: UsersService
|
private readonly usersService: UsersService,
|
||||||
|
private readonly bookingReferencesService: BookingReferencesService_2024_08_13
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -176,7 +182,7 @@ export class BookingsController_2024_08_13 {
|
|||||||
): Promise<GetBookingsOutput_2024_08_13> {
|
): Promise<GetBookingsOutput_2024_08_13> {
|
||||||
const profile = this.usersService.getUserMainProfile(user);
|
const profile = this.usersService.getUserMainProfile(user);
|
||||||
|
|
||||||
const bookings = await this.bookingsService.getBookings(queryParams, {
|
const { bookings, pagination } = await this.bookingsService.getBookings(queryParams, {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
orgId: profile?.organizationId,
|
orgId: profile?.organizationId,
|
||||||
@@ -185,6 +191,7 @@ export class BookingsController_2024_08_13 {
|
|||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: bookings,
|
data: bookings,
|
||||||
|
pagination,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,4 +393,30 @@ export class BookingsController_2024_08_13 {
|
|||||||
data: calendarLinks,
|
data: calendarLinks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("/:bookingUid/references")
|
||||||
|
@PlatformPlan("SCALE")
|
||||||
|
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||||
|
@Permissions([BOOKING_READ])
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get 'Booking References' for a booking",
|
||||||
|
})
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getBookingReferences(
|
||||||
|
@Param("bookingUid") bookingUid: string,
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Query() filter: BookingReferencesFilterInput_2024_08_13
|
||||||
|
): Promise<BookingReferencesOutput_2024_08_13> {
|
||||||
|
const bookingReferences = await this.bookingReferencesService.getBookingReferences(
|
||||||
|
bookingUid,
|
||||||
|
userId,
|
||||||
|
filter
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: bookingReferences,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+4
-4
@@ -385,7 +385,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
|||||||
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -430,7 +430,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
|||||||
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -459,7 +459,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
|||||||
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -502,7 +502,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
|||||||
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
expect(data.rating).toEqual(bookingInThePast.rating);
|
expect(data.rating).toEqual(bookingInThePast.rating);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -642,7 +642,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
await bookingsRepositoryFixture.deleteById(mockBooking.id);
|
await bookingsRepositoryFixture.deleteById(mockBooking.id);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsIn, IsOptional } from "class-validator";
|
||||||
|
|
||||||
|
export const BookingReferences = [
|
||||||
|
"google_calendar",
|
||||||
|
"office365_calendar",
|
||||||
|
"daily_video",
|
||||||
|
"google_video",
|
||||||
|
"office365_video",
|
||||||
|
"zoom_video",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export class BookingReferencesFilterInput_2024_08_13 {
|
||||||
|
@ApiProperty({
|
||||||
|
description: "Filter booking references by type",
|
||||||
|
required: false,
|
||||||
|
enum: BookingReferences,
|
||||||
|
example: "google_calendar",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(BookingReferences)
|
||||||
|
type?: (typeof BookingReferences)[number];
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsEnum, IsNumber, IsString } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class BookingReference {
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The type of the booking reference",
|
||||||
|
})
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The event uid of the booking",
|
||||||
|
})
|
||||||
|
eventUid!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The id of the calendar the event is created in",
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
destinationCalendarId!: string | null;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The id of the booking reference",
|
||||||
|
})
|
||||||
|
id!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BookingReferencesOutput_2024_08_13 {
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The status of the request, always 'success' for successful responses",
|
||||||
|
example: SUCCESS_STATUS,
|
||||||
|
})
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: "Booking References",
|
||||||
|
type: [BookingReference],
|
||||||
|
})
|
||||||
|
data!: BookingReference[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||||
|
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
|
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
|
||||||
|
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
||||||
|
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BookingReferencesService_2024_08_13 {
|
||||||
|
constructor(
|
||||||
|
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||||
|
private readonly bookingReferencesRepository: BookingReferencesRepository_2024_08_13,
|
||||||
|
private readonly outputBookingReferencesService: OutputBookingReferencesService_2024_08_13
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getBookingReferences(
|
||||||
|
bookingUid: string,
|
||||||
|
userId: number,
|
||||||
|
filter?: BookingReferencesFilterInput_2024_08_13
|
||||||
|
) {
|
||||||
|
const booking = await this.bookingsRepository.getByUidWithUser(bookingUid);
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (booking.user?.id !== userId) {
|
||||||
|
throw new BadRequestException(`Booking with uid ${bookingUid} does not belong to user`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookingReferences = await this.bookingReferencesRepository.getBookingReferences(booking.id, filter);
|
||||||
|
|
||||||
|
return this.outputBookingReferencesService.getOutputBookingReferences(bookingReferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrgBookingReferences(bookingUid: string, filter?: BookingReferencesFilterInput_2024_08_13) {
|
||||||
|
const booking = await this.bookingsRepository.getByUidWithUser(bookingUid);
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookingReferences = await this.bookingReferencesRepository.getBookingReferences(booking.id, filter);
|
||||||
|
|
||||||
|
return this.outputBookingReferencesService.getOutputBookingReferences(bookingReferences);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.
|
|||||||
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 { BillingService } from "@/modules/billing/services/billing.service";
|
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||||
|
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
@@ -78,6 +79,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
private readonly bookingSeatRepository: BookingSeatRepository,
|
private readonly bookingSeatRepository: BookingSeatRepository,
|
||||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||||
private readonly prismaReadService: PrismaReadService,
|
private readonly prismaReadService: PrismaReadService,
|
||||||
|
private readonly kyselyReadService: KyselyReadService,
|
||||||
private readonly billingService: BillingService,
|
private readonly billingService: BillingService,
|
||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
@@ -374,7 +376,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
queryParams.attendeeEmail = await this.getAttendeeEmail(queryParams.attendeeEmail, user);
|
queryParams.attendeeEmail = await this.getAttendeeEmail(queryParams.attendeeEmail, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({
|
const fetchedBookings: { bookings: { id: number }[]; totalCount: number } = await getAllUserBookings({
|
||||||
bookingListingByStatus: queryParams.status || [],
|
bookingListingByStatus: queryParams.status || [],
|
||||||
skip: queryParams.skip ?? 0,
|
skip: queryParams.skip ?? 0,
|
||||||
take: queryParams.take ?? 100,
|
take: queryParams.take ?? 100,
|
||||||
@@ -385,6 +387,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
ctx: {
|
ctx: {
|
||||||
user,
|
user,
|
||||||
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
|
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
|
||||||
|
kysely: this.kyselyReadService.kysely,
|
||||||
},
|
},
|
||||||
sort: this.inputService.transformGetBookingsSort(queryParams),
|
sort: this.inputService.transformGetBookingsSort(queryParams),
|
||||||
});
|
});
|
||||||
@@ -429,7 +432,29 @@ export class BookingsService_2024_08_13 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return formattedBookings;
|
const skip = Math.abs(queryParams?.skip ?? 0);
|
||||||
|
const take = Math.abs(queryParams?.take ?? 100);
|
||||||
|
const itemsPerPage = take;
|
||||||
|
const totalPages = itemsPerPage !== 0 ? Math.ceil(fetchedBookings.totalCount / itemsPerPage) : 0;
|
||||||
|
const currentPage = Math.floor(skip / itemsPerPage) + 1;
|
||||||
|
const hasNextPage = skip + itemsPerPage < fetchedBookings.totalCount;
|
||||||
|
const hasPreviousPage = skip > 0;
|
||||||
|
return {
|
||||||
|
bookings: formattedBookings,
|
||||||
|
pagination: {
|
||||||
|
totalItems: fetchedBookings.totalCount,
|
||||||
|
// clamp remainingItems between 0 and totalCount
|
||||||
|
remainingItems: Math.min(
|
||||||
|
Math.max(fetchedBookings.totalCount - (skip + take), 0),
|
||||||
|
fetchedBookings.totalCount
|
||||||
|
),
|
||||||
|
itemsPerPage: itemsPerPage,
|
||||||
|
currentPage: currentPage,
|
||||||
|
totalPages: totalPages,
|
||||||
|
hasNextPage: hasNextPage,
|
||||||
|
hasPreviousPage: hasPreviousPage,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAttendeeEmail(queryParamsAttendeeEmail: string, user: { id: number }) {
|
async getAttendeeEmail(queryParamsAttendeeEmail: string, user: { id: number }) {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
interface IBookingReference {
|
||||||
|
type: string;
|
||||||
|
uid: string;
|
||||||
|
id: number;
|
||||||
|
externalCalendarId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OutputBookingReferencesService_2024_08_13 {
|
||||||
|
getOutputBookingReferences(bookingReferences: IBookingReference[]) {
|
||||||
|
return bookingReferences.map((bookingReference) => ({
|
||||||
|
type: bookingReference.type,
|
||||||
|
eventUid: bookingReference.uid,
|
||||||
|
id: bookingReference.id,
|
||||||
|
destinationCalendarId: bookingReference?.externalCalendarId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
RecurringBookingOutput_2024_08_13,
|
RecurringBookingOutput_2024_08_13,
|
||||||
SeatedAttendee,
|
SeatedAttendee,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import { Booking, BookingSeat } from "@calcom/prisma/client";
|
import { Booking, BookingReference, BookingSeat } from "@calcom/prisma/client";
|
||||||
|
|
||||||
export const bookingResponsesSchema = z
|
export const bookingResponsesSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_
|
|||||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import { ConferencingModule } from "@/modules/conferencing/conferencing.module";
|
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
@@ -19,7 +18,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule, ConferencingModule],
|
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule],
|
||||||
providers: [
|
providers: [
|
||||||
EventTypesRepository_2024_06_14,
|
EventTypesRepository_2024_06_14,
|
||||||
EventTypesService_2024_06_14,
|
EventTypesService_2024_06_14,
|
||||||
|
|||||||
+29
-11
@@ -5,6 +5,9 @@ import { ConferencingService } from "@/modules/conferencing/services/conferencin
|
|||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Injectable, BadRequestException } from "@nestjs/common";
|
import { Injectable, BadRequestException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { CONFERENCING_APPS } from "@calcom/platform-constants";
|
||||||
|
import { getUsersCredentials } from "@calcom/platform-libraries";
|
||||||
|
import { getApps } from "@calcom/platform-libraries/app-store";
|
||||||
import {
|
import {
|
||||||
transformBookingFieldsApiToInternal,
|
transformBookingFieldsApiToInternal,
|
||||||
transformLocationsApiToInternal,
|
transformLocationsApiToInternal,
|
||||||
@@ -50,8 +53,7 @@ interface ValidationContext {
|
|||||||
export class InputEventTypesService_2024_06_14 {
|
export class InputEventTypesService_2024_06_14 {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||||
private readonly calendarsService: CalendarsService,
|
private readonly calendarsService: CalendarsService
|
||||||
private readonly conferencingService: ConferencingService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async transformAndValidateCreateEventTypeInput(
|
async transformAndValidateCreateEventTypeInput(
|
||||||
@@ -104,13 +106,6 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
transformInputCreateEventType(inputEventType: CreateEventTypeInput_2024_06_14) {
|
transformInputCreateEventType(inputEventType: CreateEventTypeInput_2024_06_14) {
|
||||||
const defaultLocations: CreateEventTypeInput_2024_06_14["locations"] = [
|
|
||||||
{
|
|
||||||
type: "integration",
|
|
||||||
integration: "cal-video",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
lengthInMinutes,
|
lengthInMinutes,
|
||||||
lengthInMinutesOptions,
|
lengthInMinutesOptions,
|
||||||
@@ -130,10 +125,11 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
} = inputEventType;
|
} = inputEventType;
|
||||||
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
||||||
|
|
||||||
|
const locationsTransformed = locations?.length ? this.transformInputLocations(locations) : undefined;
|
||||||
const eventType = {
|
const eventType = {
|
||||||
...rest,
|
...rest,
|
||||||
length: lengthInMinutes,
|
length: lengthInMinutes,
|
||||||
locations: this.transformInputLocations(locations || defaultLocations),
|
locations: locationsTransformed,
|
||||||
bookingFields: this.transformInputBookingFields(bookingFields),
|
bookingFields: this.transformInputBookingFields(bookingFields),
|
||||||
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||||
durationLimits: bookingLimitsDuration
|
durationLimits: bookingLimitsDuration
|
||||||
@@ -463,10 +459,32 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
if (location.type === "integration") {
|
if (location.type === "integration") {
|
||||||
// cal-video is global, so we can skip this check
|
// cal-video is global, so we can skip this check
|
||||||
if (location.integration !== "cal-video") {
|
if (location.integration !== "cal-video") {
|
||||||
await this.conferencingService.checkAppIsValidAndConnected(user, location.integration);
|
await this.checkAppIsValidAndConnected(user, location.integration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) ?? []
|
}) ?? []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkAppIsValidAndConnected(user: UserWithProfile, appSlug: string) {
|
||||||
|
const conferencingApps = ["google-meet", "office365-video", "zoom"];
|
||||||
|
if (!conferencingApps.includes(appSlug)) {
|
||||||
|
throw new BadRequestException("Invalid app, available apps are: ", conferencingApps.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appSlug === "office365-video") {
|
||||||
|
appSlug = "msteams";
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = await getUsersCredentials(user);
|
||||||
|
|
||||||
|
const foundApp = getApps(credentials, true).filter((app) => app.slug === appSlug)[0];
|
||||||
|
|
||||||
|
const appLocation = foundApp?.appData?.location;
|
||||||
|
|
||||||
|
if (!foundApp || !appLocation) {
|
||||||
|
throw new BadRequestException(`${appSlug} not connected.`);
|
||||||
|
}
|
||||||
|
return foundApp.credential;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ if (process.env.SENTRY_DSN) {
|
|||||||
// Ensure to call this before requiring any other modules!
|
// Ensure to call this before requiring any other modules!
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: getEnv("SENTRY_DSN"),
|
dsn: getEnv("SENTRY_DSN"),
|
||||||
integrations: [nodeProfilingIntegration()],
|
integrations: [nodeProfilingIntegration(), Sentry.prismaIntegration()],
|
||||||
// Performance Monitoring
|
// Performance Monitoring
|
||||||
tracesSampleRate: getEnv("SENTRY_TRACES_SAMPLE_RATE") ?? 1.0, // Capture 100% of the transactions
|
tracesSampleRate: getEnv("SENTRY_TRACES_SAMPLE_RATE") ?? 1.0, // Capture 100% of the transactions
|
||||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { RateLimitType } from "@/lib/throttler-guard";
|
||||||
|
import { Reflector } from "@nestjs/core";
|
||||||
|
|
||||||
|
export const Throttle = Reflector.createDecorator<RateLimitType>();
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getEnv } from "@/env";
|
import { getEnv } from "@/env";
|
||||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||||
|
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis";
|
import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis";
|
||||||
import { Inject, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
|
import { Inject, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
|
||||||
@@ -22,7 +23,7 @@ const rateLimitSchema = z.object({
|
|||||||
ttl: z.number(),
|
ttl: z.number(),
|
||||||
blockDuration: z.number(),
|
blockDuration: z.number(),
|
||||||
});
|
});
|
||||||
type RateLimitType = z.infer<typeof rateLimitSchema>;
|
export type RateLimitType = z.infer<typeof rateLimitSchema>;
|
||||||
const rateLimitsSchema = z.array(rateLimitSchema);
|
const rateLimitsSchema = z.array(rateLimitSchema);
|
||||||
|
|
||||||
const sixtySecondsMs = 60 * 1000;
|
const sixtySecondsMs = 60 * 1000;
|
||||||
@@ -52,7 +53,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
|||||||
|
|
||||||
protected async handleRequest(requestProps: ThrottlerRequest): Promise<boolean> {
|
protected async handleRequest(requestProps: ThrottlerRequest): Promise<boolean> {
|
||||||
const { context } = requestProps;
|
const { context } = requestProps;
|
||||||
|
const throttleOptions = this.reflector.get(Throttle, context.getHandler());
|
||||||
const request = context.switchToHttp().getRequest<Request>();
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
const IP = request?.headers?.["cf-connecting-ip"] ?? request?.headers?.["CF-Connecting-IP"] ?? request.ip;
|
const IP = request?.headers?.["cf-connecting-ip"] ?? request?.headers?.["CF-Connecting-IP"] ?? request.ip;
|
||||||
const response = context.switchToHttp().getResponse<Response>();
|
const response = context.switchToHttp().getResponse<Response>();
|
||||||
@@ -63,6 +64,10 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
|||||||
)}", OAuth client ID "${request.get(X_CAL_CLIENT_ID)}" and IP "${IP}"`
|
)}", OAuth client ID "${request.get(X_CAL_CLIENT_ID)}" and IP "${IP}"`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (throttleOptions) {
|
||||||
|
return this.handleApiEndpointThrottle(tracker, throttleOptions, response);
|
||||||
|
}
|
||||||
|
|
||||||
if (tracker.startsWith("api_key_")) {
|
if (tracker.startsWith("api_key_")) {
|
||||||
return this.handleApiKeyRequest(tracker, response);
|
return this.handleApiKeyRequest(tracker, response);
|
||||||
} else {
|
} else {
|
||||||
@@ -70,6 +75,15 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleApiEndpointThrottle(tracker: string, options: RateLimitType, response: Response) {
|
||||||
|
const { isBlocked } = await this.incrementRateLimit(`${tracker}_${options.name}`, options, response);
|
||||||
|
if (isBlocked) {
|
||||||
|
throw new ThrottlerException("CustomThrottlerGuard - Too many requests. Please try again later.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private async handleApiKeyRequest(tracker: string, response: Response): Promise<boolean> {
|
private async handleApiKeyRequest(tracker: string, response: Response): Promise<boolean> {
|
||||||
const rateLimits = await this.getRateLimitsForApiKeyTracker(tracker);
|
const rateLimits = await this.getRateLimitsForApiKeyTracker(tracker);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
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";
|
||||||
@@ -5,6 +6,7 @@ import { BillingConfigService } from "@/modules/billing/services/billing.config.
|
|||||||
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";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
|
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 { StripeModule } from "@/modules/stripe/stripe.module";
|
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||||
@@ -33,6 +35,8 @@ import { Module } from "@nestjs/common";
|
|||||||
BillingRepository,
|
BillingRepository,
|
||||||
BillingProcessor,
|
BillingProcessor,
|
||||||
ManagedOrganizationsBillingService,
|
ManagedOrganizationsBillingService,
|
||||||
|
OAuthClientRepository,
|
||||||
|
BookingsRepository_2024_08_13,
|
||||||
],
|
],
|
||||||
exports: [BillingService, BillingRepository, ManagedOrganizationsBillingService],
|
exports: [BillingService, BillingRepository, ManagedOrganizationsBillingService],
|
||||||
controllers: [BillingController],
|
controllers: [BillingController],
|
||||||
|
|||||||
@@ -15,12 +15,21 @@ export class BillingRepository {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async getBillingForTeamBySubscriptionId(subscriptionId: string) {
|
||||||
|
return this.dbRead.prisma.platformBilling.findFirst({
|
||||||
|
where: {
|
||||||
|
subscriptionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async updateTeamBilling(
|
async updateTeamBilling(
|
||||||
teamId: number,
|
teamId: number,
|
||||||
billingStart: number,
|
billingStart: number,
|
||||||
billingEnd: number,
|
billingEnd: number,
|
||||||
plan: PlatformPlan,
|
plan: PlatformPlan,
|
||||||
subscriptionId?: string
|
subscriptionId?: string,
|
||||||
|
priceId?: string
|
||||||
) {
|
) {
|
||||||
return this.dbWrite.prisma.platformBilling.update({
|
return this.dbWrite.prisma.platformBilling.update({
|
||||||
where: {
|
where: {
|
||||||
@@ -32,6 +41,7 @@ export class BillingRepository {
|
|||||||
subscriptionId,
|
subscriptionId,
|
||||||
plan: plan.toString(),
|
plan: plan.toString(),
|
||||||
overdue: false,
|
overdue: false,
|
||||||
|
priceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ export class BillingController {
|
|||||||
case "customer.subscription.deleted":
|
case "customer.subscription.deleted":
|
||||||
await this.billingService.handleStripeSubscriptionDeleted(event);
|
await this.billingService.handleStripeSubscriptionDeleted(event);
|
||||||
break;
|
break;
|
||||||
|
case "invoice.created":
|
||||||
|
await this.billingService.handleStripeSubscriptionForActiveManagedUsers(event);
|
||||||
|
break;
|
||||||
case "invoice.payment_failed":
|
case "invoice.payment_failed":
|
||||||
await this.billingService.handleStripePaymentFailed(event);
|
await this.billingService.handleStripePaymentFailed(event);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { AppConfig } from "@/config/type";
|
import { AppConfig } from "@/config/type";
|
||||||
|
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 { 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 { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { InjectQueue } from "@nestjs/bull";
|
import { InjectQueue } from "@nestjs/bull";
|
||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
@@ -30,6 +33,9 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
private readonly billingRepository: BillingRepository,
|
private 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 oAuthClientRepository: OAuthClientRepository,
|
||||||
|
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||||
@InjectQueue(BILLING_QUEUE) private readonly billingQueue: Queue
|
@InjectQueue(BILLING_QUEUE) private readonly billingQueue: Queue
|
||||||
) {
|
) {
|
||||||
this.webAppUrl = this.configService.get("app.baseUrl", { infer: true }) ?? "https://app.cal.com";
|
this.webAppUrl = this.configService.get("app.baseUrl", { infer: true }) ?? "https://app.cal.com";
|
||||||
@@ -119,7 +125,7 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSubscriptionForTeam(teamId: number, subscriptionId: string, plan: PlatformPlan) {
|
async setPerBookingSubscriptionForTeam(teamId: number, subscriptionId: string, plan: PlatformPlan) {
|
||||||
const billingCycleStart = DateTime.now().get("day");
|
const billingCycleStart = DateTime.now().get("day");
|
||||||
const billingCycleEnd = DateTime.now().plus({ month: 1 }).get("day");
|
const billingCycleEnd = DateTime.now().plus({ month: 1 }).get("day");
|
||||||
|
|
||||||
@@ -132,6 +138,25 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setPerActiveUserSubscriptionForTeam(
|
||||||
|
teamId: number,
|
||||||
|
subscriptionId: string,
|
||||||
|
plan: PlatformPlan,
|
||||||
|
priceId: string
|
||||||
|
) {
|
||||||
|
const billingCycleStart = DateTime.now().get("day");
|
||||||
|
const billingCycleEnd = DateTime.now().plus({ month: 1 }).get("day");
|
||||||
|
|
||||||
|
return this.billingRepository.updateTeamBilling(
|
||||||
|
teamId,
|
||||||
|
billingCycleStart,
|
||||||
|
billingCycleEnd,
|
||||||
|
plan,
|
||||||
|
subscriptionId,
|
||||||
|
priceId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async handleStripeSubscriptionDeleted(event: Stripe.Event) {
|
async handleStripeSubscriptionDeleted(event: Stripe.Event) {
|
||||||
const subscription = event.data.object as Stripe.Subscription;
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
const teamId = subscription?.metadata?.teamId;
|
const teamId = subscription?.metadata?.teamId;
|
||||||
@@ -204,9 +229,19 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
this.logger.log("Webhook received but not pertaining to Platform, discarding.");
|
this.logger.log("Webhook received but not pertaining to Platform, discarding.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const isPriceIdPresent = Boolean(checkoutSession.metadata?.priceId);
|
||||||
|
|
||||||
if (checkoutSession.mode === "subscription") {
|
if (checkoutSession.mode === "subscription" && isPriceIdPresent) {
|
||||||
await this.setSubscriptionForTeam(
|
await this.setPerActiveUserSubscriptionForTeam(
|
||||||
|
teamId,
|
||||||
|
checkoutSession.subscription as string,
|
||||||
|
PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan],
|
||||||
|
checkoutSession.metadata?.priceId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkoutSession.mode === "subscription" && !isPriceIdPresent) {
|
||||||
|
await this.setPerBookingSubscriptionForTeam(
|
||||||
teamId,
|
teamId,
|
||||||
checkoutSession.subscription as string,
|
checkoutSession.subscription as string,
|
||||||
PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan]
|
PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan]
|
||||||
@@ -220,6 +255,67 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event) {
|
||||||
|
const invoice = event.data.object as Stripe.Invoice;
|
||||||
|
const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
|
||||||
|
|
||||||
|
if (!subscriptionId) {
|
||||||
|
throw new NotFoundException("No subscription found for team");
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamWithBilling = await this.billingRepository.getBillingForTeamBySubscriptionId(subscriptionId);
|
||||||
|
|
||||||
|
if (teamWithBilling?.plan === "PER_ACTIVE_USER") {
|
||||||
|
const activeManagedUsersCount = await this.getActiveManagedUsersCount(
|
||||||
|
subscriptionId,
|
||||||
|
new Date(invoice.period_start * 1000),
|
||||||
|
new Date(invoice.period_end * 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.stripeService.getStripe().subscriptions.update(subscriptionId, {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
quantity: activeManagedUsersCount > 0 ? activeManagedUsersCount : 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActiveManagedUsersCount(subscriptionId: string, invoiceStart: Date, invoiceEnd: Date) {
|
||||||
|
const managedUsersEmails = await this.usersRepository.getOrgsManagedUserEmailsBySubscriptionId(
|
||||||
|
subscriptionId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!managedUsersEmails) return 0;
|
||||||
|
|
||||||
|
if (!invoiceStart || !invoiceEnd) {
|
||||||
|
this.logger.log("Invoice period start or end date is null");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeManagedUserEmailsAsHost = await this.usersRepository.getActiveManagedUsersAsHost(
|
||||||
|
subscriptionId,
|
||||||
|
invoiceStart,
|
||||||
|
invoiceEnd
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeHostEmails = activeManagedUserEmailsAsHost.map((email) => email.email);
|
||||||
|
const notActiveHostEmails = managedUsersEmails
|
||||||
|
.filter((email) => !activeHostEmails.includes(email.email))
|
||||||
|
.map((email) => email.email);
|
||||||
|
|
||||||
|
if (notActiveHostEmails.length === 0) return activeManagedUserEmailsAsHost.length;
|
||||||
|
|
||||||
|
const activeManagedUserEmailsAsAttendee = await this.usersRepository.getActiveManagedUsersAsAttendee(
|
||||||
|
notActiveHostEmails,
|
||||||
|
invoiceStart,
|
||||||
|
invoiceEnd
|
||||||
|
);
|
||||||
|
|
||||||
|
return activeManagedUserEmailsAsAttendee.length + activeManagedUserEmailsAsHost.length;
|
||||||
|
}
|
||||||
|
|
||||||
async updateStripeSubscriptionForTeam(teamId: number, plan: PlatformPlan) {
|
async updateStripeSubscriptionForTeam(teamId: number, plan: PlatformPlan) {
|
||||||
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||||
|
|
||||||
@@ -263,7 +359,7 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
proration_behavior: "create_prorations",
|
proration_behavior: "create_prorations",
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.setSubscriptionForTeam(
|
await this.setPerBookingSubscriptionForTeam(
|
||||||
teamId,
|
teamId,
|
||||||
teamWithBilling?.platformBilling?.subscriptionId,
|
teamWithBilling?.platformBilling?.subscriptionId,
|
||||||
PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan]
|
PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export enum PlatformPlan {
|
|||||||
ESSENTIALS = "ESSENTIALS",
|
ESSENTIALS = "ESSENTIALS",
|
||||||
SCALE = "SCALE",
|
SCALE = "SCALE",
|
||||||
ENTERPRISE = "ENTERPRISE",
|
ENTERPRISE = "ENTERPRISE",
|
||||||
|
PER_ACTIVE_USER = "PER_ACTIVE_USER",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlatformPlanType = "FREE" | "STARTER" | "ESSENTIALS" | "SCALE" | "ENTERPRISE";
|
export type PlatformPlanType = "FREE" | "STARTER" | "ESSENTIALS" | "SCALE" | "ENTERPRISE" | "PER_ACTIVE_USER";
|
||||||
@@ -11,6 +11,7 @@ import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/
|
|||||||
import { RouterModule } from "@/modules/router/router.module";
|
import { RouterModule } from "@/modules/router/router.module";
|
||||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||||
|
import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module";
|
||||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
|
|||||||
OrganizationsUsersBookingsModule,
|
OrganizationsUsersBookingsModule,
|
||||||
OrganizationsBookingsModule,
|
OrganizationsBookingsModule,
|
||||||
OrganizationsRoutingFormsModule,
|
OrganizationsRoutingFormsModule,
|
||||||
|
VerifiedResourcesModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { OnModuleDestroy } from "@nestjs/common";
|
||||||
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { DeduplicateJoinsPlugin, Kysely, ParseJSONResultsPlugin, PostgresDialect } from "kysely";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
|
import type { DB } from "@calcom/kysely/types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class KyselyReadService implements OnModuleDestroy {
|
||||||
|
private logger = new Logger("KyselyReadService");
|
||||||
|
|
||||||
|
public kysely: Kysely<DB>;
|
||||||
|
|
||||||
|
constructor(readonly configService: ConfigService) {
|
||||||
|
const dbUrl = configService.get("db.readUrl", { infer: true });
|
||||||
|
const pool = new Pool({ connectionString: dbUrl });
|
||||||
|
|
||||||
|
// 3. Create the Dialect, passing the configured pool instance
|
||||||
|
const dialect = new PostgresDialect({
|
||||||
|
pool: pool, // Use the pool instance created above
|
||||||
|
});
|
||||||
|
this.kysely = new Kysely<DB>({
|
||||||
|
dialect,
|
||||||
|
plugins: [new ParseJSONResultsPlugin(), new DeduplicateJoinsPlugin()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
try {
|
||||||
|
await this.kysely.destroy();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { OnModuleDestroy } from "@nestjs/common";
|
||||||
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { Kysely, ParseJSONResultsPlugin, PostgresDialect, DeduplicateJoinsPlugin } from "kysely";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
|
import type { DB } from "@calcom/kysely/types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class KyselyWriteService implements OnModuleDestroy {
|
||||||
|
private logger = new Logger("KyselyWriteService");
|
||||||
|
|
||||||
|
public kysely: Kysely<DB>;
|
||||||
|
|
||||||
|
constructor(readonly configService: ConfigService) {
|
||||||
|
const dbUrl = configService.get("db.writeUrl", { infer: true });
|
||||||
|
const pool = new Pool({ connectionString: dbUrl });
|
||||||
|
|
||||||
|
// 3. Create the Dialect, passing the configured pool instance
|
||||||
|
const dialect = new PostgresDialect({
|
||||||
|
pool: pool, // Use the pool instance created above
|
||||||
|
});
|
||||||
|
this.kysely = new Kysely<DB>({
|
||||||
|
dialect,
|
||||||
|
plugins: [new ParseJSONResultsPlugin(), new DeduplicateJoinsPlugin()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
try {
|
||||||
|
await this.kysely.destroy();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
||||||
|
import { KyselyWriteService } from "@/modules/kysely/kysely-write.service";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [KyselyReadService, KyselyWriteService],
|
||||||
|
exports: [KyselyReadService, KyselyWriteService],
|
||||||
|
})
|
||||||
|
export class KyselyModule {}
|
||||||
@@ -63,6 +63,7 @@ export class MembershipsRepository {
|
|||||||
async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) {
|
async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) {
|
||||||
const membership = await this.dbRead.prisma.membership.create({
|
const membership = await this.dbRead.prisma.membership.create({
|
||||||
data: {
|
data: {
|
||||||
|
createdAt: new Date(),
|
||||||
role,
|
role,
|
||||||
teamId,
|
teamId,
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export class OrganizationsBookingsController {
|
|||||||
): Promise<GetBookingsOutput_2024_08_13> {
|
): Promise<GetBookingsOutput_2024_08_13> {
|
||||||
const { userIds, ...restParams } = queryParams;
|
const { userIds, ...restParams } = queryParams;
|
||||||
|
|
||||||
const bookings = await this.bookingsService.getBookings(
|
const { bookings, pagination } = await this.bookingsService.getBookings(
|
||||||
{ ...restParams },
|
{ ...restParams },
|
||||||
{ email: user.email, id: user.id, orgId },
|
{ email: user.email, id: user.id, orgId },
|
||||||
userIds
|
userIds
|
||||||
@@ -54,6 +54,7 @@ export class OrganizationsBookingsController {
|
|||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: bookings,
|
data: bookings,
|
||||||
|
pagination,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
||||||
|
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
|
||||||
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
||||||
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
|
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
|
||||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||||
@@ -27,7 +28,8 @@ export class InputOrganizationsEventTypesService {
|
|||||||
private readonly teamsRepository: TeamsRepository,
|
private readonly teamsRepository: TeamsRepository,
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
|
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
|
||||||
private readonly conferencingService: OrganizationsConferencingService
|
private readonly conferencingService: OrganizationsConferencingService,
|
||||||
|
private readonly conferencingRepository: ConferencingRepository
|
||||||
) {}
|
) {}
|
||||||
async transformAndValidateCreateTeamEventTypeInput(
|
async transformAndValidateCreateTeamEventTypeInput(
|
||||||
userId: number,
|
userId: number,
|
||||||
@@ -306,6 +308,22 @@ export class InputOrganizationsEventTypesService {
|
|||||||
}) ?? []
|
}) ?? []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkAppIsValidAndConnected(teamId: number, app: string) {
|
||||||
|
const conferencingApps = ["google-meet", "office365-video", "zoom"];
|
||||||
|
if (!conferencingApps.includes(app)) {
|
||||||
|
throw new BadRequestException("Invalid app, available apps are: ", conferencingApps.join(", "));
|
||||||
|
}
|
||||||
|
if (app === "office365-video") {
|
||||||
|
app = "msteams";
|
||||||
|
}
|
||||||
|
const credential = await this.conferencingRepository.findTeamConferencingApp(teamId, app);
|
||||||
|
|
||||||
|
if (!credential) {
|
||||||
|
throw new BadRequestException(`${app} not connected.`);
|
||||||
|
}
|
||||||
|
return credential;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPriorityValue(priority: keyof typeof HostPriority): number {
|
function getPriorityValue(priority: keyof typeof HostPriority): number {
|
||||||
|
|||||||
+196
-10
@@ -6,6 +6,7 @@ import { CreateOrgMembershipOutput } from "@/modules/organizations/memberships/o
|
|||||||
import { DeleteOrgMembership } from "@/modules/organizations/memberships/outputs/delete-membership.output";
|
import { DeleteOrgMembership } from "@/modules/organizations/memberships/outputs/delete-membership.output";
|
||||||
import { GetAllOrgMemberships } from "@/modules/organizations/memberships/outputs/get-all-memberships.output";
|
import { GetAllOrgMemberships } from "@/modules/organizations/memberships/outputs/get-all-memberships.output";
|
||||||
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
|
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
|
||||||
|
import { OrgUserAttribute } from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
|
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
||||||
@@ -14,17 +15,16 @@ import { UsersModule } from "@/modules/users/users.module";
|
|||||||
import { INestApplication } from "@nestjs/common";
|
import { INestApplication } from "@nestjs/common";
|
||||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
import { Test } from "@nestjs/testing";
|
import { Test } from "@nestjs/testing";
|
||||||
import { User } from "@prisma/client";
|
import { Attribute, AttributeOption, User } from "@prisma/client";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
|
import { AttributeRepositoryFixture } from "test/fixtures/repository/attributes.repository.fixture";
|
||||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { 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 { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
|
||||||
import { Membership, Team } from "@calcom/prisma/client";
|
import { Membership, Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
describe("Organizations Memberships Endpoints", () => {
|
describe("Organizations Memberships Endpoints", () => {
|
||||||
@@ -33,7 +33,8 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
|
|
||||||
let userRepositoryFixture: UserRepositoryFixture;
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
let attributesRepositoryFixture: AttributeRepositoryFixture;
|
||||||
|
|
||||||
let org: Team;
|
let org: Team;
|
||||||
let membership: Membership;
|
let membership: Membership;
|
||||||
@@ -49,6 +50,17 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
|
|
||||||
let userToInviteViaApi: User;
|
let userToInviteViaApi: User;
|
||||||
|
|
||||||
|
let textAttribute: Attribute;
|
||||||
|
let multiSelectAttribute: Attribute;
|
||||||
|
let numberAttribute: Attribute;
|
||||||
|
let singleSelectAttribute: Attribute;
|
||||||
|
|
||||||
|
let textAttributeOption: AttributeOption;
|
||||||
|
let multiSelectAttributeOption: AttributeOption;
|
||||||
|
let multiSelectAttributeOption2: AttributeOption;
|
||||||
|
let numberAttributeOption: AttributeOption;
|
||||||
|
let singleSelectAttributeOption: AttributeOption;
|
||||||
|
|
||||||
const metadata = {
|
const metadata = {
|
||||||
some: "key",
|
some: "key",
|
||||||
};
|
};
|
||||||
@@ -64,7 +76,8 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
|
|
||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
attributesRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
@@ -91,16 +104,42 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
membership = await membershipsRepositoryFixture.create({
|
await setupAttributes();
|
||||||
|
|
||||||
|
membership = await membershipRepositoryFixture.create({
|
||||||
role: "ADMIN",
|
role: "ADMIN",
|
||||||
user: { connect: { id: user.id } },
|
user: { connect: { id: user.id } },
|
||||||
team: { connect: { id: org.id } },
|
team: { connect: { id: org.id } },
|
||||||
});
|
});
|
||||||
|
|
||||||
membership2 = await membershipsRepositoryFixture.create({
|
membership2 = await membershipRepositoryFixture.create({
|
||||||
role: "MEMBER",
|
role: "MEMBER",
|
||||||
user: { connect: { id: user2.id } },
|
user: { connect: { id: user2.id } },
|
||||||
team: { connect: { id: org.id } },
|
team: { connect: { id: org.id } },
|
||||||
|
AttributeToUser: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
attributeOption: { connect: { id: textAttributeOption.id } },
|
||||||
|
weight: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeOption: { connect: { id: multiSelectAttributeOption.id } },
|
||||||
|
weight: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeOption: { connect: { id: multiSelectAttributeOption2.id } },
|
||||||
|
weight: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeOption: { connect: { id: numberAttributeOption.id } },
|
||||||
|
weight: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeOption: { connect: { id: singleSelectAttributeOption.id } },
|
||||||
|
weight: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
@@ -109,6 +148,87 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function setupAttributes() {
|
||||||
|
textAttribute = await attributesRepositoryFixture.create({
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
type: "TEXT",
|
||||||
|
name: "team",
|
||||||
|
slug: `team-${randomString()}`,
|
||||||
|
enabled: true,
|
||||||
|
usersCanEditRelation: false,
|
||||||
|
isWeightsEnabled: false,
|
||||||
|
isLocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
multiSelectAttribute = await attributesRepositoryFixture.create({
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
type: "MULTI_SELECT",
|
||||||
|
name: "skills",
|
||||||
|
slug: `skills-${randomString()}`,
|
||||||
|
enabled: true,
|
||||||
|
usersCanEditRelation: false,
|
||||||
|
isWeightsEnabled: false,
|
||||||
|
isLocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
numberAttribute = await attributesRepositoryFixture.create({
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
type: "NUMBER",
|
||||||
|
name: "age",
|
||||||
|
slug: `age-${randomString()}`,
|
||||||
|
enabled: true,
|
||||||
|
usersCanEditRelation: false,
|
||||||
|
isWeightsEnabled: false,
|
||||||
|
isLocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
singleSelectAttribute = await attributesRepositoryFixture.create({
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
type: "SINGLE_SELECT",
|
||||||
|
name: "frontend",
|
||||||
|
slug: `frontend-${randomString()}`,
|
||||||
|
enabled: true,
|
||||||
|
usersCanEditRelation: false,
|
||||||
|
isWeightsEnabled: false,
|
||||||
|
isLocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
textAttributeOption = await attributesRepositoryFixture.createOption({
|
||||||
|
attribute: { connect: { id: textAttribute.id } },
|
||||||
|
value: "coders",
|
||||||
|
slug: "coders",
|
||||||
|
isGroup: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
multiSelectAttributeOption = await attributesRepositoryFixture.createOption({
|
||||||
|
attribute: { connect: { id: multiSelectAttribute.id } },
|
||||||
|
value: "javascript",
|
||||||
|
slug: "javascript",
|
||||||
|
isGroup: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
multiSelectAttributeOption2 = await attributesRepositoryFixture.createOption({
|
||||||
|
attribute: { connect: { id: multiSelectAttribute.id } },
|
||||||
|
value: "typescript",
|
||||||
|
slug: "typescript",
|
||||||
|
isGroup: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
numberAttributeOption = await attributesRepositoryFixture.createOption({
|
||||||
|
attribute: { connect: { id: numberAttribute.id } },
|
||||||
|
value: "18",
|
||||||
|
slug: "18",
|
||||||
|
isGroup: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
singleSelectAttributeOption = await attributesRepositoryFixture.createOption({
|
||||||
|
attribute: { connect: { id: singleSelectAttribute.id } },
|
||||||
|
value: "yes",
|
||||||
|
slug: "yes",
|
||||||
|
isGroup: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it("should be defined", () => {
|
it("should be defined", () => {
|
||||||
expect(userRepositoryFixture).toBeDefined();
|
expect(userRepositoryFixture).toBeDefined();
|
||||||
expect(organizationsRepositoryFixture).toBeDefined();
|
expect(organizationsRepositoryFixture).toBeDefined();
|
||||||
@@ -139,9 +259,54 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
expect(responseBody.data[1].user.email).toEqual(user2.email);
|
expect(responseBody.data[1].user.email).toEqual(user2.email);
|
||||||
expect(responseBody.data[1].user.username).toEqual(user2.username);
|
expect(responseBody.data[1].user.username).toEqual(user2.username);
|
||||||
expect(responseBody.data[1].teamId).toEqual(org.id);
|
expect(responseBody.data[1].teamId).toEqual(org.id);
|
||||||
|
userHasCorrectAttributes(responseBody.data[1].attributes);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function userHasCorrectAttributes(attributes: OrgUserAttribute[]) {
|
||||||
|
expect(attributes.length).toEqual(4);
|
||||||
|
const responseNumberAttribute = attributes.find((attr) => attr.type === "number");
|
||||||
|
const responseSingleSelectAttribute = attributes.find((attr) => attr.type === "singleSelect");
|
||||||
|
const responseMultiSelectAttribute = attributes.find((attr) => attr.type === "multiSelect");
|
||||||
|
const responseTextAttribute = attributes.find((attr) => attr.type === "text");
|
||||||
|
expect(responseNumberAttribute).toEqual({
|
||||||
|
id: numberAttribute.id,
|
||||||
|
name: numberAttribute.name,
|
||||||
|
optionId: numberAttributeOption.id,
|
||||||
|
option: +numberAttributeOption.value,
|
||||||
|
type: "number",
|
||||||
|
});
|
||||||
|
expect(responseSingleSelectAttribute).toEqual({
|
||||||
|
id: singleSelectAttribute.id,
|
||||||
|
name: singleSelectAttribute.name,
|
||||||
|
optionId: singleSelectAttributeOption.id,
|
||||||
|
option: singleSelectAttributeOption.value,
|
||||||
|
type: "singleSelect",
|
||||||
|
});
|
||||||
|
expect(responseMultiSelectAttribute).toEqual({
|
||||||
|
id: multiSelectAttribute.id,
|
||||||
|
name: multiSelectAttribute.name,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
optionId: multiSelectAttributeOption2.id,
|
||||||
|
option: multiSelectAttributeOption2.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
optionId: multiSelectAttributeOption.id,
|
||||||
|
option: multiSelectAttributeOption.value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: "multiSelect",
|
||||||
|
});
|
||||||
|
expect(responseTextAttribute).toEqual({
|
||||||
|
id: textAttribute.id,
|
||||||
|
name: textAttribute.name,
|
||||||
|
optionId: textAttributeOption.id,
|
||||||
|
option: textAttributeOption.value,
|
||||||
|
type: "text",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it("should get all the memberships of the org paginated", async () => {
|
it("should get all the memberships of the org paginated", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.get(`/v2/organizations/${org.id}/memberships?skip=1&take=1`)
|
.get(`/v2/organizations/${org.id}/memberships?skip=1&take=1`)
|
||||||
@@ -182,6 +347,25 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should get the membership of the org", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/memberships/${membership2.id}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetOrgMembership = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data.id).toEqual(membership2.id);
|
||||||
|
expect(responseBody.data.userId).toEqual(user2.id);
|
||||||
|
expect(responseBody.data.role).toEqual("MEMBER");
|
||||||
|
expect(responseBody.data.user.bio).toEqual(bio);
|
||||||
|
expect(responseBody.data.user.metadata).toEqual(metadata);
|
||||||
|
expect(responseBody.data.user.email).toEqual(user2.email);
|
||||||
|
expect(responseBody.data.user.username).toEqual(user2.username);
|
||||||
|
expect(responseBody.data.teamId).toEqual(org.id);
|
||||||
|
userHasCorrectAttributes(responseBody.data.attributes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should create the membership of the org", async () => {
|
it("should create the membership of the org", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/memberships`)
|
.post(`/v2/organizations/${org.id}/memberships`)
|
||||||
@@ -259,7 +443,8 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
|
|
||||||
let userRepositoryFixture: UserRepositoryFixture;
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
let attributesRepositoryFixture: AttributeRepositoryFixture;
|
||||||
|
|
||||||
let org: Team;
|
let org: Team;
|
||||||
let membership: Membership;
|
let membership: Membership;
|
||||||
@@ -277,7 +462,8 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
|
|
||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
attributesRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
@@ -289,7 +475,7 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
membership = await membershipsRepositoryFixture.create({
|
membership = await membershipRepositoryFixture.create({
|
||||||
role: "MEMBER",
|
role: "MEMBER",
|
||||||
user: { connect: { id: user.id } },
|
user: { connect: { id: user.id } },
|
||||||
team: { connect: { id: org.id } },
|
team: { connect: { id: org.id } },
|
||||||
|
|||||||
+5
-9
@@ -20,7 +20,6 @@ import { GetAllOrgMemberships } from "@/modules/organizations/memberships/output
|
|||||||
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
|
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
|
||||||
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
|
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
|
||||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
@@ -36,7 +35,6 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
import { plainToClass } from "class-transformer";
|
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { SkipTakePagination } from "@calcom/platform-types";
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
@@ -70,9 +68,7 @@ export class OrganizationsMembershipsController {
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: memberships.map((membership) =>
|
data: memberships,
|
||||||
plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" })
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +84,7 @@ export class OrganizationsMembershipsController {
|
|||||||
const membership = await this.organizationsMembershipService.createOrgMembership(orgId, body);
|
const membership = await this.organizationsMembershipService.createOrgMembership(orgId, body);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: membership,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +101,7 @@ export class OrganizationsMembershipsController {
|
|||||||
const membership = await this.organizationsMembershipService.getOrgMembership(orgId, membershipId);
|
const membership = await this.organizationsMembershipService.getOrgMembership(orgId, membershipId);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: membership,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +118,7 @@ export class OrganizationsMembershipsController {
|
|||||||
const membership = await this.organizationsMembershipService.deleteOrgMembership(orgId, membershipId);
|
const membership = await this.organizationsMembershipService.deleteOrgMembership(orgId, membershipId);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: membership,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +140,7 @@ export class OrganizationsMembershipsController {
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: membership,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+59
-6
@@ -4,8 +4,31 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
|||||||
import { MembershipUserSelect } from "@/modules/teams/memberships/teams-memberships.repository";
|
import { MembershipUserSelect } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { UpdateOrgMembershipDto } from "./inputs/update-organization-membership.input";
|
import { UpdateOrgMembershipDto } from "./inputs/update-organization-membership.input";
|
||||||
|
|
||||||
|
export type DbOrgMembership = Awaited<ReturnType<OrganizationsMembershipRepository["findOrgMembership"]>>;
|
||||||
|
|
||||||
|
const attributeToUserSelect = {
|
||||||
|
createdByDSyncId: true,
|
||||||
|
weight: true,
|
||||||
|
attributeOption: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
value: true,
|
||||||
|
slug: true,
|
||||||
|
attribute: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
type: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Prisma.AttributeToUserSelect;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationsMembershipRepository {
|
export class OrganizationsMembershipRepository {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
@@ -16,7 +39,12 @@ export class OrganizationsMembershipRepository {
|
|||||||
id: membershipId,
|
id: membershipId,
|
||||||
teamId: organizationId,
|
teamId: organizationId,
|
||||||
},
|
},
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: {
|
||||||
|
user: { select: MembershipUserSelect },
|
||||||
|
AttributeToUser: {
|
||||||
|
select: attributeToUserSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +54,12 @@ export class OrganizationsMembershipRepository {
|
|||||||
teamId: organizationId,
|
teamId: organizationId,
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: {
|
||||||
|
user: { select: MembershipUserSelect },
|
||||||
|
AttributeToUser: {
|
||||||
|
select: attributeToUserSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +69,12 @@ export class OrganizationsMembershipRepository {
|
|||||||
id: membershipId,
|
id: membershipId,
|
||||||
teamId: organizationId,
|
teamId: organizationId,
|
||||||
},
|
},
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: {
|
||||||
|
user: { select: MembershipUserSelect },
|
||||||
|
AttributeToUser: {
|
||||||
|
select: attributeToUserSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,14 +83,24 @@ export class OrganizationsMembershipRepository {
|
|||||||
create: { ...data, teamId: organizationId },
|
create: { ...data, teamId: organizationId },
|
||||||
update: { role: data.role, accepted: data.accepted, disableImpersonation: data.disableImpersonation },
|
update: { role: data.role, accepted: data.accepted, disableImpersonation: data.disableImpersonation },
|
||||||
where: { userId_teamId: { userId: data.userId, teamId: organizationId } },
|
where: { userId_teamId: { userId: data.userId, teamId: organizationId } },
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: {
|
||||||
|
user: { select: MembershipUserSelect },
|
||||||
|
AttributeToUser: {
|
||||||
|
select: attributeToUserSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
||||||
return this.dbWrite.prisma.membership.update({
|
return this.dbWrite.prisma.membership.update({
|
||||||
data: { ...data },
|
data: { ...data },
|
||||||
where: { id: membershipId, teamId: organizationId },
|
where: { id: membershipId, teamId: organizationId },
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: {
|
||||||
|
user: { select: MembershipUserSelect },
|
||||||
|
AttributeToUser: {
|
||||||
|
select: attributeToUserSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +109,12 @@ export class OrganizationsMembershipRepository {
|
|||||||
where: {
|
where: {
|
||||||
teamId: organizationId,
|
teamId: organizationId,
|
||||||
},
|
},
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: {
|
||||||
|
user: { select: MembershipUserSelect },
|
||||||
|
AttributeToUser: {
|
||||||
|
select: attributeToUserSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
@@ -11,10 +11,10 @@ export class CreateOrgMembershipOutput {
|
|||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: TeamMembershipOutput,
|
type: OrganizationMembershipOutput,
|
||||||
})
|
})
|
||||||
@IsNotEmptyObject()
|
@IsNotEmptyObject()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => TeamMembershipOutput)
|
@Type(() => OrganizationMembershipOutput)
|
||||||
data!: TeamMembershipOutput;
|
data!: OrganizationMembershipOutput;
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsEnum } from "class-validator";
|
import { IsEnum } from "class-validator";
|
||||||
|
|
||||||
@@ -9,5 +9,5 @@ export class DeleteOrgMembership {
|
|||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
data!: TeamMembershipOutput;
|
data!: OrganizationMembershipOutput;
|
||||||
}
|
}
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested, IsArray } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested, IsArray } from "class-validator";
|
||||||
@@ -11,11 +11,11 @@ export class GetAllOrgMemberships {
|
|||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: TeamMembershipOutput,
|
type: OrganizationMembershipOutput,
|
||||||
})
|
})
|
||||||
@IsNotEmptyObject()
|
@IsNotEmptyObject()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => TeamMembershipOutput)
|
@Type(() => OrganizationMembershipOutput)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
data!: TeamMembershipOutput[];
|
data!: OrganizationMembershipOutput[];
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
@@ -11,10 +11,10 @@ export class GetOrgMembership {
|
|||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: TeamMembershipOutput,
|
type: OrganizationMembershipOutput,
|
||||||
})
|
})
|
||||||
@IsNotEmptyObject()
|
@IsNotEmptyObject()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => TeamMembershipOutput)
|
@Type(() => OrganizationMembershipOutput)
|
||||||
data!: TeamMembershipOutput;
|
data!: OrganizationMembershipOutput;
|
||||||
}
|
}
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
||||||
|
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { IsArray, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
class BaseAttribute {
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
name!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextAttribute extends BaseAttribute {
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
type!: "text";
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
option!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
optionId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NumberAttribute extends BaseAttribute {
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
type!: "number";
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
option!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
optionId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SingleSelectAttribute extends BaseAttribute {
|
||||||
|
@IsString()
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
type!: "singleSelect";
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
option!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
optionId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiSelectAttributeOption {
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
optionId!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
option!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MultiSelectAttribute extends BaseAttribute {
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
type!: "multiSelect";
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
options!: MultiSelectAttributeOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrgUserAttribute = TextAttribute | NumberAttribute | SingleSelectAttribute | MultiSelectAttribute;
|
||||||
|
|
||||||
|
@ApiExtraModels(BaseAttribute, TextAttribute, NumberAttribute, SingleSelectAttribute, MultiSelectAttribute)
|
||||||
|
export class OrganizationMembershipOutput extends TeamMembershipOutput {
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty({
|
||||||
|
required: true,
|
||||||
|
oneOf: [
|
||||||
|
{ $ref: getSchemaPath(TextAttribute) },
|
||||||
|
{ $ref: getSchemaPath(NumberAttribute) },
|
||||||
|
{ $ref: getSchemaPath(SingleSelectAttribute) },
|
||||||
|
{ $ref: getSchemaPath(MultiSelectAttribute) },
|
||||||
|
],
|
||||||
|
type: "array",
|
||||||
|
})
|
||||||
|
@Type(() => Object)
|
||||||
|
attributes!: OrgUserAttribute[];
|
||||||
|
}
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
@@ -11,10 +11,10 @@ export class UpdateOrgMembership {
|
|||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: TeamMembershipOutput,
|
type: OrganizationMembershipOutput,
|
||||||
})
|
})
|
||||||
@IsNotEmptyObject()
|
@IsNotEmptyObject()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => TeamMembershipOutput)
|
@Type(() => OrganizationMembershipOutput)
|
||||||
data!: TeamMembershipOutput;
|
data!: OrganizationMembershipOutput;
|
||||||
}
|
}
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
import { DbOrgMembership } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
|
import {
|
||||||
|
MultiSelectAttribute,
|
||||||
|
NumberAttribute,
|
||||||
|
OrganizationMembershipOutput,
|
||||||
|
OrgUserAttribute,
|
||||||
|
SingleSelectAttribute,
|
||||||
|
TextAttribute,
|
||||||
|
} from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
import { groupMembershipAttributes } from "@calcom/platform-libraries";
|
||||||
|
import type { GroupedAttribute } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrganizationsMembershipOutputService {
|
||||||
|
getOrgMembershipsOutput(memberships: NonNullable<DbOrgMembership>[]) {
|
||||||
|
return memberships.map((membership) => this.getOrgMembershipOutput(membership));
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrgMembershipOutput(organizationMembership: NonNullable<DbOrgMembership>) {
|
||||||
|
const { AttributeToUser, ...orgMembership } = organizationMembership;
|
||||||
|
|
||||||
|
const groupedMembershipAttributes: GroupedAttribute[] = groupMembershipAttributes(AttributeToUser);
|
||||||
|
const attributesOutput = this.getAttributesOutput(groupedMembershipAttributes);
|
||||||
|
|
||||||
|
return plainToClass(OrganizationMembershipOutput, { ...orgMembership, attributes: attributesOutput });
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAttributesOutput(attributes: GroupedAttribute[]): OrgUserAttribute[] {
|
||||||
|
return attributes.map((attribute) => {
|
||||||
|
switch (attribute.type) {
|
||||||
|
case "TEXT":
|
||||||
|
return this.getTextAttributeOutput(attribute);
|
||||||
|
case "NUMBER":
|
||||||
|
return this.getNumberAttributeOutput(attribute);
|
||||||
|
case "SINGLE_SELECT":
|
||||||
|
return this.getSingleSelectAttributeOutput(attribute);
|
||||||
|
case "MULTI_SELECT":
|
||||||
|
return this.getMultiSelectAttributeOutput(attribute);
|
||||||
|
default:
|
||||||
|
return this.getTextAttributeOutput(attribute);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTextAttributeOutput(attribute: GroupedAttribute): TextAttribute {
|
||||||
|
return {
|
||||||
|
id: attribute.id,
|
||||||
|
name: attribute.name,
|
||||||
|
optionId: attribute.options[0].id,
|
||||||
|
option: attribute.options[0].value,
|
||||||
|
type: "text",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNumberAttributeOutput(attribute: GroupedAttribute): NumberAttribute {
|
||||||
|
return {
|
||||||
|
id: attribute.id,
|
||||||
|
name: attribute.name,
|
||||||
|
optionId: attribute.options[0].id,
|
||||||
|
option: +attribute.options[0].value,
|
||||||
|
type: "number",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSingleSelectAttributeOutput(attribute: GroupedAttribute): SingleSelectAttribute {
|
||||||
|
return {
|
||||||
|
id: attribute.id,
|
||||||
|
name: attribute.name,
|
||||||
|
optionId: attribute.options[0].id,
|
||||||
|
option: attribute.options[0].value,
|
||||||
|
type: "singleSelect",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMultiSelectAttributeOutput(attribute: GroupedAttribute): MultiSelectAttribute {
|
||||||
|
return {
|
||||||
|
id: attribute.id,
|
||||||
|
name: attribute.name,
|
||||||
|
options: attribute.options.map((option: GroupedAttribute["options"][number]) => ({
|
||||||
|
optionId: option.id,
|
||||||
|
option: option.value,
|
||||||
|
})),
|
||||||
|
type: "multiSelect",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-8
@@ -1,19 +1,30 @@
|
|||||||
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
|
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
|
||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||||
|
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationsMembershipService {
|
export class OrganizationsMembershipService {
|
||||||
constructor(private readonly organizationsMembershipRepository: OrganizationsMembershipRepository) {}
|
constructor(
|
||||||
|
private readonly organizationsMembershipRepository: OrganizationsMembershipRepository,
|
||||||
|
private readonly organizationsMembershipOutputService: OrganizationsMembershipOutputService
|
||||||
|
) {}
|
||||||
|
|
||||||
async getOrgMembership(organizationId: number, membershipId: number) {
|
async getOrgMembership(organizationId: number, membershipId: number) {
|
||||||
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
||||||
organizationId,
|
organizationId,
|
||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
return membership;
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Membership with id ${membershipId} within organization id ${organizationId} not found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrgMembershipByUserId(organizationId: number, userId: number) {
|
async getOrgMembershipByUserId(organizationId: number, userId: number) {
|
||||||
@@ -21,7 +32,13 @@ export class OrganizationsMembershipService {
|
|||||||
organizationId,
|
organizationId,
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
return membership;
|
if (!membership) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Membership for user with id ${userId} within organization id ${organizationId} not found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPaginatedOrgMemberships(organizationId: number, skip = 0, take = 250) {
|
async getPaginatedOrgMemberships(organizationId: number, skip = 0, take = 250) {
|
||||||
@@ -30,7 +47,7 @@ export class OrganizationsMembershipService {
|
|||||||
skip,
|
skip,
|
||||||
take
|
take
|
||||||
);
|
);
|
||||||
return memberships;
|
return this.organizationsMembershipOutputService.getOrgMembershipsOutput(memberships);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||||
@@ -38,7 +55,7 @@ export class OrganizationsMembershipService {
|
|||||||
organizationId,
|
organizationId,
|
||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
return membership;
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
||||||
@@ -47,11 +64,11 @@ export class OrganizationsMembershipService {
|
|||||||
membershipId,
|
membershipId,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
return membership;
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
|
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
|
||||||
const membership = await this.organizationsMembershipRepository.createOrgMembership(organizationId, data);
|
const membership = await this.organizationsMembershipRepository.createOrgMembership(organizationId, data);
|
||||||
return membership;
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,7 @@ import { OrganizationsRepository } from "@/modules/organizations/index/organizat
|
|||||||
import { OrganizationsService } from "@/modules/organizations/index/organizations.service";
|
import { OrganizationsService } from "@/modules/organizations/index/organizations.service";
|
||||||
import { OrganizationsMembershipsController } from "@/modules/organizations/memberships/organizations-membership.controller";
|
import { OrganizationsMembershipsController } from "@/modules/organizations/memberships/organizations-membership.controller";
|
||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
|
import { OrganizationsMembershipOutputService } from "@/modules/organizations/memberships/services/organizations-membership-output.service";
|
||||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||||
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
|
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
|
||||||
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
|
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
|
||||||
@@ -98,6 +99,7 @@ import { Module } from "@nestjs/common";
|
|||||||
EmailService,
|
EmailService,
|
||||||
OrganizationsMembershipRepository,
|
OrganizationsMembershipRepository,
|
||||||
OrganizationsMembershipService,
|
OrganizationsMembershipService,
|
||||||
|
OrganizationsMembershipOutputService,
|
||||||
OrganizationsEventTypesService,
|
OrganizationsEventTypesService,
|
||||||
InputOrganizationsEventTypesService,
|
InputOrganizationsEventTypesService,
|
||||||
OutputOrganizationsEventTypesService,
|
OutputOrganizationsEventTypesService,
|
||||||
|
|||||||
+2
@@ -4,6 +4,7 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
|||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
|
import { OrganizationsMembershipOutputService } from "@/modules/organizations/memberships/services/organizations-membership-output.service";
|
||||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||||
import { ManagedOrganizationsRepository } from "@/modules/organizations/organizations/managed-organizations.repository";
|
import { ManagedOrganizationsRepository } from "@/modules/organizations/organizations/managed-organizations.repository";
|
||||||
import { OrganizationsOrganizationsController } from "@/modules/organizations/organizations/organizations-organizations.controller";
|
import { OrganizationsOrganizationsController } from "@/modules/organizations/organizations/organizations-organizations.controller";
|
||||||
@@ -23,6 +24,7 @@ import { Module } from "@nestjs/common";
|
|||||||
ManagedOrganizationsBillingService,
|
ManagedOrganizationsBillingService,
|
||||||
OrganizationsRepository,
|
OrganizationsRepository,
|
||||||
OrganizationsMembershipService,
|
OrganizationsMembershipService,
|
||||||
|
OrganizationsMembershipOutputService,
|
||||||
OrganizationsMembershipRepository,
|
OrganizationsMembershipRepository,
|
||||||
ManagedOrganizationsOutputService,
|
ManagedOrganizationsOutputService,
|
||||||
],
|
],
|
||||||
|
|||||||
+40
-30
@@ -126,36 +126,6 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await prismaWriteService.prisma.app_RoutingForms_FormResponse.deleteMany({
|
|
||||||
where: {
|
|
||||||
formId: routingForm.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await prismaWriteService.prisma.app_RoutingForms_Form.deleteMany({
|
|
||||||
where: {
|
|
||||||
teamId: org.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await prismaWriteService.prisma.apiKey.deleteMany({
|
|
||||||
where: {
|
|
||||||
teamId: org.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await prismaWriteService.prisma.team.delete({
|
|
||||||
where: {
|
|
||||||
id: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await prismaWriteService.prisma.team.delete({
|
|
||||||
where: {
|
|
||||||
id: org.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`GET /v2/organizations/:orgId/routing-forms/:routingFormId/responses`, () => {
|
describe(`GET /v2/organizations/:orgId/routing-forms/:routingFormId/responses`, () => {
|
||||||
it("should not get routing form responses for non existing org", async () => {
|
it("should not get routing form responses for non existing org", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
@@ -261,4 +231,44 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await prismaWriteService.prisma.app_RoutingForms_FormResponse.delete({
|
||||||
|
where: {
|
||||||
|
id: routingFormResponse.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismaWriteService.prisma.app_RoutingForms_FormResponse.delete({
|
||||||
|
where: {
|
||||||
|
id: routingFormResponse2.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismaWriteService.prisma.app_RoutingForms_Form.deleteMany({
|
||||||
|
where: {
|
||||||
|
teamId: org.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismaWriteService.prisma.apiKey.deleteMany({
|
||||||
|
where: {
|
||||||
|
teamId: org.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismaWriteService.prisma.team.delete({
|
||||||
|
where: {
|
||||||
|
id: team.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismaWriteService.prisma.team.delete({
|
||||||
|
where: {
|
||||||
|
id: org.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismaWriteService.prisma.user.delete({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+41
-3
@@ -1,13 +1,18 @@
|
|||||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||||
|
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
|
||||||
|
import { BookingReferencesOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/booking-references.output";
|
||||||
|
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import {
|
import {
|
||||||
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
|
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
|
||||||
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
|
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
|
||||||
OPTIONAL_API_KEY_HEADER,
|
OPTIONAL_API_KEY_HEADER,
|
||||||
|
API_KEY_OR_ACCESS_TOKEN_HEADER,
|
||||||
} from "@/lib/docs/headers";
|
} from "@/lib/docs/headers";
|
||||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
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 { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
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 { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
@@ -20,7 +25,7 @@ import { UserWithProfile } from "@/modules/users/users.repository";
|
|||||||
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query, HttpStatus, HttpCode } from "@nestjs/common";
|
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query, HttpStatus, HttpCode } from "@nestjs/common";
|
||||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, BOOKING_READ } from "@calcom/platform-constants";
|
||||||
import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
@@ -33,7 +38,10 @@ import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
|||||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||||
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||||
export class OrganizationsTeamsBookingsController {
|
export class OrganizationsTeamsBookingsController {
|
||||||
constructor(private readonly bookingsService: BookingsService_2024_08_13) {}
|
constructor(
|
||||||
|
private readonly bookingsService: BookingsService_2024_08_13,
|
||||||
|
private readonly bookingReferencesService: BookingReferencesService_2024_08_13
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@ApiOperation({ summary: "Get organization team bookings" })
|
@ApiOperation({ summary: "Get organization team bookings" })
|
||||||
@@ -46,7 +54,7 @@ export class OrganizationsTeamsBookingsController {
|
|||||||
@Param("orgId", ParseIntPipe) orgId: number,
|
@Param("orgId", ParseIntPipe) orgId: number,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<GetBookingsOutput_2024_08_13> {
|
): Promise<GetBookingsOutput_2024_08_13> {
|
||||||
const bookings = await this.bookingsService.getBookings(
|
const { bookings, pagination } = await this.bookingsService.getBookings(
|
||||||
{ ...queryParams, teamId },
|
{ ...queryParams, teamId },
|
||||||
{ email: user.email, id: user.id, orgId }
|
{ email: user.email, id: user.id, orgId }
|
||||||
);
|
);
|
||||||
@@ -54,6 +62,36 @@ export class OrganizationsTeamsBookingsController {
|
|||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: bookings,
|
data: bookings,
|
||||||
|
pagination,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/:bookingUid/references")
|
||||||
|
@PlatformPlan("SCALE")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Permissions([BOOKING_READ])
|
||||||
|
@UseGuards(
|
||||||
|
ApiAuthGuard,
|
||||||
|
BookingUidGuard,
|
||||||
|
IsOrgGuard,
|
||||||
|
RolesGuard,
|
||||||
|
IsTeamInOrg,
|
||||||
|
PlatformPlanGuard,
|
||||||
|
IsAdminAPIEnabledGuard
|
||||||
|
)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get 'Booking References' for a booking",
|
||||||
|
})
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getBookingReferences(
|
||||||
|
@Param("bookingUid") bookingUid: string,
|
||||||
|
@Query() filter: BookingReferencesFilterInput_2024_08_13
|
||||||
|
): Promise<BookingReferencesOutput_2024_08_13> {
|
||||||
|
const bookingReferences = await this.bookingReferencesService.getOrgBookingReferences(bookingUid, filter);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: bookingReferences,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+12
-1
@@ -1,4 +1,8 @@
|
|||||||
|
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||||
import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module";
|
import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module";
|
||||||
|
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
|
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||||
|
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
import { OrganizationsTeamsBookingsController } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.controller";
|
import { OrganizationsTeamsBookingsController } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.controller";
|
||||||
@@ -10,7 +14,14 @@ import { Module } from "@nestjs/common";
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BookingsModule_2024_08_13, PrismaModule, StripeModule, RedisModule, MembershipsModule],
|
imports: [BookingsModule_2024_08_13, PrismaModule, StripeModule, RedisModule, MembershipsModule],
|
||||||
providers: [OrganizationsRepository, OrganizationsTeamsRepository],
|
providers: [
|
||||||
|
OrganizationsRepository,
|
||||||
|
OrganizationsTeamsRepository,
|
||||||
|
BookingReferencesService_2024_08_13,
|
||||||
|
BookingReferencesRepository_2024_08_13,
|
||||||
|
BookingsRepository_2024_08_13,
|
||||||
|
OutputBookingReferencesService_2024_08_13,
|
||||||
|
],
|
||||||
controllers: [OrganizationsTeamsBookingsController],
|
controllers: [OrganizationsTeamsBookingsController],
|
||||||
})
|
})
|
||||||
export class OrganizationsTeamsBookingsModule {}
|
export class OrganizationsTeamsBookingsModule {}
|
||||||
+5
-1
@@ -69,7 +69,11 @@ export class OrganizationsTeamsMembershipsRepository {
|
|||||||
|
|
||||||
async createOrgTeamMembership(teamId: number, data: CreateOrgTeamMembershipDto) {
|
async createOrgTeamMembership(teamId: number, data: CreateOrgTeamMembershipDto) {
|
||||||
return this.dbWrite.prisma.membership.create({
|
return this.dbWrite.prisma.membership.create({
|
||||||
data: { ...data, teamId: teamId },
|
data: {
|
||||||
|
createdAt: new Date(),
|
||||||
|
...data,
|
||||||
|
teamId: teamId,
|
||||||
|
},
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: { user: { select: MembershipUserSelect } },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -35,7 +35,7 @@ describe("Organizations Teams Routing Forms Responses", () => {
|
|||||||
|
|
||||||
let org: Team;
|
let org: Team;
|
||||||
let orgTeam: Team;
|
let orgTeam: Team;
|
||||||
let routingFormResponseId: string;
|
let routingFormResponseId: number;
|
||||||
const authEmail = `organizations-teams-routing-forms-responses-user-${randomString()}@api.com`;
|
const authEmail = `organizations-teams-routing-forms-responses-user-${randomString()}@api.com`;
|
||||||
let user: User;
|
let user: User;
|
||||||
let apiKeyString: string;
|
let apiKeyString: string;
|
||||||
@@ -43,8 +43,7 @@ describe("Organizations Teams Routing Forms Responses", () => {
|
|||||||
let routingFormId: string;
|
let routingFormId: string;
|
||||||
const routingFormResponses = [
|
const routingFormResponses = [
|
||||||
{
|
{
|
||||||
id: 1,
|
formFillerId: `${randomString()}`,
|
||||||
formFillerId: "cm78tvkvd0001kh8jq0tu5iq9",
|
|
||||||
response: {
|
response: {
|
||||||
"participant-field": {
|
"participant-field": {
|
||||||
label: "participant",
|
label: "participant",
|
||||||
@@ -184,7 +183,6 @@ describe("Organizations Teams Routing Forms Responses", () => {
|
|||||||
const responseData = responseBody.data;
|
const responseData = responseBody.data;
|
||||||
expect(responseData).toBeDefined();
|
expect(responseData).toBeDefined();
|
||||||
expect(responseData.length).toEqual(1);
|
expect(responseData.length).toEqual(1);
|
||||||
expect(responseData[0].id).toEqual(routingFormResponses[0].id);
|
|
||||||
expect(responseData[0].response).toEqual(routingFormResponses[0].response);
|
expect(responseData[0].response).toEqual(routingFormResponses[0].response);
|
||||||
expect(responseData[0].formFillerId).toEqual(routingFormResponses[0].formFillerId);
|
expect(responseData[0].formFillerId).toEqual(routingFormResponses[0].formFillerId);
|
||||||
expect(responseData[0].createdAt).toEqual(routingFormResponses[0].createdAt.toISOString());
|
expect(responseData[0].createdAt).toEqual(routingFormResponses[0].createdAt.toISOString());
|
||||||
@@ -248,6 +246,8 @@ describe("Organizations Teams Routing Forms Responses", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
await routingFormsRepositoryFixture.deleteResponse(routingFormResponseId);
|
||||||
|
await routingFormsRepositoryFixture.delete(routingFormId);
|
||||||
await userRepositoryFixture.deleteByEmail(user.email);
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
await organizationsRepositoryFixture.delete(org.id);
|
await organizationsRepositoryFixture.delete(org.id);
|
||||||
await app.close();
|
await app.close();
|
||||||
|
|||||||
+2
@@ -6,6 +6,7 @@ import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
|||||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
import { IsRoutingFormInTeam } from "@/modules/auth/guards/routing-forms/is-routing-form-in-team.guard";
|
import { IsRoutingFormInTeam } from "@/modules/auth/guards/routing-forms/is-routing-form-in-team.guard";
|
||||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||||
import { GetRoutingFormResponsesParams } from "@/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input";
|
import { GetRoutingFormResponsesParams } from "@/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input";
|
||||||
@@ -29,6 +30,7 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
|||||||
IsTeamInOrg,
|
IsTeamInOrg,
|
||||||
IsRoutingFormInTeam,
|
IsRoutingFormInTeam,
|
||||||
PlatformPlanGuard,
|
PlatformPlanGuard,
|
||||||
|
RolesGuard,
|
||||||
IsAdminAPIEnabledGuard
|
IsAdminAPIEnabledGuard
|
||||||
)
|
)
|
||||||
@ApiHeader(API_KEY_HEADER)
|
@ApiHeader(API_KEY_HEADER)
|
||||||
|
|||||||
+236
@@ -0,0 +1,236 @@
|
|||||||
|
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 { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input";
|
||||||
|
import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input";
|
||||||
|
import {
|
||||||
|
TeamVerifiedEmailOutput,
|
||||||
|
TeamVerifiedEmailsOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-email.output";
|
||||||
|
import {
|
||||||
|
TeamVerifiedPhoneOutput,
|
||||||
|
TeamVerifiedPhonesOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-phone.output";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import { TOTP as TOTPtoMock } from "@otplib/core";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import { totp } from "otplib";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { VerifiedResourcesRepositoryFixtures } from "test/fixtures/repository/verified-resources.repository.fixture";
|
||||||
|
import { randomString } from "test/utils/randomString";
|
||||||
|
|
||||||
|
import { AttendeeVerifyEmail } from "@calcom/platform-libraries/emails";
|
||||||
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
jest.spyOn(totp, "generate").mockImplementation(function () {
|
||||||
|
return "1234";
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(TOTPtoMock.prototype, "check").mockImplementation(function () {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(AttendeeVerifyEmail.prototype as any, "getNodeMailerPayload")
|
||||||
|
.mockImplementation(async function () {
|
||||||
|
return {
|
||||||
|
to: `testnotrealemail@notreal.com`,
|
||||||
|
from: `testnotrealemail@notreal.com`,
|
||||||
|
subject: "No Subject",
|
||||||
|
html: "<html><body>Mocked Email Content</body></html>",
|
||||||
|
text: "body",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Organizations Teams Verified Resources", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
|
let verifiedResourcesRepositoryFixtures: VerifiedResourcesRepositoryFixtures;
|
||||||
|
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
|
let org: Team;
|
||||||
|
let orgTeam: Team;
|
||||||
|
const emailToVerify = `org-team-e2e-verified-resources-${randomString()}@example.com`;
|
||||||
|
const phoneToVerify = "+37255556666";
|
||||||
|
const authEmail = `organizations-verified-resources-responses-user-${randomString()}@api.com`;
|
||||||
|
let user: User;
|
||||||
|
let apiKeyString: string;
|
||||||
|
let verifiedEmailId: number;
|
||||||
|
let verifiedPhoneId: number;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
verifiedResourcesRepositoryFixtures = new VerifiedResourcesRepositoryFixtures(moduleRef);
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
|
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||||
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
org = await organizationsRepositoryFixture.create({
|
||||||
|
name: `organizations-verified-resources-responses-organization-${randomString()}`,
|
||||||
|
isOrganization: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: authEmail,
|
||||||
|
username: authEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
|
||||||
|
apiKeyString = keyString;
|
||||||
|
|
||||||
|
orgTeam = await teamsRepositoryFixture.create({
|
||||||
|
name: `organizations-verified-resources-responses-team-${randomString()}`,
|
||||||
|
isOrganization: false,
|
||||||
|
parent: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "ADMIN",
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "ADMIN",
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: orgTeam.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await profileRepositoryFixture.create({
|
||||||
|
uid: `usr-${user.id}`,
|
||||||
|
username: authEmail,
|
||||||
|
organization: {
|
||||||
|
connect: {
|
||||||
|
id: org.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
verifiedPhoneId = (
|
||||||
|
await verifiedResourcesRepositoryFixtures.createPhone({
|
||||||
|
phoneNumber: phoneToVerify,
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: {
|
||||||
|
connect: {
|
||||||
|
id: orgTeam.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).id;
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trigger email verification code", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails/verification-code/request`
|
||||||
|
)
|
||||||
|
.send({ email: emailToVerify } satisfies RequestEmailVerificationInput)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should verify email", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails/verification-code/verify`
|
||||||
|
)
|
||||||
|
.send({ email: emailToVerify, code: "1234" } satisfies VerifyEmailInput)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
const response = res.body as TeamVerifiedEmailOutput;
|
||||||
|
verifiedEmailId = response.data.id;
|
||||||
|
expect(response.data.email).toEqual(emailToVerify);
|
||||||
|
expect(response.data.teamId).toEqual(orgTeam.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch verified email by id", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails/${verifiedEmailId}`)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
const response = res.body as TeamVerifiedEmailOutput;
|
||||||
|
expect(response.data.email).toEqual(emailToVerify);
|
||||||
|
expect(response.data.teamId).toEqual(orgTeam.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch verified number by id", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/phones/${verifiedPhoneId}`)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
const response = res.body as TeamVerifiedPhoneOutput;
|
||||||
|
expect(response.data.phoneNumber).toEqual(phoneToVerify);
|
||||||
|
expect(response.data.teamId).toEqual(orgTeam.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch verified emails", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails`)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
const response = res.body as TeamVerifiedEmailsOutput;
|
||||||
|
expect(response.data.find((verifiedEmail) => verifiedEmail.email === emailToVerify)).toBeDefined();
|
||||||
|
expect(response.data.find((verifiedEmail) => verifiedEmail.teamId === orgTeam.id)).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch verified phones", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/phones`)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
const response = res.body as TeamVerifiedPhonesOutput;
|
||||||
|
expect(
|
||||||
|
response.data.find((verifiedPhone) => verifiedPhone.phoneNumber === phoneToVerify)
|
||||||
|
).toBeDefined();
|
||||||
|
expect(response.data.find((verifiedPhone) => verifiedPhone.teamId === orgTeam.id)).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await verifiedResourcesRepositoryFixtures.deleteEmailById(verifiedEmailId);
|
||||||
|
await verifiedResourcesRepositoryFixtures.deletePhoneById(verifiedPhoneId);
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await organizationsRepositoryFixture.delete(org.id);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
+256
@@ -0,0 +1,256 @@
|
|||||||
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
|
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||||
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
|
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||||
|
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||||
|
import { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input";
|
||||||
|
import { RequestPhoneVerificationInput } from "@/modules/verified-resources/inputs/request-phone-verification.input";
|
||||||
|
import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input";
|
||||||
|
import { VerifyPhoneInput } from "@/modules/verified-resources/inputs/verify-phone.input";
|
||||||
|
import { RequestEmailVerificationOutput } from "@/modules/verified-resources/outputs/request-email-verification-output";
|
||||||
|
import { RequestPhoneVerificationOutput } from "@/modules/verified-resources/outputs/request-phone-verification-output";
|
||||||
|
import {
|
||||||
|
TeamVerifiedEmailOutput,
|
||||||
|
TeamVerifiedEmailOutputData,
|
||||||
|
TeamVerifiedEmailsOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-email.output";
|
||||||
|
import {
|
||||||
|
TeamVerifiedPhoneOutput,
|
||||||
|
TeamVerifiedPhoneOutputData,
|
||||||
|
TeamVerifiedPhonesOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-phone.output";
|
||||||
|
import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/organizations/:orgId/teams/:teamId/verified-resources",
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@ApiTags("Organization Team Verified Resources")
|
||||||
|
export class OrgTeamsVerifiedResourcesController {
|
||||||
|
constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {}
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request Email Verification Code",
|
||||||
|
description: `Sends a verification code to the email.`,
|
||||||
|
})
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Throttle({
|
||||||
|
limit: 3,
|
||||||
|
ttl: 60000,
|
||||||
|
blockDuration: 60000,
|
||||||
|
name: "org_teams_verified_resources_emails_requests",
|
||||||
|
})
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Post("/emails/verification-code/request")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async requestEmailVerificationCode(
|
||||||
|
@Body() body: RequestEmailVerificationInput,
|
||||||
|
@GetUser("username") username: string,
|
||||||
|
@GetUser("locale") locale: string
|
||||||
|
): Promise<RequestEmailVerificationOutput> {
|
||||||
|
const verificationCodeRequest = await this.verifiedResourcesService.requestEmailVerificationCode(
|
||||||
|
{ username, locale },
|
||||||
|
body.email
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request Phone Number Verification Code",
|
||||||
|
description: `Sends a verification code to the phone number.`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Throttle({
|
||||||
|
limit: 3,
|
||||||
|
ttl: 60000,
|
||||||
|
blockDuration: 60000,
|
||||||
|
name: "org_teams_verified_resources_phones_requests",
|
||||||
|
})
|
||||||
|
@Post("/phones/verification-code/request")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async requestPhoneVerificationCode(
|
||||||
|
@Body() body: RequestPhoneVerificationInput
|
||||||
|
): Promise<RequestPhoneVerificationOutput> {
|
||||||
|
const verificationCodeRequest = await this.verifiedResourcesService.requestPhoneVerificationCode(
|
||||||
|
body.phone
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify an email for an org team.",
|
||||||
|
description: `Use code to verify an email`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Post("/emails/verification-code/verify")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyEmail(
|
||||||
|
@Body() body: VerifyEmailInput,
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<TeamVerifiedEmailOutput> {
|
||||||
|
const verifiedEmail = await this.verifiedResourcesService.verifyEmail(
|
||||||
|
userId,
|
||||||
|
body.email,
|
||||||
|
body.code,
|
||||||
|
teamId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify a phone number for an org team.",
|
||||||
|
description: `Use code to verify a phone number`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Post("/phones/verification-code/verify")
|
||||||
|
@Throttle({
|
||||||
|
limit: 3,
|
||||||
|
ttl: 60000,
|
||||||
|
blockDuration: 60000,
|
||||||
|
name: "org_teams_verified_resources_phones_verify",
|
||||||
|
})
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyPhoneNumber(
|
||||||
|
@Body() body: VerifyPhoneInput,
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<TeamVerifiedPhoneOutput> {
|
||||||
|
const verifiedPhone = await this.verifiedResourcesService.verifyPhone(
|
||||||
|
userId,
|
||||||
|
body.phone,
|
||||||
|
body.code,
|
||||||
|
teamId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhone),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get list of verified emails of an org team.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/emails")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmails(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Query() pagination: SkipTakePagination
|
||||||
|
): Promise<TeamVerifiedEmailsOutput> {
|
||||||
|
const verifiedEmails = await this.verifiedResourcesService.getTeamVerifiedEmails(
|
||||||
|
teamId,
|
||||||
|
pagination?.skip ?? 0,
|
||||||
|
pagination?.take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: verifiedEmails.map((verifiedEmail) => plainToClass(TeamVerifiedEmailOutputData, verifiedEmail)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get list of verified phone numbers of an org team.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/phones")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedPhoneNumbers(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Query() pagination: SkipTakePagination
|
||||||
|
): Promise<TeamVerifiedPhonesOutput> {
|
||||||
|
const verifiedPhoneNumbers = await this.verifiedResourcesService.getTeamVerifiedPhoneNumbers(
|
||||||
|
teamId,
|
||||||
|
pagination?.skip ?? 0,
|
||||||
|
pagination?.take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: verifiedPhoneNumbers.map((verifiedPhoneNumber) =>
|
||||||
|
plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get verified email of an org team by id.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/emails/:id")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmailById(
|
||||||
|
@Param("id") id: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<TeamVerifiedEmailOutput> {
|
||||||
|
const verifiedEmail = await this.verifiedResourcesService.getTeamVerifiedEmailById(teamId, id);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get verified phone number of an org team by id.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/phones/:id")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedPhoneById(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Param("id") id: number
|
||||||
|
): Promise<TeamVerifiedPhoneOutput> {
|
||||||
|
const verifiedPhoneNumber = await this.verifiedResourcesService.getTeamVerifiedPhoneNumberById(
|
||||||
|
teamId,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -40,7 +40,7 @@ export class OrganizationsUsersBookingsController {
|
|||||||
@Param("userId", ParseIntPipe) userId: number,
|
@Param("userId", ParseIntPipe) userId: number,
|
||||||
@Query() query: GetBookingsInput_2024_08_13
|
@Query() query: GetBookingsInput_2024_08_13
|
||||||
): Promise<GetBookingsOutput_2024_08_13> {
|
): Promise<GetBookingsOutput_2024_08_13> {
|
||||||
const bookings = await this.organizationUsersBookingsService.getOrganizationUserBookings(
|
const { bookings, pagination } = await this.organizationUsersBookingsService.getOrganizationUserBookings(
|
||||||
orgId,
|
orgId,
|
||||||
userId,
|
userId,
|
||||||
query
|
query
|
||||||
@@ -49,6 +49,7 @@ export class OrganizationsUsersBookingsController {
|
|||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: bookings,
|
data: bookings,
|
||||||
|
pagination,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+10
@@ -29,6 +29,7 @@ import {
|
|||||||
RecurringBookingOutput_2024_08_13,
|
RecurringBookingOutput_2024_08_13,
|
||||||
GetBookingsOutput_2024_08_13,
|
GetBookingsOutput_2024_08_13,
|
||||||
GetSeatedBookingOutput_2024_08_13,
|
GetSeatedBookingOutput_2024_08_13,
|
||||||
|
PaginationMetaDto,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@@ -373,17 +374,26 @@ describe("Organizations UsersBookings Endpoints 2024-08-13", () => {
|
|||||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.pagination).toBeDefined();
|
||||||
const data: (
|
const data: (
|
||||||
| BookingOutput_2024_08_13
|
| BookingOutput_2024_08_13
|
||||||
| RecurringBookingOutput_2024_08_13
|
| RecurringBookingOutput_2024_08_13
|
||||||
| GetSeatedBookingOutput_2024_08_13
|
| GetSeatedBookingOutput_2024_08_13
|
||||||
)[] = responseBody.data;
|
)[] = responseBody.data;
|
||||||
|
const pagination: PaginationMetaDto = responseBody.pagination;
|
||||||
expect(data.length).toEqual(3);
|
expect(data.length).toEqual(3);
|
||||||
expect(data.find((booking) => booking.id === createdPersonalBooking.id)).toBeDefined();
|
expect(data.find((booking) => booking.id === createdPersonalBooking.id)).toBeDefined();
|
||||||
expect(data.find((booking) => booking.id === createdTeamBooking.id)).toBeDefined();
|
expect(data.find((booking) => booking.id === createdTeamBooking.id)).toBeDefined();
|
||||||
expect(
|
expect(
|
||||||
data.find((booking) => booking.id === createdPersonalBookingUsingUsername.id)
|
data.find((booking) => booking.id === createdPersonalBookingUsingUsername.id)
|
||||||
).toBeDefined();
|
).toBeDefined();
|
||||||
|
expect(pagination.totalItems).toEqual(3);
|
||||||
|
expect(pagination.remainingItems).toEqual(0);
|
||||||
|
expect(pagination.hasNextPage).toEqual(false);
|
||||||
|
expect(pagination.hasPreviousPage).toEqual(false);
|
||||||
|
expect(pagination.itemsPerPage).toEqual(100);
|
||||||
|
expect(pagination.totalPages).toEqual(1);
|
||||||
|
expect(pagination.currentPage).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-4
@@ -38,7 +38,7 @@ export class OrganizationsUsersRepository {
|
|||||||
member: {
|
member: {
|
||||||
teamId: orgId,
|
teamId: orgId,
|
||||||
...(teamIds && { user: { teams: { some: { teamId: { in: teamIds } } } } }),
|
...(teamIds && { user: { teams: { some: { teamId: { in: teamIds } } } } }),
|
||||||
// Filter to only get users which have ALL of the assigned attribue options
|
// Filter to only get users which have ALL of the assigned attribute options
|
||||||
...(attributeQueryOperator === "AND" && {
|
...(attributeQueryOperator === "AND" && {
|
||||||
AND: assignedOptionIds.map((optionId) => ({
|
AND: assignedOptionIds.map((optionId) => ({
|
||||||
AttributeToUser: { some: { attributeOptionId: optionId } },
|
AttributeToUser: { some: { attributeOptionId: optionId } },
|
||||||
@@ -46,11 +46,11 @@ export class OrganizationsUsersRepository {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
...(emailArray && emailArray.length ? { email: { in: emailArray } } : {}),
|
...(emailArray && emailArray.length ? { email: { in: emailArray } } : {}),
|
||||||
// Filter to get users which have AT LEAST ONE of the assigned attribue options
|
// Filter to get users which have AT LEAST ONE of the assigned attribute options
|
||||||
...(attributeQueryOperator === "OR" && {
|
...(attributeQueryOperator === "OR" && {
|
||||||
attributeOption: { id: { in: assignedOptionIds } },
|
attributeOption: { id: { in: assignedOptionIds } },
|
||||||
}),
|
}),
|
||||||
// Filter to get users that have NONE the assigned attribue options
|
// Filter to get users that have NONE the assigned attribute options
|
||||||
...(attributeQueryOperator === "NONE" && {
|
...(attributeQueryOperator === "NONE" && {
|
||||||
NOT: {
|
NOT: {
|
||||||
attributeOption: { id: { in: assignedOptionIds } },
|
attributeOption: { id: { in: assignedOptionIds } },
|
||||||
@@ -141,7 +141,16 @@ export class OrganizationsUsersRepository {
|
|||||||
return await this.dbWrite.prisma.user.delete({
|
return await this.dbWrite.prisma.user.delete({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
organizationId: orgId,
|
OR: [
|
||||||
|
{ organizationId: orgId },
|
||||||
|
{
|
||||||
|
profiles: {
|
||||||
|
some: {
|
||||||
|
organizationId: orgId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
profiles: {
|
profiles: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AppsRepository } from "@/modules/apps/apps.repository";
|
|||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository";
|
import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository";
|
||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
|
import { OrganizationsMembershipOutputService } from "@/modules/organizations/memberships/services/organizations-membership-output.service";
|
||||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { SelectedCalendarsController } from "@/modules/selected-calendars/controllers/selected-calendars.controller";
|
import { SelectedCalendarsController } from "@/modules/selected-calendars/controllers/selected-calendars.controller";
|
||||||
@@ -22,6 +23,7 @@ import { Module } from "@nestjs/common";
|
|||||||
CredentialsRepository,
|
CredentialsRepository,
|
||||||
AppsRepository,
|
AppsRepository,
|
||||||
OrganizationsMembershipService,
|
OrganizationsMembershipService,
|
||||||
|
OrganizationsMembershipOutputService,
|
||||||
OrganizationsDelegationCredentialRepository,
|
OrganizationsDelegationCredentialRepository,
|
||||||
OrganizationsMembershipRepository,
|
OrganizationsMembershipRepository,
|
||||||
SelectedCalendarsService,
|
SelectedCalendarsService,
|
||||||
|
|||||||
@@ -157,7 +157,10 @@ export class SlotsController_2024_04_15 {
|
|||||||
@Query() query: GetAvailableSlotsInput_2024_04_15,
|
@Query() query: GetAvailableSlotsInput_2024_04_15,
|
||||||
@Req() req: ExpressRequest
|
@Req() req: ExpressRequest
|
||||||
): Promise<ApiResponse<{ slots: TimeSlots["slots"] | RangeSlots["slots"] }>> {
|
): Promise<ApiResponse<{ slots: TimeSlots["slots"] | RangeSlots["slots"] }>> {
|
||||||
const isTeamEvent = await this.slotsService.checkIfIsTeamEvent(query.eventTypeId);
|
const isTeamEvent =
|
||||||
|
query.isTeamEvent === undefined
|
||||||
|
? await this.slotsService.checkIfIsTeamEvent(query.eventTypeId)
|
||||||
|
: query.isTeamEvent;
|
||||||
const availableSlots = await getAvailableSlots({
|
const availableSlots = await getAvailableSlots({
|
||||||
input: {
|
input: {
|
||||||
...query,
|
...query,
|
||||||
|
|||||||
+97
-12
@@ -181,7 +181,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
title: "frisbee match",
|
title: "frisbee match",
|
||||||
slug: `slots-2024-09-04-variable-length-event-type-${randomString()}`,
|
slug: `slots-2024-09-04-variable-length-event-type-${randomString()}`,
|
||||||
length: 15,
|
length: 15,
|
||||||
metadata: { multipleDuration: [15, 30, 45, 60] },
|
metadata: { multipleDuration: [15, 30, 45, 60, 180] },
|
||||||
},
|
},
|
||||||
user.id
|
user.id
|
||||||
);
|
);
|
||||||
@@ -506,6 +506,65 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
expect(reserveResponseBody.data).toEqual(rest);
|
expect(reserveResponseBody.data).toEqual(rest);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("overlapping slot reservations", () => {
|
||||||
|
it("start of request slot overlaps already existing reserved slot", async () => {
|
||||||
|
// Try to reserve 10:15-11:15 when 10:00-11:00 is taken (60 min event)
|
||||||
|
const newSlotStart = DateTime.fromISO(reservedSlot.slotStart).plus({ minutes: 15 }).toISO();
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.send({
|
||||||
|
eventTypeId,
|
||||||
|
slotStart: newSlotStart,
|
||||||
|
})
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||||
|
.expect(422)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
"This time slot is already reserved by another user. Please choose a different time."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("end of request slot overlaps already existing reserved slot", async () => {
|
||||||
|
// Try to reserve 9:45-10:45 when 10:00-11:00 is taken (60 min event)
|
||||||
|
const newSlotStart = DateTime.fromISO(reservedSlot.slotStart).minus({ minutes: 15 }).toISO();
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.send({
|
||||||
|
eventTypeId,
|
||||||
|
slotStart: newSlotStart,
|
||||||
|
})
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||||
|
.expect(422)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
"This time slot is already reserved by another user. Please choose a different time."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("request slot is inside already existing reserved slot", async () => {
|
||||||
|
// Try to reserve 10:10-11:10 when 10:00-11:00 is taken (60 min event)
|
||||||
|
const newSlotStart = DateTime.fromISO(reservedSlot.slotStart).plus({ minutes: 10 }).toISO();
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.send({
|
||||||
|
eventTypeId,
|
||||||
|
slotStart: newSlotStart,
|
||||||
|
})
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||||
|
.expect(422)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
"This time slot is already reserved by another user. Please choose a different time."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should update a reserved slot and it should not appear in available slots", async () => {
|
it("should update a reserved slot and it should not appear in available slots", async () => {
|
||||||
// note(Lauris): mock current date to test slots release time
|
// note(Lauris): mock current date to test slots release time
|
||||||
const now = "2049-09-05T14:00:00.000Z";
|
const now = "2049-09-05T14:00:00.000Z";
|
||||||
@@ -1266,6 +1325,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("variable length", () => {
|
describe("variable length", () => {
|
||||||
|
let responseReservedVariableSlot: ReserveSlotOutputData_2024_09_04;
|
||||||
it("should not be able to reserve a slot for variable length event type with invalid duration", async () => {
|
it("should not be able to reserve a slot for variable length event type with invalid duration", async () => {
|
||||||
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
||||||
const reserveResponse = await request(app.getHttpServer())
|
const reserveResponse = await request(app.getHttpServer())
|
||||||
@@ -1279,7 +1339,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
.expect(400);
|
.expect(400);
|
||||||
|
|
||||||
expect(reserveResponse.body.error.message).toEqual(
|
expect(reserveResponse.body.error.message).toEqual(
|
||||||
"Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: 15, 30, 45, 60"
|
"Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: 15, 30, 45, 60, 180"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1303,17 +1363,17 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
|
|
||||||
const reserveResponseBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponse.body;
|
const reserveResponseBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponse.body;
|
||||||
expect(reserveResponseBody.status).toEqual(SUCCESS_STATUS);
|
expect(reserveResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
const responseReservedSlot: ReserveSlotOutputData_2024_09_04 = reserveResponseBody.data;
|
responseReservedVariableSlot = reserveResponseBody.data;
|
||||||
expect(responseReservedSlot.reservationUid).toBeDefined();
|
expect(responseReservedVariableSlot.reservationUid).toBeDefined();
|
||||||
expect(responseReservedSlot.eventTypeId).toEqual(variableLengthEventType.id);
|
expect(responseReservedVariableSlot.eventTypeId).toEqual(variableLengthEventType.id);
|
||||||
expect(responseReservedSlot.slotStart).toEqual(slotStartTime);
|
expect(responseReservedVariableSlot.slotStart).toEqual(slotStartTime);
|
||||||
expect(responseReservedSlot.slotDuration).toEqual(slotDuration);
|
expect(responseReservedVariableSlot.slotDuration).toEqual(slotDuration);
|
||||||
expect(responseReservedSlot.slotEnd).toEqual(
|
expect(responseReservedVariableSlot.slotEnd).toEqual(
|
||||||
DateTime.fromISO(slotStartTime, { zone: "UTC" }).plus({ minutes: slotDuration }).toISO()
|
DateTime.fromISO(slotStartTime, { zone: "UTC" }).plus({ minutes: slotDuration }).toISO()
|
||||||
);
|
);
|
||||||
expect(responseReservedSlot.reservationDuration).toEqual(5);
|
expect(responseReservedVariableSlot.reservationDuration).toEqual(5);
|
||||||
|
|
||||||
if (!responseReservedSlot.reservationUid) {
|
if (!responseReservedVariableSlot.reservationUid) {
|
||||||
throw new Error("Reserved slot uid is undefined");
|
throw new Error("Reserved slot uid is undefined");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1337,16 +1397,41 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
);
|
);
|
||||||
expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 });
|
expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 });
|
||||||
|
|
||||||
const dbSlot = await selectedSlotsRepositoryFixture.getByUid(reservedSlot.reservationUid);
|
const dbSlot = await selectedSlotsRepositoryFixture.getByUid(
|
||||||
|
responseReservedVariableSlot.reservationUid
|
||||||
|
);
|
||||||
expect(dbSlot).toBeDefined();
|
expect(dbSlot).toBeDefined();
|
||||||
if (dbSlot) {
|
if (dbSlot) {
|
||||||
const dbReleaseAt = DateTime.fromJSDate(dbSlot.releaseAt, { zone: "UTC" }).toISO();
|
const dbReleaseAt = DateTime.fromJSDate(dbSlot.releaseAt, { zone: "UTC" }).toISO();
|
||||||
const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 5 }).toISO();
|
const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 5 }).toISO();
|
||||||
expect(dbReleaseAt).toEqual(expectedReleaseAt);
|
expect(dbReleaseAt).toEqual(expectedReleaseAt);
|
||||||
expect(responseReservedSlot.reservationUntil).toEqual(expectedReleaseAt);
|
expect(responseReservedVariableSlot.reservationUntil).toEqual(expectedReleaseAt);
|
||||||
}
|
}
|
||||||
clear();
|
clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("request slot contains already existing reserved slot", async () => {
|
||||||
|
// Try to reserve 9:45-12:45 when 10:00-11:00 is taken
|
||||||
|
const newSlotStart = DateTime.fromISO(responseReservedVariableSlot.slotStart)
|
||||||
|
.minus({ minutes: 15 })
|
||||||
|
.toISO();
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.send({
|
||||||
|
eventTypeId: variableLengthEventType.id,
|
||||||
|
slotStart: newSlotStart,
|
||||||
|
slotDuration: 180,
|
||||||
|
})
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||||
|
.set("Authorization", `Bearer cal_test_${apiKeyString}`)
|
||||||
|
.expect(422)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
"This time slot is already reserved by another user. Please choose a different time."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("out of office", () => {
|
describe("out of office", () => {
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ export class SlotsService_2024_09_04 {
|
|||||||
|
|
||||||
const reservationDuration = input.reservationDuration ?? DEFAULT_RESERVATION_DURATION;
|
const reservationDuration = input.reservationDuration ?? DEFAULT_RESERVATION_DURATION;
|
||||||
|
|
||||||
|
await this.checkSlotOverlap(input.eventTypeId, startDate.toISO(), endDate.toISO());
|
||||||
|
|
||||||
if (eventType.userId) {
|
if (eventType.userId) {
|
||||||
const slot = await this.slotsRepository.createSlot(
|
const slot = await this.slotsRepository.createSlot(
|
||||||
eventType.userId,
|
eventType.userId,
|
||||||
@@ -153,6 +155,20 @@ export class SlotsService_2024_09_04 {
|
|||||||
return this.slotsOutputService.getReservationSlotCreated(slot, reservationDuration);
|
return this.slotsOutputService.getReservationSlotCreated(slot, reservationDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkSlotOverlap(eventTypeId: number, startDate: string, endDate: string) {
|
||||||
|
const overlappingReservation = await this.slotsRepository.getOverlappingSlotReservation(
|
||||||
|
eventTypeId,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
if (overlappingReservation) {
|
||||||
|
throw new UnprocessableEntityException(
|
||||||
|
`This time slot is already reserved by another user. Please choose a different time.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
validateSlotDuration(eventType: EventType, inputSlotDuration: number) {
|
validateSlotDuration(eventType: EventType, inputSlotDuration: number) {
|
||||||
const eventTypeMetadata = eventTypeMetadataSchema.parse(eventType.metadata);
|
const eventTypeMetadata = eventTypeMetadataSchema.parse(eventType.metadata);
|
||||||
if (!eventTypeMetadata?.multipleDuration) {
|
if (!eventTypeMetadata?.multipleDuration) {
|
||||||
@@ -257,6 +273,8 @@ export class SlotsService_2024_09_04 {
|
|||||||
|
|
||||||
const reservationDuration = input.reservationDuration ?? DEFAULT_RESERVATION_DURATION;
|
const reservationDuration = input.reservationDuration ?? DEFAULT_RESERVATION_DURATION;
|
||||||
|
|
||||||
|
await this.checkSlotOverlap(input.eventTypeId, startDate.toISO(), endDate.toISO());
|
||||||
|
|
||||||
const slot = await this.slotsRepository.updateSlot(
|
const slot = await this.slotsRepository.updateSlot(
|
||||||
eventType.id,
|
eventType.id,
|
||||||
startDate.toISO(),
|
startDate.toISO(),
|
||||||
|
|||||||
@@ -19,6 +19,30 @@ export class SlotsRepository_2024_09_04 {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOverlappingSlotReservation(eventTypeId: number, startDate: string, endDate: string) {
|
||||||
|
return this.dbRead.prisma.selectedSlots.findFirst({
|
||||||
|
where: {
|
||||||
|
eventTypeId,
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
// Case 1: New slot starts during an existing slot
|
||||||
|
{ slotUtcStartDate: { lte: startDate }, slotUtcEndDate: { gt: startDate } },
|
||||||
|
// Case 2: New slot ends during an existing slot
|
||||||
|
{ slotUtcStartDate: { lt: endDate }, slotUtcEndDate: { gte: endDate } },
|
||||||
|
// Case 3: New slot is completely inside an existing slot
|
||||||
|
{ slotUtcStartDate: { lte: startDate }, slotUtcEndDate: { gte: endDate } },
|
||||||
|
// Case 4: New slot completely overlaps an existing slot
|
||||||
|
{ slotUtcStartDate: { gte: startDate }, slotUtcEndDate: { lte: endDate } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Only consider non-expired reservations
|
||||||
|
{ releaseAt: { gt: DateTime.utc().toJSDate() } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async createSlot(
|
async createSlot(
|
||||||
userId: number,
|
userId: number,
|
||||||
eventTypeId: number,
|
eventTypeId: number,
|
||||||
|
|||||||
@@ -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 { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OrganizationsConferencingModule } from "@/modules/organizations/conferencing/organizations-conferencing.module";
|
import { OrganizationsConferencingModule } from "@/modules/organizations/conferencing/organizations-conferencing.module";
|
||||||
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
|
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
|
||||||
@@ -31,6 +32,7 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsTeamsRepository,
|
OrganizationsTeamsRepository,
|
||||||
OutputTeamEventTypesResponsePipe,
|
OutputTeamEventTypesResponsePipe,
|
||||||
OutputOrganizationsEventTypesService,
|
OutputOrganizationsEventTypesService,
|
||||||
|
ConferencingRepository,
|
||||||
],
|
],
|
||||||
exports: [TeamsEventTypesRepository, TeamsEventTypesService],
|
exports: [TeamsEventTypesRepository, TeamsEventTypesService],
|
||||||
controllers: [TeamsEventTypesController],
|
controllers: [TeamsEventTypesController],
|
||||||
|
|||||||
+1
-1
@@ -112,7 +112,7 @@ export class TeamsMembershipsController {
|
|||||||
|
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@Patch("/:membershipId")
|
@Patch("/:membershipId")
|
||||||
@ApiOperation({ summary: "Create a membership" })
|
@ApiOperation({ summary: "Update membership" })
|
||||||
async updateTeamMembership(
|
async updateTeamMembership(
|
||||||
@Param("teamId", ParseIntPipe) teamId: number,
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
@Param("membershipId", ParseIntPipe) membershipId: number,
|
@Param("membershipId", ParseIntPipe) membershipId: number,
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ export class TeamsMembershipsRepository {
|
|||||||
|
|
||||||
async createTeamMembership(teamId: number, data: CreateTeamMembershipInput) {
|
async createTeamMembership(teamId: number, data: CreateTeamMembershipInput) {
|
||||||
return this.dbWrite.prisma.membership.create({
|
return this.dbWrite.prisma.membership.create({
|
||||||
data: { ...data, teamId: teamId },
|
data: {
|
||||||
|
createdAt: new Date(),
|
||||||
|
...data,
|
||||||
|
teamId: teamId,
|
||||||
|
},
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: { user: { select: MembershipUserSelect } },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+229
@@ -0,0 +1,229 @@
|
|||||||
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
|
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input";
|
||||||
|
import { RequestPhoneVerificationInput } from "@/modules/verified-resources/inputs/request-phone-verification.input";
|
||||||
|
import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input";
|
||||||
|
import { VerifyPhoneInput } from "@/modules/verified-resources/inputs/verify-phone.input";
|
||||||
|
import { RequestEmailVerificationOutput } from "@/modules/verified-resources/outputs/request-email-verification-output";
|
||||||
|
import { RequestPhoneVerificationOutput } from "@/modules/verified-resources/outputs/request-phone-verification-output";
|
||||||
|
import {
|
||||||
|
TeamVerifiedEmailOutput,
|
||||||
|
TeamVerifiedEmailOutputData,
|
||||||
|
TeamVerifiedEmailsOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-email.output";
|
||||||
|
import {
|
||||||
|
TeamVerifiedPhoneOutput,
|
||||||
|
TeamVerifiedPhoneOutputData,
|
||||||
|
TeamVerifiedPhonesOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-phone.output";
|
||||||
|
import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/teams/:teamId/verified-resources",
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard, RolesGuard)
|
||||||
|
@ApiTags("Teams Verified Resources")
|
||||||
|
export class TeamsVerifiedResourcesController {
|
||||||
|
constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {}
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request Email Verification Code",
|
||||||
|
description: `Sends a verification code to the Email.`,
|
||||||
|
})
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Post("/emails/verification-code/request")
|
||||||
|
@Throttle({ limit: 5, ttl: 60000, blockDuration: 60000, name: "teams_verified_resources_emails_requests" })
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async requestEmailVerificationCode(
|
||||||
|
@Body() body: RequestEmailVerificationInput,
|
||||||
|
@GetUser("username") username: string,
|
||||||
|
@GetUser("locale") locale: string
|
||||||
|
): Promise<RequestEmailVerificationOutput> {
|
||||||
|
const verificationCodeRequest = await this.verifiedResourcesService.requestEmailVerificationCode(
|
||||||
|
{ username, locale },
|
||||||
|
body.email
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request Phone Number Verification Code",
|
||||||
|
description: `Sends a verification code to the phone number.`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Post("/phones/verification-code/request")
|
||||||
|
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "teams_verified_resources_phones_requests" })
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async requestPhoneVerificationCode(
|
||||||
|
@Body() body: RequestPhoneVerificationInput
|
||||||
|
): Promise<RequestPhoneVerificationOutput> {
|
||||||
|
const verificationCodeRequest = await this.verifiedResourcesService.requestPhoneVerificationCode(
|
||||||
|
body.phone
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify an email for a team.",
|
||||||
|
description: `Use code to verify an email`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Post("/emails/verification-code/verify")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyEmail(
|
||||||
|
@Body() body: VerifyEmailInput,
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<TeamVerifiedEmailOutput> {
|
||||||
|
const verifiedEmail = await this.verifiedResourcesService.verifyEmail(
|
||||||
|
userId,
|
||||||
|
body.email,
|
||||||
|
body.code,
|
||||||
|
teamId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify a phone number for an org team.",
|
||||||
|
description: `Use code to verify a phone number`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Post("/phones/verification-code/verify")
|
||||||
|
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "teams_verified_resources_phones_verify" })
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyPhoneNumber(
|
||||||
|
@Body() body: VerifyPhoneInput,
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<TeamVerifiedPhoneOutput> {
|
||||||
|
const verifiedPhone = await this.verifiedResourcesService.verifyPhone(
|
||||||
|
userId,
|
||||||
|
body.phone,
|
||||||
|
body.code,
|
||||||
|
teamId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhone),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get list of verified emails of a team.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Get("/emails")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmails(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Query() pagination: SkipTakePagination
|
||||||
|
): Promise<TeamVerifiedEmailsOutput> {
|
||||||
|
const verifiedEmails = await this.verifiedResourcesService.getTeamVerifiedEmails(
|
||||||
|
teamId,
|
||||||
|
pagination?.skip ?? 0,
|
||||||
|
pagination?.take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: verifiedEmails.map((verifiedEmail) => plainToClass(TeamVerifiedEmailOutputData, verifiedEmail)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get list of verified phone numbers of a team.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Get("/phones")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedPhoneNumbers(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Query() pagination: SkipTakePagination
|
||||||
|
): Promise<TeamVerifiedPhonesOutput> {
|
||||||
|
const verifiedPhoneNumbers = await this.verifiedResourcesService.getTeamVerifiedPhoneNumbers(
|
||||||
|
teamId,
|
||||||
|
pagination?.skip ?? 0,
|
||||||
|
pagination?.take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: verifiedPhoneNumbers.map((verifiedPhoneNumber) =>
|
||||||
|
plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get verified email of a team by id.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Get("/emails/:id")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmailById(
|
||||||
|
@Param("id") id: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<TeamVerifiedEmailOutput> {
|
||||||
|
const verifiedEmail = await this.verifiedResourcesService.getTeamVerifiedEmailById(teamId, id);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get verified phone number of a team by id.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Get("/phones/:id")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedPhoneById(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Param("id") id: number
|
||||||
|
): Promise<TeamVerifiedPhoneOutput> {
|
||||||
|
const verifiedPhoneNumber = await this.verifiedResourcesService.getTeamVerifiedPhoneNumberById(
|
||||||
|
teamId,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,7 +149,7 @@ export class TokensRepository {
|
|||||||
this.dbWrite.prisma.refreshToken.create({
|
this.dbWrite.prisma.refreshToken.create({
|
||||||
data: {
|
data: {
|
||||||
// note(Lauris): I am leaving userId because it was before adding ownerId to standardize payload like in the createOAuthTokens function for
|
// note(Lauris): I am leaving userId because it was before adding ownerId to standardize payload like in the createOAuthTokens function for
|
||||||
// backwards compatability.
|
// backwards compatibility.
|
||||||
secret: this.jwtService.signRefreshToken({
|
secret: this.jwtService.signRefreshToken({
|
||||||
clientId,
|
clientId,
|
||||||
ownerId: tokenUserId,
|
ownerId: tokenUserId,
|
||||||
|
|||||||
@@ -317,4 +317,73 @@ export class UsersRepository {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOrgsManagedUserEmailsBySubscriptionId(subscriptionId: string) {
|
||||||
|
return await this.dbRead.prisma.user.findMany({
|
||||||
|
distinct: ["email"],
|
||||||
|
where: {
|
||||||
|
isPlatformManaged: true,
|
||||||
|
profiles: {
|
||||||
|
some: {
|
||||||
|
organization: {
|
||||||
|
platformBilling: {
|
||||||
|
subscriptionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActiveManagedUsersAsHost(subscriptionId: string, startTime: Date, endTime: Date) {
|
||||||
|
return await this.dbRead.prisma.user.findMany({
|
||||||
|
distinct: ["email"],
|
||||||
|
where: {
|
||||||
|
isPlatformManaged: true,
|
||||||
|
profiles: {
|
||||||
|
some: {
|
||||||
|
organization: {
|
||||||
|
platformBilling: {
|
||||||
|
subscriptionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bookings: {
|
||||||
|
some: {
|
||||||
|
userId: { not: null },
|
||||||
|
startTime: {
|
||||||
|
gte: startTime,
|
||||||
|
lte: endTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActiveManagedUsersAsAttendee(managedUsersEmails: string[], startTime: Date, endTime: Date) {
|
||||||
|
return await this.dbRead.prisma.attendee.findMany({
|
||||||
|
distinct: ["email"],
|
||||||
|
where: {
|
||||||
|
email: { in: managedUsersEmails },
|
||||||
|
booking: {
|
||||||
|
startTime: {
|
||||||
|
gte: startTime,
|
||||||
|
lte: endTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+196
@@ -0,0 +1,196 @@
|
|||||||
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
|
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input";
|
||||||
|
import { RequestPhoneVerificationInput } from "@/modules/verified-resources/inputs/request-phone-verification.input";
|
||||||
|
import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input";
|
||||||
|
import { VerifyPhoneInput } from "@/modules/verified-resources/inputs/verify-phone.input";
|
||||||
|
import { RequestEmailVerificationOutput } from "@/modules/verified-resources/outputs/request-email-verification-output";
|
||||||
|
import { RequestPhoneVerificationOutput } from "@/modules/verified-resources/outputs/request-phone-verification-output";
|
||||||
|
import {
|
||||||
|
UserVerifiedEmailOutput,
|
||||||
|
UserVerifiedEmailOutputData,
|
||||||
|
UserVerifiedEmailsOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-email.output";
|
||||||
|
import {
|
||||||
|
UserVerifiedPhoneOutput,
|
||||||
|
UserVerifiedPhoneOutputData,
|
||||||
|
UserVerifiedPhonesOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-phone.output";
|
||||||
|
import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service";
|
||||||
|
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from "@nestjs/common";
|
||||||
|
import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/verified-resources",
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@ApiTags("Verified Resources")
|
||||||
|
export class UserVerifiedResourcesController {
|
||||||
|
constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {}
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request Email Verification Code",
|
||||||
|
description: `Sends a verification code to the email.`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Post("/emails/verification-code/request")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Throttle({ limit: 5, ttl: 60000, blockDuration: 60000, name: "users_verified_resources_emails_requests" })
|
||||||
|
async requestEmailVerificationCode(
|
||||||
|
@Body() body: RequestEmailVerificationInput,
|
||||||
|
@GetUser("username") username: string,
|
||||||
|
@GetUser("locale") locale: string
|
||||||
|
): Promise<RequestEmailVerificationOutput> {
|
||||||
|
const verificationCodeRequest = await this.verifiedResourcesService.requestEmailVerificationCode(
|
||||||
|
{ username, locale },
|
||||||
|
body.email
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request Phone Number Verification Code",
|
||||||
|
description: `Sends a verification code to the phone number.`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Post("/phones/verification-code/request")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "users_verified_resources_phones_requests" })
|
||||||
|
async requestPhoneVerificationCode(
|
||||||
|
@Body() body: RequestPhoneVerificationInput
|
||||||
|
): Promise<RequestPhoneVerificationOutput> {
|
||||||
|
const verificationCodeRequest = await this.verifiedResourcesService.requestPhoneVerificationCode(
|
||||||
|
body.phone
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify an email.",
|
||||||
|
description: `Use code to verify an email`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Post("/emails/verification-code/verify")
|
||||||
|
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "users_verified_resources_phones_verify" })
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyEmail(
|
||||||
|
@Body() body: VerifyEmailInput,
|
||||||
|
@GetUser("id") userId: number
|
||||||
|
): Promise<UserVerifiedEmailOutput> {
|
||||||
|
const verifiedEmail = await this.verifiedResourcesService.verifyEmail(userId, body.email, body.code);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(UserVerifiedEmailOutputData, verifiedEmail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify a phone number.",
|
||||||
|
description: `Use code to verify a phone number`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Post("/phones/verification-code/verify")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyPhoneNumber(
|
||||||
|
@Body() body: VerifyPhoneInput,
|
||||||
|
@GetUser("id") userId: number
|
||||||
|
): Promise<UserVerifiedPhoneOutput> {
|
||||||
|
const verifiedPhone = await this.verifiedResourcesService.verifyPhone(userId, body.phone, body.code);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(UserVerifiedPhoneOutputData, verifiedPhone),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get list of verified emails.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Get("/emails")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmails(
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Query() pagination: SkipTakePagination
|
||||||
|
): Promise<UserVerifiedEmailsOutput> {
|
||||||
|
const verifiedEmails = await this.verifiedResourcesService.getUserVerifiedEmails(
|
||||||
|
userId,
|
||||||
|
pagination?.skip ?? 0,
|
||||||
|
pagination?.take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: verifiedEmails.map((verifiedEmail) => plainToClass(UserVerifiedEmailOutputData, verifiedEmail)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get list of verified phone numbers.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Get("/phones")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedPhoneNumbers(
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Query() pagination: SkipTakePagination
|
||||||
|
): Promise<UserVerifiedPhonesOutput> {
|
||||||
|
const verifiedPhoneNumbers = await this.verifiedResourcesService.getUserVerifiedPhoneNumbers(
|
||||||
|
userId,
|
||||||
|
pagination?.skip ?? 0,
|
||||||
|
pagination?.take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: verifiedPhoneNumbers.map((verifiedPhoneNumber) =>
|
||||||
|
plainToClass(UserVerifiedPhoneOutputData, verifiedPhoneNumber)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get verified email by id.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Get("/emails/:id")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmailById(
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("id") id: number
|
||||||
|
): Promise<UserVerifiedEmailOutput> {
|
||||||
|
const verifiedEmail = await this.verifiedResourcesService.getUserVerifiedEmailById(userId, id);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(UserVerifiedEmailOutputData, verifiedEmail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get verified phone number by id.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Get("/phones/:id")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedPhoneById(
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("id") id: number
|
||||||
|
): Promise<UserVerifiedPhoneOutput> {
|
||||||
|
const verifiedPhoneNumber = await this.verifiedResourcesService.getUserVerifiedPhoneNumberById(
|
||||||
|
userId,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(UserVerifiedPhoneOutputData, verifiedPhoneNumber),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose } from "class-transformer";
|
||||||
|
import { IsEmail } from "class-validator";
|
||||||
|
|
||||||
|
export class RequestEmailVerificationInput {
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
description: "Email to verify.",
|
||||||
|
example: "acme@example.com",
|
||||||
|
})
|
||||||
|
@IsEmail()
|
||||||
|
@Expose()
|
||||||
|
email!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose } from "class-transformer";
|
||||||
|
import { IsPhoneNumber } from "class-validator";
|
||||||
|
|
||||||
|
export class RequestPhoneVerificationInput {
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
description: "Phone number to verify.",
|
||||||
|
example: "+372 5555 6666",
|
||||||
|
})
|
||||||
|
@IsPhoneNumber()
|
||||||
|
@Expose()
|
||||||
|
phone!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose } from "class-transformer";
|
||||||
|
import { IsEmail, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class VerifyEmailInput {
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
description: "Email to verify.",
|
||||||
|
example: "example@acme.com",
|
||||||
|
})
|
||||||
|
@IsEmail()
|
||||||
|
@Expose()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
description: "verification code sent to the email to verify",
|
||||||
|
example: "1ABG2C",
|
||||||
|
})
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
code!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose } from "class-transformer";
|
||||||
|
import { IsEmail, IsPhoneNumber, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class VerifyPhoneInput {
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
description: "phone number to verify.",
|
||||||
|
example: "+37255556666",
|
||||||
|
})
|
||||||
|
@IsPhoneNumber()
|
||||||
|
@Expose()
|
||||||
|
phone!: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
description: "verification code sent to the phone number to verify",
|
||||||
|
example: "1ABG2C",
|
||||||
|
})
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
code!: string;
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEnum } from "class-validator";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class RequestEmailVerificationOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEnum } from "class-validator";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class RequestPhoneVerificationOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmptyObject,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class UserVerifiedEmailOutputData {
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The unique identifier for the verified email.",
|
||||||
|
example: 789,
|
||||||
|
})
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The verified email address.",
|
||||||
|
example: "user@example.com",
|
||||||
|
format: "email",
|
||||||
|
})
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The ID of the associated user, if applicable.",
|
||||||
|
example: 45,
|
||||||
|
})
|
||||||
|
userId!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamVerifiedEmailOutputData {
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The unique identifier for the verified email.",
|
||||||
|
example: 789,
|
||||||
|
})
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The verified email address.",
|
||||||
|
example: "user@example.com",
|
||||||
|
format: "email",
|
||||||
|
})
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The ID of the associated team, if applicable.",
|
||||||
|
example: 89,
|
||||||
|
})
|
||||||
|
teamId!: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The ID of the associated user, if applicable.",
|
||||||
|
example: 45,
|
||||||
|
nullable: true,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
userId?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamVerifiedEmailOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@Type(() => TeamVerifiedEmailOutputData)
|
||||||
|
data!: TeamVerifiedEmailOutputData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserVerifiedEmailOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@Type(() => UserVerifiedEmailOutputData)
|
||||||
|
data!: UserVerifiedEmailOutputData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamVerifiedEmailsOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => TeamVerifiedEmailOutputData)
|
||||||
|
data!: TeamVerifiedEmailOutputData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserVerifiedEmailsOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => UserVerifiedEmailOutputData)
|
||||||
|
data!: UserVerifiedEmailOutputData[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmptyObject,
|
||||||
|
IsNumber,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
IsOptional,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class UserVerifiedPhoneOutputData {
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The unique identifier for the verified email.",
|
||||||
|
example: 789,
|
||||||
|
})
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The verified phone number.",
|
||||||
|
example: "+37255556666",
|
||||||
|
format: "phone",
|
||||||
|
})
|
||||||
|
phoneNumber!: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The ID of the associated user, if applicable.",
|
||||||
|
example: 45,
|
||||||
|
})
|
||||||
|
userId!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamVerifiedPhoneOutputData {
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The unique identifier for the verified email.",
|
||||||
|
example: 789,
|
||||||
|
})
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The verified phone number.",
|
||||||
|
example: "+37255556666",
|
||||||
|
format: "phone",
|
||||||
|
})
|
||||||
|
phoneNumber!: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The ID of the associated user, if applicable.",
|
||||||
|
example: 45,
|
||||||
|
nullable: true,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
userId?: number | null;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The ID of the associated team, if applicable.",
|
||||||
|
example: 89,
|
||||||
|
})
|
||||||
|
teamId!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserVerifiedPhoneOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@Type(() => UserVerifiedPhoneOutputData)
|
||||||
|
data!: UserVerifiedPhoneOutputData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamVerifiedPhoneOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@Type(() => TeamVerifiedPhoneOutputData)
|
||||||
|
data!: TeamVerifiedPhoneOutputData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserVerifiedPhonesOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => UserVerifiedPhoneOutputData)
|
||||||
|
data!: UserVerifiedPhoneOutputData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamVerifiedPhonesOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => TeamVerifiedPhoneOutputData)
|
||||||
|
data!: TeamVerifiedPhoneOutputData[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { TeamsVerifiedResourcesRepository } from "@/modules/verified-resources/teams-verified-resources.repository";
|
||||||
|
import { UsersVerifiedResourcesRepository } from "@/modules/verified-resources/users-verified-resources.repository";
|
||||||
|
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
verifyPhoneNumber,
|
||||||
|
sendVerificationCode as sendPhoneVerificationCode,
|
||||||
|
} from "@calcom/platform-libraries";
|
||||||
|
import { sendEmailVerificationByCode, verifyEmailCodeHandler } from "@calcom/platform-libraries/emails";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VerifiedResourcesService {
|
||||||
|
constructor(
|
||||||
|
private readonly usersVerifiedResourcesRepository: UsersVerifiedResourcesRepository,
|
||||||
|
private readonly teamsVerifiedResourcesRepository: TeamsVerifiedResourcesRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async requestEmailVerificationCode(user: { username: string; locale: string }, email: string) {
|
||||||
|
const res = await sendEmailVerificationByCode({
|
||||||
|
email: email,
|
||||||
|
language: user.locale,
|
||||||
|
username: user.username,
|
||||||
|
isVerifyingEmail: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok && !res.skipped) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new BadRequestException("Email could not be verified.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.skipped) {
|
||||||
|
throw new ConflictException("Email is already verified.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestPhoneVerificationCode(phone: string) {
|
||||||
|
try {
|
||||||
|
await sendPhoneVerificationCode(phone);
|
||||||
|
} catch (err) {
|
||||||
|
throw new BadRequestException("Could not send verification code to this phone number.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyPhone(userId: number, phone: string, code: string, teamId?: number) {
|
||||||
|
const result = await verifyPhoneNumber(phone, code, userId, teamId);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
return teamId
|
||||||
|
? await this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumber(userId, phone, teamId)
|
||||||
|
: await this.usersVerifiedResourcesRepository.getUserVerifiedPhoneNumber(userId, phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestException("Could not verify phone number.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyEmail(userId: number, email: string, code: string, teamId?: number) {
|
||||||
|
const isValidToken = await verifyEmailCodeHandler({
|
||||||
|
ctx: { user: { id: userId } },
|
||||||
|
input: { email, code, teamId },
|
||||||
|
});
|
||||||
|
if (isValidToken) {
|
||||||
|
const verifiedEmail = teamId
|
||||||
|
? await this.teamsVerifiedResourcesRepository.getTeamVerifiedEmail(userId, email, teamId)
|
||||||
|
: await this.usersVerifiedResourcesRepository.getUserVerifiedEmail(userId, email);
|
||||||
|
return verifiedEmail;
|
||||||
|
}
|
||||||
|
throw new BadRequestException("Invalid Verification Code");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedEmailById(userId: number, id: number) {
|
||||||
|
return this.usersVerifiedResourcesRepository.getUserVerifiedEmailById(userId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedEmailById(teamId: number, id: number) {
|
||||||
|
return this.teamsVerifiedResourcesRepository.getTeamVerifiedEmailById(id, teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVerifiedEmail(userId: number, email: string) {
|
||||||
|
return this.usersVerifiedResourcesRepository.getUserVerifiedEmail(userId, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedEmails(userId: number, skip = 0, take = 250) {
|
||||||
|
return this.usersVerifiedResourcesRepository.getUserVerifiedEmails(userId, skip, take);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedEmails(teamId: number, skip = 0, take = 250) {
|
||||||
|
return this.teamsVerifiedResourcesRepository.getTeamVerifiedEmails(teamId, skip, take);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedPhoneNumberById(userId: number, id: number) {
|
||||||
|
return this.usersVerifiedResourcesRepository.getUserVerifiedPhoneNumberById(userId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedPhoneNumberById(teamId: number, id: number) {
|
||||||
|
return this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumberById(id, teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedPhoneNumber(userId: number, phoneNumber: string) {
|
||||||
|
return this.usersVerifiedResourcesRepository.getUserVerifiedEmail(userId, phoneNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedPhoneNumber(userId: number, phoneNumber: string, teamId: number) {
|
||||||
|
return this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumber(userId, phoneNumber, teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedPhoneNumbers(userId: number, skip = 0, take = 250) {
|
||||||
|
return this.usersVerifiedResourcesRepository.getUserVerifiedPhoneNumbers(userId, skip, take);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedPhoneNumbers(teamId: number, skip = 0, take = 250) {
|
||||||
|
return this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumbers(teamId, skip, take);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TeamsVerifiedResourcesRepository {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async getTeamVerifiedEmails(teamId: number, skip = 0, take = 250) {
|
||||||
|
return this.dbRead.prisma.verifiedEmail.findMany({
|
||||||
|
where: {
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedPhoneNumbers(teamId: number, skip = 0, take = 250) {
|
||||||
|
return this.dbRead.prisma.verifiedNumber.findMany({
|
||||||
|
where: {
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedEmail(userId: number, email: string, teamId: number) {
|
||||||
|
return this.dbRead.prisma.verifiedEmail.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedPhoneNumber(userId: number, phone: string, teamId: number) {
|
||||||
|
return this.dbRead.prisma.verifiedNumber.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
phoneNumber: phone,
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedEmailById(id: number, teamId: number) {
|
||||||
|
return this.dbRead.prisma.verifiedEmail.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedPhoneNumberById(id: number, teamId: number) {
|
||||||
|
return this.dbRead.prisma.verifiedNumber.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersVerifiedResourcesRepository {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async getUserVerifiedEmails(userId: number, skip = 0, take = 250) {
|
||||||
|
return this.dbRead.prisma.verifiedEmail.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedPhoneNumber(userId: number, phone: string) {
|
||||||
|
return this.dbRead.prisma.verifiedNumber.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
phoneNumber: phone,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedPhoneNumbers(userId: number, skip = 0, take = 250) {
|
||||||
|
return this.dbRead.prisma.verifiedNumber.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedEmail(userId: number, email: string) {
|
||||||
|
return this.dbRead.prisma.verifiedEmail.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedEmailById(userId: number, id: number) {
|
||||||
|
return this.dbRead.prisma.verifiedEmail.findFirst({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedPhoneNumberById(userId: number, id: number) {
|
||||||
|
return this.dbRead.prisma.verifiedNumber.findFirst({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
|
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||||
|
import { OrgTeamsVerifiedResourcesController } from "@/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
|
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||||
|
import { TeamsVerifiedResourcesController } from "@/modules/teams/verified-resources/teams-verified-resources.controller";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { UserVerifiedResourcesController } from "@/modules/verified-resources/controllers/users-verified-resources.controller";
|
||||||
|
import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service";
|
||||||
|
import { TeamsVerifiedResourcesRepository } from "@/modules/verified-resources/teams-verified-resources.repository";
|
||||||
|
import { UsersVerifiedResourcesRepository } from "@/modules/verified-resources/users-verified-resources.repository";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule, RedisModule],
|
||||||
|
controllers: [
|
||||||
|
UserVerifiedResourcesController,
|
||||||
|
TeamsVerifiedResourcesController,
|
||||||
|
OrgTeamsVerifiedResourcesController,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
VerifiedResourcesService,
|
||||||
|
UsersVerifiedResourcesRepository,
|
||||||
|
TeamsVerifiedResourcesRepository,
|
||||||
|
MembershipsRepository,
|
||||||
|
OrganizationsTeamsRepository,
|
||||||
|
OrganizationsRepository,
|
||||||
|
StripeService,
|
||||||
|
AppsRepository,
|
||||||
|
CredentialsRepository,
|
||||||
|
UsersRepository,
|
||||||
|
],
|
||||||
|
exports: [VerifiedResourcesService],
|
||||||
|
})
|
||||||
|
export class VerifiedResourcesModule {}
|
||||||
+2166
-162
File diff suppressed because it is too large.
Load diff
@@ -13,7 +13,7 @@ export class MembershipRepositoryFixture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(data: Prisma.MembershipCreateInput) {
|
async create(data: Prisma.MembershipCreateInput) {
|
||||||
return this.prismaWriteClient.membership.create({ data });
|
return this.prismaWriteClient.membership.create({ data: { createdAt: new Date(), ...data } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(membershipId: Membership["id"]) {
|
async delete(membershipId: Membership["id"]) {
|
||||||
@@ -30,7 +30,7 @@ export class MembershipRepositoryFixture {
|
|||||||
|
|
||||||
async addUserToOrg(user: User, org: Team, role: MembershipRole, accepted: boolean) {
|
async addUserToOrg(user: User, org: Team, role: MembershipRole, accepted: boolean) {
|
||||||
const membership = await this.prismaWriteClient.membership.create({
|
const membership = await this.prismaWriteClient.membership.create({
|
||||||
data: { teamId: org.id, userId: user.id, role, accepted },
|
data: { createdAt: new Date(), teamId: org.id, userId: user.id, role, accepted },
|
||||||
});
|
});
|
||||||
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
||||||
return membership;
|
return membership;
|
||||||
|
|||||||
Loaded 100 of 533 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user