forked from Yara724/api
Fixed lock
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user