Fixed lock mechanism, and some resendItem types

This commit is contained in:
SepehrYahyaee
2026-04-26 15:58:21 +03:30
parent b5b3b722c6
commit 885678df7d
7 changed files with 188 additions and 193 deletions

View File

@@ -2158,6 +2158,14 @@ export class ExpertClaimService {
actorRole: 'damage_expert',
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
},
'workflow.preLockQueueSnapshot': {
claimStatus: claim.claimStatus,
currentStep:
claim.workflow?.currentStep ?? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
...(claim.workflow?.nextStep != null
? { nextStep: claim.workflow.nextStep }
: {}),
},
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
$push: {
history: {
@@ -2264,6 +2272,7 @@ export class ExpertClaimService {
"workflow.lockedAt": "",
"workflow.expiredAt": "",
"workflow.lockedBy": "",
"workflow.preLockQueueSnapshot": "",
},
$push: {
history: {
@@ -2419,6 +2428,7 @@ export class ExpertClaimService {
'workflow.lockedAt': '',
'workflow.expiredAt': '',
'workflow.lockedBy': '',
'workflow.preLockQueueSnapshot': '',
},
'workflow.currentStep': nextStep,
'workflow.nextStep': needsFactorUpload
@@ -2529,6 +2539,7 @@ export class ExpertClaimService {
'workflow.lockedAt': '',
'workflow.expiredAt': '',
'workflow.lockedBy': '',
'workflow.preLockQueueSnapshot': '',
},
...(note ? { 'evaluation.visitLocation': note } : {}),
...(visitSnapshot && {
@@ -2584,6 +2595,8 @@ export class ExpertClaimService {
requireActorClientKey(actor);
const actorId = actor.sub;
const clientKey = actor.clientKey as string;
const lockTtlMs = this.claimV2WorkflowLockTtlMs;
const now = new Date();
const claims = await this.claimCaseDbService.find({
$or: [
// Available claims: waiting for expert, not locked
@@ -2596,6 +2609,27 @@ export class ExpertClaimService {
'workflow.locked': true,
'workflow.lockedBy.actorId': new Types.ObjectId(actorId),
},
// EXPERT_REVIEWING but lock cleared or TTL elapsed (reconcile on read; fixes invisible rows)
{
status: ClaimCaseStatus.EXPERT_REVIEWING,
$or: [
{ 'workflow.locked': { $ne: true } },
{ 'workflow.expiredAt': { $lte: now } },
{
$expr: {
$and: [
{ $eq: ['$workflow.locked', true] },
{
$lte: [
{ $add: ['$workflow.lockedAt', lockTtlMs] },
now,
],
},
],
},
},
],
},
// User uploaded all factors; expert must approve/reject (unlocked queue)
{
status: ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
@@ -2609,12 +2643,32 @@ export class ExpertClaimService {
claimCaseTouchesClient(c, clientKey),
);
for (const c of filtered) {
if (
const staleLockToReconcile = filtered.filter(
(c) =>
c.workflow?.locked &&
!this.isClaimV2WorkflowLockCurrentlyEnforced(c)
) {
await this.expireClaimWorkflowLockV2IfStale(String(c._id));
!this.isClaimV2WorkflowLockCurrentlyEnforced(c),
);
const touchedIds = (
await Promise.all(
staleLockToReconcile.map(async (c) => {
const updated = await this.expireClaimWorkflowLockV2IfStale(
String(c._id),
);
return updated ? String(c._id) : null;
}),
)
).filter((id): id is string => id != null);
if (touchedIds.length > 0) {
const refreshed = await this.claimCaseDbService.find({
_id: { $in: touchedIds.map((id) => new Types.ObjectId(id)) },
});
const byId = new Map(
refreshed.map((r) => [String(r._id), r] as const),
);
for (const c of filtered) {
const r = byId.get(String(c._id));
if (r) Object.assign(c, r);
}
}
@@ -2792,6 +2846,18 @@ export class ExpertClaimService {
};
}
private claimHasDamageExpertReply(claim: {
evaluation?: {
damageExpertReply?: unknown;
damageExpertReplyFinal?: unknown;
};
}): boolean {
return !!(
claim.evaluation?.damageExpertReply ||
claim.evaluation?.damageExpertReplyFinal
);
}
/**
* True while a workflow lock is still within its window (`expiredAt` when set, else lockedAt + TTL).
* Missing both timestamps on old documents: treat lock as enforced until cleared.
@@ -2810,27 +2876,31 @@ export class ExpertClaimService {
);
}
/** Persist unlock when claim V2 workflow lock TTL has passed (matches blame V2 behaviour). */
/**
* Persist unlock when claim V2 workflow lock TTL has passed.
* If the expert never submitted a damage reply, restores queue fields from
* `workflow.preLockQueueSnapshot` (or defaults) so the case shows as WAITING_FOR_DAMAGE_EXPERT again.
*/
private async expireClaimWorkflowLockV2IfStale(
claimRequestId: string,
): Promise<void> {
): Promise<boolean> {
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim?.workflow?.locked) return;
if (!claim?.workflow?.locked) return false;
const expiredAt = claim.workflow?.expiredAt;
const lockedAt = claim.workflow?.lockedAt;
const now = Date.now();
const ttl = this.claimV2WorkflowLockTtlMs;
if (expiredAt && now < new Date(expiredAt as Date).getTime()) return;
if (expiredAt && now < new Date(expiredAt as Date).getTime()) return false;
if (
!expiredAt &&
lockedAt &&
now < new Date(lockedAt as Date).getTime() + ttl
) {
return;
return false;
}
if (!expiredAt && !lockedAt) return;
if (!expiredAt && !lockedAt) return false;
const lockedById = claim.workflow.lockedBy?.actorId;
const filter: Record<string, unknown> = {
@@ -2845,18 +2915,55 @@ export class ExpertClaimService {
filter["workflow.lockedBy.actorId"] = lockedById;
}
await this.claimCaseDbService.findOneAndUpdate(
const needsQueueRestore =
claim.status === ClaimCaseStatus.EXPERT_REVIEWING &&
!this.claimHasDamageExpertReply(claim);
const snap = claim.workflow?.preLockQueueSnapshot as
| {
claimStatus?: ClaimStatus;
currentStep?: ClaimWorkflowStep;
nextStep?: ClaimWorkflowStep;
}
| undefined;
const restoreClaimStatus = snap?.claimStatus ?? ClaimStatus.PENDING;
const restoreCurrent =
snap?.currentStep ?? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
const restoreNext =
snap?.nextStep != null
? snap.nextStep
: ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
const lockFieldsUnset = {
"workflow.lockedAt": "",
"workflow.expiredAt": "",
"workflow.lockedBy": "",
"workflow.preLockQueueSnapshot": "",
};
const update = needsQueueRestore
? {
$set: {
"workflow.locked": false,
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
claimStatus: restoreClaimStatus,
"workflow.currentStep": restoreCurrent,
"workflow.nextStep": restoreNext,
},
$unset: lockFieldsUnset,
}
: {
$set: { "workflow.locked": false },
$unset: lockFieldsUnset,
};
const cleared = await this.claimCaseDbService.findOneAndUpdate(
filter as any,
{
$set: { "workflow.locked": false },
$unset: {
"workflow.lockedAt": "",
"workflow.expiredAt": "",
"workflow.lockedBy": "",
},
},
update as any,
{ new: false },
);
return !!cleared;
}
/**