From bcedd8c6f3ce77a8ecdd47eedb9913c4f1caf8c1 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Tue, 28 Apr 2026 10:01:45 +0330 Subject: [PATCH 1/7] Added GET car-other-parts in v2 as well; deprecated old endpoints and reordered swagger documents --- .../claim-request-management.controller.ts | 22 +++++++++++++++++- .../claim-request-management.v2.controller.ts | 23 +++++++++++++++++++ src/expert-blame/expert-blame.controller.ts | 11 +++++++++ src/expert-claim/expert-claim.controller.ts | 14 +++++++++++ src/main.ts | 10 ++++++++ .../expert-initiated.controller.ts | 11 +++++++-- .../request-management.controller.ts | 20 ++++++++++++---- 7 files changed, 104 insertions(+), 7 deletions(-) diff --git a/src/claim-request-management/claim-request-management.controller.ts b/src/claim-request-management/claim-request-management.controller.ts index 9f2619c..ed386cb 100644 --- a/src/claim-request-management/claim-request-management.controller.ts +++ b/src/claim-request-management/claim-request-management.controller.ts @@ -22,9 +22,9 @@ import { ApiParam, ApiQuery, ApiTags, + ApiOperation, } from "@nestjs/swagger"; import { diskStorage } from "multer"; -import { GlobalGuard } from "src/auth/guards/global.guard"; import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard"; import { RolesGuard } from "src/auth/guards/role.guard"; import { Roles } from "src/decorators/roles.decorator"; @@ -49,6 +49,7 @@ export class ClaimRequestManagementController { ) {} @ApiParam({ name: "blameId" }) + @ApiOperation({ deprecated: true }) @Post("/:blameId") async createClaimRequest( @Param("blameId") requestId: string, @@ -62,6 +63,7 @@ export class ClaimRequestManagementController { } @ApiBody({ type: CarDamagePartDto }) + @ApiOperation({ deprecated: true }) @Patch("/car-part-damage/:claimRequestID") @ApiParam({ name: "claimRequestID" }) async carPartDamage( @@ -76,6 +78,7 @@ export class ClaimRequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Get("/car-other-part") async getCarOtherParts() { const carOtherPart = await readFile( @@ -104,6 +107,7 @@ export class ClaimRequestManagementController { }), ) @ApiConsumes("multipart/form-data") + @ApiOperation({ deprecated: true }) @Patch("/car-other-part-damage/:claimRequestID") async carOtherPartDamage( @Param("claimRequestID") requestId: string, @@ -119,6 +123,7 @@ export class ClaimRequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Get("required-documents-status/:claimRequestID") @ApiParam({ name: "claimRequestID" }) async getRequiredDocumentsStatus( @@ -129,6 +134,7 @@ export class ClaimRequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Get("car-part-image-required/:claimRequestID") @ApiParam({ name: "claimRequestID" }) async getImageRequired(@Param("claimRequestID") requestId) { @@ -171,6 +177,7 @@ export class ClaimRequestManagementController { enum: ClaimRequiredDocumentType, description: "Type of required document to upload", }) + @ApiOperation({ deprecated: true }) @Patch("upload-required-document/:claimRequestID") async uploadRequiredDocument( @Param("claimRequestID") requestId: string, @@ -223,6 +230,7 @@ export class ClaimRequestManagementController { name: "partId", description: "The ID of the specific car part being photographed.", }) + @ApiOperation({ deprecated: true }) @Patch("capture-car-part-damage/:claimRequestID/:partId") async captureCarPartDamage( @Param("partId") partId: string, @@ -263,6 +271,7 @@ export class ClaimRequestManagementController { ) @ApiConsumes("multipart/form-data") @ApiParam({ name: "claimRequestID" }) + @ApiOperation({ deprecated: true }) @Patch("car-capture/:claimRequestID") async captureVideoCapture( @Param("claimRequestID") requestId: string, @@ -274,11 +283,13 @@ export class ClaimRequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Get("requests/") async getRequest(@CurrentUser() currentUser) { return await this.claimRequestManagementService.myRequests(currentUser); } + @ApiOperation({ deprecated: true }) @Get("request/:claimRequestId") @ApiParam({ name: "claimRequestId" }) myRequests( @@ -288,6 +299,7 @@ export class ClaimRequestManagementController { return this.claimRequestManagementService.requestDetails(requestId, user); } + @ApiOperation({ deprecated: true }) @Put("request/reply/:claimRequestId") @ApiParam({ name: "claimRequestId" }) @UseInterceptors( @@ -326,6 +338,7 @@ export class ClaimRequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Put("request/resend/:claimRequestId/objection") @ApiParam({ name: "claimRequestId" }) @ApiConsumes("application/json") @@ -343,6 +356,7 @@ export class ClaimRequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Patch("request/resend/:claimRequestId") @ApiConsumes("multipart/form-data") @ApiParam({ name: "claimRequestId" }) @@ -393,6 +407,7 @@ export class ClaimRequestManagementController { * User satisfaction rating for a completed claim file. * Only the damaged user (claim owner) can rate their claim after it is closed. */ + @ApiOperation({ deprecated: true }) @Put("request/:claimRequestId/user-rating") @ApiParam({ name: "claimRequestId" }) @ApiBody({ type: UserRatingDto }) @@ -408,6 +423,7 @@ export class ClaimRequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Patch("request/reply/:claimRequestId/:partId/upload-factor") @ApiConsumes("multipart/form-data") @ApiParam({ name: "claimRequestId" }) @@ -452,6 +468,7 @@ export class ClaimRequestManagementController { @ApiBody({ type: InPersonVisitDto }) @ApiParam({ name: "id" }) + @ApiOperation({ deprecated: true }) @Patch(":id/visit") async inPersonVisit( @Param("id") requestId: string, @@ -466,6 +483,7 @@ export class ClaimRequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Get("branches/:insuranceId") async insuranceBranches(@Param("insuranceId") insuranceId: string) { return await this.claimRequestManagementService.retrieveInsuranceBranches( @@ -473,6 +491,7 @@ export class ClaimRequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Get("fanavaran-submit/:claimRequestId") @ApiParam({ name: "claimRequestId" }) async fanavaranSubmit(@Param("claimRequestId") claimRequestId: string) { @@ -481,6 +500,7 @@ export class ClaimRequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Post("fanavaran-submit/:claimRequestId") @ApiParam({ name: "claimRequestId" }) async submitToFanavaran(@Param("claimRequestId") claimRequestId: string) { diff --git a/src/claim-request-management/claim-request-management.v2.controller.ts b/src/claim-request-management/claim-request-management.v2.controller.ts index 14165db..22bc1e5 100644 --- a/src/claim-request-management/claim-request-management.v2.controller.ts +++ b/src/claim-request-management/claim-request-management.v2.controller.ts @@ -13,6 +13,7 @@ import { UseInterceptors, UploadedFile, } from "@nestjs/common"; +import { readFile } from "node:fs/promises"; import { ApiBearerAuth, ApiParam, ApiTags, ApiOperation, ApiResponse, ApiBody, ApiConsumes } from "@nestjs/swagger"; import { FileInterceptor } from "@nestjs/platform-express"; import { diskStorage } from "multer"; @@ -273,6 +274,28 @@ export class ClaimRequestManagementV2Controller { return this.claimRequestManagementService.getOuterPartsCatalogV2(carType); } + @Get("car-other-part") + @ApiOperation({ + summary: "Get other (non-body) parts catalog", + description: + "Returns legacy other-parts catalog used by frontend. Response is parsed JSON.", + }) + @ApiResponse({ + status: 200, + description: "Other parts catalog", + }) + async getCarOtherPartsV2() { + const raw = await readFile( + `${process.cwd()}/src/static/car-part.json`, + "utf-8", + ); + try { + return JSON.parse(raw); + } catch { + return raw; + } + } + @Patch("select-outer-parts/:claimRequestId") @ApiOperation({ summary: "Select Damaged Outer Car Parts (V2 - Step 2)", diff --git a/src/expert-blame/expert-blame.controller.ts b/src/expert-blame/expert-blame.controller.ts index b6e5699..427ccf6 100644 --- a/src/expert-blame/expert-blame.controller.ts +++ b/src/expert-blame/expert-blame.controller.ts @@ -18,6 +18,7 @@ import { ApiParam, ApiProduces, ApiTags, + ApiOperation, } from "@nestjs/swagger"; import { Response, Request } from "express"; import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard"; @@ -45,30 +46,35 @@ export class ExpertBlameController { // TODO role guard for expert fix @Roles(RoleEnum.EXPERT) + @ApiOperation({ deprecated: true }) @Get() async findAll(@CurrentUser() actor, @ClientKey() client) { return await this.expertBlameService.findAll(actor); } @Roles(RoleEnum.EXPERT) + @ApiOperation({ deprecated: true }) @Get(":id") async findOne(@Param("id") id: string, @CurrentUser() actor) { return await this.expertBlameService.findOne(id, actor.sub); } @Roles(RoleEnum.EXPERT) + @ApiOperation({ deprecated: true }) @Put("lock/:id") async lockRequest(@Param("id") id: string, @CurrentUser() actor) { return await this.expertBlameService.lockRequest(id, actor); } @Roles(RoleEnum.EXPERT) + @ApiOperation({ deprecated: true }) @Get("request/accident-fields") async getAccidentFields() { return await this.expertBlameService.getAccidentField(); } @Roles(RoleEnum.EXPERT) + @ApiOperation({ deprecated: true }) @Put("reply/submit/:id") @ApiBody({ type: SubmitReplyDto }) @ApiParam({ name: "id" }) @@ -95,6 +101,7 @@ export class ExpertBlameController { } @Roles(RoleEnum.EXPERT) + @ApiOperation({ deprecated: true }) @Put("reply/resend/first/:id") @ApiBody({ type: ResendFirstPartyDto }) @ApiParam({ name: "id" }) @@ -107,6 +114,7 @@ export class ExpertBlameController { return this.handleResendRequest(id, body, actor.sub, req); } @Roles(RoleEnum.EXPERT) + @ApiOperation({ deprecated: true }) @Put("reply/resend/second/:id") @Roles(RoleEnum.EXPERT) @ApiBody({ type: ResendSecondPartyDto }) @@ -120,6 +128,7 @@ export class ExpertBlameController { return this.handleResendRequest(id, body, actor.sub, req); } @Roles(RoleEnum.EXPERT) + @ApiOperation({ deprecated: true }) @Get("stream/:requestId") @ApiParam({ name: "requestId" }) async streamVideo(@Param("requestId") requestId: string) { @@ -127,6 +136,7 @@ export class ExpertBlameController { } @UseInterceptors(LoggingInterceptor) + @ApiOperation({ deprecated: true }) @Get("voice/:requestId/:voiceId") @Roles(RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT) @ApiParam({ name: "requestId" }) @@ -149,6 +159,7 @@ export class ExpertBlameController { } @ApiParam({ name: "id" }) + @ApiOperation({ deprecated: true }) @Patch(":id/visit") async inPersonVisit(@Param("id") requestId: string, @CurrentUser() actor) { return await this.expertBlameService.inPersonVisit(requestId, actor); diff --git a/src/expert-claim/expert-claim.controller.ts b/src/expert-claim/expert-claim.controller.ts index 2b64117..0eab237 100644 --- a/src/expert-claim/expert-claim.controller.ts +++ b/src/expert-claim/expert-claim.controller.ts @@ -17,6 +17,7 @@ import { ApiParam, ApiQuery, ApiTags, + ApiOperation, } from "@nestjs/swagger"; import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard"; import { RolesGuard } from "src/auth/guards/role.guard"; @@ -40,11 +41,13 @@ import { FactorValidationDto } from "./dto/factor-validation.dto"; export class ExpertClaimController { constructor(private readonly expertClaimService: ExpertClaimService) {} + @ApiOperation({ deprecated: true }) @Get() async getAllClaimRequests(@CurrentUser() actor, @ClientKey() client) { return await this.expertClaimService.getClaimRequestsListForExpert(actor); } + @ApiOperation({ deprecated: true }) @Get("/:id") @ApiParam({ name: "id" }) @ApiQuery({ @@ -64,12 +67,14 @@ export class ExpertClaimController { ); } + @ApiOperation({ deprecated: true }) @Get("imageRequired/:id") @ApiParam({ name: "id" }) async getImageRequired(@Param("id") id: string, @CurrentUser() currentUser) { return await this.expertClaimService.imageRequired(id, currentUser); } + @ApiOperation({ deprecated: true }) @Get("required-documents/:id") @ApiParam({ name: "id" }) async getRequiredDocuments( @@ -79,11 +84,13 @@ export class ExpertClaimController { return await this.expertClaimService.getRequiredDocuments(id, currentUser); } + @ApiOperation({ deprecated: true }) @Put("lock/:id") async lockClaimRequest(@Param("id") requestId: string, @CurrentUser() actor) { return await this.expertClaimService.lockClaimRequest(requestId, actor); } + @ApiOperation({ deprecated: true }) @Put("reply/submit/:id") @ApiParam({ name: "id" }) async submitReplyRequest( @@ -98,6 +105,7 @@ export class ExpertClaimController { ); } + @ApiOperation({ deprecated: true }) @Put("resend/submit/:id") @ApiParam({ name: "id" }) async submitResend( @@ -117,6 +125,7 @@ export class ExpertClaimController { enum: ["car-capture", "accident"], required: true, }) + @ApiOperation({ deprecated: true }) @Get("stream/:id/video") @Header("Accept-Ranges", "bytes") @Header("Content-Type", "video/mp4") @@ -134,6 +143,7 @@ export class ExpertClaimController { enum: ["car-capture", "accident"], required: true, }) + @ApiOperation({ deprecated: true }) @Get("link/:id/video") @ApiParam({ name: "id" }) async getClaimVideoLink( @@ -143,12 +153,14 @@ export class ExpertClaimController { return this.expertClaimService.getVideoLink(id, query); } + @ApiOperation({ deprecated: true }) @Get("stream/:id/voice") @ApiParam({ name: "id" }) async getClaimVoiceLink(@Param("id") id: string) { return await this.expertClaimService.getVoiceLink(id); } + @ApiOperation({ deprecated: true }) @Patch("validate-factors/:claimId") @ApiParam({ name: "claimId" }) @ApiBody({ type: FactorValidationDto }) @@ -166,11 +178,13 @@ export class ExpertClaimController { } @ApiParam({ name: "id" }) + @ApiOperation({ deprecated: true }) @Patch(":id/visit") async inPersonVisit(@Param("id") requestId: string, @CurrentUser() actor) { return await this.expertClaimService.inPersonVisit(requestId, actor); } + @ApiOperation({ deprecated: true }) @Get("branches/:insuranceId") @ApiParam({ name: "insuranceId" }) async getInsuranceBranches(@Param("insuranceId") insuranceId: string) { diff --git a/src/main.ts b/src/main.ts index 3e66699..09e0018 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,6 +42,16 @@ async function bootstrap() { swaggerOptions: { persistAuthorization: true, docExpansion: "none", + tagsSorter: (a: string, b: string) => { + const priority: Record = { + user: 0, + actor: 1, + }; + const pa = priority[a] ?? 100; + const pb = priority[b] ?? 100; + if (pa !== pb) return pa - pb; + return a.localeCompare(b); + }, }, }); await app.listen(process.env.PORT); diff --git a/src/request-management/expert-initiated.controller.ts b/src/request-management/expert-initiated.controller.ts index f99ce73..eb2a38f 100644 --- a/src/request-management/expert-initiated.controller.ts +++ b/src/request-management/expert-initiated.controller.ts @@ -6,7 +6,6 @@ import { Param, UseGuards, Get, - Query, UseInterceptors, UploadedFile, } from "@nestjs/common"; @@ -18,7 +17,6 @@ import { ApiBearerAuth, ApiBody, ApiParam, - ApiQuery, ApiTags, ApiOperation, ApiResponse, @@ -48,6 +46,7 @@ export class ExpertInitiatedController { private readonly claimRequestManagementService: ClaimRequestManagementService, ) {} + @ApiOperation({ deprecated: true }) @Get("my-files") @ApiOperation({ summary: "List my expert-initiated files", @@ -61,6 +60,7 @@ export class ExpertInitiatedController { ); } + @ApiOperation({ deprecated: true }) @Post("complete-blame-data/:requestId") @ApiOperation({ summary: "Submit all blame-needed data", @@ -86,6 +86,7 @@ export class ExpertInitiatedController { ); } + @ApiOperation({ deprecated: true }) @Post("complete-claim-data/:claimRequestId") @ApiOperation({ summary: "Submit claim-needed data", @@ -111,6 +112,7 @@ export class ExpertInitiatedController { ); } + @ApiOperation({ deprecated: true }) @Post("create") @ApiOperation({ summary: "Create expert-initiated blame file", @@ -138,6 +140,7 @@ export class ExpertInitiatedController { ); } + @ApiOperation({ deprecated: true }) @Post("complete-form-third-party/:requestId") @ApiOperation({ summary: "Expert completes all information for IN_PERSON THIRD_PARTY file", @@ -167,6 +170,7 @@ export class ExpertInitiatedController { ); } + @ApiOperation({ deprecated: true }) @Post("complete-form-car-body/:requestId") @ApiOperation({ summary: "Expert completes all information for IN_PERSON CAR_BODY file", @@ -196,6 +200,7 @@ export class ExpertInitiatedController { ); } + @ApiOperation({ deprecated: true }) @Post("upload-video/:requestId") @ApiOperation({ summary: "Expert uploads video for expert-initiated file", @@ -252,6 +257,7 @@ export class ExpertInitiatedController { ); } + @ApiOperation({ deprecated: true }) @Post("upload-voice/:requestId") @ApiOperation({ summary: "Expert uploads voice for expert-initiated file", @@ -309,6 +315,7 @@ export class ExpertInitiatedController { ); } + @ApiOperation({ deprecated: true }) @Post("add-accident-fields/:requestId") @ApiOperation({ summary: "Expert adds accident fields to expert-initiated file", diff --git a/src/request-management/request-management.controller.ts b/src/request-management/request-management.controller.ts index 6151187..ac656bb 100644 --- a/src/request-management/request-management.controller.ts +++ b/src/request-management/request-management.controller.ts @@ -11,8 +11,6 @@ import { Req, Put, UploadedFiles, - Patch, - Query, } from "@nestjs/common"; import { FileFieldsInterceptor, @@ -23,8 +21,8 @@ import { ApiBody, ApiConsumes, ApiParam, - ApiQuery, ApiTags, + ApiOperation, } from "@nestjs/swagger"; import { Request } from "express"; import { diskStorage } from "multer"; @@ -44,7 +42,6 @@ import { LocationDto, } from "./dto/create-request-management.dto"; import { RequestManagementService } from "./request-management.service"; -import { InPersonVisitDto } from "src/claim-request-management/dto/in-person-visit.dto"; @Controller("blame-request-management") @ApiTags("blame-request-management") @@ -56,6 +53,7 @@ export class RequestManagementController { private readonly requestManagementService: RequestManagementService, ) {} + @ApiOperation({ deprecated: true }) @Post() @UseGuards(GlobalGuard) async createRequest( @@ -68,6 +66,7 @@ export class RequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Post("/initial-form/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: InitialFormDto }) @@ -115,6 +114,7 @@ export class RequestManagementController { ) @UseGuards(GlobalGuard) @ApiParam({ name: "requestId" }) + @ApiOperation({ deprecated: true }) @Post("upload-video/:requestId") @UseGuards(GlobalGuard) async videoUploader( @@ -129,6 +129,7 @@ export class RequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Post("/add-plate/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: AddPlateDto }) @@ -146,6 +147,7 @@ export class RequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Post("/carbody-form/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: CarBodyFormDto }) @@ -163,6 +165,7 @@ export class RequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Post("/carbody-secondform/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: CarBodySecondForm }) @@ -180,6 +183,7 @@ export class RequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Post("/add-detail-location/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: LocationDto }) @@ -228,6 +232,7 @@ export class RequestManagementController { }), ) @ApiParam({ name: "requestId" }) + @ApiOperation({ deprecated: true }) @Post("upload-voice/:requestId") @UseGuards(GlobalGuard) async voiceUploader( @@ -242,6 +247,7 @@ export class RequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Post("/add-detail-description/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: DescriptionDto }) @@ -259,6 +265,7 @@ export class RequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Post("add-second-party-details/:phoneNumber/:requestId/:frontendRoutes") @ApiParam({ name: "phoneNumber" }) @ApiParam({ name: "requestId" }) @@ -280,17 +287,20 @@ export class RequestManagementController { ); } + @ApiOperation({ deprecated: true }) @Get("requests") async requests(@CurrentUser() user) { return await this.requestManagementService.myRequests(user.username); } + @ApiOperation({ deprecated: true }) @Get("creatable-claims") @UseGuards(GlobalGuard) async getCreatableClaims(@CurrentUser() user) { return await this.requestManagementService.getCreatableClaims(user); } + @ApiOperation({ deprecated: true }) @Get("request/:requestId") @ApiParam({ name: "requestId" }) @Roles(RoleEnum.USER) @@ -329,6 +339,7 @@ export class RequestManagementController { }), }), ) + @ApiOperation({ deprecated: true }) @Put("request/reply/:requestId") @ApiParam({ name: "requestId" }) @UseGuards(GlobalGuard) @@ -385,6 +396,7 @@ export class RequestManagementController { ) @UseGuards(GlobalGuard) @ApiParam({ name: "requestId" }) + @ApiOperation({ deprecated: true }) @Put("request/resend/:requestId") userResendReply( @Body("description") description: string, From f456443342cc5ea753f7cb37084b1662ff62b2ce Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Tue, 28 Apr 2026 10:15:03 +0330 Subject: [PATCH 2/7] Fixed blame resend --- src/request-management/request-management.v2.controller.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/request-management/request-management.v2.controller.ts b/src/request-management/request-management.v2.controller.ts index b3f2bd3..ed8b239 100644 --- a/src/request-management/request-management.v2.controller.ts +++ b/src/request-management/request-management.v2.controller.ts @@ -1,4 +1,5 @@ import { extname } from "node:path"; +import { mkdirSync } from "node:fs"; import { Body, Controller, @@ -414,7 +415,9 @@ export class RequestManagementV2Controller { { storage: diskStorage({ destination: (req, file, cb) => { - cb(null, "./uploads"); + const uploadDir = "./files/blame-resend"; + mkdirSync(uploadDir, { recursive: true }); + cb(null, uploadDir); }, filename: (req, file, cb) => { const uniqueSuffix = From 929679516641900b4c07fbb7896a0acbcab3d27e Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Tue, 28 Apr 2026 13:31:02 +0330 Subject: [PATCH 3/7] Added factorLink and branchName support --- .../claim-request-management.service.ts | 93 ++++++++++++++++++- .../claim-request-management.v2.controller.ts | 21 +++++ .../request-management.service.ts | 48 ++++++---- 3 files changed, 141 insertions(+), 21 deletions(-) diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index eff852d..c751ba3 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -1900,6 +1900,90 @@ export class ClaimRequestManagementService { } } + /** + * V2 response mapper: + * - convert factorLink ObjectId -> public URL + * - enrich daghi.branchId with branchName when available + */ + private async mapEvaluationForClient(evaluation: any): Promise { + if (!evaluation) return undefined; + + const mapReply = async (reply: any) => { + if (!reply) return reply; + + const mapped = { + ...(typeof reply?.toObject === "function" ? reply.toObject() : reply), + }; + const parts = Array.isArray(mapped.parts) ? mapped.parts : []; + + const factorIds = new Set(); + const branchIds = new Set(); + for (const p of parts) { + if (p?.factorLink && Types.ObjectId.isValid(String(p.factorLink))) { + factorIds.add(String(p.factorLink)); + } + const daghi = p?.daghi; + const rawBranchId = + daghi && typeof daghi === "object" ? (daghi as any).branchId : undefined; + if (rawBranchId && Types.ObjectId.isValid(String(rawBranchId))) { + branchIds.add(String(rawBranchId)); + } + } + + const factorMap = new Map(); + await Promise.all( + Array.from(factorIds).map(async (id) => { + const factorDoc = await this.claimFactorsImageDbService.findById(id); + if (factorDoc?.path) { + factorMap.set(id, buildFileLink(factorDoc.path)); + } + }), + ); + + const branchMap = new Map(); + await Promise.all( + Array.from(branchIds).map(async (id) => { + const branch = await this.branchDbService.findById(id); + if (branch?.name) { + branchMap.set(id, branch.name); + } + }), + ); + + mapped.parts = parts.map((part: any) => { + const nextPart = { ...part }; + + if (nextPart.factorLink && Types.ObjectId.isValid(String(nextPart.factorLink))) { + const link = factorMap.get(String(nextPart.factorLink)); + if (link) nextPart.factorLink = link; + } + + const daghi = nextPart.daghi; + if (daghi && typeof daghi === "object" && !Array.isArray(daghi)) { + const branchId = (daghi as any).branchId; + const branchName = + branchId && Types.ObjectId.isValid(String(branchId)) + ? branchMap.get(String(branchId)) + : undefined; + nextPart.daghi = { + ...daghi, + ...(branchName ? { branchName } : {}), + }; + } + + return nextPart; + }); + + return mapped; + }; + + return { + ...evaluation, + damageExpertReply: await mapReply(evaluation.damageExpertReply), + damageExpertReplyFinal: await mapReply(evaluation.damageExpertReplyFinal), + }; + } + async submitUserReply( requestId: string, body: UserCommentDto, @@ -2189,6 +2273,7 @@ export class ClaimRequestManagementService { return { message: "Factor uploaded successfully. Awaiting expert validation.", factorId: factorRecord._id, + factorLink: buildFileLink(file.path), url: buildFileLink(file.path), }; } catch (error) { @@ -5191,6 +5276,8 @@ export class ClaimRequestManagementService { } : undefined; + const mappedEvaluation = await this.mapEvaluationForClient(claim.evaluation); + return { claimRequestId: claim._id.toString(), publicId: claim.publicId, @@ -5210,10 +5297,10 @@ export class ClaimRequestManagementService { carAngles, damagedParts, expertResend, - evaluation: claim.evaluation + evaluation: mappedEvaluation ? { - damageExpertReply: claim.evaluation.damageExpertReply, - damageExpertReplyFinal: claim.evaluation.damageExpertReplyFinal, + damageExpertReply: mappedEvaluation.damageExpertReply, + damageExpertReplyFinal: mappedEvaluation.damageExpertReplyFinal, } : undefined, ...(isExpertViewer diff --git a/src/claim-request-management/claim-request-management.v2.controller.ts b/src/claim-request-management/claim-request-management.v2.controller.ts index 22bc1e5..f291af1 100644 --- a/src/claim-request-management/claim-request-management.v2.controller.ts +++ b/src/claim-request-management/claim-request-management.v2.controller.ts @@ -274,6 +274,27 @@ export class ClaimRequestManagementV2Controller { return this.claimRequestManagementService.getOuterPartsCatalogV2(carType); } + @Get("branches/:insuranceId") + @ApiOperation({ + summary: "Get insurer branches (V2)", + description: + "Returns branch list for a given insurer/client id so frontend can render branch options (name/code/address/city/state) and submit selected branchId in daghi part options.", + }) + @ApiParam({ + name: "insuranceId", + description: "Insurer client id (MongoDB ObjectId)", + example: "60d5ec49e7b2f8001c8e4d2a", + }) + @ApiResponse({ + status: 200, + description: "List of branches for insurer", + }) + async getInsuranceBranchesV2(@Param("insuranceId") insuranceId: string) { + return await this.claimRequestManagementService.retrieveInsuranceBranches( + insuranceId, + ); + } + @Get("car-other-part") @ApiOperation({ summary: "Get other (non-body) parts catalog", diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 608bd91..581349f 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -6408,26 +6408,38 @@ export class RequestManagementService { }, }); - if (updatedRequest.type === BlameRequestType.THIRD_PARTY) { - const guiltyPartyId = updatedRequest.expert?.decision?.guiltyPartyId - ? String(updatedRequest.expert.decision.guiltyPartyId) - : null; - if (guiltyPartyId) { - const damagedParty = updatedRequest.parties?.find( - (p) => p.person?.userId && String(p.person.userId) !== guiltyPartyId, - ); - const phone = damagedParty?.person?.phoneNumber?.trim(); - if (phone) { - const publicId = String(updatedRequest.publicId); - const link = this.smsOrchestrationService.buildClaimLink(requestId); - await this.smsOrchestrationService.sendThirdPartyDamagedPartyClaimLinkNotice( - { - receptor: phone, - publicId, - link, - }, + if ( + updatedRequest.type === BlameRequestType.THIRD_PARTY || + updatedRequest.type === BlameRequestType.CAR_BODY + ) { + let targetPhone: string | undefined; + + if (updatedRequest.type === BlameRequestType.THIRD_PARTY) { + const guiltyPartyId = updatedRequest.expert?.decision?.guiltyPartyId + ? String(updatedRequest.expert.decision.guiltyPartyId) + : null; + if (guiltyPartyId) { + const damagedParty = updatedRequest.parties?.find( + (p) => p.person?.userId && String(p.person.userId) !== guiltyPartyId, ); + targetPhone = damagedParty?.person?.phoneNumber?.trim(); } + } else { + // CAR_BODY has a single owner/damaged party: notify that user too. + const ownerParty = updatedRequest.parties?.[partyIndex] ?? updatedRequest.parties?.[0]; + targetPhone = ownerParty?.person?.phoneNumber?.trim(); + } + + if (targetPhone) { + const publicId = String(updatedRequest.publicId); + const link = this.smsOrchestrationService.buildClaimLink(requestId); + await this.smsOrchestrationService.sendThirdPartyDamagedPartyClaimLinkNotice( + { + receptor: targetPhone, + publicId, + link, + }, + ); } } } else { From bbd83da2d585ea2a8d80145add804c148c0b6dec Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Tue, 28 Apr 2026 14:27:12 +0330 Subject: [PATCH 4/7] YARA-858 --- src/client/client.controller.ts | 10 +- src/client/dto/create-client.dto.ts | 22 +++- .../entities/db-service/branch.db.service.ts | 2 +- src/decorators/customeHeader.decorator.ts | 6 - .../dto/create-insurer-expert.dto.ts | 83 +++++++++++++ .../expert-insurer.controller.ts | 84 +++++++++---- src/expert-insurer/expert-insurer.module.ts | 2 + src/expert-insurer/expert-insurer.service.ts | 115 ++++++++++++++++++ src/expert-insurer/panel.enum.ts | 4 - src/main.ts | 1 - .../entities/schema/damage-expert.schema.ts | 3 + .../schema/expert-file-activity.schema.ts | 2 +- src/users/entities/schema/expert.schema.ts | 4 + 13 files changed, 291 insertions(+), 47 deletions(-) delete mode 100644 src/decorators/customeHeader.decorator.ts create mode 100644 src/expert-insurer/dto/create-insurer-expert.dto.ts delete mode 100644 src/expert-insurer/panel.enum.ts diff --git a/src/client/client.controller.ts b/src/client/client.controller.ts index b32ea6b..0042443 100644 --- a/src/client/client.controller.ts +++ b/src/client/client.controller.ts @@ -7,25 +7,27 @@ import { } from "@nestjs/common"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { GlobalGuard } from "src/auth/guards/global.guard"; +import { RolesGuard } from "src/auth/guards/role.guard"; +import { Roles } from "src/decorators/roles.decorator"; import { CurrentUser } from "src/decorators/user.decorator"; +import { RoleEnum } from "src/Types&Enums/role.enum"; import { ClientService } from "./client.service"; import { ClientDto } from "./dto/create-client.dto"; @Controller("client") @ApiTags("client-management") +@ApiBearerAuth() +@UseGuards(GlobalGuard, RolesGuard) +@Roles(RoleEnum.ADMIN) export class ClientController { constructor(private readonly clientService: ClientService) {} @Post() - @UseGuards(GlobalGuard) - @ApiBearerAuth() async addClient(@Body() client: ClientDto) { return await this.clientService.addClient(client); } @Get() - @ApiBearerAuth() - @UseGuards(GlobalGuard) async getClient(@CurrentUser() user) { return await this.clientService.getClients(); } diff --git a/src/client/dto/create-client.dto.ts b/src/client/dto/create-client.dto.ts index 9644946..2101d77 100644 --- a/src/client/dto/create-client.dto.ts +++ b/src/client/dto/create-client.dto.ts @@ -1,31 +1,44 @@ -import { Injectable } from "@nestjs/common"; import { ApiProperty } from "@nestjs/swagger"; +import { IsIn, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; import { Types } from "mongoose"; class ClientName { @ApiProperty({}) + @IsString() + @IsNotEmpty() persian: string; @ApiProperty({}) + @IsString() + @IsNotEmpty() english: string; } class Property { @ApiProperty({}) + @IsString() + @IsNotEmpty() smsApiKey: string; } -@Injectable() export class ClientDto { @ApiProperty({ required: true }) + @ValidateNested() + @Type(() => ClientName) clientName: ClientName; @ApiProperty({ required: true }) + @IsNumber() clientCode: number; @ApiProperty({ required: false }) - property: Property; + @IsOptional() + @ValidateNested() + @Type(() => Property) + property?: Property; @ApiProperty({ examples: ["legal", "genuine"] }) + @IsIn(["legal", "genuine"]) useExpertMode: "legal" | "genuine"; } export class ClientDtoRs { @@ -35,6 +48,9 @@ export class ClientDtoRs { useExpertsMode: string; constructor(readonly client) { this.persian = client.clientName.persian; + this.english = client.clientName.english; + this.clientId = client._id; + this.useExpertsMode = client.useExpertMode; } } diff --git a/src/client/entities/db-service/branch.db.service.ts b/src/client/entities/db-service/branch.db.service.ts index 8b2af9f..66b79ed 100644 --- a/src/client/entities/db-service/branch.db.service.ts +++ b/src/client/entities/db-service/branch.db.service.ts @@ -25,7 +25,7 @@ export class BranchDbService { async findAll(insuranceId: string): Promise { return await this.branchModel.find({ clientKey: new Types.ObjectId(insuranceId), - }); + }, { _id: 1, name: 1, code: 1, city: 1, state: 1, address: 1, phoneNumber: 1, createdAtFa: 1, updatedAtFa: 1 }); } async findById(id: string): Promise { diff --git a/src/decorators/customeHeader.decorator.ts b/src/decorators/customeHeader.decorator.ts deleted file mode 100644 index 31d9cb7..0000000 --- a/src/decorators/customeHeader.decorator.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ExecutionContext, createParamDecorator } from "@nestjs/common"; - -export const CustomHeader = createParamDecorator( - (data, ctx: ExecutionContext) => { - }, -); diff --git a/src/expert-insurer/dto/create-insurer-expert.dto.ts b/src/expert-insurer/dto/create-insurer-expert.dto.ts new file mode 100644 index 0000000..b1e7caa --- /dev/null +++ b/src/expert-insurer/dto/create-insurer-expert.dto.ts @@ -0,0 +1,83 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsEmail, IsEnum, IsMongoId, IsNotEmpty, IsOptional, IsString } from "class-validator"; +import { RoleEnum } from "src/Types&Enums/role.enum"; +import { UserType } from "src/Types&Enums/userType.enum"; + +export class CreateInsurerExpertDto { + @ApiProperty({ example: "Ali" }) + @IsString() + @IsNotEmpty() + firstName: string; + + @ApiProperty({ example: "Ahmadi" }) + @IsString() + @IsNotEmpty() + lastName: string; + + @ApiProperty({ example: "1234567890" }) + @IsString() + @IsNotEmpty() + nationalCode: string; + + @ApiProperty({ example: "expert@example.com" }) + @IsEmail() + email: string; + + @ApiProperty({ example: "StrongPass!123" }) + @IsString() + @IsNotEmpty() + password: string; + + @ApiProperty({ example: "09121234567" }) + @IsString() + @IsNotEmpty() + mobile: string; + + @ApiProperty({ + example: "665f0e5ab74d670939b08920", + description: "Branch id this expert belongs to", + }) + @IsMongoId() + branchId: string; + + @ApiPropertyOptional({ enum: UserType, example: UserType.LEGAL }) + @IsOptional() + @IsEnum(UserType) + userType?: UserType; + + @ApiPropertyOptional({ example: "02112345678" }) + @IsOptional() + @IsString() + phone?: string; + + @ApiPropertyOptional({ example: "Tehran" }) + @IsOptional() + @IsString() + state?: string; + + @ApiPropertyOptional({ example: "Tehran" }) + @IsOptional() + @IsString() + city?: string; + + @ApiPropertyOptional({ example: "No. 12, Sample St." }) + @IsOptional() + @IsString() + address?: string; +} + +export class CreateBlameExpertByInsurerDto extends CreateInsurerExpertDto { + @ApiPropertyOptional({ + enum: [RoleEnum.EXPERT], + default: RoleEnum.EXPERT, + }) + role?: RoleEnum.EXPERT; +} + +export class CreateClaimExpertByInsurerDto extends CreateInsurerExpertDto { + @ApiPropertyOptional({ + enum: [RoleEnum.DAMAGE_EXPERT], + default: RoleEnum.DAMAGE_EXPERT, + }) + role?: RoleEnum.DAMAGE_EXPERT; +} diff --git a/src/expert-insurer/expert-insurer.controller.ts b/src/expert-insurer/expert-insurer.controller.ts index 3fa567c..5a7b81c 100644 --- a/src/expert-insurer/expert-insurer.controller.ts +++ b/src/expert-insurer/expert-insurer.controller.ts @@ -26,6 +26,10 @@ import { FileRating } from "src/request-management/entities/schema/request-manag import { RoleEnum } from "src/Types&Enums/role.enum"; import { ExpertInsurerService } from "./expert-insurer.service"; import { CreateBranchDto } from "src/client/dto/create-branch.dto"; +import { + CreateBlameExpertByInsurerDto, + CreateClaimExpertByInsurerDto, +} from "./dto/create-insurer-expert.dto"; @Controller("expert-insurer") @ApiTags("expert-insurer-panel") @@ -35,13 +39,56 @@ import { CreateBranchDto } from "src/client/dto/create-branch.dto"; export class ExpertInsurerController { constructor(private readonly expertInsurerService: ExpertInsurerService) {} - @Get("files") - async getAllFiles(@CurrentUser() insurer) { - return await this.expertInsurerService.retrieveAllFilesOfClient( + @Get("branches") + async getInsuranceBranches(@CurrentUser() insurer) { + if (!insurer) { + throw new UnauthorizedException("Could not identify the current user."); + } + return await this.expertInsurerService.retrieveInsuranceBranches( insurer.clientKey, ); } + @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, + ); + } + + @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); + } + @ApiQuery({ name: "page", type: Number }) @ApiQuery({ name: "response_count", type: Number }) @Get("list") @@ -62,6 +109,13 @@ export class ExpertInsurerController { return await this.expertInsurerService.getTopExpertsForClient(actor); } + @Get("files") + async getAllFiles(@CurrentUser() insurer) { + return await this.expertInsurerService.retrieveAllFilesOfClient( + insurer.clientKey, + ); + } + @Get("top-files") async getTopFiles(@CurrentUser() insurer) { return await this.expertInsurerService.getTopFilesForClient( @@ -174,28 +228,4 @@ export class ExpertInsurerController { insurer.clientKey, ); } - - @Get("branches/:insuranceId") - @ApiParam({ name: "insuranceId" }) - async getInsuranceBranches(@Param("insuranceId") insuranceId: string) { - return await this.expertInsurerService.retrieveInsuranceBranches( - insuranceId, - ); - } - - @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, - ); - } } diff --git a/src/expert-insurer/expert-insurer.module.ts b/src/expert-insurer/expert-insurer.module.ts index 0546bb4..34a1ee0 100644 --- a/src/expert-insurer/expert-insurer.module.ts +++ b/src/expert-insurer/expert-insurer.module.ts @@ -19,12 +19,14 @@ import { BranchModel, BranchSchema, } from "src/client/entities/schema/branch.schema"; +import { HashModule } from "src/utils/hash/hash.module"; @Module({ imports: [ ClaimRequestManagementModule, RequestManagementModule, AuthModule, + HashModule, UsersModule, ClientModule, MongooseModule.forFeature([ diff --git a/src/expert-insurer/expert-insurer.service.ts b/src/expert-insurer/expert-insurer.service.ts index 49860f9..98fc932 100644 --- a/src/expert-insurer/expert-insurer.service.ts +++ b/src/expert-insurer/expert-insurer.service.ts @@ -9,6 +9,11 @@ import { Types } from "mongoose"; import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service"; import { CreateBranchDto } from "src/client/dto/create-branch.dto"; import { BranchDbService } from "src/client/entities/db-service/branch.db.service"; +import { + CreateBlameExpertByInsurerDto, + CreateClaimExpertByInsurerDto, + CreateInsurerExpertDto, +} from "./dto/create-insurer-expert.dto"; import { blameCaseTouchesClient, claimCaseTouchesClient, @@ -20,6 +25,9 @@ import { DamageExpertDbService } from "src/users/entities/db-service/damage-expe import { ExpertDbService } from "src/users/entities/db-service/expert.db.service"; import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service"; import { ExpertFileActivityType } from "src/users/entities/schema/expert-file-activity.schema"; +import { HashService } from "src/utils/hash/hash.service"; +import { RoleEnum } from "src/Types&Enums/role.enum"; +import { UserType } from "src/Types&Enums/userType.enum"; @Injectable() export class ExpertInsurerService { @@ -30,6 +38,7 @@ export class ExpertInsurerService { private readonly blameRequestDbService: BlameRequestDbService, private readonly claimCaseDbService: ClaimCaseDbService, private readonly branchDbService: BranchDbService, + private readonly hashService: HashService, ) {} private getClientId(actorOrId: any): Types.ObjectId { @@ -420,6 +429,112 @@ export class ExpertInsurerService { return newBranch; } + private async assertBranchBelongsToClient( + branchId: string, + clientId: Types.ObjectId, + ) { + const branch = await this.branchDbService.findById(branchId); + if (!branch) { + throw new NotFoundException("Branch not found"); + } + if (String(branch.clientKey) !== String(clientId)) { + throw new ForbiddenException( + "Selected branch does not belong to your insurance company.", + ); + } + return branch; + } + + private sanitizeExpertResponse(doc: any) { + const obj = typeof doc?.toObject === "function" ? doc.toObject() : doc; + if (!obj) return obj; + const { password, otp, ...rest } = obj; + return rest; + } + + private async createExpertForInsurer( + insurerClientKey: string, + payload: CreateInsurerExpertDto, + role: RoleEnum.EXPERT | RoleEnum.DAMAGE_EXPERT, + ) { + const clientObjectId = this.getClientId(insurerClientKey); + const branch = await this.assertBranchBelongsToClient( + payload.branchId, + clientObjectId, + ); + + const email = payload.email.trim().toLowerCase(); + const existingByEmail = await this.expertDbService.findOne({ email }); + const existingDamageByEmail = await this.damageExpertDbService.findOne({ email }); + if (existingByEmail || existingDamageByEmail) { + throw new ConflictException("An expert with this email already exists."); + } + + const nationalCode = payload.nationalCode.trim(); + const existingByNational = await this.expertDbService.findOne({ nationalCode }); + const existingDamageByNational = await this.damageExpertDbService.findOne({ + nationalCode, + }); + if (existingByNational || existingDamageByNational) { + throw new ConflictException( + "An expert with this national code already exists.", + ); + } + + const hashedPassword = await this.hashService.hash(payload.password); + const basePayload: any = { + ...payload, + email, + nationalCode, + password: hashedPassword, + role, + userType: payload.userType ?? UserType.LEGAL, + clientKey: + role === RoleEnum.EXPERT ? String(clientObjectId) : clientObjectId, + branchId: new Types.ObjectId(payload.branchId), + insuActivityCo: String(clientObjectId), + }; + + const created = + role === RoleEnum.EXPERT + ? await this.expertDbService.create(basePayload) + : await this.damageExpertDbService.create(basePayload); + + return { + expert: this.sanitizeExpertResponse(created), + branch: { + _id: (branch as any)._id, + name: branch.name, + code: branch.code, + city: branch.city, + state: branch.state, + address: branch.address, + }, + }; + } + + async addBlameExpert( + insurerClientKey: string, + payload: CreateBlameExpertByInsurerDto, + ) { + return this.createExpertForInsurer( + insurerClientKey, + payload, + RoleEnum.EXPERT, + ); + } + + async addClaimExpert( + insurerClientKey: string, + payload: CreateClaimExpertByInsurerDto, + ) { + return this.createExpertForInsurer( + insurerClientKey, + payload, + RoleEnum.DAMAGE_EXPERT, + ); + } + /** * Get comprehensive statistics for all experts of a client * Returns: diff --git a/src/expert-insurer/panel.enum.ts b/src/expert-insurer/panel.enum.ts deleted file mode 100644 index c3119eb..0000000 --- a/src/expert-insurer/panel.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum PanelModels { - BLAME = "blame", - CLAIM = "claim", -} diff --git a/src/main.ts b/src/main.ts index 09e0018..6e47176 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,7 +32,6 @@ async function bootstrap() { .setVersion("1.0.0") .addServer(process.env.URL) .addServer(process.env.BASE_URL + "/api") - .addServer("http://192.168.20.249:9001") .addServer("http://192.168.20.170:9001") .addServer("http://localhost:9001") .addBearerAuth() diff --git a/src/users/entities/schema/damage-expert.schema.ts b/src/users/entities/schema/damage-expert.schema.ts index 58fa125..84f6029 100644 --- a/src/users/entities/schema/damage-expert.schema.ts +++ b/src/users/entities/schema/damage-expert.schema.ts @@ -123,6 +123,9 @@ export class DamageExpertModel { @Prop({ type: Types.ObjectId }) clientKey?: Types.ObjectId; + @Prop({ type: Types.ObjectId, index: true }) + branchId?: Types.ObjectId; + @Prop({ type: PersonalInfoSchema }) personalInfo?: PersonalInfo; diff --git a/src/users/entities/schema/expert-file-activity.schema.ts b/src/users/entities/schema/expert-file-activity.schema.ts index 2554dec..21b7d57 100644 --- a/src/users/entities/schema/expert-file-activity.schema.ts +++ b/src/users/entities/schema/expert-file-activity.schema.ts @@ -39,7 +39,7 @@ export class ExpertFileActivity { @Prop({ type: Date, default: Date.now, index: true }) occurredAt: Date; - @Prop({ type: String, required: false, index: true }) + @Prop({ type: String, required: false }) idempotencyKey?: string; } diff --git a/src/users/entities/schema/expert.schema.ts b/src/users/entities/schema/expert.schema.ts index 71163ec..d328f9d 100644 --- a/src/users/entities/schema/expert.schema.ts +++ b/src/users/entities/schema/expert.schema.ts @@ -1,4 +1,5 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; +import { Types } from "mongoose"; import { Degrees } from "src/Types&Enums/degrees.enum"; import { RoleEnum } from "src/Types&Enums/role.enum"; import { UserType } from "src/Types&Enums/userType.enum"; @@ -65,6 +66,9 @@ export class ExpertModel { @Prop({}) clientKey?: string; + @Prop({ type: Types.ObjectId, index: true }) + branchId?: Types.ObjectId; + @Prop({ type: "string" }) otp: string; From 98f1d2caf5993883b78925be581d950c47013e55 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Tue, 28 Apr 2026 14:41:47 +0330 Subject: [PATCH 5/7] YARA-857 --- .../expert-insurer.controller.ts | 27 ++++- src/expert-insurer/expert-insurer.service.ts | 110 ++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/src/expert-insurer/expert-insurer.controller.ts b/src/expert-insurer/expert-insurer.controller.ts index 5a7b81c..c8e4727 100644 --- a/src/expert-insurer/expert-insurer.controller.ts +++ b/src/expert-insurer/expert-insurer.controller.ts @@ -91,7 +91,7 @@ export class ExpertInsurerController { @ApiQuery({ name: "page", type: Number }) @ApiQuery({ name: "response_count", type: Number }) - @Get("list") + @Get("experts/list") async getAllExperts( @Query("page") page: number, @Query("response_count") count: number, @@ -104,7 +104,7 @@ export class ExpertInsurerController { ); } - @Get("top-experts") + @Get("experts/top") async getTopExperts(@CurrentUser() actor) { return await this.expertInsurerService.getTopExpertsForClient(actor); } @@ -128,6 +128,29 @@ export class ExpertInsurerController { return await this.expertInsurerService.getExpertStatisticsReport(actor); } + @Get("report/status-counts") + @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" }) @ApiQuery({ name: "role", enum: ["claim", "blame"] }) @Get("/:expertId") diff --git a/src/expert-insurer/expert-insurer.service.ts b/src/expert-insurer/expert-insurer.service.ts index 98fc932..af702be 100644 --- a/src/expert-insurer/expert-insurer.service.ts +++ b/src/expert-insurer/expert-insurer.service.ts @@ -21,6 +21,7 @@ import { import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service"; import { FileRating } from "src/request-management/entities/schema/request-management.schema"; 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 { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service"; import { ExpertDbService } from "src/users/entities/db-service/expert.db.service"; import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service"; @@ -664,4 +665,113 @@ export class ExpertInsurerService { async retrieveInsuranceBranches(insuranceId: string) { return this.branchDbService.findAll(insuranceId); } + + 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 isInDateRange(value: any, fromDate?: Date, toDate?: Date): boolean { + if (!fromDate && !toDate) return true; + if (!value) return false; + const d = new Date(value); + if (Number.isNaN(d.getTime())) return false; + if (fromDate && d < fromDate) return false; + if (toDate && d > toDate) return false; + return true; + } + + async getInsurerFileStatusCounts( + actor: any, + from?: string, + to?: string, + ): Promise<{ + all: number; + completed: number; + under_review: number; + waiting_for_documents_resend: number; + }> { + const clientObjectId = this.getClientId(actor); + const { fromDate, toDate } = this.parseDateRange(from, to); + + const [blameFilesRaw, claimFilesRaw] = await Promise.all([ + this.getClientBlameFiles(clientObjectId), + this.getClientClaimFiles(clientObjectId), + ]); + + const blameFiles = blameFilesRaw.filter((f) => + this.isInDateRange((f as any)?.createdAt, fromDate, toDate), + ); + const claimFiles = claimFilesRaw.filter((f) => + this.isInDateRange((f as any)?.createdAt, fromDate, toDate), + ); + + const fileMap = new Map< + string, + { blame?: any; claim?: any } + >(); + + for (const b of blameFiles) { + const key = String((b as any).publicId || (b as any)._id || ""); + if (!key) continue; + const prev = fileMap.get(key) ?? {}; + prev.blame = b; + fileMap.set(key, prev); + } + for (const c of claimFiles) { + const key = String((c as any).publicId || (c as any)._id || ""); + if (!key) continue; + const prev = fileMap.get(key) ?? {}; + prev.claim = c; + fileMap.set(key, prev); + } + + let completed = 0; + let underReview = 0; + let waitingForDocumentsResend = 0; + + for (const entry of fileMap.values()) { + const blameStatus = entry.blame?.status; + const claimStatus = entry.claim?.status; + + if (claimStatus === ClaimCaseStatus.COMPLETED) { + completed += 1; + } + + const blameUnderReview = + blameStatus === CaseStatus.WAITING_FOR_EXPERT; + const claimUnderReview = + claimStatus === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT || + claimStatus === ClaimCaseStatus.EXPERT_REVIEWING; + if (blameUnderReview || claimUnderReview) { + underReview += 1; + } + + const blameResend = + blameStatus === CaseStatus.WAITING_FOR_DOCUMENT_RESEND; + const claimResend = + claimStatus === ClaimCaseStatus.WAITING_FOR_USER_RESEND; + if (blameResend || claimResend) { + waitingForDocumentsResend += 1; + } + } + + return { + all: fileMap.size, + completed, + under_review: underReview, + waiting_for_documents_resend: waitingForDocumentsResend, + }; + } } From f999313476b1d86cda160d12d2072e4518d7fcc1 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Tue, 28 Apr 2026 15:22:04 +0330 Subject: [PATCH 6/7] YARA-853 --- .../expert-insurer.controller.ts | 12 + src/expert-insurer/expert-insurer.service.ts | 242 +++++++++++++++++- 2 files changed, 247 insertions(+), 7 deletions(-) diff --git a/src/expert-insurer/expert-insurer.controller.ts b/src/expert-insurer/expert-insurer.controller.ts index c8e4727..926306a 100644 --- a/src/expert-insurer/expert-insurer.controller.ts +++ b/src/expert-insurer/expert-insurer.controller.ts @@ -116,6 +116,18 @@ export class ExpertInsurerController { ); } + @Get("files/:publicId") + @ApiParam({ name: "publicId" }) + async getFileDetailsByPublicId( + @CurrentUser() insurer, + @Param("publicId") publicId: string, + ) { + return await this.expertInsurerService.retrieveFileDetailsByPublicId( + insurer.clientKey, + publicId, + ); + } + @Get("top-files") async getTopFiles(@CurrentUser() insurer) { return await this.expertInsurerService.getTopFilesForClient( diff --git a/src/expert-insurer/expert-insurer.service.ts b/src/expert-insurer/expert-insurer.service.ts index af702be..aa7e550 100644 --- a/src/expert-insurer/expert-insurer.service.ts +++ b/src/expert-insurer/expert-insurer.service.ts @@ -134,6 +134,19 @@ export class ExpertInsurerService { }; } + private buildPartiesPreview(parties: any[] | undefined) { + if (!Array.isArray(parties) || !parties.length) return undefined; + return parties.map((p: any) => ({ + role: p?.role, + fullName: p?.person?.fullName, + vehicle: { + carName: p?.vehicle?.carName ?? p?.vehicle?.name, + carModel: p?.vehicle?.carModel ?? p?.vehicle?.model, + plate: p?.vehicle?.plate ?? p?.vehicle?.plateId, + }, + })); + } + private getCombinedFileScore(file: any): number | null { const insurerRating = file?.rating; const userRating = file?.userRating; @@ -165,11 +178,6 @@ export class ExpertInsurerService { .filter((f) => (f?.parties || []).some((p) => String(p?.person?.clientId || "") === idStr), ) - .filter( - (f) => - f?.status !== CaseStatus.OPEN && - f?.status !== CaseStatus.WAITING_FOR_SECOND_PARTY, - ) .map((f) => this.normalizeBlameCase(f)); } @@ -177,7 +185,7 @@ export class ExpertInsurerService { const all = (await this.claimCaseDbService.find({}, { lean: true })) as any[]; const idStr = String(clientObjectId); return all - .filter((f) => String(f?.owner?.userClientKey || "") === idStr) + .filter((f) => claimCaseTouchesClient(f, idStr)) .map((f) => this.normalizeClaimCase(f)); } @@ -405,7 +413,227 @@ export class ExpertInsurerService { this.getClientBlameFiles(id), this.getClientClaimFiles(id), ]); - return { blameFiles, claimFiles }; + + const byPublicId = new Map< + string, + { + publicId: string; + fileType?: string; + parties?: Array<{ + role?: string; + fullName?: string; + vehicle?: { + carName?: string; + carModel?: string; + plate?: string; + }; + }>; + blame?: { + requestId?: string; + requestNo?: string; + status?: string; + parties?: Array<{ + role?: string; + fullName?: string; + vehicle?: { + carName?: string; + carModel?: string; + plate?: string; + }; + }>; + createdAt?: Date | string; + updatedAt?: Date | string; + }; + claim?: { + requestId?: string; + requestNo?: string; + status?: string; + claimStatus?: string; + currentStep?: string; + createdAt?: Date | string; + updatedAt?: Date | string; + }; + createdAt?: Date | string; + updatedAt?: Date | string; + } + >(); + + for (const b of blameFiles) { + const key = String((b as any).publicId || (b as any)._id || ""); + if (!key) continue; + const prev = byPublicId.get(key) ?? { publicId: key }; + const partiesPreview = this.buildPartiesPreview((b as any).parties); + prev.fileType = prev.fileType ?? (b as any).type; + prev.blame = { + requestId: (b as any)?._id?.toString?.() || String((b as any)?._id || ""), + requestNo: (b as any).requestNo || String((b as any).requestNumber || ""), + status: (b as any).status, + parties: partiesPreview, + createdAt: (b as any).createdAt, + updatedAt: (b as any).updatedAt, + }; + prev.parties = prev.parties ?? partiesPreview; + prev.createdAt = prev.createdAt ?? (b as any).createdAt; + prev.updatedAt = (b as any).updatedAt ?? prev.updatedAt; + byPublicId.set(key, prev); + } + + for (const c of claimFiles) { + const key = String((c as any).publicId || (c as any)._id || ""); + if (!key) continue; + const prev = byPublicId.get(key) ?? { publicId: key }; + const claimPartiesPreview = this.buildPartiesPreview((c as any)?.snapshot?.parties); + prev.fileType = prev.fileType ?? (c as any)?.snapshot?.accident?.type; + prev.claim = { + requestId: (c as any)?._id?.toString?.() || String((c as any)?._id || ""), + requestNo: (c as any).requestNo || String((c as any).requestNumber || ""), + status: (c as any).status, + claimStatus: (c as any).claimStatus, + currentStep: (c as any).currentStep, + createdAt: (c as any).createdAt, + updatedAt: (c as any).updatedAt, + }; + prev.parties = prev.parties ?? claimPartiesPreview; + prev.createdAt = prev.createdAt ?? (c as any).createdAt; + prev.updatedAt = (c as any).updatedAt ?? prev.updatedAt; + byPublicId.set(key, prev); + } + + const files = Array.from(byPublicId.values()) + .map((f) => ({ + publicId: f.publicId, + fileType: f.fileType, + hasClaim: !!f.claim, + parties: f.parties, + blame: f.blame, + claim: f.claim, + createdAt: f.createdAt, + updatedAt: f.updatedAt, + })) + .sort((a, b) => { + const at = a.updatedAt ? new Date(a.updatedAt).getTime() : 0; + const bt = b.updatedAt ? new Date(b.updatedAt).getTime() : 0; + return bt - at; + }); + + return { + total: files.length, + files, + }; + } + + async retrieveFileDetailsByPublicId(insurerId: string, publicId: string) { + if (!publicId?.trim()) { + throw new BadRequestException("publicId is required"); + } + + const id = this.getClientId(insurerId); + const [blameFiles, claimFiles] = await Promise.all([ + this.getClientBlameFiles(id), + this.getClientClaimFiles(id), + ]); + + const blame = blameFiles.find((b) => String((b as any).publicId) === publicId); + const claim = claimFiles.find((c) => String((c as any).publicId) === publicId); + + if (!blame && !claim) { + throw new NotFoundException("File not found for this publicId"); + } + + const parties = Array.isArray((blame as any)?.parties) ? (blame as any).parties : []; + const firstParty = parties.find((p: any) => p?.role === "FIRST"); + const secondParty = parties.find((p: any) => p?.role === "SECOND"); + + const overview = { + publicId, + requestNo: + (blame as any)?.requestNo || + (claim as any)?.requestNo || + (blame as any)?.requestNumber || + (claim as any)?.requestNumber, + type: (blame as any)?.type, + hasClaim: !!claim, + blameStatus: (blame as any)?.status, + claimStatus: (claim as any)?.status, + claimReviewStatus: (claim as any)?.claimStatus, + createdAt: (blame as any)?.createdAt ?? (claim as any)?.createdAt, + updatedAt: (claim as any)?.updatedAt ?? (blame as any)?.updatedAt, + }; + + const blameDetails = blame + ? { + requestId: + (blame as any)?._id?.toString?.() || String((blame as any)?._id || ""), + requestNo: + (blame as any).requestNo || String((blame as any).requestNumber || ""), + status: (blame as any).status, + blameStatus: (blame as any).blameStatus, + workflow: (blame as any).workflow, + parties: { + first: firstParty + ? { + fullName: firstParty?.person?.fullName, + phoneNumber: firstParty?.person?.phoneNumber, + role: firstParty?.role, + vehicle: firstParty?.vehicle, + } + : undefined, + second: secondParty + ? { + fullName: secondParty?.person?.fullName, + phoneNumber: secondParty?.person?.phoneNumber, + role: secondParty?.role, + vehicle: secondParty?.vehicle, + } + : undefined, + }, + expert: (blame as any).expert + ? { + assignedExpertId: (blame as any).expert?.assignedExpertId, + decision: (blame as any).expert?.decision, + resend: (blame as any).expert?.resend, + rating: (blame as any).expert?.rating, + } + : undefined, + createdAt: (blame as any).createdAt, + updatedAt: (blame as any).updatedAt, + } + : undefined; + + // Keep claim section claim-specific to avoid duplicating shared data already in blame/overview. + const claimDetails = claim + ? { + requestId: + (claim as any)?._id?.toString?.() || String((claim as any)?._id || ""), + requestNo: + (claim as any).requestNo || String((claim as any).requestNumber || ""), + status: (claim as any).status, + claimStatus: (claim as any).claimStatus, + workflow: { + currentStep: (claim as any).currentStep, + nextStep: (claim as any).nextStep, + }, + evaluation: { + damageExpertReply: (claim as any).damageExpertReply, + damageExpertReplyFinal: (claim as any).damageExpertReplyFinal, + damageExpertResend: (claim as any).damageExpertResend, + objection: (claim as any).objection, + priceDrop: (claim as any).priceDrop, + visitLocation: (claim as any).visitLocation, + }, + userResendDocuments: (claim as any).userResendDocuments, + userRating: (claim as any).userRating, + insurerRating: (claim as any).rating, + createdAt: (claim as any).createdAt, + updatedAt: (claim as any).updatedAt, + } + : undefined; + + return { + overview, + blame: blameDetails, + claim: claimDetails, + }; } async addBranch(clientKey: string, branchDto: CreateBranchDto) { From 8ae6f2c91b4184d036243ec0e646371b1a8dde69 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Tue, 28 Apr 2026 15:39:06 +0330 Subject: [PATCH 7/7] Fix get details for insurer --- src/expert-insurer/expert-insurer.service.ts | 41 +++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/expert-insurer/expert-insurer.service.ts b/src/expert-insurer/expert-insurer.service.ts index aa7e550..ff44f3b 100644 --- a/src/expert-insurer/expert-insurer.service.ts +++ b/src/expert-insurer/expert-insurer.service.ts @@ -147,6 +147,25 @@ export class ExpertInsurerService { })); } + private sanitizeVehicleInquiry(vehicle: any) { + if (!vehicle || typeof vehicle !== "object") return vehicle; + const inquiry = vehicle.inquiry; + if (!inquiry || typeof inquiry !== "object") return vehicle; + const { raw, mapped, ...safeInquiry } = inquiry; + return { + ...vehicle, + inquiry: safeInquiry, + }; + } + + private sanitizePartyForInsurerView(party: any) { + if (!party || typeof party !== "object") return party; + return { + ...party, + vehicle: this.sanitizeVehicleInquiry(party.vehicle), + }; + } + private getCombinedFileScore(file: any): number | null { const insurerRating = file?.rating; const userRating = file?.userRating; @@ -543,6 +562,9 @@ export class ExpertInsurerService { const parties = Array.isArray((blame as any)?.parties) ? (blame as any).parties : []; const firstParty = parties.find((p: any) => p?.role === "FIRST"); const secondParty = parties.find((p: any) => p?.role === "SECOND"); + const claimSnapshotParties = Array.isArray((claim as any)?.snapshot?.parties) + ? (claim as any).snapshot.parties + : undefined; const overview = { publicId, @@ -575,7 +597,7 @@ export class ExpertInsurerService { fullName: firstParty?.person?.fullName, phoneNumber: firstParty?.person?.phoneNumber, role: firstParty?.role, - vehicle: firstParty?.vehicle, + vehicle: this.sanitizeVehicleInquiry(firstParty?.vehicle), } : undefined, second: secondParty @@ -583,7 +605,7 @@ export class ExpertInsurerService { fullName: secondParty?.person?.fullName, phoneNumber: secondParty?.person?.phoneNumber, role: secondParty?.role, - vehicle: secondParty?.vehicle, + vehicle: this.sanitizeVehicleInquiry(secondParty?.vehicle), } : undefined, }, @@ -609,6 +631,20 @@ export class ExpertInsurerService { (claim as any).requestNo || String((claim as any).requestNumber || ""), status: (claim as any).status, claimStatus: (claim as any).claimStatus, + owner: (claim as any).owner, + vehicle: (claim as any).vehicle, + money: (claim as any).money, + damage: (claim as any).damage, + media: (claim as any).media, + requiredDocuments: (claim as any).requiredDocuments, + snapshot: (claim as any).snapshot + ? { + ...(claim as any).snapshot, + parties: claimSnapshotParties?.map((p: any) => + this.sanitizePartyForInsurerView(p), + ), + } + : undefined, workflow: { currentStep: (claim as any).currentStep, nextStep: (claim as any).nextStep, @@ -620,6 +656,7 @@ export class ExpertInsurerService { objection: (claim as any).objection, priceDrop: (claim as any).priceDrop, visitLocation: (claim as any).visitLocation, + factors: (claim as any).evaluation?.factors, }, userResendDocuments: (claim as any).userResendDocuments, userRating: (claim as any).userRating,