a40c3eebd6
Co-authored-by: unknown <> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
412 lines
13 KiB
TypeScript
412 lines
13 KiB
TypeScript
import type { PrismaClient } from "@calcom/prisma";
|
|
import type { BookingReportStatus, SystemReportStatus } from "@calcom/prisma/enums";
|
|
import type { Prisma } from "@calcom/prisma/generated/prisma/client";
|
|
|
|
import type {
|
|
IBookingReportRepository,
|
|
CreateBookingReportInput,
|
|
BookingReportWithDetails,
|
|
GroupedBookingReportWithDetails,
|
|
ListBookingReportsFilters,
|
|
SystemBookingReportsFilters,
|
|
} from "./IBookingReportRepository";
|
|
|
|
export class PrismaBookingReportRepository implements IBookingReportRepository {
|
|
constructor(private readonly prismaClient: PrismaClient) {}
|
|
|
|
private buildWhereClause(params: {
|
|
organizationId?: number;
|
|
searchTerm?: string;
|
|
filters?: ListBookingReportsFilters;
|
|
systemFilters?: SystemBookingReportsFilters;
|
|
}): Prisma.BookingReportWhereInput {
|
|
const where: Prisma.BookingReportWhereInput = {};
|
|
|
|
if (params.organizationId) {
|
|
where.organizationId = params.organizationId;
|
|
}
|
|
|
|
if (params.searchTerm) {
|
|
where.OR = [
|
|
{ bookerEmail: { contains: params.searchTerm, mode: "insensitive" } },
|
|
{ reportedBy: { email: { contains: params.searchTerm, mode: "insensitive" } } },
|
|
];
|
|
}
|
|
|
|
if (params.filters?.hasWatchlist !== undefined) {
|
|
where.watchlistId = params.filters.hasWatchlist ? { not: null } : null;
|
|
}
|
|
|
|
if (params.filters?.reason && params.filters.reason.length > 0) {
|
|
where.reason = { in: params.filters.reason };
|
|
}
|
|
|
|
if (params.filters?.cancelled !== undefined) {
|
|
where.cancelled = params.filters.cancelled;
|
|
}
|
|
|
|
if (params.filters?.status && params.filters.status.length > 0) {
|
|
where.status = { in: params.filters.status };
|
|
}
|
|
|
|
if (params.systemFilters?.systemStatus && params.systemFilters.systemStatus.length > 0) {
|
|
where.systemStatus = { in: params.systemFilters.systemStatus };
|
|
}
|
|
|
|
return where;
|
|
}
|
|
|
|
private readonly reportSelect = {
|
|
id: true,
|
|
bookingUid: true,
|
|
bookerEmail: true,
|
|
reportedById: true,
|
|
reason: true,
|
|
description: true,
|
|
cancelled: true,
|
|
createdAt: true,
|
|
status: true,
|
|
systemStatus: true,
|
|
watchlistId: true,
|
|
globalWatchlistId: true,
|
|
organizationId: true,
|
|
reportedBy: { select: { id: true, email: true, name: true } },
|
|
booking: { select: { id: true, uid: true, title: true, startTime: true, endTime: true } },
|
|
watchlist: { select: { id: true, type: true, value: true, action: true, description: true } },
|
|
globalWatchlist: { select: { id: true, type: true, value: true, action: true, description: true } },
|
|
organization: { select: { id: true, name: true, slug: true } },
|
|
} as const;
|
|
|
|
async createReport(input: CreateBookingReportInput): Promise<{ id: string }> {
|
|
const report = await this.prismaClient.bookingReport.create({
|
|
data: {
|
|
bookingUid: input.bookingUid,
|
|
bookerEmail: input.bookerEmail,
|
|
reportedById: input.reportedById,
|
|
reason: input.reason,
|
|
description: input.description,
|
|
cancelled: input.cancelled,
|
|
organizationId: input.organizationId,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
return report;
|
|
}
|
|
|
|
async findAllReportedBookings(params: {
|
|
organizationId?: number;
|
|
skip?: number;
|
|
take?: number;
|
|
searchTerm?: string;
|
|
filters?: ListBookingReportsFilters;
|
|
systemFilters?: SystemBookingReportsFilters;
|
|
}): Promise<{
|
|
rows: BookingReportWithDetails[];
|
|
meta: { totalRowCount: number };
|
|
}> {
|
|
const where = this.buildWhereClause(params);
|
|
|
|
if (params.systemFilters?.systemStatus && params.systemFilters.systemStatus.length > 0) {
|
|
where.systemStatus = { in: params.systemFilters.systemStatus };
|
|
}
|
|
|
|
const [reports, totalCount] = await Promise.all([
|
|
this.prismaClient.bookingReport.findMany({
|
|
where,
|
|
skip: params.skip,
|
|
take: params.take,
|
|
select: this.reportSelect,
|
|
orderBy: { createdAt: "desc" },
|
|
}),
|
|
this.prismaClient.bookingReport.count({ where }),
|
|
]);
|
|
|
|
const rows = reports.map((report) => ({ ...report, reporter: report.reportedBy }));
|
|
|
|
return { rows, meta: { totalRowCount: totalCount } };
|
|
}
|
|
|
|
async findGroupedReportedBookings(params: {
|
|
organizationId?: number;
|
|
skip?: number;
|
|
take?: number;
|
|
searchTerm?: string;
|
|
filters?: ListBookingReportsFilters;
|
|
systemFilters?: SystemBookingReportsFilters;
|
|
sortBy?: "createdAt" | "reportCount";
|
|
}): Promise<{
|
|
rows: GroupedBookingReportWithDetails[];
|
|
meta: { totalRowCount: number };
|
|
}> {
|
|
const where = this.buildWhereClause(params);
|
|
|
|
const orderBy =
|
|
params.sortBy === "reportCount"
|
|
? { _count: { bookerEmail: "desc" as const } }
|
|
: { _max: { createdAt: "desc" as const } };
|
|
|
|
const groupedEmails = await this.prismaClient.bookingReport.groupBy({
|
|
by: ["bookerEmail"],
|
|
where,
|
|
_count: { bookerEmail: true },
|
|
_max: { createdAt: true },
|
|
orderBy,
|
|
});
|
|
|
|
const totalRowCount = groupedEmails.length;
|
|
const paginatedEmails = groupedEmails.slice(params.skip ?? 0, (params.skip ?? 0) + (params.take ?? 25));
|
|
const emailList = paginatedEmails.map((g) => g.bookerEmail);
|
|
|
|
if (emailList.length === 0) {
|
|
return { rows: [], meta: { totalRowCount } };
|
|
}
|
|
|
|
const reports = await this.prismaClient.bookingReport.findMany({
|
|
where: { ...where, bookerEmail: { in: emailList } },
|
|
select: this.reportSelect,
|
|
orderBy: { createdAt: "asc" },
|
|
});
|
|
|
|
const reportsByEmail = new Map<string, BookingReportWithDetails[]>();
|
|
for (const report of reports) {
|
|
const mapped: BookingReportWithDetails = { ...report, reporter: report.reportedBy };
|
|
const existing = reportsByEmail.get(report.bookerEmail) || [];
|
|
existing.push(mapped);
|
|
reportsByEmail.set(report.bookerEmail, existing);
|
|
}
|
|
|
|
const rows: GroupedBookingReportWithDetails[] = paginatedEmails.map((group) => {
|
|
const emailReports = reportsByEmail.get(group.bookerEmail) || [];
|
|
const firstReport = emailReports[0];
|
|
return { ...firstReport, reportCount: group._count.bookerEmail, reports: emailReports };
|
|
});
|
|
|
|
return { rows, meta: { totalRowCount } };
|
|
}
|
|
|
|
async findReportsByIds(params: {
|
|
reportIds: string[];
|
|
organizationId?: number;
|
|
}): Promise<BookingReportWithDetails[]> {
|
|
const where: Prisma.BookingReportWhereInput = { id: { in: params.reportIds } };
|
|
|
|
if (params.organizationId) {
|
|
where.organizationId = params.organizationId;
|
|
}
|
|
|
|
const reports = await this.prismaClient.bookingReport.findMany({
|
|
where,
|
|
select: this.reportSelect,
|
|
});
|
|
|
|
return reports.map((report) => ({ ...report, reporter: report.reportedBy }));
|
|
}
|
|
|
|
async linkWatchlistToReport(params: { reportId: string; watchlistId: string }): Promise<void> {
|
|
await this.prismaClient.bookingReport.update({
|
|
where: { id: params.reportId },
|
|
data: { watchlistId: params.watchlistId },
|
|
});
|
|
}
|
|
|
|
async updateReportStatus(params: {
|
|
reportId: string;
|
|
status: BookingReportStatus;
|
|
organizationId?: number;
|
|
}): Promise<void> {
|
|
const where: Prisma.BookingReportWhereInput = {
|
|
id: params.reportId,
|
|
};
|
|
|
|
if (params.organizationId) {
|
|
where.organizationId = params.organizationId;
|
|
}
|
|
|
|
await this.prismaClient.bookingReport.updateMany({
|
|
where,
|
|
data: { status: params.status },
|
|
});
|
|
}
|
|
|
|
async bulkLinkWatchlistWithStatus(params: {
|
|
links: Array<{ reportId: string; watchlistId: string }>;
|
|
status: BookingReportStatus;
|
|
}): Promise<void> {
|
|
if (params.links.length === 0) return;
|
|
|
|
const groupedByWatchlist = new Map<string, string[]>();
|
|
for (const link of params.links) {
|
|
const reportIds = groupedByWatchlist.get(link.watchlistId) || [];
|
|
reportIds.push(link.reportId);
|
|
groupedByWatchlist.set(link.watchlistId, reportIds);
|
|
}
|
|
|
|
await Promise.all(
|
|
Array.from(groupedByWatchlist.entries()).map(([watchlistId, reportIds]) =>
|
|
this.prismaClient.bookingReport.updateMany({
|
|
where: { id: { in: reportIds } },
|
|
data: { watchlistId, status: params.status },
|
|
})
|
|
)
|
|
);
|
|
}
|
|
|
|
async bulkLinkGlobalWatchlistWithSystemStatus(params: {
|
|
links: Array<{ reportId: string; globalWatchlistId: string }>;
|
|
systemStatus: SystemReportStatus;
|
|
}): Promise<void> {
|
|
if (params.links.length === 0) return;
|
|
|
|
const groupedByWatchlist = new Map<string, string[]>();
|
|
for (const link of params.links) {
|
|
const reportIds = groupedByWatchlist.get(link.globalWatchlistId) || [];
|
|
reportIds.push(link.reportId);
|
|
groupedByWatchlist.set(link.globalWatchlistId, reportIds);
|
|
}
|
|
|
|
await Promise.all(
|
|
Array.from(groupedByWatchlist.entries()).map(([globalWatchlistId, reportIds]) =>
|
|
this.prismaClient.bookingReport.updateMany({
|
|
where: { id: { in: reportIds } },
|
|
data: { globalWatchlistId, systemStatus: params.systemStatus },
|
|
})
|
|
)
|
|
);
|
|
}
|
|
|
|
async countPendingReports(params: { organizationId: number }): Promise<number> {
|
|
return this.prismaClient.bookingReport.count({
|
|
where: {
|
|
organizationId: params.organizationId,
|
|
status: "PENDING",
|
|
watchlistId: null,
|
|
},
|
|
});
|
|
}
|
|
|
|
async updateSystemReportStatus(params: {
|
|
reportId: string;
|
|
systemStatus: SystemReportStatus;
|
|
globalWatchlistId?: string | null;
|
|
}): Promise<void> {
|
|
await this.prismaClient.bookingReport.update({
|
|
where: { id: params.reportId },
|
|
data: {
|
|
systemStatus: params.systemStatus,
|
|
...(params.globalWatchlistId !== undefined && { globalWatchlistId: params.globalWatchlistId }),
|
|
},
|
|
});
|
|
}
|
|
|
|
async bulkUpdateSystemReportStatus(params: {
|
|
reportIds: string[];
|
|
systemStatus: SystemReportStatus;
|
|
globalWatchlistId?: string | null;
|
|
}): Promise<{ updated: number }> {
|
|
const result = await this.prismaClient.bookingReport.updateMany({
|
|
where: { id: { in: params.reportIds } },
|
|
data: {
|
|
systemStatus: params.systemStatus,
|
|
...(params.globalWatchlistId !== undefined && { globalWatchlistId: params.globalWatchlistId }),
|
|
},
|
|
});
|
|
return { updated: result.count };
|
|
}
|
|
|
|
async countSystemPendingReports(): Promise<number> {
|
|
return this.prismaClient.bookingReport.count({
|
|
where: {
|
|
systemStatus: "PENDING",
|
|
},
|
|
});
|
|
}
|
|
|
|
async dismissReportsByEmail(params: {
|
|
email: string;
|
|
status: BookingReportStatus;
|
|
organizationId: number;
|
|
}): Promise<{ count: number }> {
|
|
const result = await this.prismaClient.bookingReport.updateMany({
|
|
where: {
|
|
bookerEmail: { equals: params.email, mode: "insensitive" },
|
|
watchlistId: null,
|
|
status: "PENDING",
|
|
organizationId: params.organizationId,
|
|
},
|
|
data: { status: params.status },
|
|
});
|
|
return { count: result.count };
|
|
}
|
|
|
|
async dismissSystemReportsByEmail(params: {
|
|
email: string;
|
|
systemStatus: SystemReportStatus;
|
|
}): Promise<{ count: number }> {
|
|
const result = await this.prismaClient.bookingReport.updateMany({
|
|
where: {
|
|
bookerEmail: { equals: params.email, mode: "insensitive" },
|
|
globalWatchlistId: null,
|
|
systemStatus: "PENDING",
|
|
},
|
|
data: { systemStatus: params.systemStatus },
|
|
});
|
|
return { count: result.count };
|
|
}
|
|
|
|
async findPendingReportsByEmail(params: {
|
|
email: string;
|
|
organizationId: number;
|
|
}): Promise<Array<{ id: string; bookerEmail: string; watchlistId: string | null }>> {
|
|
return this.prismaClient.bookingReport.findMany({
|
|
where: {
|
|
bookerEmail: { equals: params.email, mode: "insensitive" },
|
|
status: "PENDING",
|
|
watchlistId: null,
|
|
organizationId: params.organizationId,
|
|
},
|
|
select: { id: true, bookerEmail: true, watchlistId: true },
|
|
});
|
|
}
|
|
|
|
async findPendingReportsByDomain(params: {
|
|
domain: string;
|
|
organizationId: number;
|
|
}): Promise<Array<{ id: string; bookerEmail: string; watchlistId: string | null }>> {
|
|
return this.prismaClient.bookingReport.findMany({
|
|
where: {
|
|
bookerEmail: { endsWith: `@${params.domain}`, mode: "insensitive" },
|
|
status: "PENDING",
|
|
watchlistId: null,
|
|
organizationId: params.organizationId,
|
|
},
|
|
select: { id: true, bookerEmail: true, watchlistId: true },
|
|
});
|
|
}
|
|
|
|
async findPendingSystemReportsByEmail(params: {
|
|
email: string;
|
|
}): Promise<Array<{ id: string; bookerEmail: string; globalWatchlistId: string | null }>> {
|
|
return this.prismaClient.bookingReport.findMany({
|
|
where: {
|
|
bookerEmail: { equals: params.email, mode: "insensitive" },
|
|
systemStatus: "PENDING",
|
|
globalWatchlistId: null,
|
|
},
|
|
select: { id: true, bookerEmail: true, globalWatchlistId: true },
|
|
});
|
|
}
|
|
|
|
async findPendingSystemReportsByDomain(params: {
|
|
domain: string;
|
|
}): Promise<Array<{ id: string; bookerEmail: string; globalWatchlistId: string | null }>> {
|
|
return this.prismaClient.bookingReport.findMany({
|
|
where: {
|
|
bookerEmail: { endsWith: `@${params.domain}`, mode: "insensitive" },
|
|
systemStatus: "PENDING",
|
|
globalWatchlistId: null,
|
|
},
|
|
select: { id: true, bookerEmail: true, globalWatchlistId: true },
|
|
});
|
|
}
|
|
}
|