1
0
forked from Yara724/api

Compare commits

..

3 Commits

Author SHA1 Message Date
SepehrYahyaee
168e52a475 YARA-1117 and fixed completed status after in person visit has been called 2026-07-14 11:51:52 +03:30
SepehrYahyaee
4aa6e03afb YARA-1119 2026-07-14 11:32:03 +03:30
SepehrYahyaee
36a34e27b3 YARA-1115 2026-07-14 11:23:39 +03:30
6 changed files with 144 additions and 73 deletions

View File

@@ -161,6 +161,7 @@ import {
getClaimCaptureProgress,
isCapturePhaseDamagedPartyDocKey,
isClaimCaptureStepComplete,
OPTIONAL_CAPTURE_PHASE_DOC_KEYS_V4V5,
} from "src/helpers/claim-capture-phase";
import { HttpService } from "@nestjs/axios";
import { firstValueFrom } from "rxjs";
@@ -8138,7 +8139,7 @@ export class ClaimRequestManagementService {
file: Express.Multer.File,
currentUserId: string,
actor?: { sub: string; role?: string },
options?: { v3InPersonFlow?: boolean },
options?: { v3InPersonFlow?: boolean; skipMetalPlate?: boolean },
): Promise<UploadRequiredDocumentV2ResponseDto> {
try {
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
@@ -8315,12 +8316,16 @@ export class ClaimRequestManagementService {
k === body.documentKey ||
this.isRequiredDocumentUploadedOnClaim(claimCase, k);
remaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter(
(k) => !afterThis(k),
(k) => {
if (options?.skipMetalPlate && OPTIONAL_CAPTURE_PHASE_DOC_KEYS_V4V5.includes(k as any)) return false;
return !afterThis(k);
},
).length;
if (remaining === 0) {
const progressAfterDoc = getClaimCaptureProgress(claimCase, {
assumeCapturePhaseDocKey: body.documentKey,
skipMetalPlate: options?.skipMetalPlate,
});
if (
@@ -10196,9 +10201,10 @@ export class ClaimRequestManagementService {
private shapeCaptureRequirementsForV3(
claimCase: any,
_blame: any,
blame: any,
base: GetCaptureRequirementsV2ResponseDto,
): GetCaptureRequirementsV2ResponseDto {
const skipMetalPlate = !!(blame as any)?.isMadeByFileMaker;
const step = claimCase.workflow?.currentStep;
const capturePartDone = this.claimV3StepCompleted(
claimCase,
@@ -10258,7 +10264,9 @@ export class ClaimRequestManagementService {
if (step === ClaimWorkflowStep.CAPTURE_PART_DAMAGES) {
const capturePhaseDocs = base.requiredDocuments.filter(
(d) => d.preferUploadDuringCapture,
(d) =>
d.preferUploadDuringCapture &&
!(skipMetalPlate && OPTIONAL_CAPTURE_PHASE_DOC_KEYS_V4V5.includes(d.key as any)),
);
return {
...base,
@@ -10298,6 +10306,7 @@ export class ClaimRequestManagementService {
);
}
const blame = await this.assertV3InPersonClaim(claimCase);
const skipMetalPlate = !!(blame as any).isMadeByFileMaker;
const isCaptureDoc = isCapturePhaseDamagedPartyDocKey(body.documentKey);
@@ -10306,7 +10315,7 @@ export class ClaimRequestManagementService {
this.assertV3ClaimWorkflowStep(
claimCase,
ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
"Upload chassis, engine, and metal plate photos during capture after damaged-part photos and car angles.",
"Upload chassis and engine photos during capture after damaged-part photos and car angles.",
);
if (
!this.claimV3StepCompleted(
@@ -10318,15 +10327,15 @@ export class ClaimRequestManagementService {
"Complete outer and other part selection before capture-phase vehicle documents.",
);
}
const captureProgress = getClaimCaptureProgress(claimCase);
const captureProgress = getClaimCaptureProgress(claimCase, { skipMetalPlate });
if (!captureProgress.partsComplete) {
throw new BadRequestException(
"Upload photos for all selected damaged parts before chassis, engine, or metal plate photos.",
"Upload photos for all selected damaged parts before chassis or engine photos.",
);
}
if (!captureProgress.anglesComplete) {
throw new BadRequestException(
"Capture all four car angles before chassis, engine, or metal plate photos.",
"Capture all four car angles before chassis or engine photos.",
);
}
} else {
@@ -10346,7 +10355,7 @@ export class ClaimRequestManagementService {
file,
currentUserId,
actor,
{ v3InPersonFlow: true },
{ v3InPersonFlow: true, skipMetalPlate },
);
}
@@ -10524,7 +10533,8 @@ export class ClaimRequestManagementService {
`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) {
throw new BadRequestException("Video file is required.");
@@ -10544,7 +10554,7 @@ export class ClaimRequestManagementService {
);
}
const captureProgress = getClaimCaptureProgress(claimCase);
const captureProgress = getClaimCaptureProgress(claimCase, { skipMetalPlate });
if (!captureProgress.partsComplete) {
throw new BadRequestException(
"Upload photos for all selected damaged parts before the walk-around video.",
@@ -10555,9 +10565,11 @@ export class ClaimRequestManagementService {
"Capture all four car angles before the walk-around video.",
);
}
if (!isClaimCaptureStepComplete(claimCase)) {
if (!isClaimCaptureStepComplete(claimCase, { skipMetalPlate })) {
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, {
claimStatus: ClaimStatus.NEEDS_REVISION,
status: ClaimCaseStatus.COMPLETED,
"workflow.locked": false,
$unset: {
"workflow.lockedAt": "",
@@ -3567,7 +3568,7 @@ export class ExpertClaimService {
return {
claimRequestId,
status: ClaimWorkflowStep.CLAIM_COMPLETED,
status: ClaimCaseStatus.COMPLETED,
claimStatus: ClaimStatus.NEEDS_REVISION,
message: "In-person visit requested. User will be notified.",
};

View File

@@ -16,6 +16,12 @@ export const CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS = [
"damaged_metal_plate",
] as const;
/** Metal-plate keys that are optional in the V4/V5 FileMaker flow. */
export const OPTIONAL_CAPTURE_PHASE_DOC_KEYS_V4V5 = [
"damaged_metal_plate",
"guilty_metal_plate",
] as const;
export type CapturePhaseSequence =
| "parts"
| "angles"
@@ -52,7 +58,7 @@ function isRequiredDocumentUploadedOnClaim(
export function getClaimCaptureProgress(
claimCase: any,
options?: { assumeCapturePhaseDocKey?: string },
options?: { assumeCapturePhaseDocKey?: string; skipMetalPlate?: boolean },
): ClaimCaptureProgress {
const carType = claimCase?.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
const selectedNorm = normalizeDamageSelectedParts(
@@ -83,6 +89,7 @@ export function getClaimCaptureProgress(
const capturePhaseDocsRemaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter(
(k) => {
if (options?.skipMetalPlate && OPTIONAL_CAPTURE_PHASE_DOC_KEYS_V4V5.includes(k as any)) return false;
if (k === options?.assumeCapturePhaseDocKey) return false;
return !isRequiredDocumentUploadedOnClaim(claimCase, k);
},
@@ -111,8 +118,11 @@ export function getClaimCaptureProgress(
};
}
export function isClaimCaptureStepComplete(claimCase: any): boolean {
const p = getClaimCaptureProgress(claimCase);
export function isClaimCaptureStepComplete(
claimCase: any,
options?: { skipMetalPlate?: boolean },
): boolean {
const p = getClaimCaptureProgress(claimCase, options);
return (
p.partsComplete && p.anglesComplete && p.capturePhaseDocsComplete
);

View File

@@ -364,7 +364,6 @@ export class FileReviewerBlameV4Controller {
@ApiBody({
schema: {
type: "object",
required: ["file"],
properties: { file: { type: "string", format: "binary" } },
},
})
@@ -385,17 +384,19 @@ export class FileReviewerBlameV4Controller {
@Post("upload-video/:requestId")
@ApiParam({ name: "requestId" })
@ApiOperation({
summary: "Blame accident video (FileReviewer final step)",
summary: "Blame accident video (FileReviewer final step — optional)",
description:
"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(
@Param("requestId") requestId: string,
@CurrentUser() fileReviewer: any,
@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(
fileReviewer,
requestId,

View File

@@ -365,7 +365,6 @@ export class FileReviewerBlameV5Controller {
@ApiBody({
schema: {
type: "object",
required: ["file"],
properties: { file: { type: "string", format: "binary" } },
},
})
@@ -386,17 +385,19 @@ export class FileReviewerBlameV5Controller {
@Post("upload-video/:requestId")
@ApiParam({ name: "requestId" })
@ApiOperation({
summary: "Blame accident video (FileReviewer final step — V5)",
summary: "Blame accident video (FileReviewer final step — V5, optional)",
description:
"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(
@Param("requestId") requestId: string,
@CurrentUser() fileReviewer: any,
@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(
fileReviewer,
requestId,

View File

@@ -8695,13 +8695,16 @@ export class RequestManagementService {
partyRole: PartyRole,
actor: any,
): void {
// V4/V5 FileMaker files skip the location step — it is auto-completed here.
const skipLocation = !!(req as any).isMadeByFileMaker;
if (partyRole === PartyRole.FIRST) {
this.pushWorkflowSteps(req, [
WorkflowStep.CREATED,
WorkflowStep.FIRST_INITIAL_FORM,
]);
req.workflow.currentStep = WorkflowStep.FIRST_LOCATION;
req.workflow.nextStep = WorkflowStep.FIRST_VOICE;
const completedSteps = skipLocation
? [WorkflowStep.CREATED, WorkflowStep.FIRST_INITIAL_FORM, WorkflowStep.FIRST_LOCATION]
: [WorkflowStep.CREATED, WorkflowStep.FIRST_INITIAL_FORM];
this.pushWorkflowSteps(req, completedSteps);
req.workflow.currentStep = skipLocation ? WorkflowStep.FIRST_VOICE : WorkflowStep.FIRST_LOCATION;
req.workflow.nextStep = skipLocation ? WorkflowStep.FIRST_DESCRIPTION : WorkflowStep.FIRST_VOICE;
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
const guiltyUserId = req.parties?.[firstIdx]?.person?.userId;
@@ -8716,9 +8719,12 @@ export class RequestManagementService {
(req as any).markModified("expert");
}
} else {
this.pushWorkflowSteps(req, [WorkflowStep.SECOND_INITIAL_FORM]);
req.workflow.currentStep = WorkflowStep.SECOND_LOCATION;
req.workflow.nextStep = WorkflowStep.SECOND_VOICE;
const completedSteps = skipLocation
? [WorkflowStep.SECOND_INITIAL_FORM, WorkflowStep.SECOND_LOCATION]
: [WorkflowStep.SECOND_INITIAL_FORM];
this.pushWorkflowSteps(req, completedSteps);
req.workflow.currentStep = skipLocation ? WorkflowStep.SECOND_VOICE : WorkflowStep.SECOND_LOCATION;
req.workflow.nextStep = skipLocation ? WorkflowStep.SECOND_DESCRIPTION : WorkflowStep.SECOND_VOICE;
}
}
@@ -9319,21 +9325,31 @@ export class RequestManagementService {
async expertUploadBlameVideoV3(
expert: any,
requestId: string,
file: Express.Multer.File,
file: Express.Multer.File | undefined,
): Promise<{
requestId: string;
publicId: string;
videoId: string;
videoId: string | undefined;
status: string;
message: string;
}> {
if (!file) throw new BadRequestException("Video file is required");
const req = await this.blameRequestDbService.findById(requestId);
if (!req) throw new NotFoundException("Request not found");
this.assertBlameV3ExpertInPerson(req);
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) {
throw new BadRequestException(
"Submit accident fields before uploading the blame accident video.",
@@ -9375,19 +9391,23 @@ export class RequestManagementService {
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
if (firstIdx === -1) throw new BadRequestException("First party not found");
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, [
WorkflowStep.FIRST_VIDEO,
WorkflowStep.WAITING_FOR_GUILT_DECISION,
@@ -9404,7 +9424,7 @@ export class RequestManagementService {
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
actorType: "field_expert",
},
metadata: { videoId: firstParty.evidence.videoId },
metadata: { videoId: videoId ?? null },
} as any);
if (typeof (req as any).markModified === "function") {
@@ -9443,10 +9463,11 @@ export class RequestManagementService {
return {
requestId: String(req._id),
publicId: req.publicId,
videoId: firstParty.evidence.videoId,
videoId,
status: req.status,
message:
"Blame accident video uploaded. File is now waiting for expert review.",
message: file
? "Blame accident video uploaded. File is now waiting for expert review."
: "File completed. Claim is now ready for damage expert review.",
};
}
@@ -9558,6 +9579,17 @@ export class RequestManagementService {
const req = await this.blameRequestDbService.findById(requestId);
if (!req) throw new NotFoundException("Request not found");
await this.verifyExpertAccessForBlameV2(req, actor);
// V4/V5: location step is skipped — store whatever is sent but do not
// validate against workflow phase (the step was auto-completed at inquiry time).
if ((req as any).isMadeByFileMaker) {
const role = this.resolvePartyRoleV3(req, partyRole);
const idx = this.getPartyIndex(req, role);
if (idx !== -1 && body) req.parties[idx].location = body as any;
await (req as any).save();
return { requestId: req._id, publicId: req.publicId, partyRole: role };
}
const role = this.resolvePartyRoleV3(req, partyRole);
this.assertBlameV3PartyDetailPhase(req, role);
@@ -9677,21 +9709,31 @@ export class RequestManagementService {
async expertUploadBlameVideoV5(
expert: any,
requestId: string,
file: Express.Multer.File,
file: Express.Multer.File | undefined,
): Promise<{
requestId: string;
publicId: string;
videoId: string;
videoId: string | undefined;
status: string;
message: string;
}> {
if (!file) throw new BadRequestException("Video file is required");
const req = await this.blameRequestDbService.findById(requestId);
if (!req) throw new NotFoundException("Request not found");
this.assertBlameV3ExpertInPerson(req);
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) {
throw new BadRequestException(
"Submit accident fields before uploading the blame accident video.",
@@ -9733,19 +9775,23 @@ export class RequestManagementService {
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
if (firstIdx === -1) throw new BadRequestException("First party not found");
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, [
WorkflowStep.FIRST_VIDEO,
WorkflowStep.WAITING_FOR_GUILT_DECISION,
@@ -9762,7 +9808,7 @@ export class RequestManagementService {
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
actorType: "file_reviewer",
},
metadata: { videoId: firstParty.evidence.videoId },
metadata: { videoId: videoId ?? null },
} as any);
if (typeof (req as any).markModified === "function") {
@@ -9808,11 +9854,11 @@ export class RequestManagementService {
return {
requestId: String(req._id),
publicId: req.publicId,
videoId: firstParty.evidence.videoId,
videoId,
status: req.status,
message:
"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.",
message: file
? "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."
: "File completed. Claim is now ready for damage expert review.",
};
}