1
0
forked from Yara724/api
Files
yara724-api/src/expert-insurer/expert-insurer.controller.ts
2026-07-12 14:07:27 +03:30

403 lines
12 KiB
TypeScript

import {
BadRequestException,
Body,
Controller,
Get,
Param,
Post,
Put,
Query,
UnauthorizedException,
UseGuards,
} from "@nestjs/common";
import {
ApiBearerAuth,
ApiBody,
ApiOperation,
ApiParam,
ApiQuery,
ApiResponse,
ApiTags,
} from "@nestjs/swagger";
import { Types } from "mongoose";
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
import { RolesGuard } from "src/auth/guards/role.guard";
import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator";
import { FileRating } from "src/request-management/entities/schema/request-management.schema";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { ExpertInsurerService } from "./expert-insurer.service";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import { CreateBranchDto } from "src/client/dto/create-branch.dto";
import {
CreateBlameExpertByInsurerDto,
CreateClaimExpertByInsurerDto,
CreateFileMakerByInsurerDto,
CreateFileReviewerByInsurerDto,
CreateFinancialExpertByInsurerDto,
} from "./dto/create-insurer-expert.dto";
@Controller("expert-insurer")
@ApiTags("expert-insurer-panel")
@ApiBearerAuth()
@UseGuards(LocalActorAuthGuard, RolesGuard)
@Roles(RoleEnum.COMPANY)
export class ExpertInsurerController {
constructor(private readonly expertInsurerService: ExpertInsurerService) {}
@Get("branches")
@ApiQuery({ name: "search", required: false, type: String })
@ApiQuery({
name: "from",
required: false,
description: "Optional start datetime (ISO string)",
})
@ApiQuery({
name: "to",
required: false,
description: "Optional end datetime (ISO string)",
})
@ApiQuery({
name: "isActive",
required: false,
description: "Filter active state (true/false)",
})
async getInsuranceBranches(
@CurrentUser() insurer,
@Query("search") search?: string,
@Query("from") from?: string,
@Query("to") to?: string,
@Query("isActive") isActive?: string,
) {
if (!insurer) {
throw new UnauthorizedException("Could not identify the current user.");
}
return await this.expertInsurerService.retrieveInsuranceBranches(
insurer.clientKey,
{ search, from, to, isActive },
);
}
@Post("branches")
@ApiBody({ type: CreateBranchDto })
async addBranch(
@CurrentUser() insurer,
@Body() createBranchDto: CreateBranchDto,
) {
if (!insurer) {
throw new UnauthorizedException("Could not identify the current user.");
}
return await this.expertInsurerService.addBranch(
insurer.clientKey,
createBranchDto,
);
}
@Put("branches/:branchId/status")
@ApiParam({ name: "branchId" })
@ApiBody({
schema: {
type: "object",
properties: { isActive: { type: "boolean" } },
required: ["isActive"],
},
})
async setBranchStatus(
@CurrentUser() insurer,
@Param("branchId") branchId: string,
@Body("isActive") isActive: unknown,
) {
if (!insurer) {
throw new UnauthorizedException("Could not identify the current user.");
}
// Accept native boolean (JSON body) or string coercion (legacy query/form usage)
let active: boolean;
if (typeof isActive === "boolean") {
active = isActive;
} else {
const normalized = String(isActive ?? "").trim().toLowerCase();
if (!["true", "false", "1", "0", "yes", "no"].includes(normalized)) {
throw new BadRequestException("isActive must be a boolean");
}
active = ["true", "1", "yes"].includes(normalized);
}
return this.expertInsurerService.setBranchActive(
insurer.clientKey,
branchId,
active,
);
}
@Post("experts/blame")
@ApiBody({ type: CreateBlameExpertByInsurerDto })
async addBlameExpert(
@CurrentUser() insurer,
@Body() body: CreateBlameExpertByInsurerDto,
) {
if (!insurer) {
throw new UnauthorizedException("Could not identify the current user.");
}
return this.expertInsurerService.addBlameExpert(insurer.clientKey, body);
}
@Post("experts/claim")
@ApiBody({ type: CreateClaimExpertByInsurerDto })
async addClaimExpert(
@CurrentUser() insurer,
@Body() body: CreateClaimExpertByInsurerDto,
) {
if (!insurer) {
throw new UnauthorizedException("Could not identify the current user.");
}
return this.expertInsurerService.addClaimExpert(insurer.clientKey, body);
}
@Post("experts/file-maker")
@ApiBody({ type: CreateFileMakerByInsurerDto })
@ApiOperation({ summary: "Create a FileMaker account under this insurer" })
async addFileMaker(
@CurrentUser() insurer,
@Body() body: CreateFileMakerByInsurerDto,
) {
if (!insurer) {
throw new UnauthorizedException("Could not identify the current user.");
}
return this.expertInsurerService.addFileMaker(insurer.clientKey, body);
}
@Post("experts/file-reviewer")
@ApiBody({ type: CreateFileReviewerByInsurerDto })
@ApiOperation({ summary: "Create a FileReviewer account under this insurer" })
async addFileReviewer(
@CurrentUser() insurer,
@Body() body: CreateFileReviewerByInsurerDto,
) {
if (!insurer) {
throw new UnauthorizedException("Could not identify the current user.");
}
return this.expertInsurerService.addFileReviewer(insurer.clientKey, body);
}
@Post("experts/financial-expert")
@ApiBody({ type: CreateFinancialExpertByInsurerDto })
@ApiOperation({
summary: "Create a FinancialExpert account under this insurer (V5 flow)",
description:
"FinancialExperts are the final approvers in the V5 blame flow before fanavaran submission.",
})
async addFinancialExpert(
@CurrentUser() insurer,
@Body() body: CreateFinancialExpertByInsurerDto,
) {
if (!insurer) {
throw new UnauthorizedException("Could not identify the current user.");
}
return this.expertInsurerService.addFinancialExpert(insurer.clientKey, body);
}
@ApiQuery({ name: "page", type: Number })
@ApiQuery({ name: "response_count", type: Number })
@Get("experts/list")
async getAllExperts(
@Query("page") page: number,
@Query("response_count") count: number,
@CurrentUser() actor,
) {
return await this.expertInsurerService.retrieveAllExpertsOfClient(
actor,
page,
count,
);
}
@Get("experts/top")
@ApiOperation({
summary: "Top blame vs claim experts for this insurer",
description:
"Response has two arrays: `blameExperts` (roster from expert / blame files) and `claimExperts` (damage-expert roster / claim files). Each item includes `overallAverageRating` derived from ratings stored on those files plus user ratings where present.",
})
async getTopExperts(@CurrentUser() actor) {
return await this.expertInsurerService.getTopExpertsForClient(actor);
}
@Get("files")
@ApiOperation({
summary: "List insurer files (blame + claim merged by publicId)",
description:
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus`, `fileType` (THIRD_PARTY | CAR_BODY).",
})
async getAllFiles(
@CurrentUser() insurer,
@Query() query: ListQueryV2Dto,
) {
return await this.expertInsurerService.retrieveAllFilesOfClient(
insurer.clientKey,
query,
);
}
@Get("report/unified-file-statuses")
@ApiOperation({
summary: "Unified file status catalog + counts (blame + claim)",
description:
"Calculatable unified statuses and per-status counts for this insurer's files. Filter lists with `unifiedStatus` and `fileType`. Optional `from` / `to` ISO dates narrow the counted portfolio.",
})
@ApiResponse({ status: 200, type: UnifiedFileStatusReportDto })
async getUnifiedFileStatusReport(
@CurrentUser() actor,
@Query() query: UnifiedFileStatusReportQueryDto,
): Promise<UnifiedFileStatusReportDto> {
return await this.expertInsurerService.getInsurerUnifiedFileStatusReport(
actor,
query,
);
}
@Get("files/:publicId")
@ApiParam({ name: "publicId" })
async getFileDetailsByPublicId(
@CurrentUser() insurer,
@Param("publicId") publicId: string,
) {
return await this.expertInsurerService.retrieveFileDetailsByPublicId(
insurer.clientKey,
publicId,
);
}
@ApiBody({
description:
"One insurer rating for the shared publicId; persisted on claim and/or blame case documents when both exist.",
schema: {
type: "object",
properties: {
collisionMethodAccuracy: {
type: "number",
minimum: 0,
maximum: 5,
example: 4,
description: "تشخیص درست نحوه برخورد",
},
evaluationTimeliness: {
type: "number",
minimum: 0,
maximum: 5,
example: 3,
description: "زمان ارزیابی",
},
accidentCauseAccuracy: {
type: "number",
minimum: 0,
maximum: 5,
example: 5,
description: "تشخیص درست علت تصادف",
},
guiltyVehicleIdentification: {
type: "number",
minimum: 0,
maximum: 5,
example: 4,
description: "تشخیص درست وسیله نقلیه مقصر",
},
botRating: {
type: "number",
minimum: 0,
maximum: 5,
example: 4,
description: "برای امتیاز دادن به عملکرد بات",
},
},
required: [
"collisionMethodAccuracy",
"evaluationTimeliness",
"accidentCauseAccuracy",
"guiltyVehicleIdentification",
"botRating",
],
example: {
collisionMethodAccuracy: 4,
evaluationTimeliness: 3,
accidentCauseAccuracy: 5,
guiltyVehicleIdentification: 4,
botRating: 4,
},
},
})
@ApiParam({ name: "publicId" })
@Put("files/:publicId/rating")
async rateExpertsByPublicId(
@CurrentUser() insurer,
@Param("publicId") publicId: string,
@Body() rating: FileRating,
) {
return await this.expertInsurerService.rateExpertByPublicId(
publicId,
rating,
insurer.clientKey,
);
}
@Get("top-files")
async getTopFiles(@CurrentUser() insurer) {
return await this.expertInsurerService.getTopFilesForClient(
insurer.clientKey,
);
}
@Get("statistics")
async getExpertStatistics(@CurrentUser() actor) {
return await this.expertInsurerService.getExpertStatisticsReport(actor);
}
@Get("report/status-counts")
@ApiOperation({
summary: "Legacy status counts (mapped from unified statuses)",
deprecated: true,
description: "Prefer GET report/unified-file-statuses for full calculatable blame+claim statuses.",
})
@ApiQuery({
name: "from",
required: false,
description: "Optional start datetime (ISO string)",
})
@ApiQuery({
name: "to",
required: false,
description: "Optional end datetime (ISO string)",
})
async getInsurerStatusReport(
@CurrentUser() actor,
@Query("from") from?: string,
@Query("to") to?: string,
) {
return await this.expertInsurerService.getInsurerFileStatusCounts(
actor,
from,
to,
);
}
@ApiParam({ name: "expertId" })
@ApiOperation({
summary: "Files handled by one roster expert (summary rows)",
description:
"Resolves id against this insurer's `expert` then `damage-expert` roster. Blame branch runs when the id is on the expert roster (field experts). Each item is a small summary (`kind`, ids, statuses, dates)—not full file payloads. Claim matches use final or draft damage-expert reply.",
})
@Get("/:expertId")
async requestDetail(@CurrentUser() insurer, @Param("expertId") id: string) {
if (!Types.ObjectId.isValid(id)) {
throw new BadRequestException("Invalid expert ID");
}
return await this.expertInsurerService.getAllFilesForInsurerExpert(
id,
insurer.clientKey,
);
}
}