Files
calendar/packages/features/bookingReport/repositories/PrismaBookingReportRepository.ts
T
Udit TakkarGitHubunknown <>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
a40c3eebd6 feat: group same booking reports together (#26529)
Co-authored-by: unknown <>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-23 16:31:56 +00:00

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 },
});
}
}