forked from Yara724/api
YARA-1110
This commit is contained in:
@@ -4458,7 +4458,7 @@ export class RequestManagementService {
|
||||
/**
|
||||
* Verify the current user (field expert) can access this BlameRequest (v2).
|
||||
*/
|
||||
private verifyExpertAccessForBlameV2(req: any, expert: any): void {
|
||||
private async verifyExpertAccessForBlameV2(req: any, expert: any): Promise<void> {
|
||||
// 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) {
|
||||
@@ -4470,15 +4470,18 @@ export class RequestManagementService {
|
||||
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(
|
||||
"This file has not been sealed by a FileMaker yet.",
|
||||
);
|
||||
}
|
||||
// Enforce reviewer assignment: must be assigned to them or open (no reviewer yet)
|
||||
// Enforce reviewer assignment: must be assigned to them or open (no reviewer yet).
|
||||
// If the file is open (no assignedFileReviewerId) we atomically claim it now —
|
||||
// this handles the case where a reviewer skipped the explicit lock step and went
|
||||
// straight to working on the file (e.g. accident-fields before calling the lock
|
||||
// endpoint). Without this, assignedFileReviewerId is never written and the file
|
||||
// disappears from the reviewer's list after the blame moves to WAITING_FOR_EXPERT.
|
||||
const assignedId = req.assignedFileReviewerId
|
||||
? String(req.assignedFileReviewerId)
|
||||
: null;
|
||||
@@ -4487,23 +4490,19 @@ export class RequestManagementService {
|
||||
"This file has been taken by another FileReviewer.",
|
||||
);
|
||||
}
|
||||
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.",
|
||||
if (!assignedId) {
|
||||
// Atomically claim — ignore if another reviewer won the race (they would have
|
||||
// been caught by the assignedId check above on their own first call).
|
||||
await this.blameRequestDbService.findOneAndUpdate(
|
||||
{
|
||||
_id: req._id,
|
||||
$or: [
|
||||
{ assignedFileReviewerId: { $exists: false } },
|
||||
{ assignedFileReviewerId: null },
|
||||
],
|
||||
},
|
||||
{ $set: { assignedFileReviewerId: new Types.ObjectId(String(expert.sub)) } },
|
||||
{ new: false },
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -4854,7 +4853,7 @@ export class RequestManagementService {
|
||||
"This endpoint is only for expert-initiated LINK blame files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
const phone = (dto?.phoneNumber || "").trim();
|
||||
if (!phone) throw new BadRequestException("phoneNumber is required");
|
||||
@@ -4937,7 +4936,7 @@ export class RequestManagementService {
|
||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
if (
|
||||
req.type === BlameRequestType.THIRD_PARTY &&
|
||||
!dto.secondPartyPhoneNumber
|
||||
@@ -5007,7 +5006,7 @@ export class RequestManagementService {
|
||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
const now = Date.now();
|
||||
const verifyOne = async (
|
||||
@@ -5134,7 +5133,7 @@ export class RequestManagementService {
|
||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, actor);
|
||||
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||
const phone = (dto?.phoneNumber || "").trim();
|
||||
if (!phone) throw new BadRequestException("phoneNumber is required");
|
||||
|
||||
@@ -5220,7 +5219,7 @@ export class RequestManagementService {
|
||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, actor);
|
||||
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||
|
||||
const phone = (dto?.phoneNumber || "").trim();
|
||||
const otp = (dto?.otp || "").trim();
|
||||
@@ -5991,7 +5990,7 @@ export class RequestManagementService {
|
||||
"This endpoint is only for expert-initiated IN_PERSON BlameRequest files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
if (req.type === BlameRequestType.THIRD_PARTY) {
|
||||
return this.expertCompleteThirdPartyFormV2(expert, requestId, formData);
|
||||
@@ -6025,7 +6024,7 @@ export class RequestManagementService {
|
||||
if (req.type !== BlameRequestType.CAR_BODY) {
|
||||
throw new BadRequestException("File type is not CAR_BODY.");
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
if (!formData.carBodyForm) {
|
||||
throw new BadRequestException("carBodyForm is required.");
|
||||
@@ -6230,7 +6229,7 @@ export class RequestManagementService {
|
||||
if (req.type !== BlameRequestType.THIRD_PARTY) {
|
||||
throw new BadRequestException("File type is not THIRD_PARTY.");
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
if (!formData.secondParty || !formData.guiltyPartyPhoneNumber) {
|
||||
throw new BadRequestException(
|
||||
@@ -6434,7 +6433,7 @@ export class RequestManagementService {
|
||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
if (firstIdx === -1) throw new BadRequestException("First party not found");
|
||||
@@ -6499,7 +6498,7 @@ export class RequestManagementService {
|
||||
"This endpoint is only for expert-initiated files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
if (firstPartyIndex === -1)
|
||||
@@ -6552,7 +6551,7 @@ export class RequestManagementService {
|
||||
"This endpoint is only for expert-initiated files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
if (firstPartyIndex === -1)
|
||||
@@ -6617,7 +6616,7 @@ export class RequestManagementService {
|
||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
if (req.status !== CaseStatus.WAITING_FOR_SIGNATURES) {
|
||||
throw new BadRequestException(
|
||||
"Request is not waiting for signatures. Current status: " + req.status,
|
||||
@@ -6733,7 +6732,7 @@ export class RequestManagementService {
|
||||
"This endpoint is only for expert- or registrar-initiated files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
if (!req.expert) req.expert = {} as any;
|
||||
if (!req.expert.decision) req.expert.decision = {} as any;
|
||||
@@ -8371,7 +8370,7 @@ export class RequestManagementService {
|
||||
const req = await this.blameRequestDbService.findById(blameRequestId);
|
||||
if (!req) throw new NotFoundException("Blame request not found");
|
||||
this.assertBlameV3ExpertInPerson(req);
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
const claim = await this.claimCaseDbService.findOne({
|
||||
blameRequestId: (req as any)._id,
|
||||
@@ -8952,7 +8951,7 @@ export class RequestManagementService {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Blame request not found");
|
||||
this.assertBlameV3ExpertInPerson(req);
|
||||
this.verifyExpertAccessForBlameV2(req, actor);
|
||||
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||
|
||||
const partyRole = this.resolveV3InquiryPartyRole(req);
|
||||
if (!partyRole) {
|
||||
@@ -9125,7 +9124,7 @@ export class RequestManagementService {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
this.assertBlameV3ExpertInPerson(req);
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
if (partyRole === PartyRole.FIRST) {
|
||||
if (!this.hasPartyInquiriesComplete(req, PartyRole.FIRST)) {
|
||||
@@ -9263,7 +9262,7 @@ export class RequestManagementService {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
this.assertBlameV3ExpertInPerson(req);
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
this.assertBlameV3AccidentFieldsPhase(req);
|
||||
|
||||
if (!req.expert) req.expert = {} as any;
|
||||
@@ -9333,7 +9332,7 @@ export class RequestManagementService {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
this.assertBlameV3ExpertInPerson(req);
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
if (!(req.expert?.decision as any)?.fields?.accidentWay) {
|
||||
throw new BadRequestException(
|
||||
@@ -9517,7 +9516,7 @@ export class RequestManagementService {
|
||||
if (!voice) throw new BadRequestException("File is required");
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
this.verifyExpertAccessForBlameV2(req, actor);
|
||||
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||
const role = this.resolvePartyRoleV3(req, partyRole);
|
||||
this.assertBlameV3PartyDetailPhase(req, role);
|
||||
const idx = this.getPartyIndex(req, role);
|
||||
@@ -9558,7 +9557,7 @@ export class RequestManagementService {
|
||||
) {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
this.verifyExpertAccessForBlameV2(req, actor);
|
||||
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||
const role = this.resolvePartyRoleV3(req, partyRole);
|
||||
this.assertBlameV3PartyDetailPhase(req, role);
|
||||
|
||||
@@ -9590,7 +9589,7 @@ export class RequestManagementService {
|
||||
) {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
this.verifyExpertAccessForBlameV2(req, actor);
|
||||
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||
const role = this.resolvePartyRoleV3(req, partyRole);
|
||||
this.assertBlameV3PartyDetailPhase(req, role);
|
||||
|
||||
@@ -9628,7 +9627,7 @@ export class RequestManagementService {
|
||||
"CAR_BODY accident type is only for CAR_BODY files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, actor);
|
||||
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||
this.assertBlameV3ExpertInPerson(req);
|
||||
|
||||
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
@@ -9670,8 +9669,10 @@ export class RequestManagementService {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Same flow as V3/V4 but additionally marks the linked claim with
|
||||
* `requiresFileMakerApproval: true` so that after the owner signs,
|
||||
* the claim is held at WAITING_FOR_FILE_MAKER_APPROVAL rather than
|
||||
* being auto-submitted to fanavaran.
|
||||
*/
|
||||
async expertUploadBlameVideoV5(
|
||||
expert: any,
|
||||
@@ -9689,7 +9690,7 @@ export class RequestManagementService {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
this.assertBlameV3ExpertInPerson(req);
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
if (!(req.expert?.decision as any)?.fields?.accidentWay) {
|
||||
throw new BadRequestException(
|
||||
@@ -9751,7 +9752,7 @@ export class RequestManagementService {
|
||||
]);
|
||||
req.workflow.currentStep = WorkflowStep.WAITING_FOR_GUILT_DECISION;
|
||||
req.workflow.nextStep = WorkflowStep.WAITING_FOR_SIGNATURES;
|
||||
req.status = CaseStatus.WAITING_FOR_FINANCIAL_EXPERT;
|
||||
req.status = CaseStatus.WAITING_FOR_EXPERT;
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
@@ -9769,206 +9770,196 @@ export class RequestManagementService {
|
||||
}
|
||||
await (req as any).save();
|
||||
|
||||
// Advance the claim into the damage-expert queue AND mark FileMaker approval
|
||||
// required before fanavaran submission.
|
||||
const fileMakerActorId = req.initiatedByFieldExpertId ?? null;
|
||||
await this.claimCaseDbService.findByIdAndUpdate(String(claim._id), {
|
||||
$set: {
|
||||
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
||||
claimStatus: ClaimStatus.PENDING,
|
||||
"workflow.currentStep": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
"workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||
requiresFileMakerApproval: true,
|
||||
...(fileMakerActorId
|
||||
? { fileMakerApprovalActorId: new Types.ObjectId(String(fileMakerActorId)) }
|
||||
: {}),
|
||||
},
|
||||
$push: {
|
||||
"workflow.completedSteps": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
history: {
|
||||
type: "V5_BLAME_ACCIDENT_VIDEO_UPLOADED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(expert.sub),
|
||||
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||
actorType: "file_reviewer",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
description:
|
||||
"V5 field workflow complete. Claim ready for damage expert review (FileMaker approval required before fanavaran).",
|
||||
v5InPersonFlow: true,
|
||||
fileMakerActorId: fileMakerActorId ? String(fileMakerActorId) : null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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.",
|
||||
"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.",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V5 FinancialExpert approves the file.
|
||||
* Moves blame status from WAITING_FOR_FINANCIAL_EXPERT → WAITING_FOR_EXPERT.
|
||||
* V5: FileMaker approves the completed claim.
|
||||
*
|
||||
* Preconditions:
|
||||
* - claim.requiresFileMakerApproval === true
|
||||
* - claim.status === WAITING_FOR_FILE_MAKER_APPROVAL
|
||||
* - actor is FILE_MAKER and is the original creator of the linked blame file
|
||||
*
|
||||
* On approval: triggers fanavaran submission and moves claim to COMPLETED.
|
||||
*/
|
||||
async financialExpertApprove(
|
||||
financialExpert: any,
|
||||
requestId: string,
|
||||
async fileMakerApproveV5(
|
||||
fileMaker: any,
|
||||
claimRequestId: string,
|
||||
): Promise<{
|
||||
requestId: string;
|
||||
claimRequestId: 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);
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!claim) throw new NotFoundException("Claim not found");
|
||||
|
||||
if (req.status !== CaseStatus.WAITING_FOR_FINANCIAL_EXPERT) {
|
||||
this.assertFileMakerApprovalAccess(claim, fileMaker);
|
||||
|
||||
if (claim.status !== ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL) {
|
||||
throw new BadRequestException(
|
||||
`File must be in WAITING_FOR_FINANCIAL_EXPERT status to approve. Current: ${req.status}`,
|
||||
`Claim must be in WAITING_FOR_FILE_MAKER_APPROVAL status to approve. Current: ${claim.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
req.status = CaseStatus.WAITING_FOR_EXPERT;
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: "V5_FINANCIAL_EXPERT_APPROVED",
|
||||
const actorName = `${fileMaker.firstName || ""} ${fileMaker.lastName || ""}`.trim();
|
||||
const historyEntry = {
|
||||
type: "V5_FILE_MAKER_APPROVED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(financialExpert.sub),
|
||||
actorName: `${financialExpert.firstName || ""} ${financialExpert.lastName || ""}`.trim(),
|
||||
actorType: "financial_expert",
|
||||
actorId: new Types.ObjectId(fileMaker.sub),
|
||||
actorName,
|
||||
actorType: "file_maker",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Move claim to COMPLETED and clear the approval gate
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: {
|
||||
status: ClaimCaseStatus.COMPLETED,
|
||||
claimStatus: ClaimStatus.APPROVED,
|
||||
requiresFileMakerApproval: false,
|
||||
"workflow.currentStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||
},
|
||||
);
|
||||
|
||||
await (req as any).save();
|
||||
$push: {
|
||||
"workflow.completedSteps": ClaimWorkflowStep.INSURER_REVIEW,
|
||||
history: historyEntry,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
requestId: String(req._id),
|
||||
publicId: req.publicId,
|
||||
status: req.status,
|
||||
message: "File approved by FinancialExpert. Now waiting for expert review.",
|
||||
claimRequestId,
|
||||
publicId: claim.publicId,
|
||||
status: ClaimCaseStatus.COMPLETED,
|
||||
message:
|
||||
"Claim approved by FileMaker. Claim is now marked COMPLETED. " +
|
||||
"Proceed with fanavaran submission via the claim service.",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V5 FinancialExpert rejects the file back to the FileReviewer.
|
||||
* Moves blame status from WAITING_FOR_FINANCIAL_EXPERT → FINANCIAL_EXPERT_REJECTED.
|
||||
* V5: FileMaker rejects the completed claim back to expert review.
|
||||
*
|
||||
* Moves claim from WAITING_FOR_FILE_MAKER_APPROVAL → WAITING_FOR_DAMAGE_EXPERT
|
||||
* so the FileReviewer can re-lock it and adjust the damage assessment or
|
||||
* restart the back-and-forth with the user.
|
||||
*/
|
||||
async financialExpertReject(
|
||||
financialExpert: any,
|
||||
requestId: string,
|
||||
async fileMakerRejectV5(
|
||||
fileMaker: any,
|
||||
claimRequestId: string,
|
||||
reason?: string,
|
||||
): Promise<{
|
||||
requestId: string;
|
||||
claimRequestId: 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);
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!claim) throw new NotFoundException("Claim not found");
|
||||
|
||||
if (req.status !== CaseStatus.WAITING_FOR_FINANCIAL_EXPERT) {
|
||||
this.assertFileMakerApprovalAccess(claim, fileMaker);
|
||||
|
||||
if (claim.status !== ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL) {
|
||||
throw new BadRequestException(
|
||||
`File must be in WAITING_FOR_FINANCIAL_EXPERT status to reject. Current: ${req.status}`,
|
||||
`Claim must be in WAITING_FOR_FILE_MAKER_APPROVAL status to reject. Current: ${claim.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
req.status = CaseStatus.FINANCIAL_EXPERT_REJECTED;
|
||||
const actorName = `${fileMaker.firstName || ""} ${fileMaker.lastName || ""}`.trim();
|
||||
|
||||
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",
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: {
|
||||
status: ClaimCaseStatus.FILE_MAKER_REJECTED,
|
||||
claimStatus: ClaimStatus.NEEDS_REVISION,
|
||||
"workflow.currentStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||
"workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||
},
|
||||
metadata: { reason: reason ?? null },
|
||||
} as any);
|
||||
|
||||
await (req as any).save();
|
||||
$push: {
|
||||
history: {
|
||||
type: "V5_FILE_MAKER_REJECTED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(fileMaker.sub),
|
||||
actorName,
|
||||
actorType: "file_maker",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: { reason: reason ?? null },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
requestId: String(req._id),
|
||||
publicId: req.publicId,
|
||||
status: req.status,
|
||||
message: "File rejected by FinancialExpert. FileReviewer must correct and re-submit.",
|
||||
claimRequestId,
|
||||
publicId: claim.publicId,
|
||||
status: ClaimCaseStatus.FILE_MAKER_REJECTED,
|
||||
message:
|
||||
"Claim rejected by FileMaker. FileReviewer can re-lock the claim and adjust the damage assessment or restart the user interaction.",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}`,
|
||||
);
|
||||
/** Guard for V5 FileMaker approval endpoints. */
|
||||
private assertFileMakerApprovalAccess(claim: any, fileMaker: any): void {
|
||||
if (fileMaker?.role !== RoleEnum.FILE_MAKER) {
|
||||
throw new ForbiddenException("Only FileMakers can perform this action.");
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!claim.requiresFileMakerApproval) {
|
||||
throw new ForbiddenException(
|
||||
"FinancialExpert can only access V5 FileMaker files.",
|
||||
"This claim does not require FileMaker approval (not a V5 file).",
|
||||
);
|
||||
}
|
||||
// Scope to same insurer client
|
||||
// Scope to the FileMaker who created the linked blame file
|
||||
if (
|
||||
req.clientKey &&
|
||||
financialExpert.clientKey &&
|
||||
String(req.clientKey) !== String(financialExpert.clientKey)
|
||||
claim.fileMakerApprovalActorId &&
|
||||
String(claim.fileMakerApprovalActorId) !== String(fileMaker.sub)
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
"You do not have access to files from a different insurer.",
|
||||
"Only the FileMaker who created this file can approve or reject it.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user