1
0
forked from Yara724/api
This commit is contained in:
SepehrYahyaee
2026-06-01 13:06:09 +03:30
parent 06af79fa47
commit fde6464739

View File

@@ -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<string, unknown>[]).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<string>();
for (const doc of visibleCases) {
const w = doc.workflow as Record<string, unknown> | undefined;
@@ -854,75 +846,79 @@ export class ExpertBlameService {
actor: any,
): Promise<Record<string, unknown>> {
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<string, unknown>;
}>) {
if (!party.evidence) continue;
const evidence = party.evidence as Record<string, unknown>;
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<string, unknown> | 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<string, unknown>;
}>) {
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