forked from Yara724/api
Compare commits
7 Commits
5ef8310cb0
...
b43f1a86dc
| Author | SHA1 | Date | |
|---|---|---|---|
| b43f1a86dc | |||
|
|
5df39b502e | ||
| 49564cc1c6 | |||
|
|
29939eee20 | ||
| ae789323d8 | |||
|
|
6be9ff16e1 | ||
|
|
0c5a2fe38b |
@@ -11,6 +11,12 @@ export enum CaseStatus {
|
|||||||
|
|
||||||
WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES",
|
WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileMaker has collected all signatures; the file is sealed and waiting
|
||||||
|
* for a FileReviewer to complete it (accident fields → capture → video).
|
||||||
|
*/
|
||||||
|
WAITING_FOR_FILE_REVIEWER = "WAITING_FOR_FILE_REVIEWER",
|
||||||
|
|
||||||
COMPLETED = "COMPLETED",
|
COMPLETED = "COMPLETED",
|
||||||
|
|
||||||
CANCELLED = "CANCELLED",
|
CANCELLED = "CANCELLED",
|
||||||
|
|||||||
@@ -6812,10 +6812,14 @@ export class ClaimRequestManagementService {
|
|||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
actorRole?: string,
|
actorRole?: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (
|
const isFieldLikeRole = [
|
||||||
![RoleEnum.FIELD_EXPERT, RoleEnum.REGISTRAR].includes(actorRole as any) ||
|
RoleEnum.FIELD_EXPERT,
|
||||||
!claimCase?.blameRequestId
|
RoleEnum.REGISTRAR,
|
||||||
) {
|
RoleEnum.FILE_MAKER,
|
||||||
|
RoleEnum.FILE_REVIEWER,
|
||||||
|
].includes(actorRole as any);
|
||||||
|
|
||||||
|
if (!isFieldLikeRole || !claimCase?.blameRequestId) {
|
||||||
return currentUserId;
|
return currentUserId;
|
||||||
}
|
}
|
||||||
const blameRequest = await this.blameRequestDbService.findById(
|
const blameRequest = await this.blameRequestDbService.findById(
|
||||||
@@ -6823,7 +6827,7 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
if (!blameRequest || blameRequest.creationMethod !== "IN_PERSON")
|
if (!blameRequest || blameRequest.creationMethod !== "IN_PERSON")
|
||||||
return currentUserId;
|
return currentUserId;
|
||||||
if (actorRole === RoleEnum.FIELD_EXPERT) {
|
if (actorRole === RoleEnum.FIELD_EXPERT || actorRole === RoleEnum.FILE_MAKER) {
|
||||||
if (
|
if (
|
||||||
!blameRequest.expertInitiated ||
|
!blameRequest.expertInitiated ||
|
||||||
!blameRequest.initiatedByFieldExpertId ||
|
!blameRequest.initiatedByFieldExpertId ||
|
||||||
@@ -6831,7 +6835,7 @@ export class ClaimRequestManagementService {
|
|||||||
) {
|
) {
|
||||||
return currentUserId;
|
return currentUserId;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (actorRole === RoleEnum.REGISTRAR) {
|
||||||
if (
|
if (
|
||||||
!blameRequest.registrarInitiated ||
|
!blameRequest.registrarInitiated ||
|
||||||
!blameRequest.initiatedByRegistrarId ||
|
!blameRequest.initiatedByRegistrarId ||
|
||||||
@@ -6840,6 +6844,8 @@ export class ClaimRequestManagementService {
|
|||||||
return currentUserId;
|
return currentUserId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// FILE_REVIEWER: no initiator-id check — they pick up any IN_PERSON expert
|
||||||
|
// file after the FileMaker seals it, so we resolve the owner unconditionally.
|
||||||
return (
|
return (
|
||||||
resolveDamagedPartyUserId(blameRequest) ??
|
resolveDamagedPartyUserId(blameRequest) ??
|
||||||
(claimCase.owner?.userId ? String(claimCase.owner.userId) : currentUserId)
|
(claimCase.owner?.userId ? String(claimCase.owner.userId) : currentUserId)
|
||||||
@@ -9794,8 +9800,14 @@ export class ClaimRequestManagementService {
|
|||||||
return !!party?.confirmation;
|
return !!party?.confirmation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertV3ClaimDocumentsPhase(blame: any): void {
|
private assertV3ClaimDocumentsPhase(
|
||||||
if (!(blame.expert?.decision as any)?.fields?.accidentWay) {
|
blame: any,
|
||||||
|
options?: { skipAccidentFieldsCheck?: boolean },
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
!options?.skipAccidentFieldsCheck &&
|
||||||
|
!(blame.expert?.decision as any)?.fields?.accidentWay
|
||||||
|
) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Submit accident fields before uploading required documents.",
|
"Submit accident fields before uploading required documents.",
|
||||||
);
|
);
|
||||||
@@ -10079,7 +10091,9 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.assertV3ClaimDocumentsPhase(blame);
|
this.assertV3ClaimDocumentsPhase(blame, {
|
||||||
|
skipAccidentFieldsCheck: actor?.role === "file_maker",
|
||||||
|
});
|
||||||
if (!this.isV3InitialDocumentsPhase(claimCase)) {
|
if (!this.isV3InitialDocumentsPhase(claimCase)) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`Upload initial required documents during ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS} before part selection. Current step: ${claimCase.workflow?.currentStep}`,
|
`Upload initial required documents during ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS} before part selection. Current step: ${claimCase.workflow?.currentStep}`,
|
||||||
@@ -10111,7 +10125,9 @@ export class ClaimRequestManagementService {
|
|||||||
const blame = await this.assertV3InPersonClaim(claimCase);
|
const blame = await this.assertV3InPersonClaim(claimCase);
|
||||||
|
|
||||||
if (this.isV3InitialDocumentsPhase(claimCase)) {
|
if (this.isV3InitialDocumentsPhase(claimCase)) {
|
||||||
this.assertV3ClaimDocumentsPhase(blame);
|
this.assertV3ClaimDocumentsPhase(blame, {
|
||||||
|
skipAccidentFieldsCheck: actor?.role === "file_maker",
|
||||||
|
});
|
||||||
} else if (
|
} else if (
|
||||||
claimCase.workflow?.currentStep === ClaimWorkflowStep.CAPTURE_PART_DAMAGES
|
claimCase.workflow?.currentStep === ClaimWorkflowStep.CAPTURE_PART_DAMAGES
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -121,6 +121,12 @@ export class AllRequestDtoV2 {
|
|||||||
unifiedFileStatus?: string;
|
unifiedFileStatus?: string;
|
||||||
partiesInitialForms: { firstParty: string; secondParty: string };
|
partiesInitialForms: { firstParty: string; secondParty: string };
|
||||||
partiesVehicles: { firstPartyVehicle: string; secondPartyVehicle: string };
|
partiesVehicles: { firstPartyVehicle: string; secondPartyVehicle: string };
|
||||||
|
/**
|
||||||
|
* True when the file is in WAITING_FOR_FILE_REVIEWER status — the FileReviewer
|
||||||
|
* must complete accident fields, capture, and upload-video via v4 endpoints
|
||||||
|
* before the file enters the normal expert-blame review queue.
|
||||||
|
*/
|
||||||
|
needsFileReviewerCompletion?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AllRequestDtoRsV2 {
|
export class AllRequestDtoRsV2 {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
assertBlameCaseForExpertTenant,
|
assertBlameCaseForExpertTenant,
|
||||||
blameCaseAccessibleToExpert,
|
blameCaseAccessibleToExpert,
|
||||||
blameCaseInitiatedByFieldExpert,
|
blameCaseInitiatedByFieldExpert,
|
||||||
|
blameCaseTouchesClient,
|
||||||
requireActorClientKey,
|
requireActorClientKey,
|
||||||
} from "src/helpers/tenant-scope";
|
} from "src/helpers/tenant-scope";
|
||||||
import {
|
import {
|
||||||
@@ -438,6 +439,9 @@ export class ExpertBlameService {
|
|||||||
if (actor.role === RoleEnum.FIELD_EXPERT) {
|
if (actor.role === RoleEnum.FIELD_EXPERT) {
|
||||||
return this.getFieldExpertBlameListV2(actor, query);
|
return this.getFieldExpertBlameListV2(actor, query);
|
||||||
}
|
}
|
||||||
|
if (actor.role === RoleEnum.FILE_REVIEWER) {
|
||||||
|
return this.getFileReviewerBlameListV2(actor, query);
|
||||||
|
}
|
||||||
requireActorClientKey(actor);
|
requireActorClientKey(actor);
|
||||||
const expertId = actor.sub;
|
const expertId = actor.sub;
|
||||||
|
|
||||||
@@ -579,6 +583,44 @@ export class ExpertBlameService {
|
|||||||
return pagedResult;
|
return pagedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blame inbox for FILE_REVIEWER — all expert-initiated IN_PERSON files that
|
||||||
|
* have been sealed by a FileMaker (WAITING_FOR_FILE_REVIEWER) and belong to
|
||||||
|
* this reviewer's insurance company (clientKey).
|
||||||
|
*
|
||||||
|
* Once the FileReviewer completes their steps the file moves to
|
||||||
|
* WAITING_FOR_EXPERT (via upload-video), after which it is visible in the
|
||||||
|
* standard expert-blame panel as usual.
|
||||||
|
*/
|
||||||
|
private async getFileReviewerBlameListV2(
|
||||||
|
actor: { sub: string; clientKey?: string },
|
||||||
|
query: ListQueryV2Dto = {},
|
||||||
|
): Promise<AllRequestDtoRsV2> {
|
||||||
|
const clientKey = requireActorClientKey(actor);
|
||||||
|
|
||||||
|
const allSealed = (await this.blameRequestDbService.find(
|
||||||
|
{
|
||||||
|
expertInitiated: true,
|
||||||
|
status: {
|
||||||
|
$in: [
|
||||||
|
CaseStatus.WAITING_FOR_FILE_REVIEWER,
|
||||||
|
CaseStatus.WAITING_FOR_EXPERT,
|
||||||
|
CaseStatus.COMPLETED,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ lean: true },
|
||||||
|
)) as Record<string, unknown>[];
|
||||||
|
|
||||||
|
// Scope to this reviewer's insurance company via blame party clientId
|
||||||
|
const visibleCases = allSealed.filter((doc) =>
|
||||||
|
blameCaseTouchesClient(doc, clientKey),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pagedResult = await this.paginateBlameListV2(visibleCases, query);
|
||||||
|
return pagedResult;
|
||||||
|
}
|
||||||
|
|
||||||
private async paginateBlameListV2(
|
private async paginateBlameListV2(
|
||||||
visibleCases: Record<string, unknown>[],
|
visibleCases: Record<string, unknown>[],
|
||||||
query: ListQueryV2Dto,
|
query: ListQueryV2Dto,
|
||||||
@@ -727,6 +769,8 @@ export class ExpertBlameService {
|
|||||||
secondParty?.vehicle?.inquiry?.mapped?.CarName ||
|
secondParty?.vehicle?.inquiry?.mapped?.CarName ||
|
||||||
"",
|
"",
|
||||||
},
|
},
|
||||||
|
needsFileReviewerCompletion:
|
||||||
|
String(doc.status ?? "") === CaseStatus.WAITING_FOR_FILE_REVIEWER,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ export class ClaimListItemV2Dto {
|
|||||||
})
|
})
|
||||||
carBodyFirstForm?: { car?: boolean; object?: boolean };
|
carBodyFirstForm?: { car?: boolean; object?: boolean };
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Linked blame file ID (present when the claim originates from a blame case)',
|
||||||
|
example: '6a3a6f171aadfa0cc313c582',
|
||||||
|
})
|
||||||
|
blameRequestId?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Submission date', example: '2026-02-22T10:00:00.000Z' })
|
@ApiProperty({ description: 'Submission date', example: '2026-02-22T10:00:00.000Z' })
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import { RoleEnum } from "src/Types&Enums/role.enum";
|
|||||||
import {
|
import {
|
||||||
assertClaimCaseForTenant,
|
assertClaimCaseForTenant,
|
||||||
assertClaimCaseForExpertActor,
|
assertClaimCaseForExpertActor,
|
||||||
|
blameCaseTouchesClient,
|
||||||
claimCaseInitiatedByFieldExpert,
|
claimCaseInitiatedByFieldExpert,
|
||||||
claimCaseTouchesClient,
|
claimCaseTouchesClient,
|
||||||
requireActorClientKey,
|
requireActorClientKey,
|
||||||
@@ -3648,6 +3649,9 @@ export class ExpertClaimService {
|
|||||||
if (actor.role === RoleEnum.FIELD_EXPERT) {
|
if (actor.role === RoleEnum.FIELD_EXPERT) {
|
||||||
return this.getFieldExpertClaimListV2(actor, query);
|
return this.getFieldExpertClaimListV2(actor, query);
|
||||||
}
|
}
|
||||||
|
if (actor.role === RoleEnum.FILE_REVIEWER) {
|
||||||
|
return this.getFileReviewerClaimListV2(actor, query);
|
||||||
|
}
|
||||||
requireActorClientKey(actor);
|
requireActorClientKey(actor);
|
||||||
const actorId = actor.sub;
|
const actorId = actor.sub;
|
||||||
const clientKey = actor.clientKey as string;
|
const clientKey = actor.clientKey as string;
|
||||||
@@ -3822,6 +3826,7 @@ export class ExpertClaimService {
|
|||||||
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
||||||
: undefined,
|
: undefined,
|
||||||
...fileCtx,
|
...fileCtx,
|
||||||
|
blameRequestId: c.blameRequestId?.toString(),
|
||||||
createdAt: c.createdAt,
|
createdAt: c.createdAt,
|
||||||
awaitingFactorValidation,
|
awaitingFactorValidation,
|
||||||
};
|
};
|
||||||
@@ -3868,7 +3873,7 @@ export class ExpertClaimService {
|
|||||||
blameIds.length > 0
|
blameIds.length > 0
|
||||||
? ((await this.blameRequestDbService.find(
|
? ((await this.blameRequestDbService.find(
|
||||||
{ _id: { $in: blameIds.map((id) => new Types.ObjectId(id)) } },
|
{ _id: { $in: blameIds.map((id) => new Types.ObjectId(id)) } },
|
||||||
{ lean: true, select: "type parties expert.decision blameStatus" },
|
{ lean: true, select: "type parties expert.decision blameStatus status" },
|
||||||
)) as any[])
|
)) as any[])
|
||||||
: [];
|
: [];
|
||||||
const blameById = new Map<string, any>(
|
const blameById = new Map<string, any>(
|
||||||
@@ -3907,8 +3912,102 @@ export class ExpertClaimService {
|
|||||||
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
||||||
: undefined,
|
: undefined,
|
||||||
...fileCtx,
|
...fileCtx,
|
||||||
|
blameRequestId: c.blameRequestId?.toString(),
|
||||||
createdAt: c.createdAt,
|
createdAt: c.createdAt,
|
||||||
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
|
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
|
||||||
|
needsFileReviewerCompletion:
|
||||||
|
String(blame?.status ?? "") === "WAITING_FOR_FILE_REVIEWER",
|
||||||
|
};
|
||||||
|
}) as ClaimListItemV2Dto[];
|
||||||
|
|
||||||
|
return this.paginateClaimListV2(list, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claim list for FILE_REVIEWER — returns claims linked to sealed expert-initiated
|
||||||
|
* IN_PERSON blame files (WAITING_FOR_FILE_REVIEWER, WAITING_FOR_EXPERT, COMPLETED)
|
||||||
|
* that belong to this reviewer's insurance company.
|
||||||
|
*/
|
||||||
|
private async getFileReviewerClaimListV2(
|
||||||
|
actor: any,
|
||||||
|
query: ListQueryV2Dto = {},
|
||||||
|
): Promise<GetClaimListV2ResponseDto> {
|
||||||
|
const clientKey = requireActorClientKey(actor);
|
||||||
|
|
||||||
|
// Find all sealed blame files (across statuses the reviewer cares about)
|
||||||
|
const sealedBlames = (await this.blameRequestDbService.find(
|
||||||
|
{
|
||||||
|
expertInitiated: true,
|
||||||
|
creationMethod: "IN_PERSON",
|
||||||
|
status: {
|
||||||
|
$in: [
|
||||||
|
"WAITING_FOR_FILE_REVIEWER",
|
||||||
|
"WAITING_FOR_EXPERT",
|
||||||
|
"COMPLETED",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ lean: true, select: "_id type parties blameStatus status expert.decision" },
|
||||||
|
)) as any[];
|
||||||
|
|
||||||
|
if (sealedBlames.length === 0) {
|
||||||
|
return this.paginateClaimListV2([], query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scope to this reviewer's insurer via blame party clientId
|
||||||
|
const scopedBlameIds = sealedBlames
|
||||||
|
.filter((b) => blameCaseTouchesClient(b, clientKey))
|
||||||
|
.map((b) => b._id);
|
||||||
|
|
||||||
|
if (scopedBlameIds.length === 0) {
|
||||||
|
return this.paginateClaimListV2([], query);
|
||||||
|
}
|
||||||
|
|
||||||
|
const claims = (await this.claimCaseDbService.find({
|
||||||
|
blameRequestId: { $in: scopedBlameIds },
|
||||||
|
})) as any[];
|
||||||
|
|
||||||
|
const blameById = new Map<string, any>(
|
||||||
|
sealedBlames.map((b) => [String(b._id), b]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const list = claims.map((c) => {
|
||||||
|
const blame = c.blameRequestId
|
||||||
|
? blameById.get(c.blameRequestId.toString())
|
||||||
|
: undefined;
|
||||||
|
const v = this.vehicleForExpertFromClaimAndBlameMap(c, blameById);
|
||||||
|
const fileCtx = blame ? this.blameFileContextForExpert(blame) : {};
|
||||||
|
const lockActive = !!(
|
||||||
|
c.workflow?.locked && this.isClaimV2WorkflowLockCurrentlyEnforced(c)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
claimRequestId: c._id.toString(),
|
||||||
|
publicId: c.publicId,
|
||||||
|
status: c.status,
|
||||||
|
unifiedFileStatus: resolveUnifiedFileStatus({
|
||||||
|
blameStatus: blame?.status,
|
||||||
|
claimStatus: c.status,
|
||||||
|
}),
|
||||||
|
currentStep: c.workflow?.currentStep || "",
|
||||||
|
locked: lockActive,
|
||||||
|
lockedBy:
|
||||||
|
lockActive && c.workflow?.lockedBy
|
||||||
|
? {
|
||||||
|
actorId: c.workflow.lockedBy.actorId?.toString(),
|
||||||
|
actorName: c.workflow.lockedBy.actorName,
|
||||||
|
lockedAt: (c.workflow as any).lockedAt?.toISOString?.(),
|
||||||
|
expiredAt: (c.workflow as any).expiredAt?.toISOString?.(),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
vehicle: v
|
||||||
|
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
||||||
|
: undefined,
|
||||||
|
...fileCtx,
|
||||||
|
blameRequestId: c.blameRequestId?.toString(),
|
||||||
|
createdAt: c.createdAt,
|
||||||
|
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
|
||||||
|
needsFileReviewerCompletion:
|
||||||
|
String(blame?.status ?? "") === "WAITING_FOR_FILE_REVIEWER",
|
||||||
};
|
};
|
||||||
}) as ClaimListItemV2Dto[];
|
}) as ClaimListItemV2Dto[];
|
||||||
|
|
||||||
@@ -4092,7 +4191,10 @@ export class ExpertClaimService {
|
|||||||
clientKey?: string;
|
clientKey?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if ((actor as any).role === RoleEnum.FIELD_EXPERT) return;
|
if (
|
||||||
|
(actor as any).role === RoleEnum.FIELD_EXPERT ||
|
||||||
|
(actor as any).role === RoleEnum.FILE_REVIEWER
|
||||||
|
) return;
|
||||||
const clientKey = requireActorClientKey(actor);
|
const clientKey = requireActorClientKey(actor);
|
||||||
const lockTtlMs = EXPERT_WORKFLOW_LOCK_TTL_MS;
|
const lockTtlMs = EXPERT_WORKFLOW_LOCK_TTL_MS;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -4272,10 +4374,28 @@ export class ExpertClaimService {
|
|||||||
const isFactorValidationPending =
|
const isFactorValidationPending =
|
||||||
claimIsAwaitingExpertFactorValidationV2(claim);
|
claimIsAwaitingExpertFactorValidationV2(claim);
|
||||||
|
|
||||||
// Field experts can always view their own initiated claims regardless of status
|
// Field experts and FileReviewers can always view IN_PERSON expert-initiated
|
||||||
if (actor.role === RoleEnum.FIELD_EXPERT) {
|
// claims regardless of claim status (they work across multiple non-standard steps).
|
||||||
|
if (
|
||||||
|
actor.role === RoleEnum.FIELD_EXPERT ||
|
||||||
|
actor.role === RoleEnum.FILE_REVIEWER
|
||||||
|
) {
|
||||||
if (!claimCaseInitiatedByFieldExpert(claim, actor, linkedBlame)) {
|
if (!claimCaseInitiatedByFieldExpert(claim, actor, linkedBlame)) {
|
||||||
throw new ForbiddenException("This claim was not initiated by you.");
|
// For FILE_REVIEWER: also accept if the linked blame is sealed for them
|
||||||
|
const isFileReviewerEligible =
|
||||||
|
actor.role === RoleEnum.FILE_REVIEWER &&
|
||||||
|
linkedBlame?.expertInitiated &&
|
||||||
|
linkedBlame?.creationMethod === "IN_PERSON" &&
|
||||||
|
[
|
||||||
|
"WAITING_FOR_FILE_REVIEWER",
|
||||||
|
"WAITING_FOR_EXPERT",
|
||||||
|
"COMPLETED",
|
||||||
|
].includes(String(linkedBlame?.status ?? ""));
|
||||||
|
if (!isFileReviewerEligible) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"This claim is not accessible to you.",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Fall through to the detail build below (skip the damage-expert gate)
|
// Fall through to the detail build below (skip the damage-expert gate)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4129,6 +4129,28 @@ export class RequestManagementService {
|
|||||||
* Verify the current user (field expert) can access this BlameRequest (v2).
|
* Verify the current user (field expert) can access this BlameRequest (v2).
|
||||||
*/
|
*/
|
||||||
private verifyExpertAccessForBlameV2(req: any, expert: any): void {
|
private verifyExpertAccessForBlameV2(req: any, expert: any): void {
|
||||||
|
// FILE_REVIEWER can access any sealed expert-initiated IN_PERSON file
|
||||||
|
// (status WAITING_FOR_FILE_REVIEWER or beyond) scoped to their clientKey.
|
||||||
|
if (expert?.role === RoleEnum.FILE_REVIEWER) {
|
||||||
|
if (
|
||||||
|
!req?.expertInitiated ||
|
||||||
|
req?.creationMethod !== CreationMethod.IN_PERSON
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"FileReviewer can only access expert-initiated IN_PERSON files.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
req.status !== CaseStatus.WAITING_FOR_FILE_REVIEWER &&
|
||||||
|
req.status !== CaseStatus.WAITING_FOR_EXPERT &&
|
||||||
|
req.status !== CaseStatus.COMPLETED
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"This file has not been sealed by a FileMaker yet.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (req?.expertInitiated && req?.initiatedByFieldExpertId) {
|
if (req?.expertInitiated && req?.initiatedByFieldExpertId) {
|
||||||
if (String(req.initiatedByFieldExpertId) !== String(expert?.sub)) {
|
if (String(req.initiatedByFieldExpertId) !== String(expert?.sub)) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
@@ -8815,6 +8837,7 @@ export class RequestManagementService {
|
|||||||
|
|
||||||
const fresh = await this.blameRequestDbService.findById(requestId);
|
const fresh = await this.blameRequestDbService.findById(requestId);
|
||||||
if (fresh) {
|
if (fresh) {
|
||||||
|
const isFileMakerActor = expert?.role === RoleEnum.FILE_MAKER;
|
||||||
if (partyRole === PartyRole.FIRST) {
|
if (partyRole === PartyRole.FIRST) {
|
||||||
this.pushWorkflowSteps(fresh, [
|
this.pushWorkflowSteps(fresh, [
|
||||||
WorkflowStep.FIRST_LOCATION,
|
WorkflowStep.FIRST_LOCATION,
|
||||||
@@ -8823,14 +8846,17 @@ export class RequestManagementService {
|
|||||||
WorkflowStep.FIRST_SIGN,
|
WorkflowStep.FIRST_SIGN,
|
||||||
WorkflowStep.FIRST_COMPLETED,
|
WorkflowStep.FIRST_COMPLETED,
|
||||||
]);
|
]);
|
||||||
fresh.workflow.currentStep =
|
if (fresh.type === BlameRequestType.THIRD_PARTY) {
|
||||||
fresh.type === BlameRequestType.THIRD_PARTY
|
fresh.workflow.currentStep = WorkflowStep.FIRST_INVITE_SECOND;
|
||||||
? WorkflowStep.FIRST_INVITE_SECOND
|
fresh.workflow.nextStep = WorkflowStep.SECOND_INITIAL_FORM;
|
||||||
: WorkflowStep.FIRST_COMPLETED;
|
} else {
|
||||||
fresh.workflow.nextStep =
|
// CAR_BODY: FileMaker is done — seal the file for FileReviewer pickup
|
||||||
fresh.type === BlameRequestType.THIRD_PARTY
|
fresh.workflow.currentStep = WorkflowStep.FIRST_COMPLETED;
|
||||||
? WorkflowStep.SECOND_INITIAL_FORM
|
fresh.workflow.nextStep = WorkflowStep.WAITING_FOR_GUILT_DECISION;
|
||||||
: WorkflowStep.WAITING_FOR_GUILT_DECISION;
|
if (isFileMakerActor) {
|
||||||
|
fresh.status = CaseStatus.WAITING_FOR_FILE_REVIEWER;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.pushWorkflowSteps(fresh, [
|
this.pushWorkflowSteps(fresh, [
|
||||||
WorkflowStep.SECOND_LOCATION,
|
WorkflowStep.SECOND_LOCATION,
|
||||||
@@ -8841,6 +8867,10 @@ export class RequestManagementService {
|
|||||||
]);
|
]);
|
||||||
fresh.workflow.currentStep = WorkflowStep.SECOND_COMPLETED;
|
fresh.workflow.currentStep = WorkflowStep.SECOND_COMPLETED;
|
||||||
fresh.workflow.nextStep = WorkflowStep.WAITING_FOR_GUILT_DECISION;
|
fresh.workflow.nextStep = WorkflowStep.WAITING_FOR_GUILT_DECISION;
|
||||||
|
// THIRD_PARTY: second party sign is FileMaker's last step — seal for FileReviewer
|
||||||
|
if (isFileMakerActor) {
|
||||||
|
fresh.status = CaseStatus.WAITING_FOR_FILE_REVIEWER;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await (fresh as any).save();
|
await (fresh as any).save();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user