import { Types } from "mongoose"; import { ExpertFileActivityType } from "src/users/entities/schema/expert-file-activity.schema"; import { blameCaseAccessibleToExpert, claimCaseTouchesClient, } from "src/helpers/tenant-scope"; export interface ExpertFileActivityEventRow { fileId: Types.ObjectId | string; eventType: ExpertFileActivityType; occurredAt: Date; } /** * Distinct file ids where the expert is actively checking (CHECKED, not UNCHECKED) * or has handled the file (HANDLED), replayed in chronological order. */ export function expertPortfolioFileIdsFromActivityEvents( events: ExpertFileActivityEventRow[], ): Set { const sorted = [...events].sort( (a, b) => a.occurredAt.getTime() - b.occurredAt.getTime() || String(a.fileId).localeCompare(String(b.fileId)), ); const stateByFile = new Map(); for (const e of sorted) { const fid = String(e.fileId); const prev = stateByFile.get(fid) ?? { checked: false, handled: false }; if (e.eventType === ExpertFileActivityType.CHECKED) { if (!prev.handled) prev.checked = true; } else if (e.eventType === ExpertFileActivityType.UNCHECKED) { prev.checked = false; } else if (e.eventType === ExpertFileActivityType.HANDLED) { prev.handled = true; prev.checked = false; } stateByFile.set(fid, prev); } const ids = new Set(); for (const [fid, st] of stateByFile) { if (st.handled || st.checked) ids.add(fid); } return ids; } export function objectIdsFromStringSet(ids: Set): Types.ObjectId[] { return [...ids] .filter((id) => Types.ObjectId.isValid(id)) .map((id) => new Types.ObjectId(id)); } /** Damage-expert claim portfolio: assigned (lock/check) or substantive work on the file. */ export function claimDocInExpertPortfolio( doc: Record, expertId: string, activityFileIds: Set, clientKey: string, ): boolean { if (!claimCaseTouchesClient(doc, clientKey)) return false; const id = String(doc._id ?? ""); if (activityFileIds.has(id)) return true; const lockedBy = (doc.workflow as { lockedBy?: { actorId?: unknown } } | undefined) ?.lockedBy?.actorId; if (lockedBy != null && String(lockedBy) === String(expertId)) return true; const evaluation = doc.evaluation as Record | undefined; for (const key of ["damageExpertReply", "damageExpertReplyFinal"] as const) { const reply = evaluation?.[key] as | { actorDetail?: { actorId?: unknown } } | undefined; const actorId = reply?.actorDetail?.actorId; if (actorId != null && String(actorId) === String(expertId)) return true; } const resendBy = ( evaluation?.damageExpertResend as { requestedByExpertId?: unknown } | undefined )?.requestedByExpertId; if (resendBy != null && String(resendBy) === String(expertId)) return true; return false; } /** Field-expert blame portfolio: initiated, decided, locked, or file-activity touch. */ export function blameDocInExpertPortfolio( doc: Record, actor: { sub: string; clientKey?: string }, activityFileIds: Set, ): boolean { if (!blameCaseAccessibleToExpert(doc, actor)) return false; const expertId = String(actor.sub); const id = String(doc._id ?? ""); if (activityFileIds.has(id)) return true; if (doc.expertInitiated && doc.initiatedByFieldExpertId) { return String(doc.initiatedByFieldExpertId) === expertId; } const expert = doc.expert as Record | undefined; const decidedBy = (expert?.decision as { decidedByExpertId?: unknown } | undefined) ?.decidedByExpertId; if (decidedBy != null && String(decidedBy) === expertId) return true; const lockedBy = (expert?.resend as { requestedByExpertId?: unknown } | undefined) ?.requestedByExpertId ?? (doc.workflow as { lockedBy?: { actorId?: unknown } } | undefined)?.lockedBy ?.actorId; if (lockedBy != null && String(lockedBy) === expertId) return true; return false; }