Fixed Lock for Field expert + user view of files

This commit is contained in:
2026-06-18 13:31:56 +03:30
parent 6df8044c5a
commit d8f7766f10
8 changed files with 328 additions and 234 deletions

View File

@@ -11,6 +11,7 @@ import {
import {
assertBlameCaseForExpertTenant,
blameCaseAccessibleToExpert,
blameCaseInitiatedByFieldExpert,
requireActorClientKey,
} from "src/helpers/tenant-scope";
import {
@@ -436,8 +437,9 @@ export class ExpertBlameService {
}
/**
* Blame review inbox for FIELD_EXPERT — expert-initiated DISAGREEMENT files only.
* IN_PERSON flows are AGREED and handled outside this panel; completed blames appear in expert-claim.
* Blame review inbox for FIELD_EXPERT — expert-initiated DISAGREEMENT files.
* Covers LINK flows (parties complete via link → WAITING_FOR_EXPERT) and in-progress
* disputes while parties are still filling. IN_PERSON AGREED blames use expert-claim instead.
*/
private async getFieldExpertBlameListV2(
actor: { sub: string },
@@ -448,9 +450,25 @@ export class ExpertBlameService {
const visibleCases = (await this.blameRequestDbService.find(
{
blameStatus: BlameStatus.DISAGREEMENT,
expertInitiated: true,
initiatedByFieldExpertId: expertOid,
blameStatus: BlameStatus.DISAGREEMENT,
type: { $ne: BlameRequestType.CAR_BODY },
$or: [
{ status: CaseStatus.WAITING_FOR_EXPERT },
{
status: {
$in: [
CaseStatus.OPEN,
CaseStatus.WAITING_FOR_SECOND_PARTY,
CaseStatus.WAITING_FOR_DOCUMENT_RESEND,
],
},
},
{ "workflow.lockedBy.actorId": expertOid },
{ "workflow.assignedForReviewBy.actorId": expertOid },
{ "expert.decision.decidedByExpertId": expertOid },
],
},
{ lean: true },
)) as Record<string, unknown>[];
@@ -966,6 +984,8 @@ export class ExpertBlameService {
);
}
const isFieldExpertOwner = blameCaseInitiatedByFieldExpert(doc, actor);
const decision = (doc.expert as any)?.decision;
const decidedByExpertId = decision?.decidedByExpertId
? String(decision.decidedByExpertId)
@@ -978,6 +998,7 @@ export class ExpertBlameService {
(doc.workflow as any)?.assignedForReviewBy?.actorId ?? "",
);
if (!isFieldExpertOwner) {
// Access gates — must satisfy at least one bucket
const isAvailable =
doc.status === CaseStatus.WAITING_FOR_EXPERT && !decidedByExpertId;
@@ -1002,6 +1023,7 @@ export class ExpertBlameService {
"You do not have permission to view this request.",
);
}
}
// Build evidence URLs
const parties = Array.isArray(doc.parties) ? doc.parties : [];
@@ -1158,7 +1180,12 @@ export class ExpertBlameService {
*/
async assignBlameCaseForReviewV2(
requestId: string,
actor: { sub: string; fullName?: string; clientKey?: string },
actor: {
sub: string;
fullName?: string;
clientKey?: string;
role?: string;
},
): Promise<ExpertFileAssignResultDto> {
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
@@ -1169,6 +1196,8 @@ export class ExpertBlameService {
assertBlameCaseForExpertTenant(request, actor);
const isFieldExpertOwner = blameCaseInitiatedByFieldExpert(request, actor);
if (request.type === BlameRequestType.CAR_BODY) {
throw new BadRequestException({
success: false,
@@ -1177,9 +1206,17 @@ export class ExpertBlameService {
});
}
if (request.blameStatus !== BlameStatus.DISAGREEMENT) {
throw new BadRequestException({
success: false,
status: "unavailable" satisfies ExpertFileAssignStatus,
message: "Request is not available for expert review",
});
}
if (
request.status !== CaseStatus.WAITING_FOR_EXPERT ||
request.blameStatus !== BlameStatus.DISAGREEMENT
!isFieldExpertOwner &&
request.status !== CaseStatus.WAITING_FOR_EXPERT
) {
throw new BadRequestException({
success: false,
@@ -1188,6 +1225,26 @@ export class ExpertBlameService {
});
}
if (isFieldExpertOwner && request.expert?.decision) {
throw new BadRequestException({
success: false,
status: "unavailable" satisfies ExpertFileAssignStatus,
message: "This request already has an expert decision",
});
}
if (
isFieldExpertOwner &&
request.status !== CaseStatus.WAITING_FOR_EXPERT
) {
throw new BadRequestException({
success: false,
status: "unavailable" satisfies ExpertFileAssignStatus,
message:
"Parties are still completing this file. Lock and review become available when status is WAITING_FOR_EXPERT (e.g. after both parties finish via link).",
});
}
if (
request.expertInitiated &&
request.initiatedByFieldExpertId &&
@@ -1272,7 +1329,6 @@ export class ExpertBlameService {
const assignFilter: Record<string, unknown> = {
_id: new Types.ObjectId(requestId),
status: CaseStatus.WAITING_FOR_EXPERT,
blameStatus: BlameStatus.DISAGREEMENT,
$and: [
{
@@ -1293,8 +1349,11 @@ export class ExpertBlameService {
],
};
if (request.expertInitiated) {
if (isFieldExpertOwner) {
assignFilter.initiatedByFieldExpertId = expertOid;
assignFilter.status = CaseStatus.WAITING_FOR_EXPERT;
} else {
assignFilter.status = CaseStatus.WAITING_FOR_EXPERT;
}
const updated = await this.blameRequestDbService.findOneAndUpdate(