forked from Yara724/api
YARA-854
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user