diff --git a/src/reports/dto/reports.dto.ts b/src/reports/dto/reports.dto.ts index 8864f19..efb11f7 100644 --- a/src/reports/dto/reports.dto.ts +++ b/src/reports/dto/reports.dto.ts @@ -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) { - Object.assign(this, report); - } -} - -export class ClaimCaseStatusCountReportDtoRs { - [key: string]: number; - - constructor(report: Record) { - Object.assign(this, report); - } -} - -export class CompanyAllRequestsCountReportDtoRs { - blame: Record; +export class InsurerRequestsSummaryDtoRs { claim: Record; + blame: Record; + unifiedByPublicId: { total: number }; - constructor(blame: Record, claim: Record) { - this.blame = blame; + constructor( + claim: Record, + blame: Record, + 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; } } diff --git a/src/reports/reports.controller.ts b/src/reports/reports.controller.ts index 592f93c..8b45fa9 100644 --- a/src/reports/reports.controller.ts +++ b/src/reports/reports.controller.ts @@ -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); - } } diff --git a/src/reports/reports.service.ts b/src/reports/reports.service.ts index c82e782..2012e3e 100644 --- a/src/reports/reports.service.ts +++ b/src/reports/reports.service.ts @@ -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 { + return String(doc.publicId ?? doc._id ?? ""); } - private countBlameByCaseStatus( - blames: any[], - clientId: Types.ObjectId, + private buildClaimBucketsForInsurer( + rows: Record[], + clientKey: string, + dateFrom?: Date, + dateTo?: Date, ): Record { - const out: Record = { 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[], + clientKey: string, + dateFrom?: Date, + dateTo?: Date, ): Record { - const out: Record = { 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[], + claimRows: Record[], + clientKey: string, + dateFrom?: Date, + dateTo?: Date, + ): number { + const keys = new Set(); + 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[], + claims: Record[], + 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 { + 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 = { all: 0 }; - const claimOut: Record = { 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 = { 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 = { 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); - } - if (actor.role === "expert") { - const data = await this.getAllRequestsCountByRole(actor.role, client); - return new BlameCaseStatusCountReportDtoRs(data as Record); - } - const companyPayload = await this.getAllRequestsCountByRole( - "company", - client, - ); - return new CompanyAllRequestsCountReportDtoRs( - (companyPayload as { blame: Record }).blame, - (companyPayload as { claim: Record }).claim, + return this.buildInsurerSummary( + blames as Record[], + claims as Record[], + clientKey, ); } - async getAllRequestsReportPerMonth(actor: any, client: string | undefined) { - requireActorClientKey(actor); - const result: any[] = []; + async getInsurerRequestsSummaryPerMonth(actor: { + clientKey?: string; + }): Promise { + 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[]; + const claimRows = claims as Record[]; + + 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 { + 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[], + claims as Record[], + clientKey, + fromDate, + toDate, + ); } }