1
0
forked from Yara724/api

Merge pull request 'YARA-1115, YARA-1117, YARA-1119' (#188) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#188
This commit is contained in:
2026-07-14 12:07:20 +03:30
6 changed files with 152 additions and 74 deletions

View File

@@ -161,6 +161,7 @@ import {
getClaimCaptureProgress, getClaimCaptureProgress,
isCapturePhaseDamagedPartyDocKey, isCapturePhaseDamagedPartyDocKey,
isClaimCaptureStepComplete, isClaimCaptureStepComplete,
OPTIONAL_CAPTURE_PHASE_DOC_KEYS_V4V5,
} from "src/helpers/claim-capture-phase"; } from "src/helpers/claim-capture-phase";
import { HttpService } from "@nestjs/axios"; import { HttpService } from "@nestjs/axios";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
@@ -8138,7 +8139,7 @@ export class ClaimRequestManagementService {
file: Express.Multer.File, file: Express.Multer.File,
currentUserId: string, currentUserId: string,
actor?: { sub: string; role?: string }, actor?: { sub: string; role?: string },
options?: { v3InPersonFlow?: boolean }, options?: { v3InPersonFlow?: boolean; skipMetalPlate?: boolean },
): Promise<UploadRequiredDocumentV2ResponseDto> { ): Promise<UploadRequiredDocumentV2ResponseDto> {
try { try {
const claimCase = await this.claimCaseDbService.findById(claimRequestId); const claimCase = await this.claimCaseDbService.findById(claimRequestId);
@@ -8315,12 +8316,16 @@ export class ClaimRequestManagementService {
k === body.documentKey || k === body.documentKey ||
this.isRequiredDocumentUploadedOnClaim(claimCase, k); this.isRequiredDocumentUploadedOnClaim(claimCase, k);
remaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter( 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; ).length;
if (remaining === 0) { if (remaining === 0) {
const progressAfterDoc = getClaimCaptureProgress(claimCase, { const progressAfterDoc = getClaimCaptureProgress(claimCase, {
assumeCapturePhaseDocKey: body.documentKey, assumeCapturePhaseDocKey: body.documentKey,
skipMetalPlate: options?.skipMetalPlate,
}); });
if ( if (
@@ -10196,9 +10201,10 @@ export class ClaimRequestManagementService {
private shapeCaptureRequirementsForV3( private shapeCaptureRequirementsForV3(
claimCase: any, claimCase: any,
_blame: any, blame: any,
base: GetCaptureRequirementsV2ResponseDto, base: GetCaptureRequirementsV2ResponseDto,
): GetCaptureRequirementsV2ResponseDto { ): GetCaptureRequirementsV2ResponseDto {
const skipMetalPlate = !!(blame as any)?.isMadeByFileMaker;
const step = claimCase.workflow?.currentStep; const step = claimCase.workflow?.currentStep;
const capturePartDone = this.claimV3StepCompleted( const capturePartDone = this.claimV3StepCompleted(
claimCase, claimCase,
@@ -10258,7 +10264,9 @@ export class ClaimRequestManagementService {
if (step === ClaimWorkflowStep.CAPTURE_PART_DAMAGES) { if (step === ClaimWorkflowStep.CAPTURE_PART_DAMAGES) {
const capturePhaseDocs = base.requiredDocuments.filter( const capturePhaseDocs = base.requiredDocuments.filter(
(d) => d.preferUploadDuringCapture, (d) =>
d.preferUploadDuringCapture &&
!(skipMetalPlate && OPTIONAL_CAPTURE_PHASE_DOC_KEYS_V4V5.includes(d.key as any)),
); );
return { return {
...base, ...base,
@@ -10298,6 +10306,7 @@ export class ClaimRequestManagementService {
); );
} }
const blame = await this.assertV3InPersonClaim(claimCase); const blame = await this.assertV3InPersonClaim(claimCase);
const skipMetalPlate = !!(blame as any).isMadeByFileMaker;
const isCaptureDoc = isCapturePhaseDamagedPartyDocKey(body.documentKey); const isCaptureDoc = isCapturePhaseDamagedPartyDocKey(body.documentKey);
@@ -10306,7 +10315,7 @@ export class ClaimRequestManagementService {
this.assertV3ClaimWorkflowStep( this.assertV3ClaimWorkflowStep(
claimCase, claimCase,
ClaimWorkflowStep.CAPTURE_PART_DAMAGES, 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 ( if (
!this.claimV3StepCompleted( !this.claimV3StepCompleted(
@@ -10318,15 +10327,15 @@ export class ClaimRequestManagementService {
"Complete outer and other part selection before capture-phase vehicle documents.", "Complete outer and other part selection before capture-phase vehicle documents.",
); );
} }
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 chassis, engine, or metal plate photos.", "Upload photos for all selected damaged parts before chassis or engine photos.",
); );
} }
if (!captureProgress.anglesComplete) { if (!captureProgress.anglesComplete) {
throw new BadRequestException( 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 { } else {
@@ -10346,7 +10355,7 @@ export class ClaimRequestManagementService {
file, file,
currentUserId, currentUserId,
actor, actor,
{ v3InPersonFlow: true }, { v3InPersonFlow: true, skipMetalPlate },
); );
} }
@@ -10524,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.");
@@ -10544,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.",
@@ -10555,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.",
}; };
@@ -4266,6 +4267,7 @@ export class ExpertClaimService {
const blameIds = makerBlames.map((b) => b._id); const blameIds = makerBlames.map((b) => b._id);
const claims = (await this.claimCaseDbService.find({ const claims = (await this.claimCaseDbService.find({
blameRequestId: { $in: blameIds }, blameRequestId: { $in: blameIds },
requiresFileMakerApproval: true,
})) as any[]; })) as any[];
const blameById = new Map<string, any>( const blameById = new Map<string, any>(
@@ -4690,7 +4692,8 @@ export class ExpertClaimService {
const isFactorValidationPending = const isFactorValidationPending =
claimIsAwaitingExpertFactorValidationV2(claim); claimIsAwaitingExpertFactorValidationV2(claim);
// FileMaker: can always view their own V4 files at any status. // FileMaker: can only view V5 files (requiresFileMakerApproval: true) that they created.
// V4 files do not require FileMaker approval and must not appear in their panel.
if (actor.role === RoleEnum.FILE_MAKER) { if (actor.role === RoleEnum.FILE_MAKER) {
const isOwn = claimCaseInitiatedByFieldExpert(claim, actor, linkedBlame); const isOwn = claimCaseInitiatedByFieldExpert(claim, actor, linkedBlame);
if (!isOwn) { if (!isOwn) {
@@ -4698,6 +4701,11 @@ export class ExpertClaimService {
"FileMakers can only view files they created.", "FileMakers can only view files they created.",
); );
} }
if (!(claim as any).requiresFileMakerApproval) {
throw new ForbiddenException(
"This file does not require FileMaker approval.",
);
}
// Fall through to the detail build below // Fall through to the detail build below
} else if ( } else if (
// Field experts and FileReviewers can view IN_PERSON expert-initiated claims // Field experts and FileReviewers can view IN_PERSON expert-initiated claims

View File

@@ -16,6 +16,12 @@ export const CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS = [
"damaged_metal_plate", "damaged_metal_plate",
] as const; ] 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 = export type CapturePhaseSequence =
| "parts" | "parts"
| "angles" | "angles"
@@ -52,7 +58,7 @@ function isRequiredDocumentUploadedOnClaim(
export function getClaimCaptureProgress( export function getClaimCaptureProgress(
claimCase: any, claimCase: any,
options?: { assumeCapturePhaseDocKey?: string }, options?: { assumeCapturePhaseDocKey?: string; skipMetalPlate?: boolean },
): ClaimCaptureProgress { ): ClaimCaptureProgress {
const carType = claimCase?.vehicle?.carType as ClaimVehicleTypeV2 | undefined; const carType = claimCase?.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
const selectedNorm = normalizeDamageSelectedParts( const selectedNorm = normalizeDamageSelectedParts(
@@ -83,6 +89,7 @@ export function getClaimCaptureProgress(
const capturePhaseDocsRemaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter( const capturePhaseDocsRemaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter(
(k) => { (k) => {
if (options?.skipMetalPlate && OPTIONAL_CAPTURE_PHASE_DOC_KEYS_V4V5.includes(k as any)) return false;
if (k === options?.assumeCapturePhaseDocKey) return false; if (k === options?.assumeCapturePhaseDocKey) return false;
return !isRequiredDocumentUploadedOnClaim(claimCase, k); return !isRequiredDocumentUploadedOnClaim(claimCase, k);
}, },
@@ -111,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,
) { ) {
if (file) {
await this.mediaPolicyService.assertForBlame(file, requestId, "video"); 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,
) { ) {
if (file) {
await this.mediaPolicyService.assertForBlame(file, requestId, "video"); await this.mediaPolicyService.assertForBlame(file, requestId, "video");
}
return this.requestManagementService.expertUploadBlameVideoV5( return this.requestManagementService.expertUploadBlameVideoV5(
fileReviewer, fileReviewer,
requestId, requestId,

View File

@@ -8695,13 +8695,16 @@ export class RequestManagementService {
partyRole: PartyRole, partyRole: PartyRole,
actor: any, actor: any,
): void { ): void {
// V4/V5 FileMaker files skip the location step — it is auto-completed here.
const skipLocation = !!(req as any).isMadeByFileMaker;
if (partyRole === PartyRole.FIRST) { if (partyRole === PartyRole.FIRST) {
this.pushWorkflowSteps(req, [ const completedSteps = skipLocation
WorkflowStep.CREATED, ? [WorkflowStep.CREATED, WorkflowStep.FIRST_INITIAL_FORM, WorkflowStep.FIRST_LOCATION]
WorkflowStep.FIRST_INITIAL_FORM, : [WorkflowStep.CREATED, WorkflowStep.FIRST_INITIAL_FORM];
]); this.pushWorkflowSteps(req, completedSteps);
req.workflow.currentStep = WorkflowStep.FIRST_LOCATION; req.workflow.currentStep = skipLocation ? WorkflowStep.FIRST_VOICE : WorkflowStep.FIRST_LOCATION;
req.workflow.nextStep = WorkflowStep.FIRST_VOICE; req.workflow.nextStep = skipLocation ? WorkflowStep.FIRST_DESCRIPTION : WorkflowStep.FIRST_VOICE;
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST); const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
const guiltyUserId = req.parties?.[firstIdx]?.person?.userId; const guiltyUserId = req.parties?.[firstIdx]?.person?.userId;
@@ -8716,9 +8719,12 @@ export class RequestManagementService {
(req as any).markModified("expert"); (req as any).markModified("expert");
} }
} else { } else {
this.pushWorkflowSteps(req, [WorkflowStep.SECOND_INITIAL_FORM]); const completedSteps = skipLocation
req.workflow.currentStep = WorkflowStep.SECOND_LOCATION; ? [WorkflowStep.SECOND_INITIAL_FORM, WorkflowStep.SECOND_LOCATION]
req.workflow.nextStep = WorkflowStep.SECOND_VOICE; : [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( 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.",
@@ -9375,18 +9391,22 @@ 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];
// Store video if provided; skip silently for V4/V5 where it is optional.
let videoId: string | undefined;
if (file) {
if (firstParty?.evidence?.videoId) { if (firstParty?.evidence?.videoId) {
throw new ConflictException("Blame accident video already uploaded"); throw new ConflictException("Blame accident video already uploaded");
} }
const videoDocument = await this.blameVideoDbService.create({ const videoDocument = await this.blameVideoDbService.create({
fileName: file.filename, fileName: file.filename,
path: file.path, path: file.path,
requestId: new Types.ObjectId(requestId), requestId: new Types.ObjectId(requestId),
} as any); } as any);
if (!firstParty.evidence) firstParty.evidence = {} as any; if (!firstParty.evidence) firstParty.evidence = {} as any;
firstParty.evidence.videoId = String((videoDocument as any)._id); firstParty.evidence.videoId = String((videoDocument as any)._id);
videoId = firstParty.evidence.videoId;
}
this.pushWorkflowSteps(req, [ this.pushWorkflowSteps(req, [
WorkflowStep.FIRST_VIDEO, WorkflowStep.FIRST_VIDEO,
@@ -9404,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") {
@@ -9443,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.",
}; };
} }
@@ -9558,6 +9579,17 @@ export class RequestManagementService {
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");
await this.verifyExpertAccessForBlameV2(req, actor); 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); const role = this.resolvePartyRoleV3(req, partyRole);
this.assertBlameV3PartyDetailPhase(req, role); this.assertBlameV3PartyDetailPhase(req, role);
@@ -9677,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.",
@@ -9733,18 +9775,22 @@ 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];
// Store video if provided; skip silently for V4/V5 where it is optional.
let videoId: string | undefined;
if (file) {
if (firstParty?.evidence?.videoId) { if (firstParty?.evidence?.videoId) {
throw new ConflictException("Blame accident video already uploaded"); throw new ConflictException("Blame accident video already uploaded");
} }
const videoDocument = await this.blameVideoDbService.create({ const videoDocument = await this.blameVideoDbService.create({
fileName: file.filename, fileName: file.filename,
path: file.path, path: file.path,
requestId: new Types.ObjectId(requestId), requestId: new Types.ObjectId(requestId),
} as any); } as any);
if (!firstParty.evidence) firstParty.evidence = {} as any; if (!firstParty.evidence) firstParty.evidence = {} as any;
firstParty.evidence.videoId = String((videoDocument as any)._id); firstParty.evidence.videoId = String((videoDocument as any)._id);
videoId = firstParty.evidence.videoId;
}
this.pushWorkflowSteps(req, [ this.pushWorkflowSteps(req, [
WorkflowStep.FIRST_VIDEO, WorkflowStep.FIRST_VIDEO,
@@ -9762,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") {
@@ -9808,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.",
}; };
} }