forked from Yara724/api
YARA-951
This commit is contained in:
@@ -303,66 +303,58 @@ export class ExpertBlameService {
|
|||||||
requireActorClientKey(actor);
|
requireActorClientKey(actor);
|
||||||
const expertId = actor.sub;
|
const expertId = actor.sub;
|
||||||
|
|
||||||
// Fetch all DISAGREEMENT cases
|
|
||||||
const allCases = await this.blameRequestDbService.find(
|
const allCases = await this.blameRequestDbService.find(
|
||||||
{
|
{ blameStatus: BlameStatus.DISAGREEMENT },
|
||||||
blameStatus: BlameStatus.DISAGREEMENT,
|
|
||||||
},
|
|
||||||
{ lean: true },
|
{ 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(
|
const visibleCases = (allCases as Record<string, unknown>[]).filter(
|
||||||
(doc) => {
|
(doc) => {
|
||||||
if (!blameCaseAccessibleToExpert(doc, actor)) {
|
// Always filter by tenant first
|
||||||
return false;
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = doc.status as string;
|
const status = doc.status as string;
|
||||||
const decision = doc.expert as any;
|
const decision = (doc.expert as any)?.decision;
|
||||||
const decidedByExpertId = decision?.decision?.decidedByExpertId;
|
const decidedByExpertId = decision?.decidedByExpertId
|
||||||
const hasDecision = !!decision?.decision;
|
? 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)
|
// Expert-initiated files: only the initiating expert sees them
|
||||||
if (status === CaseStatus.WAITING_FOR_EXPERT && !hasDecision) {
|
if (doc.expertInitiated === true && doc.initiatedByFieldExpertId) {
|
||||||
return true;
|
return String(doc.initiatedByFieldExpertId) === expertId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request decided by current expert
|
// Bucket 1: Available — waiting, no decision, no active lock by someone else
|
||||||
if (decidedByExpertId && String(decidedByExpertId) === expertId) {
|
const isAvailable =
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Locked by current expert but no decision yet
|
|
||||||
const lockedBy =
|
|
||||||
decision?.resend?.requestedByExpertId ||
|
|
||||||
(doc.workflow as any)?.lockedBy?.actorId;
|
|
||||||
if (
|
|
||||||
status === CaseStatus.WAITING_FOR_EXPERT &&
|
status === CaseStatus.WAITING_FOR_EXPERT &&
|
||||||
lockedBy &&
|
!decidedByExpertId &&
|
||||||
String(lockedBy) === expertId
|
(!lockEnforced || lockedById === expertId);
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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>();
|
const staleIds = new Set<string>();
|
||||||
for (const doc of visibleCases) {
|
for (const doc of visibleCases) {
|
||||||
const w = doc.workflow as Record<string, unknown> | undefined;
|
const w = doc.workflow as Record<string, unknown> | undefined;
|
||||||
@@ -854,75 +846,79 @@ export class ExpertBlameService {
|
|||||||
actor: any,
|
actor: any,
|
||||||
): Promise<Record<string, unknown>> {
|
): Promise<Record<string, unknown>> {
|
||||||
try {
|
try {
|
||||||
// requireActorClientKey(actor);
|
|
||||||
const actorId = actor.sub;
|
const actorId = actor.sub;
|
||||||
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
|
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
|
||||||
|
|
||||||
const doc =
|
const doc =
|
||||||
await this.blameRequestDbService.findByIdWithoutHistory(requestId);
|
await this.blameRequestDbService.findByIdWithoutHistory(requestId);
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
throw new NotFoundException("Request not found");
|
throw new NotFoundException("Request not found");
|
||||||
}
|
}
|
||||||
// assertBlameCaseForExpertTenant(doc, actor);
|
|
||||||
const type = doc.type as string;
|
// Tenant check
|
||||||
if (type === BlameRequestType.CAR_BODY) {
|
assertBlameCaseForExpertTenant(doc, actor);
|
||||||
|
|
||||||
|
if (doc.type === BlameRequestType.CAR_BODY) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
"CAR_BODY type requests are automatically handled and do not require expert review.",
|
"CAR_BODY type requests are automatically handled and do not require expert review.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Access control
|
// Expert-initiated: only the initiating expert
|
||||||
const expertInitiated = doc.expertInitiated === true;
|
|
||||||
const initiatedByFieldExpertId = doc.initiatedByFieldExpertId;
|
|
||||||
if (
|
if (
|
||||||
expertInitiated &&
|
doc.expertInitiated === true &&
|
||||||
initiatedByFieldExpertId &&
|
doc.initiatedByFieldExpertId &&
|
||||||
String(initiatedByFieldExpertId) !== actorId
|
String(doc.initiatedByFieldExpertId) !== actorId
|
||||||
) {
|
) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
"Only the field expert who created this file can view and review it.",
|
"Only the field expert who created this file can view and review it.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isBlameV2WorkflowLockCurrentlyEnforced(doc)) {
|
const decision = (doc.expert as any)?.decision;
|
||||||
const w = doc.workflow as
|
const decidedByExpertId = decision?.decidedByExpertId
|
||||||
| { lockedBy?: { actorId?: unknown } }
|
? String(decision.decidedByExpertId)
|
||||||
| undefined;
|
: null;
|
||||||
const lockerId = String(w?.lockedBy?.actorId ?? "");
|
const lockedById = String((doc.workflow as any)?.lockedBy?.actorId ?? "");
|
||||||
if (lockerId && lockerId !== 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(
|
throw new ForbiddenException(
|
||||||
"This request is locked by another expert.",
|
"This request is locked by another expert.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
if (decidedByExpertId && decidedByExpertId !== actorId) {
|
||||||
|
|
||||||
const decision = (doc.expert as any)?.decision;
|
|
||||||
const decidedByExpertId = decision?.decidedByExpertId;
|
|
||||||
if (decidedByExpertId && String(decidedByExpertId) !== actorId) {
|
|
||||||
throw new ForbiddenException(
|
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. It has been handled by another expert.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"You do not have permission to view this request.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build evidence URLs
|
||||||
const parties = Array.isArray(doc.parties) ? doc.parties : [];
|
const parties = Array.isArray(doc.parties) ? doc.parties : [];
|
||||||
|
for (const party of parties as Array<{
|
||||||
const typedParties = parties as Array<{
|
evidence?: Record<string, unknown>;
|
||||||
vehicle?: {
|
}>) {
|
||||||
inquiry?: {
|
|
||||||
mapped?: any;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
evidence?: {
|
|
||||||
videoId?: string | number;
|
|
||||||
voices?: (string | number)[];
|
|
||||||
videoUrl?: string;
|
|
||||||
voiceUrls?: string[];
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
|
|
||||||
// Evidence (videos + voices)
|
|
||||||
for (const party of typedParties) {
|
|
||||||
if (!party.evidence) continue;
|
if (!party.evidence) continue;
|
||||||
const evidence = party.evidence as Record<string, unknown>;
|
const evidence = party.evidence;
|
||||||
|
|
||||||
if (evidence.videoId) {
|
if (evidence.videoId) {
|
||||||
const videoDoc = await this.blameVideoDbService.findById(
|
const videoDoc = await this.blameVideoDbService.findById(
|
||||||
@@ -931,7 +927,7 @@ export class ExpertBlameService {
|
|||||||
if (videoDoc?.path) evidence.videoUrl = buildFileLink(videoDoc.path);
|
if (videoDoc?.path) evidence.videoUrl = buildFileLink(videoDoc.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evidence.voices && Array.isArray(evidence.voices)) {
|
if (Array.isArray(evidence.voices)) {
|
||||||
const voiceUrls: string[] = [];
|
const voiceUrls: string[] = [];
|
||||||
for (const voiceId of evidence.voices) {
|
for (const voiceId of evidence.voices) {
|
||||||
const voiceDoc = await this.blameVoiceDbService.findById(
|
const voiceDoc = await this.blameVoiceDbService.findById(
|
||||||
@@ -950,31 +946,31 @@ export class ExpertBlameService {
|
|||||||
const updatedAt = doc.updatedAt
|
const updatedAt = doc.updatedAt
|
||||||
? new Date(doc.updatedAt as string | number)
|
? new Date(doc.updatedAt as string | number)
|
||||||
: new Date();
|
: new Date();
|
||||||
|
|
||||||
const [createdDate, createdTime] = toJalaliDateAndTime(createdAt);
|
const [createdDate, createdTime] = toJalaliDateAndTime(createdAt);
|
||||||
const [updatedDate, updatedTime] = toJalaliDateAndTime(updatedAt);
|
const [updatedDate, updatedTime] = toJalaliDateAndTime(updatedAt);
|
||||||
|
|
||||||
doc.createdAtFormatted = `${createdDate} ${createdTime}`;
|
doc.createdAtFormatted = `${createdDate} ${createdTime}`;
|
||||||
doc.updatedAtFormatted = `${updatedDate} ${updatedTime}`;
|
doc.updatedAtFormatted = `${updatedDate} ${updatedTime}`;
|
||||||
|
|
||||||
// Omit heavy SandHub inquiry blob from expert response (keep other vehicle fields)
|
// Strip heavy SandHub inquiry blob
|
||||||
for (const party of typedParties) {
|
for (const party of parties as Array<{
|
||||||
const veh = party?.vehicle as Record<string, unknown> | undefined;
|
vehicle?: Record<string, unknown>;
|
||||||
if (veh && Object.prototype.hasOwnProperty.call(veh, "inquiry")) {
|
}>) {
|
||||||
delete veh.inquiry;
|
if (
|
||||||
|
party.vehicle &&
|
||||||
|
Object.prototype.hasOwnProperty.call(party.vehicle, "inquiry")
|
||||||
|
) {
|
||||||
|
delete party.vehicle.inquiry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
|
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
"findOneV2 failed",
|
"findOneV2 failed",
|
||||||
requestId,
|
requestId,
|
||||||
error instanceof Error ? error.stack : String(error),
|
error instanceof Error ? error.stack : String(error),
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
|
|||||||
Reference in New Issue
Block a user