diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 73d841f..79e3f49 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -2595,8 +2595,9 @@ export class ExpertClaimService { requireActorClientKey(actor); const actorId = actor.sub; const clientKey = actor.clientKey as string; - const lockTtlMs = this.claimV2WorkflowLockTtlMs; - const now = new Date(); + + await this.reconcileStaleExpertReviewingClaimLocksForTenant(actor); + const claims = await this.claimCaseDbService.find({ $or: [ // Available claims: waiting for expert, not locked @@ -2609,27 +2610,6 @@ 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, @@ -2702,10 +2682,14 @@ export class ExpertClaimService { const fileCtx = blame ? this.blameFileContextForExpert(blame) : {}; const lockActive = !!(c.workflow?.locked && this.isClaimV2WorkflowLockCurrentlyEnforced(c)); + const statusForList = + c.status === ClaimCaseStatus.EXPERT_REVIEWING + ? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT + : c.status; return { claimRequestId: c._id.toString(), publicId: c.publicId, - status: c.status, + status: statusForList, currentStep: c.workflow?.currentStep || "", locked: lockActive, lockedBy: @@ -2876,6 +2860,51 @@ export class ExpertClaimService { ); } + /** + * EXPERT_REVIEWING + expired lock is not listed until status is reverted. Run expire for this tenant + * so the next query returns those cases as WAITING_FOR_DAMAGE_EXPERT. + */ + private async reconcileStaleExpertReviewingClaimLocksForTenant(actor: { + sub: string; + clientKey?: string; + }): Promise { + const clientKey = requireActorClientKey(actor); + const lockTtlMs = this.claimV2WorkflowLockTtlMs; + const now = new Date(); + const candidates = (await this.claimCaseDbService.find( + { + status: ClaimCaseStatus.EXPERT_REVIEWING, + "workflow.locked": true, + $or: [ + { "workflow.expiredAt": { $lte: now } }, + { + $expr: { + $and: [ + { $eq: ["$workflow.locked", true] }, + { + $lte: [ + { $add: ["$workflow.lockedAt", lockTtlMs] }, + now, + ], + }, + ], + }, + }, + ], + }, + { lean: true, select: "_id owner" }, + )) as Array<{ _id: unknown; owner?: unknown }>; + + const relevant = candidates.filter((c) => + claimCaseTouchesClient(c, clientKey), + ); + await Promise.all( + relevant.map((c) => + this.expireClaimWorkflowLockV2IfStale(String(c._id)), + ), + ); + } + /** * Persist unlock when claim V2 workflow lock TTL has passed. * If the expert never submitted a damage reply, restores queue fields from @@ -3158,10 +3187,15 @@ export class ExpertClaimService { } : undefined; + const statusForExpertApi = + claim.status === ClaimCaseStatus.EXPERT_REVIEWING + ? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT + : claim.status; + return { claimRequestId: claim._id.toString(), publicId: claim.publicId, - status: claim.status, + status: statusForExpertApi, claimStatus: claim.claimStatus || 'PENDING', currentStep: claim.workflow?.currentStep || '', nextStep: claim.workflow?.nextStep,