This commit is contained in:
2026-04-29 14:26:04 +03:30
parent 80ef885d3b
commit 993d809de2
3 changed files with 251 additions and 223 deletions

View File

@@ -1,29 +1,38 @@
/**
* Native workflow status keys from blameCases (`CaseStatus`) or claimCases (`ClaimCaseStatus`),
* plus `all` = total matching documents for the tenant.
* V2-style status buckets (`IN_PROGRESS` + native statuses) for insurer tenant scope.
* `unifiedByPublicId.total` matches merging blame + claim by `publicId` (see insurer `files` API).
*/
export class BlameCaseStatusCountReportDtoRs {
[key: string]: number;
constructor(report: Record<string, number>) {
Object.assign(this, report);
}
}
export class ClaimCaseStatusCountReportDtoRs {
[key: string]: number;
constructor(report: Record<string, number>) {
Object.assign(this, report);
}
}
export class CompanyAllRequestsCountReportDtoRs {
blame: Record<string, number>;
export class InsurerRequestsSummaryDtoRs {
claim: Record<string, number>;
blame: Record<string, number>;
unifiedByPublicId: { total: number };
constructor(blame: Record<string, number>, claim: Record<string, number>) {
this.blame = blame;
constructor(
claim: Record<string, number>,
blame: Record<string, number>,
unifiedTotal: number,
) {
this.claim = claim;
this.blame = blame;
this.unifiedByPublicId = { total: unifiedTotal };
}
}
export class InsurerPerMonthReportRowDtoRs {
stDate: Date;
enDate: Date;
faLabel: string;
data: InsurerRequestsSummaryDtoRs;
constructor(
stDate: Date,
enDate: Date,
faLabel: string,
data: InsurerRequestsSummaryDtoRs,
) {
this.stDate = stDate;
this.enDate = enDate;
this.faLabel = faLabel;
this.data = data;
}
}

View File

@@ -1,8 +1,12 @@
import { Controller, Get, UseGuards } from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { Controller, Get, Query, UseGuards } from "@nestjs/common";
import {
ApiBearerAuth,
ApiOperation,
ApiQuery,
ApiTags,
} from "@nestjs/swagger";
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
import { RolesGuard } from "src/auth/guards/role.guard";
import { ClientKey } from "src/decorators/clientKey.decorator";
import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator";
import { RoleEnum } from "src/Types&Enums/role.enum";
@@ -11,29 +15,56 @@ import { ReportsService } from "./reports.service";
@ApiTags("reports")
@ApiBearerAuth()
@UseGuards(LocalActorAuthGuard, RolesGuard)
@Roles(RoleEnum.DAMAGE_EXPERT, RoleEnum.EXPERT, RoleEnum.COMPANY)
@Roles(RoleEnum.COMPANY)
@Controller("reports")
export class ReportsController {
constructor(private readonly reportsService: ReportsService) {}
@Get("/report/requests")
async getAllRequestsReport(@CurrentUser() actor, @ClientKey() client) {
return await this.reportsService.getAllRequestsReportCount(actor, client);
@Get("report/insurer/requests")
@ApiOperation({
summary: "Insurer: claim/blame status buckets + unified file count",
description:
"Claim and blame counts use the same V2 bucket rules as expert panels (IN_PROGRESS grouping), scoped to the insurer JWT tenant. unifiedByPublicId.total counts distinct publicId values like GET expert-insurer/files.",
})
async getInsurerRequestsSummary(@CurrentUser() actor) {
return await this.reportsService.getInsurerRequestsSummary(actor);
}
@Get("/report/perMonthRequests")
async getAllRequestsReportPerMonth(
@Get("report/insurer/per-month-requests")
@ApiOperation({
summary: "Insurer: same summary as report/insurer/requests, per calendar month",
description:
"Last five calendar months; each slice filters blame/claim rows by createdAt in that month, then claim buckets, blame buckets, and unified publicId count.",
})
async getInsurerRequestsSummaryPerMonth(@CurrentUser() actor) {
return await this.reportsService.getInsurerRequestsSummaryPerMonth(actor);
}
@Get("report/insurer/checked-requests")
@ApiOperation({
summary: "Insurer: summary filtered by optional createdAt range",
description:
"Same payload as report/insurer/requests. When from/to are omitted, all tenant files are included. When provided, only blame/claim rows whose createdAt falls in the range are counted (inclusive).",
})
@ApiQuery({
name: "from",
required: false,
description: "Optional start datetime (ISO string)",
})
@ApiQuery({
name: "to",
required: false,
description: "Optional end datetime (ISO string)",
})
async getInsurerRequestsSummaryByDateRange(
@CurrentUser() actor,
@ClientKey() client,
@Query("from") from?: string,
@Query("to") to?: string,
) {
return await this.reportsService.getAllRequestsReportPerMonth(
return await this.reportsService.getInsurerRequestsSummaryByDateRange(
actor,
client,
from,
to,
);
}
@Get("/report/checkedRequests")
async getAllCheckedRequestsCount(@CurrentUser() actor, @ClientKey() client) {
return await this.reportsService.getAllCheckedRequestsCount(actor, client);
}
}

View File

@@ -1,14 +1,20 @@
import { BadRequestException, Injectable } from "@nestjs/common";
import { Types } from "mongoose";
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
import { requireActorClientKey } from "src/helpers/tenant-scope";
import {
BlameCaseStatusCountReportDtoRs,
ClaimCaseStatusCountReportDtoRs,
CompanyAllRequestsCountReportDtoRs,
blameCaseStatusToReportBucket,
claimCaseStatusToReportBucket,
initialBlameExpertReportBuckets,
initialClaimExpertReportBuckets,
} from "src/helpers/expert-panel-status-report";
import {
blameCaseTouchesClient,
claimCaseTouchesClient,
requireActorClientKey,
} from "src/helpers/tenant-scope";
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import {
InsurerPerMonthReportRowDtoRs,
InsurerRequestsSummaryDtoRs,
} from "./dto/reports.dto";
@Injectable()
@@ -18,180 +24,158 @@ export class ReportsService {
private readonly claimCaseDbService: ClaimCaseDbService,
) {}
private clientObjectId(client: string | undefined): Types.ObjectId {
const ck = requireActorClientKey({ clientKey: client });
return new Types.ObjectId(ck);
private parseDateRange(from?: string, to?: string) {
const fromDate = from ? new Date(from) : undefined;
const toDate = to ? new Date(to) : undefined;
if (fromDate && Number.isNaN(fromDate.getTime())) {
throw new BadRequestException("Invalid 'from' date");
}
if (toDate && Number.isNaN(toDate.getTime())) {
throw new BadRequestException("Invalid 'to' date");
}
if (fromDate && toDate && fromDate > toDate) {
throw new BadRequestException("'from' must be before or equal to 'to'");
}
return { fromDate, toDate };
}
private isBlameForClient(r: any, client: Types.ObjectId): boolean {
const idStr = String(client);
return (r?.parties || []).some(
(p) => String(p?.person?.clientId || "") === idStr,
);
private isInDateRange(
value: unknown,
fromDate?: Date,
toDate?: Date,
): boolean {
if (!fromDate && !toDate) return true;
if (!value) return false;
const d = new Date(value as string | Date);
if (Number.isNaN(d.getTime())) return false;
if (fromDate && d < fromDate) return false;
if (toDate && d > toDate) return false;
return true;
}
private isClaimForClient(r: any, client: Types.ObjectId): boolean {
return String(r?.owner?.userClientKey || "") === String(client);
private publicIdKey(doc: Record<string, unknown>): string {
return String(doc.publicId ?? doc._id ?? "");
}
private countBlameByCaseStatus(
blames: any[],
clientId: Types.ObjectId,
private buildClaimBucketsForInsurer(
rows: Record<string, unknown>[],
clientKey: string,
dateFrom?: Date,
dateTo?: Date,
): Record<string, number> {
const out: Record<string, number> = { all: 0 };
for (const s of Object.values(CaseStatus)) {
out[s] = 0;
const buckets = initialClaimExpertReportBuckets();
for (const doc of rows) {
if (!claimCaseTouchesClient(doc, clientKey)) continue;
if (!this.isInDateRange(doc.createdAt, dateFrom, dateTo)) continue;
const st = String(doc.status ?? "");
const key = claimCaseStatusToReportBucket(st);
buckets.all++;
buckets[key] = (buckets[key] ?? 0) + 1;
}
for (const r of blames) {
if (!this.isBlameForClient(r, clientId)) continue;
const st = r?.status as string;
if (st in out) out[st]++;
else out[st] = (out[st] ?? 0) + 1;
out.all++;
}
return out;
return buckets;
}
private countClaimByCaseStatus(
claims: any[],
clientId: Types.ObjectId,
private buildBlameBucketsForInsurer(
rows: Record<string, unknown>[],
clientKey: string,
dateFrom?: Date,
dateTo?: Date,
): Record<string, number> {
const out: Record<string, number> = { all: 0 };
for (const s of Object.values(ClaimCaseStatus)) {
out[s] = 0;
const buckets = initialBlameExpertReportBuckets();
for (const doc of rows) {
if (!blameCaseTouchesClient(doc, clientKey)) continue;
if (!this.isInDateRange(doc.createdAt, dateFrom, dateTo)) continue;
const st = String(doc.status ?? "");
const key = blameCaseStatusToReportBucket(st);
buckets.all++;
buckets[key] = (buckets[key] ?? 0) + 1;
}
for (const r of claims) {
if (!this.isClaimForClient(r, clientId)) continue;
const st = r?.status as string;
if (st in out) out[st]++;
else out[st] = (out[st] ?? 0) + 1;
out.all++;
}
return out;
}
async getAllRequestsCountByRole(role: string, client: string | undefined) {
const clientId = this.clientObjectId(client);
const [blames, claims] = await Promise.all([
this.blameRequestDbService.find({}, { lean: true }),
this.claimCaseDbService.find({}, { lean: true }),
]);
if (role === "expert") {
return this.countBlameByCaseStatus(blames as any[], clientId);
}
if (role === "damage_expert") {
return this.countClaimByCaseStatus(claims as any[], clientId);
}
if (role === "company") {
return {
blame: this.countBlameByCaseStatus(blames as any[], clientId),
claim: this.countClaimByCaseStatus(claims as any[], clientId),
};
}
return null;
return buckets;
}
/**
* Per calendar month, counts by native `CaseStatus` / `ClaimCaseStatus`.
* Company gets both blame and claim maps; expert / damage_expert get one map.
* One insurer “file” per `publicId`, aligned with `ExpertInsurerService.retrieveAllFilesOfClient`.
*/
async getDateFilteredRequestByStatus(
role: string,
client: string | undefined,
start: Date,
end: Date,
) {
const clientId = this.clientObjectId(client);
private countUnifiedByPublicId(
blameRows: Record<string, unknown>[],
claimRows: Record<string, unknown>[],
clientKey: string,
dateFrom?: Date,
dateTo?: Date,
): number {
const keys = new Set<string>();
for (const doc of blameRows) {
if (!blameCaseTouchesClient(doc, clientKey)) continue;
if (!this.isInDateRange(doc.createdAt, dateFrom, dateTo)) continue;
const k = this.publicIdKey(doc);
if (k) keys.add(k);
}
for (const doc of claimRows) {
if (!claimCaseTouchesClient(doc, clientKey)) continue;
if (!this.isInDateRange(doc.createdAt, dateFrom, dateTo)) continue;
const k = this.publicIdKey(doc);
if (k) keys.add(k);
}
return keys.size;
}
private buildInsurerSummary(
blames: Record<string, unknown>[],
claims: Record<string, unknown>[],
clientKey: string,
dateFrom?: Date,
dateTo?: Date,
): InsurerRequestsSummaryDtoRs {
const claim = this.buildClaimBucketsForInsurer(
claims,
clientKey,
dateFrom,
dateTo,
);
const blame = this.buildBlameBucketsForInsurer(
blames,
clientKey,
dateFrom,
dateTo,
);
const unifiedTotal = this.countUnifiedByPublicId(
blames,
claims,
clientKey,
dateFrom,
dateTo,
);
return new InsurerRequestsSummaryDtoRs(claim, blame, unifiedTotal);
}
async getInsurerRequestsSummary(actor: {
clientKey?: string;
}): Promise<InsurerRequestsSummaryDtoRs> {
const clientKey = requireActorClientKey(actor);
const [blames, claims] = await Promise.all([
this.blameRequestDbService.find({}, { lean: true }),
this.claimCaseDbService.find({}, { lean: true }),
]);
const inRange = (r: any) => {
const createdAt = new Date(r.createdAt);
return createdAt >= start && createdAt <= end;
};
if (role === "company") {
const blameOut: Record<string, number> = { all: 0 };
const claimOut: Record<string, number> = { all: 0 };
for (const s of Object.values(CaseStatus)) blameOut[s] = 0;
for (const s of Object.values(ClaimCaseStatus)) claimOut[s] = 0;
for (const r of blames as any[]) {
if (!this.isBlameForClient(r, clientId) || !inRange(r)) continue;
const st = r?.status as string;
if (st in blameOut) blameOut[st]++;
else blameOut[st] = (blameOut[st] ?? 0) + 1;
blameOut.all++;
}
for (const r of claims as any[]) {
if (!this.isClaimForClient(r, clientId) || !inRange(r)) continue;
const st = r?.status as string;
if (st in claimOut) claimOut[st]++;
else claimOut[st] = (claimOut[st] ?? 0) + 1;
claimOut.all++;
}
return { blame: blameOut, claim: claimOut };
}
if (role === "expert") {
const out: Record<string, number> = { all: 0 };
for (const s of Object.values(CaseStatus)) out[s] = 0;
for (const r of blames as any[]) {
if (!this.isBlameForClient(r, clientId) || !inRange(r)) continue;
const st = r?.status as string;
if (st in out) out[st]++;
else out[st] = (out[st] ?? 0) + 1;
out.all++;
}
return out;
}
if (role === "damage_expert") {
const out: Record<string, number> = { all: 0 };
for (const s of Object.values(ClaimCaseStatus)) out[s] = 0;
for (const r of claims as any[]) {
if (!this.isClaimForClient(r, clientId) || !inRange(r)) continue;
const st = r?.status as string;
if (st in out) out[st]++;
else out[st] = (out[st] ?? 0) + 1;
out.all++;
}
return out;
}
throw new BadRequestException("Unsupported role for reports");
}
async getAllRequestsReportCount(actor: any, client: string | undefined) {
requireActorClientKey(actor);
if (actor.role === "damage_expert") {
const data = await this.getAllRequestsCountByRole(actor.role, client);
return new ClaimCaseStatusCountReportDtoRs(data as Record<string, number>);
}
if (actor.role === "expert") {
const data = await this.getAllRequestsCountByRole(actor.role, client);
return new BlameCaseStatusCountReportDtoRs(data as Record<string, number>);
}
const companyPayload = await this.getAllRequestsCountByRole(
"company",
client,
);
return new CompanyAllRequestsCountReportDtoRs(
(companyPayload as { blame: Record<string, number> }).blame,
(companyPayload as { claim: Record<string, number> }).claim,
return this.buildInsurerSummary(
blames as Record<string, unknown>[],
claims as Record<string, unknown>[],
clientKey,
);
}
async getAllRequestsReportPerMonth(actor: any, client: string | undefined) {
requireActorClientKey(actor);
const result: any[] = [];
async getInsurerRequestsSummaryPerMonth(actor: {
clientKey?: string;
}): Promise<InsurerPerMonthReportRowDtoRs[]> {
const clientKey = requireActorClientKey(actor);
const [blames, claims] = await Promise.all([
this.blameRequestDbService.find({}, { lean: true }),
this.claimCaseDbService.find({}, { lean: true }),
]);
const blameRows = blames as Record<string, unknown>[];
const claimRows = claims as Record<string, unknown>[];
const result: InsurerPerMonthReportRowDtoRs[] = [];
const now = new Date();
for (let i = 0; i < 5; i++) {
@@ -209,35 +193,39 @@ export class ReportsService {
year: "numeric",
});
let data: any;
if (actor.role === "company") {
data = await this.getDateFilteredRequestByStatus(
"company",
client,
monthStart,
monthEnd,
);
} else {
data = await this.getDateFilteredRequestByStatus(
actor.role,
client,
monthStart,
monthEnd,
);
}
const data = this.buildInsurerSummary(
blameRows,
claimRows,
clientKey,
monthStart,
monthEnd,
);
result.unshift({
stDate: monthStart,
enDate: monthEnd,
faLabel: label,
data,
});
result.unshift(
new InsurerPerMonthReportRowDtoRs(monthStart, monthEnd, label, data),
);
}
return result;
}
/** Same aggregates as `GET /reports/report/requests` (native status keys). */
async getAllCheckedRequestsCount(actor: any, client: string | undefined) {
return this.getAllRequestsReportCount(actor, client);
async getInsurerRequestsSummaryByDateRange(
actor: { clientKey?: string },
from?: string,
to?: string,
): Promise<InsurerRequestsSummaryDtoRs> {
const clientKey = requireActorClientKey(actor);
const { fromDate, toDate } = this.parseDateRange(from, to);
const [blames, claims] = await Promise.all([
this.blameRequestDbService.find({}, { lean: true }),
this.claimCaseDbService.find({}, { lean: true }),
]);
return this.buildInsurerSummary(
blames as Record<string, unknown>[],
claims as Record<string, unknown>[],
clientKey,
fromDate,
toDate,
);
}
}