1
0
forked from Yara724/api

YARA-1094, YARA-1095, YARA-1096

This commit is contained in:
SepehrYahyaee
2026-07-12 14:07:27 +03:30
parent c955deda5c
commit 72dec7a917
17 changed files with 1589 additions and 2 deletions

View File

@@ -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.",
);
}
}
}