1
0
forked from Yara724/api

Added roles for GET car parts APIs

This commit is contained in:
2026-07-02 13:17:04 +03:30
parent b43f1a86dc
commit 9e2cf9bcdf
2 changed files with 98 additions and 33 deletions

View File

@@ -15,7 +15,15 @@ import {
UploadedFile, UploadedFile,
} from "@nestjs/common"; } from "@nestjs/common";
import { readFile } from "node:fs/promises"; 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 { FileInterceptor } from "@nestjs/platform-express";
import { diskStorage } from "multer"; import { diskStorage } from "multer";
import { extname } from "node:path"; import { extname } from "node:path";
@@ -33,9 +41,15 @@ import {
SelectOuterPartsV2Dto, SelectOuterPartsV2Dto,
SelectOuterPartsV2ResponseDto, SelectOuterPartsV2ResponseDto,
} from "./dto/select-outer-parts-v2.dto"; } 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 { 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 { import {
CapturePartV2Dto, CapturePartV2Dto,
CapturePartV2ResponseDto, CapturePartV2ResponseDto,
@@ -52,7 +66,13 @@ import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
@Controller("v2/claim-request-management") @Controller("v2/claim-request-management")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(GlobalGuard, RolesGuard) @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 { export class ClaimRequestManagementV2Controller {
constructor( constructor(
private readonly claimRequestManagementService: ClaimRequestManagementService, private readonly claimRequestManagementService: ClaimRequestManagementService,
@@ -142,7 +162,10 @@ export class ClaimRequestManagementV2Controller {
}) })
@ApiParam({ name: "claimRequestId" }) @ApiParam({ name: "claimRequestId" })
@ApiResponse({ status: 200, description: "Claim returned to expert queue" }) @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( async acknowledgeExpertResend(
@Param("claimRequestId") claimRequestId: string, @Param("claimRequestId") claimRequestId: string,
@CurrentUser() user: any, @CurrentUser() user: any,
@@ -156,7 +179,9 @@ export class ClaimRequestManagementV2Controller {
} catch (error) { } catch (error) {
if (error instanceof HttpException) throw error; if (error instanceof HttpException) throw error;
throw new InternalServerErrorException( 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 }) @ApiBody({ type: UserObjectionV2Dto })
@ApiResponse({ status: 200, description: "Objection stored" }) @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: 403, description: "Not the claim owner" })
@ApiResponse({ status: 404, description: "Claim not found" }) @ApiResponse({ status: 404, description: "Claim not found" })
@ApiResponse({ status: 409, description: "Objection already submitted" }) @ApiResponse({ status: 409, description: "Objection already submitted" })
@@ -222,7 +250,10 @@ export class ClaimRequestManagementV2Controller {
}) })
@ApiBody({ type: UserRatingDto }) @ApiBody({ type: UserRatingDto })
@ApiResponse({ status: 200, description: "Rating saved" }) @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: 403, description: "Not the claim owner" })
@ApiResponse({ status: 404, description: "Claim not found" }) @ApiResponse({ status: 404, description: "Claim not found" })
@ApiResponse({ status: 409, description: "Rating already submitted" }) @ApiResponse({ status: 409, description: "Rating already submitted" })
@@ -270,21 +301,32 @@ export class ClaimRequestManagementV2Controller {
type: "object", type: "object",
required: ["sign", "agree", "branchId"], required: ["sign", "agree", "branchId"],
properties: { properties: {
sign: { type: "string", format: "binary", description: "Signature image" }, sign: {
type: "string",
format: "binary",
description: "Signature image",
},
agree: { agree: {
type: "boolean", type: "boolean",
description: "true to accept expert pricing and complete the claim", description: "true to accept expert pricing and complete the claim",
}, },
branchId: { branchId: {
type: "string", 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", example: "507f1f77bcf86cd799439011",
}, },
}, },
}, },
}) })
@ApiResponse({ status: 200, description: "Signature stored; claim completed or rejected" }) @ApiResponse({
@ApiResponse({ status: 400, description: "Wrong step/status or missing file" }) 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: 403, description: "Not the claim owner" })
@ApiResponse({ status: 404, description: "Claim not found" }) @ApiResponse({ status: 404, description: "Claim not found" })
@ApiResponse({ status: 409, description: "Already signed" }) @ApiResponse({ status: 409, description: "Already signed" })
@@ -314,7 +356,9 @@ export class ClaimRequestManagementV2Controller {
} }
await this.mediaPolicyService.assertForClaim(sign, claimRequestId, "image"); await this.mediaPolicyService.assertForClaim(sign, claimRequestId, "image");
const agreed = const agreed =
typeof agree === "string" ? agree === "true" || agree === "1" : Boolean(agree); typeof agree === "string"
? agree === "true" || agree === "1"
: Boolean(agree);
try { try {
return await this.claimRequestManagementService.submitOwnerInsurerApprovalSignV2( return await this.claimRequestManagementService.submitOwnerInsurerApprovalSignV2(
claimRequestId, claimRequestId,
@@ -453,8 +497,7 @@ export class ClaimRequestManagementV2Controller {
}) })
@ApiBody({ @ApiBody({
type: SelectOuterPartsV2Dto, type: SelectOuterPartsV2Dto,
description: description: "Selected vehicle type + selected outer part IDs from catalog",
"Selected vehicle type + selected outer part IDs from catalog",
examples: { examples: {
example1: { example1: {
summary: "Sedan - minor front damage", summary: "Sedan - minor front damage",
@@ -522,9 +565,7 @@ export class ClaimRequestManagementV2Controller {
} catch (error) { } catch (error) {
if (error instanceof HttpException) throw error; if (error instanceof HttpException) throw error;
throw new InternalServerErrorException( throw new InternalServerErrorException(
error instanceof Error error instanceof Error ? error.message : "Failed to select outer parts",
? error.message
: "Failed to select outer parts",
); );
} }
} }
@@ -638,9 +679,7 @@ Optional: upload car green card file in the same step.
} catch (error) { } catch (error) {
if (error instanceof HttpException) throw error; if (error instanceof HttpException) throw error;
throw new InternalServerErrorException( throw new InternalServerErrorException(
error instanceof Error error instanceof Error ? error.message : "Failed to select other parts",
? error.message
: "Failed to select other parts",
); );
} }
} }
@@ -862,7 +901,7 @@ Returns status of each item (uploaded/captured or not).
type: "string", type: "string",
example: "front", example: "front",
description: 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: { file: {
type: "string", type: "string",
@@ -1005,7 +1044,10 @@ Returns status of each item (uploaded/captured or not).
description: "Video uploaded successfully", description: "Video uploaded successfully",
type: VideoCaptureV2ResponseDto, 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: 403, description: "Not the claim owner" })
@ApiResponse({ status: 404, description: "Claim not found" }) @ApiResponse({ status: 404, description: "Claim not found" })
@ApiResponse({ status: 409, description: "Video already uploaded" }) @ApiResponse({ status: 409, description: "Video already uploaded" })
@@ -1025,9 +1067,10 @@ Returns status of each item (uploaded/captured or not).
} catch (error) { } catch (error) {
if (error instanceof HttpException) throw error; if (error instanceof HttpException) throw error;
throw new InternalServerErrorException( 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",
); );
} }
} }
} }

View File

@@ -1165,7 +1165,9 @@ export class RequestManagementService {
{}, {},
err, err,
); );
await (req as any).save(); await this.blameRequestDbService.findByIdAndUpdate(req._id, {
$set: { inquiries: req.inquiries },
});
throw new HttpException("Inquiry failed", HttpStatus.BAD_REQUEST); throw new HttpException("Inquiry failed", HttpStatus.BAD_REQUEST);
} }
@@ -1180,7 +1182,9 @@ export class RequestManagementService {
raw: inquiryRaw, raw: inquiryRaw,
mapped: inquiryMapped, mapped: inquiryMapped,
}); });
await (req as any).save(); await this.blameRequestDbService.findByIdAndUpdate(req._id, {
$set: { inquiries: req.inquiries },
});
throw new HttpException( throw new HttpException(
inquiryMapped.Error.Message || "Inquiry returned error", inquiryMapped.Error.Message || "Inquiry returned error",
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
@@ -1232,7 +1236,9 @@ export class RequestManagementService {
`[SANDHUB] personal inquiry failed request=${req._id} nationalCode=${personalNationalCode}: ${err?.message || err}`, `[SANDHUB] personal inquiry failed request=${req._id} nationalCode=${personalNationalCode}: ${err?.message || err}`,
); );
this.recordPartyCaseInquiryStatus(req, "person", role, false, {}, 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( throw new HttpException(
"Personal identity inquiry failed", "Personal identity inquiry failed",
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
@@ -1353,7 +1359,9 @@ export class RequestManagementService {
{}, {},
err, err,
); );
await (req as any).save(); await this.blameRequestDbService.findByIdAndUpdate(req._id, {
$set: { inquiries: req.inquiries },
});
throw new HttpException( throw new HttpException(
"Car body inquiry failed", "Car body inquiry failed",
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
@@ -1422,8 +1430,7 @@ export class RequestManagementService {
await this.advanceWorkflowToNext(req, stepKey); await this.advanceWorkflowToNext(req, stepKey);
// History // History
if (!Array.isArray(req.history)) req.history = []; const historyEntry: any = {
req.history.push({
type: `${stepKey}_SUBMITTED`, type: `${stepKey}_SUBMITTED`,
actor: { actor: {
actorId: Types.ObjectId.isValid(user?.sub) actorId: Types.ObjectId.isValid(user?.sub)
@@ -1437,9 +1444,24 @@ export class RequestManagementService {
companyCode, companyCode,
companyName: clientName, 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 { return {
requestId: req._id, requestId: req._id,