From 9e2cf9bcdfcc889c89364bad2b4053338b52b98b Mon Sep 17 00:00:00 2001 From: Sepehr Yahyaee Date: Thu, 2 Jul 2026 13:17:04 +0330 Subject: [PATCH] Added roles for GET car parts APIs --- .../claim-request-management.v2.controller.ts | 93 ++++++++++++++----- .../request-management.service.ts | 38 ++++++-- 2 files changed, 98 insertions(+), 33 deletions(-) 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 ff3bf64..0e78a83 100644 --- a/src/claim-request-management/claim-request-management.v2.controller.ts +++ b/src/claim-request-management/claim-request-management.v2.controller.ts @@ -15,7 +15,15 @@ import { UploadedFile, } from "@nestjs/common"; import { readFile } from "node:fs/promises"; -import { ApiBearerAuth, ApiParam, ApiTags, ApiOperation, ApiResponse, ApiBody, ApiConsumes } from "@nestjs/swagger"; +import { + ApiBearerAuth, + ApiParam, + ApiTags, + ApiOperation, + ApiResponse, + ApiBody, + ApiConsumes, +} from "@nestjs/swagger"; import { FileInterceptor } from "@nestjs/platform-express"; import { diskStorage } from "multer"; import { extname } from "node:path"; @@ -33,9 +41,15 @@ import { SelectOuterPartsV2Dto, SelectOuterPartsV2ResponseDto, } from "./dto/select-outer-parts-v2.dto"; -import { SelectOtherPartsV2Dto, SelectOtherPartsV2ResponseDto } from "./dto/select-other-parts-v2.dto"; +import { + SelectOtherPartsV2Dto, + SelectOtherPartsV2ResponseDto, +} from "./dto/select-other-parts-v2.dto"; import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto"; -import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto } from "./dto/upload-document-v2.dto"; +import { + UploadRequiredDocumentV2Dto, + UploadRequiredDocumentV2ResponseDto, +} from "./dto/upload-document-v2.dto"; import { CapturePartV2Dto, CapturePartV2ResponseDto, @@ -52,7 +66,13 @@ import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog"; @Controller("v2/claim-request-management") @ApiBearerAuth() @UseGuards(GlobalGuard, RolesGuard) -@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT, RoleEnum.REGISTRAR) +@Roles( + RoleEnum.USER, + RoleEnum.FIELD_EXPERT, + RoleEnum.REGISTRAR, + RoleEnum.FILE_MAKER, + RoleEnum.FILE_REVIEWER, +) export class ClaimRequestManagementV2Controller { constructor( private readonly claimRequestManagementService: ClaimRequestManagementService, @@ -142,7 +162,10 @@ export class ClaimRequestManagementV2Controller { }) @ApiParam({ name: "claimRequestId" }) @ApiResponse({ status: 200, description: "Claim returned to expert queue" }) - @ApiResponse({ status: 400, description: "Resend requires uploads or wrong step" }) + @ApiResponse({ + status: 400, + description: "Resend requires uploads or wrong step", + }) async acknowledgeExpertResend( @Param("claimRequestId") claimRequestId: string, @CurrentUser() user: any, @@ -156,7 +179,9 @@ export class ClaimRequestManagementV2Controller { } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( - error instanceof Error ? error.message : "Failed to acknowledge expert resend", + error instanceof Error + ? error.message + : "Failed to acknowledge expert resend", ); } } @@ -181,7 +206,10 @@ export class ClaimRequestManagementV2Controller { }) @ApiBody({ type: UserObjectionV2Dto }) @ApiResponse({ status: 200, description: "Objection stored" }) - @ApiResponse({ status: 400, description: "No active resend or empty payload" }) + @ApiResponse({ + status: 400, + description: "No active resend or empty payload", + }) @ApiResponse({ status: 403, description: "Not the claim owner" }) @ApiResponse({ status: 404, description: "Claim not found" }) @ApiResponse({ status: 409, description: "Objection already submitted" }) @@ -222,7 +250,10 @@ export class ClaimRequestManagementV2Controller { }) @ApiBody({ type: UserRatingDto }) @ApiResponse({ status: 200, description: "Rating saved" }) - @ApiResponse({ status: 400, description: "Claim not completed or invalid scores" }) + @ApiResponse({ + status: 400, + description: "Claim not completed or invalid scores", + }) @ApiResponse({ status: 403, description: "Not the claim owner" }) @ApiResponse({ status: 404, description: "Claim not found" }) @ApiResponse({ status: 409, description: "Rating already submitted" }) @@ -270,21 +301,32 @@ export class ClaimRequestManagementV2Controller { type: "object", required: ["sign", "agree", "branchId"], properties: { - sign: { type: "string", format: "binary", description: "Signature image" }, + sign: { + type: "string", + format: "binary", + description: "Signature image", + }, agree: { type: "boolean", description: "true to accept expert pricing and complete the claim", }, branchId: { type: "string", - description: "Insurer branch id (must belong to the claim owner's insurer; if pricing lists branch options, must match one of them)", + description: + "Insurer branch id (must belong to the claim owner's insurer; if pricing lists branch options, must match one of them)", example: "507f1f77bcf86cd799439011", }, }, }, }) - @ApiResponse({ status: 200, description: "Signature stored; claim completed or rejected" }) - @ApiResponse({ status: 400, description: "Wrong step/status or missing file" }) + @ApiResponse({ + status: 200, + description: "Signature stored; claim completed or rejected", + }) + @ApiResponse({ + status: 400, + description: "Wrong step/status or missing file", + }) @ApiResponse({ status: 403, description: "Not the claim owner" }) @ApiResponse({ status: 404, description: "Claim not found" }) @ApiResponse({ status: 409, description: "Already signed" }) @@ -314,7 +356,9 @@ export class ClaimRequestManagementV2Controller { } await this.mediaPolicyService.assertForClaim(sign, claimRequestId, "image"); const agreed = - typeof agree === "string" ? agree === "true" || agree === "1" : Boolean(agree); + typeof agree === "string" + ? agree === "true" || agree === "1" + : Boolean(agree); try { return await this.claimRequestManagementService.submitOwnerInsurerApprovalSignV2( claimRequestId, @@ -453,8 +497,7 @@ export class ClaimRequestManagementV2Controller { }) @ApiBody({ type: SelectOuterPartsV2Dto, - description: - "Selected vehicle type + selected outer part IDs from catalog", + description: "Selected vehicle type + selected outer part IDs from catalog", examples: { example1: { summary: "Sedan - minor front damage", @@ -522,9 +565,7 @@ export class ClaimRequestManagementV2Controller { } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( - error instanceof Error - ? error.message - : "Failed to select outer parts", + error instanceof Error ? error.message : "Failed to select outer parts", ); } } @@ -638,9 +679,7 @@ Optional: upload car green card file in the same step. } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( - error instanceof Error - ? error.message - : "Failed to select other parts", + error instanceof Error ? error.message : "Failed to select other parts", ); } } @@ -862,7 +901,7 @@ Returns status of each item (uploaded/captured or not). type: "string", example: "front", description: - 'For angle: front/back/left/right. For part: hood/front_bumper/etc.', + "For angle: front/back/left/right. For part: hood/front_bumper/etc.", }, file: { type: "string", @@ -1005,7 +1044,10 @@ Returns status of each item (uploaded/captured or not). description: "Video uploaded successfully", type: VideoCaptureV2ResponseDto, }) - @ApiResponse({ status: 400, description: "Wrong workflow step or missing file" }) + @ApiResponse({ + status: 400, + description: "Wrong workflow step or missing file", + }) @ApiResponse({ status: 403, description: "Not the claim owner" }) @ApiResponse({ status: 404, description: "Claim not found" }) @ApiResponse({ status: 409, description: "Video already uploaded" }) @@ -1025,9 +1067,10 @@ Returns status of each item (uploaded/captured or not). } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( - error instanceof Error ? error.message : "Failed to upload car capture video", + error instanceof Error + ? error.message + : "Failed to upload car capture video", ); } } - } diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 13493ad..2ae576a 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -1165,7 +1165,9 @@ export class RequestManagementService { {}, err, ); - await (req as any).save(); + await this.blameRequestDbService.findByIdAndUpdate(req._id, { + $set: { inquiries: req.inquiries }, + }); throw new HttpException("Inquiry failed", HttpStatus.BAD_REQUEST); } @@ -1180,7 +1182,9 @@ export class RequestManagementService { raw: inquiryRaw, mapped: inquiryMapped, }); - await (req as any).save(); + await this.blameRequestDbService.findByIdAndUpdate(req._id, { + $set: { inquiries: req.inquiries }, + }); throw new HttpException( inquiryMapped.Error.Message || "Inquiry returned error", HttpStatus.BAD_REQUEST, @@ -1232,7 +1236,9 @@ export class RequestManagementService { `[SANDHUB] personal inquiry failed request=${req._id} nationalCode=${personalNationalCode}: ${err?.message || err}`, ); this.recordPartyCaseInquiryStatus(req, "person", role, false, {}, err); - await (req as any).save(); + await this.blameRequestDbService.findByIdAndUpdate(req._id, { + $set: { inquiries: req.inquiries }, + }); throw new HttpException( "Personal identity inquiry failed", HttpStatus.BAD_REQUEST, @@ -1353,7 +1359,9 @@ export class RequestManagementService { {}, err, ); - await (req as any).save(); + await this.blameRequestDbService.findByIdAndUpdate(req._id, { + $set: { inquiries: req.inquiries }, + }); throw new HttpException( "Car body inquiry failed", HttpStatus.BAD_REQUEST, @@ -1422,8 +1430,7 @@ export class RequestManagementService { await this.advanceWorkflowToNext(req, stepKey); // History - if (!Array.isArray(req.history)) req.history = []; - req.history.push({ + const historyEntry: any = { type: `${stepKey}_SUBMITTED`, actor: { actorId: Types.ObjectId.isValid(user?.sub) @@ -1437,9 +1444,24 @@ export class RequestManagementService { companyCode, companyName: clientName, }, - } as any); + }; - await (req as any).save(); + // Use findByIdAndUpdate instead of save() to avoid Mongoose VersionError + // when two parties submit their forms concurrently. Each party only writes + // its own paths, so the $set operations are non-overlapping and safe. + await this.blameRequestDbService.findByIdAndUpdate(req._id, { + $set: { + [`parties.${idx}.person`]: party.person, + [`parties.${idx}.vehicle`]: party.vehicle, + [`parties.${idx}.insurance`]: party.insurance, + inquiries: req.inquiries, + "workflow.completedSteps": req.workflow.completedSteps, + "workflow.currentStep": req.workflow.currentStep, + "workflow.nextStep": req.workflow.nextStep, + status: req.status, + }, + $push: { history: historyEntry }, + }); return { requestId: req._id,