forked from Yara724/api
YARA-1094, YARA-1095, YARA-1096
This commit is contained in:
@@ -4459,17 +4459,19 @@ export class RequestManagementService {
|
||||
* Verify the current user (field expert) can access this BlameRequest (v2).
|
||||
*/
|
||||
private verifyExpertAccessForBlameV2(req: any, expert: any): void {
|
||||
// FILE_REVIEWER can access V4 FileMaker-sealed files.
|
||||
// FILE_REVIEWER can access V4/V5 FileMaker-sealed files.
|
||||
// They must be the assigned reviewer (or the file is still open for taking).
|
||||
if (expert?.role === RoleEnum.FILE_REVIEWER) {
|
||||
if (!req?.isMadeByFileMaker || !req?.expertInitiated || req?.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
throw new ForbiddenException(
|
||||
"FileReviewer can only access V4 FileMaker files.",
|
||||
"FileReviewer can only access V4/V5 FileMaker files.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
req.status !== CaseStatus.WAITING_FOR_FILE_REVIEWER &&
|
||||
req.status !== CaseStatus.WAITING_FOR_EXPERT &&
|
||||
req.status !== CaseStatus.WAITING_FOR_FINANCIAL_EXPERT &&
|
||||
req.status !== CaseStatus.FINANCIAL_EXPERT_REJECTED &&
|
||||
req.status !== CaseStatus.COMPLETED
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
@@ -4487,6 +4489,25 @@ export class RequestManagementService {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// FINANCIAL_EXPERT can read V5 FileMaker-sealed files from the same insurer.
|
||||
if (expert?.role === RoleEnum.FINANCIAL_EXPERT) {
|
||||
if (!req?.isMadeByFileMaker || !req?.expertInitiated || req?.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
throw new ForbiddenException(
|
||||
"FinancialExpert can only access V5 FileMaker files.",
|
||||
);
|
||||
}
|
||||
// Scope to same insurer client
|
||||
if (
|
||||
req.clientKey &&
|
||||
expert.clientKey &&
|
||||
String(req.clientKey) !== String(expert.clientKey)
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
"You do not have access to files from a different insurer.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (req?.expertInitiated && req?.initiatedByFieldExpertId) {
|
||||
if (String(req.initiatedByFieldExpertId) !== String(expert?.sub)) {
|
||||
throw new ForbiddenException(
|
||||
@@ -9646,4 +9667,309 @@ export class RequestManagementService {
|
||||
await (req as any).save();
|
||||
return { requestId: req._id, publicId: req.publicId };
|
||||
}
|
||||
|
||||
/**
|
||||
* V5 variant of expertUploadBlameVideoV3.
|
||||
* Identical logic but sets blame status to WAITING_FOR_FINANCIAL_EXPERT instead
|
||||
* of WAITING_FOR_EXPERT so that a FinancialExpert must approve before fanavaran.
|
||||
*/
|
||||
async expertUploadBlameVideoV5(
|
||||
expert: any,
|
||||
requestId: string,
|
||||
file: Express.Multer.File,
|
||||
): Promise<{
|
||||
requestId: string;
|
||||
publicId: string;
|
||||
videoId: string;
|
||||
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);
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
if (!(req.expert?.decision as any)?.fields?.accidentWay) {
|
||||
throw new BadRequestException(
|
||||
"Submit accident fields before uploading the blame accident video.",
|
||||
);
|
||||
}
|
||||
|
||||
const requiredSigs = req.type === BlameRequestType.CAR_BODY ? 1 : 2;
|
||||
const signed = (req.parties ?? []).filter(
|
||||
(p: any) => p?.confirmation != null,
|
||||
);
|
||||
if (signed.length < requiredSigs) {
|
||||
throw new BadRequestException(
|
||||
"All required party signatures must be collected before the blame accident video.",
|
||||
);
|
||||
}
|
||||
|
||||
const claim = await this.claimCaseDbService.findOne({
|
||||
blameRequestId: (req as any)._id,
|
||||
});
|
||||
if (!claim) {
|
||||
throw new BadRequestException(
|
||||
"Claim not found. Complete guilty-party inquiries first.",
|
||||
);
|
||||
}
|
||||
if (!claim.media?.videoCaptureId) {
|
||||
throw new BadRequestException(
|
||||
"Upload the claim walk-around video (car-capture) before the blame accident video.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
claim.workflow?.currentStep !==
|
||||
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Complete capture-part (parts, angles, and capture-phase documents) before the blame accident video.",
|
||||
);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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,
|
||||
]);
|
||||
req.workflow.currentStep = WorkflowStep.WAITING_FOR_GUILT_DECISION;
|
||||
req.workflow.nextStep = WorkflowStep.WAITING_FOR_SIGNATURES;
|
||||
req.status = CaseStatus.WAITING_FOR_FINANCIAL_EXPERT;
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: "V5_BLAME_ACCIDENT_VIDEO_UPLOADED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(expert.sub),
|
||||
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||
actorType: "file_reviewer",
|
||||
},
|
||||
metadata: { videoId: firstParty.evidence.videoId },
|
||||
} as any);
|
||||
|
||||
if (typeof (req as any).markModified === "function") {
|
||||
(req as any).markModified("parties");
|
||||
}
|
||||
await (req as any).save();
|
||||
|
||||
return {
|
||||
requestId: String(req._id),
|
||||
publicId: req.publicId,
|
||||
videoId: firstParty.evidence.videoId,
|
||||
status: req.status,
|
||||
message:
|
||||
"Blame accident video uploaded. File is now waiting for FinancialExpert approval.",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V5 FinancialExpert approves the file.
|
||||
* Moves blame status from WAITING_FOR_FINANCIAL_EXPERT → WAITING_FOR_EXPERT.
|
||||
*/
|
||||
async financialExpertApprove(
|
||||
financialExpert: any,
|
||||
requestId: string,
|
||||
): Promise<{
|
||||
requestId: string;
|
||||
publicId: string;
|
||||
status: string;
|
||||
message: string;
|
||||
}> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
this.assertBlameV3ExpertInPerson(req);
|
||||
this.verifyFinancialExpertAccess(req, financialExpert);
|
||||
|
||||
if (req.status !== CaseStatus.WAITING_FOR_FINANCIAL_EXPERT) {
|
||||
throw new BadRequestException(
|
||||
`File must be in WAITING_FOR_FINANCIAL_EXPERT status to approve. Current: ${req.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
req.status = CaseStatus.WAITING_FOR_EXPERT;
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: "V5_FINANCIAL_EXPERT_APPROVED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(financialExpert.sub),
|
||||
actorName: `${financialExpert.firstName || ""} ${financialExpert.lastName || ""}`.trim(),
|
||||
actorType: "financial_expert",
|
||||
},
|
||||
metadata: {},
|
||||
} as any);
|
||||
|
||||
await this.claimCaseDbService.findOneAndUpdate(
|
||||
{ blameRequestId: (req as any)._id },
|
||||
{
|
||||
$set: {
|
||||
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
||||
claimStatus: ClaimStatus.PENDING,
|
||||
"workflow.currentStep": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
"workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||
},
|
||||
$push: {
|
||||
"workflow.completedSteps": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
history: {
|
||||
type: "STEP_COMPLETED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(financialExpert.sub),
|
||||
actorName: `${financialExpert.firstName || ""} ${financialExpert.lastName || ""}`.trim(),
|
||||
actorType: "financial_expert",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
description:
|
||||
"V5 flow: FinancialExpert approved. Claim ready for damage expert review.",
|
||||
v5Flow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await (req as any).save();
|
||||
|
||||
return {
|
||||
requestId: String(req._id),
|
||||
publicId: req.publicId,
|
||||
status: req.status,
|
||||
message: "File approved by FinancialExpert. Now waiting for expert review.",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V5 FinancialExpert rejects the file back to the FileReviewer.
|
||||
* Moves blame status from WAITING_FOR_FINANCIAL_EXPERT → FINANCIAL_EXPERT_REJECTED.
|
||||
*/
|
||||
async financialExpertReject(
|
||||
financialExpert: any,
|
||||
requestId: string,
|
||||
reason?: string,
|
||||
): Promise<{
|
||||
requestId: string;
|
||||
publicId: string;
|
||||
status: string;
|
||||
message: string;
|
||||
}> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
this.assertBlameV3ExpertInPerson(req);
|
||||
this.verifyFinancialExpertAccess(req, financialExpert);
|
||||
|
||||
if (req.status !== CaseStatus.WAITING_FOR_FINANCIAL_EXPERT) {
|
||||
throw new BadRequestException(
|
||||
`File must be in WAITING_FOR_FINANCIAL_EXPERT status to reject. Current: ${req.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
req.status = CaseStatus.FINANCIAL_EXPERT_REJECTED;
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: "V5_FINANCIAL_EXPERT_REJECTED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(financialExpert.sub),
|
||||
actorName: `${financialExpert.firstName || ""} ${financialExpert.lastName || ""}`.trim(),
|
||||
actorType: "financial_expert",
|
||||
},
|
||||
metadata: { reason: reason ?? null },
|
||||
} as any);
|
||||
|
||||
await (req as any).save();
|
||||
|
||||
return {
|
||||
requestId: String(req._id),
|
||||
publicId: req.publicId,
|
||||
status: req.status,
|
||||
message: "File rejected by FinancialExpert. FileReviewer must correct and re-submit.",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V5 FileReviewer re-submits a rejected file for FinancialExpert re-review.
|
||||
* Moves blame status from FINANCIAL_EXPERT_REJECTED → WAITING_FOR_FINANCIAL_EXPERT.
|
||||
*/
|
||||
async fileReviewerResubmitToFinancialExpert(
|
||||
fileReviewer: any,
|
||||
requestId: string,
|
||||
): Promise<{
|
||||
requestId: string;
|
||||
publicId: string;
|
||||
status: string;
|
||||
message: string;
|
||||
}> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
this.assertBlameV3ExpertInPerson(req);
|
||||
this.verifyExpertAccessForBlameV2(req, fileReviewer);
|
||||
|
||||
if (req.status !== CaseStatus.FINANCIAL_EXPERT_REJECTED) {
|
||||
throw new BadRequestException(
|
||||
`File must be in FINANCIAL_EXPERT_REJECTED status to re-submit. Current: ${req.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
req.status = CaseStatus.WAITING_FOR_FINANCIAL_EXPERT;
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: "V5_FILE_REVIEWER_RESUBMITTED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(fileReviewer.sub),
|
||||
actorName: `${fileReviewer.firstName || ""} ${fileReviewer.lastName || ""}`.trim(),
|
||||
actorType: "file_reviewer",
|
||||
},
|
||||
metadata: {},
|
||||
} as any);
|
||||
|
||||
await (req as any).save();
|
||||
|
||||
return {
|
||||
requestId: String(req._id),
|
||||
publicId: req.publicId,
|
||||
status: req.status,
|
||||
message: "File re-submitted to FinancialExpert for approval.",
|
||||
};
|
||||
}
|
||||
|
||||
/** Guard: ensures the actor is a FinancialExpert with access to this V5 file. */
|
||||
private verifyFinancialExpertAccess(req: any, financialExpert: any): void {
|
||||
if (financialExpert?.role !== RoleEnum.FINANCIAL_EXPERT) {
|
||||
throw new ForbiddenException("Only FinancialExperts can perform this action.");
|
||||
}
|
||||
if (!req?.isMadeByFileMaker || !req?.expertInitiated || req?.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
throw new ForbiddenException(
|
||||
"FinancialExpert can only access V5 FileMaker files.",
|
||||
);
|
||||
}
|
||||
// Scope to same insurer client
|
||||
if (
|
||||
req.clientKey &&
|
||||
financialExpert.clientKey &&
|
||||
String(req.clientKey) !== String(financialExpert.clientKey)
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
"You do not have access to files from a different insurer.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user