1
0
forked from Yara724/api

Fixed lock

This commit is contained in:
SepehrYahyaee
2026-06-01 13:21:32 +03:30
parent fde6464739
commit 6261af8a29
3 changed files with 117 additions and 39 deletions

View File

@@ -556,6 +556,7 @@ export class ExpertBlameService {
const assigneeToPersist = blameWorkflowAssigneeToPersistOnLockExpiry( const assigneeToPersist = blameWorkflowAssigneeToPersistOnLockExpiry(
request.workflow, request.workflow,
request,
); );
const expireSet: Record<string, unknown> = { "workflow.locked": false }; const expireSet: Record<string, unknown> = { "workflow.locked": false };
if (assigneeToPersist) { if (assigneeToPersist) {

View File

@@ -3294,22 +3294,26 @@ export class ExpertClaimService {
const claims = await this.claimCaseDbService.find({ const claims = await this.claimCaseDbService.find({
$or: [ $or: [
// Available claims: waiting for expert, not locked // Available: waiting, not locked
{ {
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT, status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
"workflow.locked": { $ne: true }, "workflow.locked": { $ne: true },
// No persistent assignee (or expired without decision)
$or: [
{ "workflow.assignedForReviewBy": { $exists: false } },
{ "workflow.assignedForReviewBy": null },
],
}, },
// Expert reviewing but lock cleared (e.g. TTL expiry) — still list until status is reconciled // This expert's locked/in-progress claims
{
status: ClaimCaseStatus.EXPERT_REVIEWING,
"workflow.locked": { $ne: true },
},
// This expert's own locked/in-progress claims
{ {
"workflow.locked": true, "workflow.locked": true,
"workflow.lockedBy.actorId": new Types.ObjectId(actorId), "workflow.lockedBy.actorId": new Types.ObjectId(actorId),
}, },
// User uploaded all factors; expert must approve/reject (unlocked queue) // This expert's persistently assigned claims (decided or resend/objection follow-up)
{
"workflow.assignedForReviewBy.actorId": new Types.ObjectId(actorId),
},
// Factor validation queue (open to all tenant experts — no lock needed)
{ {
$or: [ $or: [
{ {
@@ -3324,6 +3328,16 @@ export class ExpertClaimService {
}, },
], ],
}, },
// WAITING_FOR_USER_RESEND assigned to this expert
{
status: ClaimCaseStatus.WAITING_FOR_USER_RESEND,
"workflow.assignedForReviewBy.actorId": new Types.ObjectId(actorId),
},
// EXPERT_REVIEWING with stale lock (reconciliation will fix status, show in meantime)
{
status: ClaimCaseStatus.EXPERT_REVIEWING,
"workflow.locked": { $ne: true },
},
], ],
}); });
@@ -3331,10 +3345,12 @@ export class ExpertClaimService {
claimCaseTouchesClient(c, clientKey), claimCaseTouchesClient(c, clientKey),
); );
// Reconcile stale locks — if no decision was made, also clear assignedForReviewBy
const staleLockToReconcile = filtered.filter( const staleLockToReconcile = filtered.filter(
(c) => (c) =>
c.workflow?.locked && !this.isClaimV2WorkflowLockCurrentlyEnforced(c), c.workflow?.locked && !this.isClaimV2WorkflowLockCurrentlyEnforced(c),
); );
const touchedIds = ( const touchedIds = (
await Promise.all( await Promise.all(
staleLockToReconcile.map(async (c) => { staleLockToReconcile.map(async (c) => {
@@ -3357,9 +3373,39 @@ export class ExpertClaimService {
} }
} }
// Post-filter: remove files that belong to another expert after reconciliation
const finalFiltered = filtered.filter((c) => {
const assignedId = String(c.workflow?.assignedForReviewBy?.actorId ?? "");
const lockEnforced =
c.workflow?.locked && this.isClaimV2WorkflowLockCurrentlyEnforced(c);
const lockedById = String(c.workflow?.lockedBy?.actorId ?? "");
const isFactorValidation = claimIsAwaitingExpertFactorValidationV2(c);
// Factor validation is open to all tenant experts
if (isFactorValidation) return true;
// Available: no assignee, not locked by someone else
const isAvailable =
c.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT &&
!assignedId &&
(!lockEnforced || lockedById === actorId);
// Mine: assigned to me (covers decided, resend, objection follow-up)
const isAssignedToMe = !!assignedId && assignedId === actorId;
// Mine: currently locked by me
const isLockedByMe = lockEnforced && lockedById === actorId;
// Mine: resend pending and I'm the assigned expert
const isResendMine =
c.status === ClaimCaseStatus.WAITING_FOR_USER_RESEND && isAssignedToMe;
return isAvailable || isAssignedToMe || isLockedByMe || isResendMine;
});
const blameIds = [ const blameIds = [
...new Set( ...new Set(
filtered finalFiltered
.map((c) => c.blameRequestId?.toString()) .map((c) => c.blameRequestId?.toString())
.filter((id): id is string => !!id), .filter((id): id is string => !!id),
), ),
@@ -3375,7 +3421,7 @@ export class ExpertClaimService {
blames.map((b) => [String(b._id), b]), blames.map((b) => [String(b._id), b]),
); );
const list = filtered.map((c) => { const list = finalFiltered.map((c) => {
const awaitingFactorValidation = const awaitingFactorValidation =
claimIsAwaitingExpertFactorValidationV2(c); claimIsAwaitingExpertFactorValidationV2(c);
const v = this.vehicleForExpertFromClaimAndBlameMap(c, blameById); const v = this.vehicleForExpertFromClaimAndBlameMap(c, blameById);
@@ -3390,6 +3436,7 @@ export class ExpertClaimService {
c.status === ClaimCaseStatus.EXPERT_REVIEWING c.status === ClaimCaseStatus.EXPERT_REVIEWING
? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT ? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT
: c.status; : c.status;
return { return {
claimRequestId: c._id.toString(), claimRequestId: c._id.toString(),
publicId: c.publicId, publicId: c.publicId,
@@ -3406,11 +3453,7 @@ export class ExpertClaimService {
} }
: undefined, : undefined,
vehicle: v vehicle: v
? { ? { carName: v.carName, carModel: v.carModel, carType: v.carType }
carName: v.carName,
carModel: v.carModel,
carType: v.carType,
}
: undefined, : undefined,
...fileCtx, ...fileCtx,
createdAt: c.createdAt, createdAt: c.createdAt,
@@ -3727,6 +3770,7 @@ export class ExpertClaimService {
const assigneeToPersist = claimWorkflowAssigneeToPersistOnLockExpiry( const assigneeToPersist = claimWorkflowAssigneeToPersistOnLockExpiry(
claim.workflow, claim.workflow,
claim,
); );
const expireLockSet: Record<string, unknown> = { "workflow.locked": false }; const expireLockSet: Record<string, unknown> = { "workflow.locked": false };
if (assigneeToPersist) { if (assigneeToPersist) {
@@ -3795,21 +3839,53 @@ export class ExpertClaimService {
claim.status === ClaimCaseStatus.EXPERT_REVIEWING; claim.status === ClaimCaseStatus.EXPERT_REVIEWING;
const isFactorValidationPending = const isFactorValidationPending =
claimIsAwaitingExpertFactorValidationV2(claim); claimIsAwaitingExpertFactorValidationV2(claim);
const isResendPending =
claim.status === ClaimCaseStatus.WAITING_FOR_USER_RESEND;
if (!isDamageExpertPhase && !isFactorValidationPending) { if (
!isDamageExpertPhase &&
!isFactorValidationPending &&
!isResendPending
) {
throw new ForbiddenException( throw new ForbiddenException(
`This claim is not available for expert review. Current status: ${claim.status}`, `This claim is not available for expert review. Current status: ${claim.status}`,
); );
} }
const lockBlocksOthers = this.isClaimV2WorkflowLockCurrentlyEnforced(claim); // Ownership access control — same 4-bucket logic as list
if (lockBlocksOthers) { const assignedForReviewById = String(
const lockerId = claim.workflow?.lockedBy?.actorId?.toString(); (claim.workflow as any)?.assignedForReviewBy?.actorId ?? "",
if (lockerId && lockerId !== actorId) { );
const lockEnforced = this.isClaimV2WorkflowLockCurrentlyEnforced(claim);
const lockedById = String(claim.workflow?.lockedBy?.actorId ?? "");
const isAvailable =
isDamageExpertPhase && !assignedForReviewById && !lockEnforced;
const isAssignedToMe =
!!assignedForReviewById && assignedForReviewById === actorId;
const isLockedByMe = lockEnforced && lockedById === actorId;
// Factor validation is open to all tenant experts (no individual ownership)
const isFactorValidationOpen = isFactorValidationPending;
if (
!isAvailable &&
!isAssignedToMe &&
!isLockedByMe &&
!isFactorValidationOpen
) {
if (lockEnforced && lockedById && lockedById !== actorId) {
throw new ForbiddenException( throw new ForbiddenException(
`This claim is locked by another expert: ${claim.workflow.lockedBy?.actorName}`, `This claim is locked by another expert: ${claim.workflow?.lockedBy?.actorName}`,
); );
} }
if (assignedForReviewById && assignedForReviewById !== actorId) {
throw new ForbiddenException(
"This claim has been assigned to another expert.",
);
}
throw new ForbiddenException(
"You do not have permission to view this claim.",
);
} }
const hasCapture = (data: any, key: string) => const hasCapture = (data: any, key: string) =>

View File

@@ -25,9 +25,8 @@ export function resolvePersistentReviewAssigneeId(
if (fromField != null && String(fromField).length > 0) { if (fromField != null && String(fromField).length > 0) {
return String(fromField); return String(fromField);
} }
const fromHistory = history?.find( const fromHistory = history?.find((h) => h.type === assignedHistoryType)
(h) => h.type === assignedHistoryType, ?.actor?.actorId;
)?.actor?.actorId;
if (fromHistory != null && String(fromHistory).length > 0) { if (fromHistory != null && String(fromHistory).length > 0) {
return String(fromHistory); return String(fromHistory);
} }
@@ -43,8 +42,7 @@ export function buildExpertAssignConflictForOtherReviewer(
message: string; message: string;
lockedBy: { actorId: string; actorName?: string; lockedAt?: string }; lockedBy: { actorId: string; actorName?: string; lockedAt?: string };
} { } {
const assignee = const assignee = workflow?.assignedForReviewBy ?? workflow?.lockedBy;
workflow?.assignedForReviewBy ?? workflow?.lockedBy;
const lockedAt = workflow?.lockedAt; const lockedAt = workflow?.lockedAt;
return { return {
success: false, success: false,
@@ -53,25 +51,28 @@ export function buildExpertAssignConflictForOtherReviewer(
lockedBy: { lockedBy: {
actorId: assigneeId, actorId: assigneeId,
actorName: assignee?.actorName, actorName: assignee?.actorName,
lockedAt: lockedAt lockedAt: lockedAt ? new Date(lockedAt as Date).toISOString() : undefined,
? new Date(lockedAt as Date).toISOString()
: undefined,
}, },
}; };
} }
/** Copy lock holder into `assignedForReviewBy` when persisting a TTL unlock. */
export function blameWorkflowAssigneeToPersistOnLockExpiry( export function blameWorkflowAssigneeToPersistOnLockExpiry(
workflow: WorkflowAssigneeRef | undefined, workflow: any,
): WorkflowAssigneeRef["assignedForReviewBy"] | undefined { request: any,
if (workflow?.assignedForReviewBy) { ) {
return undefined; const hasDecision = !!request?.expert?.decision;
} return hasDecision ? (workflow?.assignedForReviewBy ?? null) : null;
return workflow?.lockedBy;
} }
export function claimWorkflowAssigneeToPersistOnLockExpiry( export function claimWorkflowAssigneeToPersistOnLockExpiry(
workflow: WorkflowAssigneeRef | undefined, workflow: any,
): WorkflowAssigneeRef["assignedForReviewBy"] | undefined { claim: any,
return blameWorkflowAssigneeToPersistOnLockExpiry(workflow); ): typeof workflow.assignedForReviewBy | null {
// If a decision or resend was already submitted, preserve ownership
const hasDecision =
!!claim?.evaluation?.damageExpertReply ||
!!claim?.evaluation?.damageExpertReplyFinal ||
!!claim?.evaluation?.damageExpertResend;
return hasDecision ? (workflow?.assignedForReviewBy ?? null) : null;
} }