YARA-1117 and fixed completed status after in person visit has been called

This commit is contained in:
SepehrYahyaee
2026-07-14 11:51:52 +03:30
parent 4aa6e03afb
commit 168e52a475
6 changed files with 92 additions and 54 deletions

View File

@@ -10533,7 +10533,8 @@ export class ClaimRequestManagementService {
`Claim case with ID ${claimRequestId} not found`, `Claim case with ID ${claimRequestId} not found`,
); );
} }
await this.assertV3InPersonClaim(claimCase); const blame = await this.assertV3InPersonClaim(claimCase);
const skipMetalPlate = !!(blame as any).isMadeByFileMaker;
if (!fileDetail) { if (!fileDetail) {
throw new BadRequestException("Video file is required."); throw new BadRequestException("Video file is required.");
@@ -10553,7 +10554,7 @@ export class ClaimRequestManagementService {
); );
} }
const captureProgress = getClaimCaptureProgress(claimCase); const captureProgress = getClaimCaptureProgress(claimCase, { skipMetalPlate });
if (!captureProgress.partsComplete) { if (!captureProgress.partsComplete) {
throw new BadRequestException( throw new BadRequestException(
"Upload photos for all selected damaged parts before the walk-around video.", "Upload photos for all selected damaged parts before the walk-around video.",
@@ -10564,9 +10565,11 @@ export class ClaimRequestManagementService {
"Capture all four car angles before the walk-around video.", "Capture all four car angles before the walk-around video.",
); );
} }
if (!isClaimCaptureStepComplete(claimCase)) { if (!isClaimCaptureStepComplete(claimCase, { skipMetalPlate })) {
throw new BadRequestException( throw new BadRequestException(
"Upload capture-phase vehicle documents (chassis, engine, metal plate) before the walk-around video.", skipMetalPlate
? "Upload capture-phase vehicle documents (chassis and engine) before the walk-around video."
: "Upload capture-phase vehicle documents (chassis, engine, metal plate) before the walk-around video.",
); );
} }

View File

@@ -3530,6 +3530,7 @@ export class ExpertClaimService {
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
claimStatus: ClaimStatus.NEEDS_REVISION, claimStatus: ClaimStatus.NEEDS_REVISION,
status: ClaimCaseStatus.COMPLETED,
"workflow.locked": false, "workflow.locked": false,
$unset: { $unset: {
"workflow.lockedAt": "", "workflow.lockedAt": "",
@@ -3567,7 +3568,7 @@ export class ExpertClaimService {
return { return {
claimRequestId, claimRequestId,
status: ClaimWorkflowStep.CLAIM_COMPLETED, status: ClaimCaseStatus.COMPLETED,
claimStatus: ClaimStatus.NEEDS_REVISION, claimStatus: ClaimStatus.NEEDS_REVISION,
message: "In-person visit requested. User will be notified.", message: "In-person visit requested. User will be notified.",
}; };

View File

@@ -118,8 +118,11 @@ export function getClaimCaptureProgress(
}; };
} }
export function isClaimCaptureStepComplete(claimCase: any): boolean { export function isClaimCaptureStepComplete(
const p = getClaimCaptureProgress(claimCase); claimCase: any,
options?: { skipMetalPlate?: boolean },
): boolean {
const p = getClaimCaptureProgress(claimCase, options);
return ( return (
p.partsComplete && p.anglesComplete && p.capturePhaseDocsComplete p.partsComplete && p.anglesComplete && p.capturePhaseDocsComplete
); );

View File

@@ -364,7 +364,6 @@ export class FileReviewerBlameV4Controller {
@ApiBody({ @ApiBody({
schema: { schema: {
type: "object", type: "object",
required: ["file"],
properties: { file: { type: "string", format: "binary" } }, properties: { file: { type: "string", format: "binary" } },
}, },
}) })
@@ -385,17 +384,19 @@ export class FileReviewerBlameV4Controller {
@Post("upload-video/:requestId") @Post("upload-video/:requestId")
@ApiParam({ name: "requestId" }) @ApiParam({ name: "requestId" })
@ApiOperation({ @ApiOperation({
summary: "Blame accident video (FileReviewer final step)", summary: "Blame accident video (FileReviewer final step — optional)",
description: description:
"Last step of the V4 flow. Requires claim walk-around video and completed capture. " + "Last step of the V4 flow. Requires claim walk-around video and completed capture. " +
"Sets blame status to WAITING_FOR_EXPERT.", "File is optional — omit it to complete the blame without attaching a video.",
}) })
async uploadBlameVideo( async uploadBlameVideo(
@Param("requestId") requestId: string, @Param("requestId") requestId: string,
@CurrentUser() fileReviewer: any, @CurrentUser() fileReviewer: any,
@UploadedFile() file: Express.Multer.File, @UploadedFile() file: Express.Multer.File,
) { ) {
await this.mediaPolicyService.assertForBlame(file, requestId, "video"); if (file) {
await this.mediaPolicyService.assertForBlame(file, requestId, "video");
}
return this.requestManagementService.expertUploadBlameVideoV3( return this.requestManagementService.expertUploadBlameVideoV3(
fileReviewer, fileReviewer,
requestId, requestId,

View File

@@ -365,7 +365,6 @@ export class FileReviewerBlameV5Controller {
@ApiBody({ @ApiBody({
schema: { schema: {
type: "object", type: "object",
required: ["file"],
properties: { file: { type: "string", format: "binary" } }, properties: { file: { type: "string", format: "binary" } },
}, },
}) })
@@ -386,17 +385,19 @@ export class FileReviewerBlameV5Controller {
@Post("upload-video/:requestId") @Post("upload-video/:requestId")
@ApiParam({ name: "requestId" }) @ApiParam({ name: "requestId" })
@ApiOperation({ @ApiOperation({
summary: "Blame accident video (FileReviewer final step — V5)", summary: "Blame accident video (FileReviewer final step — V5, optional)",
description: description:
"Last step of the V5 FileReviewer flow. Requires claim walk-around video and completed capture. " + "Last step of the V5 FileReviewer flow. Requires claim walk-around video and completed capture. " +
"Sets blame status to WAITING_FOR_FINANCIAL_EXPERT (pending FinancialExpert approval).", "File is optional — omit it to complete the blame without attaching a video.",
}) })
async uploadBlameVideo( async uploadBlameVideo(
@Param("requestId") requestId: string, @Param("requestId") requestId: string,
@CurrentUser() fileReviewer: any, @CurrentUser() fileReviewer: any,
@UploadedFile() file: Express.Multer.File, @UploadedFile() file: Express.Multer.File,
) { ) {
await this.mediaPolicyService.assertForBlame(file, requestId, "video"); if (file) {
await this.mediaPolicyService.assertForBlame(file, requestId, "video");
}
return this.requestManagementService.expertUploadBlameVideoV5( return this.requestManagementService.expertUploadBlameVideoV5(
fileReviewer, fileReviewer,
requestId, requestId,

View File

@@ -9325,21 +9325,31 @@ export class RequestManagementService {
async expertUploadBlameVideoV3( async expertUploadBlameVideoV3(
expert: any, expert: any,
requestId: string, requestId: string,
file: Express.Multer.File, file: Express.Multer.File | undefined,
): Promise<{ ): Promise<{
requestId: string; requestId: string;
publicId: string; publicId: string;
videoId: string; videoId: string | undefined;
status: string; status: string;
message: string; message: string;
}> { }> {
if (!file) throw new BadRequestException("Video file is required");
const req = await this.blameRequestDbService.findById(requestId); const req = await this.blameRequestDbService.findById(requestId);
if (!req) throw new NotFoundException("Request not found"); if (!req) throw new NotFoundException("Request not found");
this.assertBlameV3ExpertInPerson(req); this.assertBlameV3ExpertInPerson(req);
await this.verifyExpertAccessForBlameV2(req, expert); await this.verifyExpertAccessForBlameV2(req, expert);
// Idempotent: if blame is already completed (car-capture auto-completed it
// for V4/V5, or a previous call already ran), return success immediately.
if ((req as any).status === CaseStatus.COMPLETED) {
return {
requestId: String(req._id),
publicId: req.publicId,
videoId: undefined,
status: (req as any).status,
message: "File already completed.",
};
}
if (!(req.expert?.decision as any)?.fields?.accidentWay) { if (!(req.expert?.decision as any)?.fields?.accidentWay) {
throw new BadRequestException( throw new BadRequestException(
"Submit accident fields before uploading the blame accident video.", "Submit accident fields before uploading the blame accident video.",
@@ -9381,19 +9391,23 @@ export class RequestManagementService {
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST); const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
if (firstIdx === -1) throw new BadRequestException("First party not found"); if (firstIdx === -1) throw new BadRequestException("First party not found");
const firstParty = req.parties[firstIdx]; const firstParty = req.parties[firstIdx];
if (firstParty?.evidence?.videoId) {
throw new ConflictException("Blame accident video already uploaded"); // Store video if provided; skip silently for V4/V5 where it is optional.
let videoId: string | undefined;
if (file) {
if (firstParty?.evidence?.videoId) {
throw new ConflictException("Blame accident video already uploaded");
}
const videoDocument = await this.blameVideoDbService.create({
fileName: file.filename,
path: file.path,
requestId: new Types.ObjectId(requestId),
} as any);
if (!firstParty.evidence) firstParty.evidence = {} as any;
firstParty.evidence.videoId = String((videoDocument as any)._id);
videoId = firstParty.evidence.videoId;
} }
const videoDocument = await this.blameVideoDbService.create({
fileName: file.filename,
path: file.path,
requestId: new Types.ObjectId(requestId),
} as any);
if (!firstParty.evidence) firstParty.evidence = {} as any;
firstParty.evidence.videoId = String((videoDocument as any)._id);
this.pushWorkflowSteps(req, [ this.pushWorkflowSteps(req, [
WorkflowStep.FIRST_VIDEO, WorkflowStep.FIRST_VIDEO,
WorkflowStep.WAITING_FOR_GUILT_DECISION, WorkflowStep.WAITING_FOR_GUILT_DECISION,
@@ -9410,7 +9424,7 @@ export class RequestManagementService {
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
actorType: "field_expert", actorType: "field_expert",
}, },
metadata: { videoId: firstParty.evidence.videoId }, metadata: { videoId: videoId ?? null },
} as any); } as any);
if (typeof (req as any).markModified === "function") { if (typeof (req as any).markModified === "function") {
@@ -9449,10 +9463,11 @@ export class RequestManagementService {
return { return {
requestId: String(req._id), requestId: String(req._id),
publicId: req.publicId, publicId: req.publicId,
videoId: firstParty.evidence.videoId, videoId,
status: req.status, status: req.status,
message: message: file
"Blame accident video uploaded. File is now waiting for expert review.", ? "Blame accident video uploaded. File is now waiting for expert review."
: "File completed. Claim is now ready for damage expert review.",
}; };
} }
@@ -9694,21 +9709,31 @@ export class RequestManagementService {
async expertUploadBlameVideoV5( async expertUploadBlameVideoV5(
expert: any, expert: any,
requestId: string, requestId: string,
file: Express.Multer.File, file: Express.Multer.File | undefined,
): Promise<{ ): Promise<{
requestId: string; requestId: string;
publicId: string; publicId: string;
videoId: string; videoId: string | undefined;
status: string; status: string;
message: string; message: string;
}> { }> {
if (!file) throw new BadRequestException("Video file is required");
const req = await this.blameRequestDbService.findById(requestId); const req = await this.blameRequestDbService.findById(requestId);
if (!req) throw new NotFoundException("Request not found"); if (!req) throw new NotFoundException("Request not found");
this.assertBlameV3ExpertInPerson(req); this.assertBlameV3ExpertInPerson(req);
await this.verifyExpertAccessForBlameV2(req, expert); await this.verifyExpertAccessForBlameV2(req, expert);
// Idempotent: blame already completed (car-capture auto-completed it for
// V4/V5, or a previous call already ran).
if ((req as any).status === CaseStatus.COMPLETED) {
return {
requestId: String(req._id),
publicId: req.publicId,
videoId: undefined,
status: (req as any).status,
message: "File already completed.",
};
}
if (!(req.expert?.decision as any)?.fields?.accidentWay) { if (!(req.expert?.decision as any)?.fields?.accidentWay) {
throw new BadRequestException( throw new BadRequestException(
"Submit accident fields before uploading the blame accident video.", "Submit accident fields before uploading the blame accident video.",
@@ -9750,19 +9775,23 @@ export class RequestManagementService {
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST); const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
if (firstIdx === -1) throw new BadRequestException("First party not found"); if (firstIdx === -1) throw new BadRequestException("First party not found");
const firstParty = req.parties[firstIdx]; const firstParty = req.parties[firstIdx];
if (firstParty?.evidence?.videoId) {
throw new ConflictException("Blame accident video already uploaded"); // Store video if provided; skip silently for V4/V5 where it is optional.
let videoId: string | undefined;
if (file) {
if (firstParty?.evidence?.videoId) {
throw new ConflictException("Blame accident video already uploaded");
}
const videoDocument = await this.blameVideoDbService.create({
fileName: file.filename,
path: file.path,
requestId: new Types.ObjectId(requestId),
} as any);
if (!firstParty.evidence) firstParty.evidence = {} as any;
firstParty.evidence.videoId = String((videoDocument as any)._id);
videoId = firstParty.evidence.videoId;
} }
const videoDocument = await this.blameVideoDbService.create({
fileName: file.filename,
path: file.path,
requestId: new Types.ObjectId(requestId),
} as any);
if (!firstParty.evidence) firstParty.evidence = {} as any;
firstParty.evidence.videoId = String((videoDocument as any)._id);
this.pushWorkflowSteps(req, [ this.pushWorkflowSteps(req, [
WorkflowStep.FIRST_VIDEO, WorkflowStep.FIRST_VIDEO,
WorkflowStep.WAITING_FOR_GUILT_DECISION, WorkflowStep.WAITING_FOR_GUILT_DECISION,
@@ -9779,7 +9808,7 @@ export class RequestManagementService {
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
actorType: "file_reviewer", actorType: "file_reviewer",
}, },
metadata: { videoId: firstParty.evidence.videoId }, metadata: { videoId: videoId ?? null },
} as any); } as any);
if (typeof (req as any).markModified === "function") { if (typeof (req as any).markModified === "function") {
@@ -9825,11 +9854,11 @@ export class RequestManagementService {
return { return {
requestId: String(req._id), requestId: String(req._id),
publicId: req.publicId, publicId: req.publicId,
videoId: firstParty.evidence.videoId, videoId,
status: req.status, status: req.status,
message: message: file
"Blame accident video uploaded. File is now in expert review queue. " + ? "Blame accident video uploaded. File is now in expert review queue. After the full claim flow completes and the owner signs, the FileMaker must approve before fanavaran submission."
"After the full claim flow completes and the owner signs, the FileMaker must approve before fanavaran submission.", : "File completed. Claim is now ready for damage expert review.",
}; };
} }