From fde6464739a94c37b88aa9874724e01cc7dc20b4 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Mon, 1 Jun 2026 13:06:09 +0330 Subject: [PATCH] YARA-951 --- src/expert-blame/expert-blame.service.ts | 184 +++++++++++------------ 1 file changed, 90 insertions(+), 94 deletions(-) diff --git a/src/expert-blame/expert-blame.service.ts b/src/expert-blame/expert-blame.service.ts index 1a3331a..1339b4e 100644 --- a/src/expert-blame/expert-blame.service.ts +++ b/src/expert-blame/expert-blame.service.ts @@ -303,66 +303,58 @@ export class ExpertBlameService { requireActorClientKey(actor); const expertId = actor.sub; - // Fetch all DISAGREEMENT cases const allCases = await this.blameRequestDbService.find( - { - blameStatus: BlameStatus.DISAGREEMENT, - }, + { blameStatus: BlameStatus.DISAGREEMENT }, { lean: true }, ); - // Filter to show only: - // 1. Same insurance tenant (party clientId or expert-initiated by this actor) - // 2. Fresh requests (WAITING_FOR_EXPERT and no decision) - // 3. Requests decided by current expert - // 4. Expert-initiated: only the initiating field expert sees them const visibleCases = (allCases as Record[]).filter( (doc) => { - if (!blameCaseAccessibleToExpert(doc, actor)) { - return false; - } - - const expertInitiated = doc.expertInitiated === true; - const initiatedByFieldExpertId = doc.initiatedByFieldExpertId; - - if (expertInitiated && initiatedByFieldExpertId) { - if (String(initiatedByFieldExpertId) !== expertId) { - return false; // Only the initiating field expert can see this file - } - return true; // Initiating expert can see their expert-initiated file - } + // Always filter by tenant first + if (!blameCaseAccessibleToExpert(doc, actor)) return false; const status = doc.status as string; - const decision = doc.expert as any; - const decidedByExpertId = decision?.decision?.decidedByExpertId; - const hasDecision = !!decision?.decision; + const decision = (doc.expert as any)?.decision; + const decidedByExpertId = decision?.decidedByExpertId + ? String(decision.decidedByExpertId) + : null; + const lockedById = String( + (doc.workflow as any)?.lockedBy?.actorId ?? "", + ); + const lockEnforced = + (doc.workflow as any)?.locked && + this.isBlameV2WorkflowLockCurrentlyEnforced(doc as any); - // Fresh request (no decision yet) - if (status === CaseStatus.WAITING_FOR_EXPERT && !hasDecision) { - return true; + // Expert-initiated files: only the initiating expert sees them + if (doc.expertInitiated === true && doc.initiatedByFieldExpertId) { + return String(doc.initiatedByFieldExpertId) === expertId; } - // Request decided by current expert - if (decidedByExpertId && String(decidedByExpertId) === expertId) { - return true; - } - - // Locked by current expert but no decision yet - const lockedBy = - decision?.resend?.requestedByExpertId || - (doc.workflow as any)?.lockedBy?.actorId; - if ( + // Bucket 1: Available — waiting, no decision, no active lock by someone else + const isAvailable = status === CaseStatus.WAITING_FOR_EXPERT && - lockedBy && - String(lockedBy) === expertId - ) { - return true; - } + !decidedByExpertId && + (!lockEnforced || lockedById === expertId); - return false; + // Bucket 2: Mine — decided by me + const isDecidedByMe = + !!decidedByExpertId && decidedByExpertId === expertId; + + // Bucket 3: Mine — currently locked/assigned to me (in progress) + const isLockedByMe = lockEnforced && lockedById === expertId; + + // Bucket 4: Mine — persistently assigned to me for review + const assignedForReviewById = String( + (doc.workflow as any)?.assignedForReviewBy?.actorId ?? "", + ); + const isAssignedToMe = + !!assignedForReviewById && assignedForReviewById === expertId; + + return isAvailable || isDecidedByMe || isLockedByMe || isAssignedToMe; }, ); + // Reconcile stale locks in-memory (same as before) const staleIds = new Set(); for (const doc of visibleCases) { const w = doc.workflow as Record | undefined; @@ -854,75 +846,79 @@ export class ExpertBlameService { actor: any, ): Promise> { try { - // requireActorClientKey(actor); const actorId = actor.sub; await this.expireBlameCaseWorkflowLockV2IfStale(requestId); + const doc = await this.blameRequestDbService.findByIdWithoutHistory(requestId); if (!doc) { throw new NotFoundException("Request not found"); } - // assertBlameCaseForExpertTenant(doc, actor); - const type = doc.type as string; - if (type === BlameRequestType.CAR_BODY) { + + // Tenant check + assertBlameCaseForExpertTenant(doc, actor); + + if (doc.type === BlameRequestType.CAR_BODY) { throw new ForbiddenException( "CAR_BODY type requests are automatically handled and do not require expert review.", ); } - // Access control - const expertInitiated = doc.expertInitiated === true; - const initiatedByFieldExpertId = doc.initiatedByFieldExpertId; + // Expert-initiated: only the initiating expert if ( - expertInitiated && - initiatedByFieldExpertId && - String(initiatedByFieldExpertId) !== actorId + doc.expertInitiated === true && + doc.initiatedByFieldExpertId && + String(doc.initiatedByFieldExpertId) !== actorId ) { throw new ForbiddenException( "Only the field expert who created this file can view and review it.", ); } - if (this.isBlameV2WorkflowLockCurrentlyEnforced(doc)) { - const w = doc.workflow as - | { lockedBy?: { actorId?: unknown } } - | undefined; - const lockerId = String(w?.lockedBy?.actorId ?? ""); - if (lockerId && lockerId !== actorId) { + const decision = (doc.expert as any)?.decision; + const decidedByExpertId = decision?.decidedByExpertId + ? String(decision.decidedByExpertId) + : null; + const lockedById = String((doc.workflow as any)?.lockedBy?.actorId ?? ""); + const lockEnforced = + (doc.workflow as any)?.locked && + this.isBlameV2WorkflowLockCurrentlyEnforced(doc); + const assignedForReviewById = String( + (doc.workflow as any)?.assignedForReviewBy?.actorId ?? "", + ); + + // Access gates — must satisfy at least one bucket + const isAvailable = + doc.status === CaseStatus.WAITING_FOR_EXPERT && !decidedByExpertId; + const isDecidedByMe = decidedByExpertId === actorId; + const isLockedByMe = lockEnforced && lockedById === actorId; + const isAssignedToMe = + !!assignedForReviewById && assignedForReviewById === actorId; + + if (!isAvailable && !isDecidedByMe && !isLockedByMe && !isAssignedToMe) { + // Give a specific reason if possible + if (lockEnforced && lockedById && lockedById !== actorId) { throw new ForbiddenException( "This request is locked by another expert.", ); } - } - - const decision = (doc.expert as any)?.decision; - const decidedByExpertId = decision?.decidedByExpertId; - if (decidedByExpertId && String(decidedByExpertId) !== actorId) { + if (decidedByExpertId && decidedByExpertId !== actorId) { + throw new ForbiddenException( + "You do not have permission to view this request. It has been handled by another expert.", + ); + } throw new ForbiddenException( - "You do not have permission to view this request. It has been handled by another expert.", + "You do not have permission to view this request.", ); } + // Build evidence URLs const parties = Array.isArray(doc.parties) ? doc.parties : []; - - const typedParties = parties as Array<{ - vehicle?: { - inquiry?: { - mapped?: any; - }; - }; - evidence?: { - videoId?: string | number; - voices?: (string | number)[]; - videoUrl?: string; - voiceUrls?: string[]; - }; - }>; - - // Evidence (videos + voices) - for (const party of typedParties) { + for (const party of parties as Array<{ + evidence?: Record; + }>) { if (!party.evidence) continue; - const evidence = party.evidence as Record; + const evidence = party.evidence; if (evidence.videoId) { const videoDoc = await this.blameVideoDbService.findById( @@ -931,7 +927,7 @@ export class ExpertBlameService { if (videoDoc?.path) evidence.videoUrl = buildFileLink(videoDoc.path); } - if (evidence.voices && Array.isArray(evidence.voices)) { + if (Array.isArray(evidence.voices)) { const voiceUrls: string[] = []; for (const voiceId of evidence.voices) { const voiceDoc = await this.blameVoiceDbService.findById( @@ -950,31 +946,31 @@ export class ExpertBlameService { const updatedAt = doc.updatedAt ? new Date(doc.updatedAt as string | number) : new Date(); - const [createdDate, createdTime] = toJalaliDateAndTime(createdAt); const [updatedDate, updatedTime] = toJalaliDateAndTime(updatedAt); - doc.createdAtFormatted = `${createdDate} ${createdTime}`; doc.updatedAtFormatted = `${updatedDate} ${updatedTime}`; - // Omit heavy SandHub inquiry blob from expert response (keep other vehicle fields) - for (const party of typedParties) { - const veh = party?.vehicle as Record | undefined; - if (veh && Object.prototype.hasOwnProperty.call(veh, "inquiry")) { - delete veh.inquiry; + // Strip heavy SandHub inquiry blob + for (const party of parties as Array<{ + vehicle?: Record; + }>) { + if ( + party.vehicle && + Object.prototype.hasOwnProperty.call(party.vehicle, "inquiry") + ) { + delete party.vehicle.inquiry; } } return doc; } catch (error) { if (error instanceof HttpException) throw error; - this.logger.error( "findOneV2 failed", requestId, error instanceof Error ? error.stack : String(error), ); - throw new InternalServerErrorException( error instanceof Error ? error.message