diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 47d3ad9..996c956 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -63,6 +63,9 @@ import { export class ExpertClaimService { private readonly logger = new Logger(ExpertClaimService.name); + /** Matches blame v2 `workflow` lock TTL: locker may re-enter until expired, then others may open. */ + private readonly claimV2WorkflowLockTtlMs = 15 * 60 * 1000; + private readonly priceDropPart = { backFender: { Minor: 2, @@ -2528,13 +2531,28 @@ export class ExpertClaimService { }; } + /** + * True when another expert still holds an active (non-expired) workflow lock. + * Missing `lockedAt` on old documents: treat lock as still enforced until cleared. + */ + private isClaimV2WorkflowLockCurrentlyEnforced(claim: any): boolean { + if (!claim?.workflow?.locked) return false; + const la = claim.workflow?.lockedAt as Date | string | undefined; + if (!la) return true; + return ( + Date.now() < + new Date(la).getTime() + this.claimV2WorkflowLockTtlMs + ); + } + /** * V2: Get claim detail for damage expert * * Validations: * - Claim must exist and belong to the actor's insurer (`assertClaimCaseForTenant`) - * - Allowed when picking up/reviewing: WAITING_FOR_DAMAGE_EXPERT (if locked, only the locking expert) - * - Or when validating factors: WAITING_FOR_INSURER_APPROVAL + UNDER_REVIEW at EXPERT_COST_EVALUATION + * - Allowed when: WAITING_FOR_DAMAGE_EXPERT (queue), or EXPERT_REVIEWING (after lock — same expert must be able to reopen detail), + * or factor-validation queue (WAITING_FOR_INSURER_APPROVAL + UNDER_REVIEW + EXPERT_COST_EVALUATION) + * - If an active workflow lock exists (15 min from lockedAt): only the locking expert; after expiry, any expert (tenant) may view */ async getClaimDetailV2( claimRequestId: string, @@ -2549,28 +2567,28 @@ export class ExpertClaimService { assertClaimCaseForTenant(claim, actor); - const isPickupReview = - claim.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT; + const isDamageExpertPhase = + claim.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT || + claim.status === ClaimCaseStatus.EXPERT_REVIEWING; const isFactorValidationPending = claim.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL && claim.claimStatus === ClaimStatus.UNDER_REVIEW && claim.workflow?.currentStep === ClaimWorkflowStep.EXPERT_COST_EVALUATION; - if (!isPickupReview && !isFactorValidationPending) { + if (!isDamageExpertPhase && !isFactorValidationPending) { throw new ForbiddenException( `This claim is not available for expert review. Current status: ${claim.status}`, ); } - // If locked by someone else, deny access (initial pickup / assessment only) - if ( - isPickupReview && - claim.workflow?.locked && - claim.workflow.lockedBy?.actorId?.toString() !== actorId - ) { - throw new ForbiddenException( - `This claim is locked by another expert: ${claim.workflow.lockedBy?.actorName}`, - ); + const lockBlocksOthers = this.isClaimV2WorkflowLockCurrentlyEnforced(claim); + if (lockBlocksOthers) { + const lockerId = claim.workflow?.lockedBy?.actorId?.toString(); + if (lockerId && lockerId !== actorId) { + throw new ForbiddenException( + `This claim is locked by another expert: ${claim.workflow.lockedBy?.actorName}`, + ); + } } const hasCapture = (data: any, key: string) =>