From cca3ed01a49101846cbe642a107c3b1cef513715 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Tue, 23 Jun 2026 13:31:04 +0330 Subject: [PATCH] YARA-1045 --- .../schema/claim-case.workflow.schema.ts | 4 +-- src/expert-blame/expert-blame.service.ts | 31 ++++++++++--------- src/expert-claim/expert-claim.service.ts | 23 +++++++------- src/helpers/expert-workflow-lock.ts | 8 +++++ .../entities/schema/workflow.type.ts | 4 +-- 5 files changed, 40 insertions(+), 30 deletions(-) create mode 100644 src/helpers/expert-workflow-lock.ts diff --git a/src/claim-request-management/entites/schema/claim-case.workflow.schema.ts b/src/claim-request-management/entites/schema/claim-case.workflow.schema.ts index f3169d6..bceb6b9 100644 --- a/src/claim-request-management/entites/schema/claim-case.workflow.schema.ts +++ b/src/claim-request-management/entites/schema/claim-case.workflow.schema.ts @@ -57,7 +57,7 @@ export class ClaimWorkflow { @Prop({ type: Date }) lockedAt?: Date; - /** Lock expiry used by UI countdown (typically lockedAt + 15m). */ + /** Lock expiry used by UI countdown (typically lockedAt + 30m). */ @Prop({ type: Date }) expiredAt?: Date; @@ -68,7 +68,7 @@ export class ClaimWorkflow { preLockQueueSnapshot?: ClaimPreLockQueueSnapshot; /** - * First damage expert who called review assign; kept after the 15m lock expires + * First damage expert who called review assign; kept after the 30m lock expires * so no other expert can take the case while it remains in the expert queue. */ @Prop({ type: ClaimActorLockSchema }) diff --git a/src/expert-blame/expert-blame.service.ts b/src/expert-blame/expert-blame.service.ts index f7de7e0..cc6f987 100644 --- a/src/expert-blame/expert-blame.service.ts +++ b/src/expert-blame/expert-blame.service.ts @@ -45,6 +45,10 @@ import { buildExpertAssignConflictForOtherReviewer, resolvePersistentReviewAssigneeId, } from "src/helpers/expert-workflow-review-assignee"; +import { + EXPERT_WORKFLOW_LOCK_TTL_MS, + expertWorkflowLockExpiredAt, +} from "src/helpers/expert-workflow-lock"; import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service"; import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service"; import { @@ -110,9 +114,6 @@ function statementToFormKey(statement?: { export class ExpertBlameService { private readonly logger = new Logger(ExpertBlameService.name); - /** Blame V2 `workflow.locked` TTL (must match checks in lock/reply/resend). */ - private readonly blameV2LockTtlMs = 15 * 60 * 1000; - constructor( private readonly requestManagementDbService: RequestManagementDbService, private readonly blameRequestDbService: BlameRequestDbService, @@ -743,11 +744,11 @@ export class ExpertBlameService { } const la = doc.workflow.lockedAt; if (!la) return true; - return Date.now() < new Date(la as Date).getTime() + this.blameV2LockTtlMs; + return Date.now() < new Date(la as Date).getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS; } /** - * Persist unlock when blame V2 workflow lock is older than {@link blameV2LockTtlMs}. + * Persist unlock when blame V2 workflow lock is older than {@link EXPERT_WORKFLOW_LOCK_TTL_MS}. * V1 uses in-memory `scheduleUnlock`; V2 only stored `workflow.lockedAt`, so locks never cleared until now. */ private async expireBlameCaseWorkflowLockV2IfStale( @@ -765,7 +766,7 @@ export class ExpertBlameService { } if ( lockedAt && - Date.now() < new Date(lockedAt as Date).getTime() + this.blameV2LockTtlMs + Date.now() < new Date(lockedAt as Date).getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS ) { return; } @@ -1244,7 +1245,7 @@ export class ExpertBlameService { throw new BadRequestException("Request is locked by another expert"); } - const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000); + const lockExpiresAt = expertWorkflowLockExpiredAt(); const lockSnapshot = await this.snapshotFieldExpert(actorDetail.sub); const updateResult = await this.requestManagementDbService.findOneAndUpdate( @@ -1256,7 +1257,7 @@ export class ExpertBlameService { $set: { lockFile: true, blameStatus: ReqBlameStatus.ReviewRequest, - unlockTime: fifteenMinutes, + unlockTime: lockExpiresAt, lockTime: new Date(), actorLocked: { fullName: actorDetail.fullName, @@ -1433,7 +1434,7 @@ export class ExpertBlameService { $set: { "workflow.locked": true, "workflow.lockedAt": now, - "workflow.expiredAt": new Date(now.getTime() + this.blameV2LockTtlMs), + "workflow.expiredAt": new Date(now.getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS), "workflow.lockedBy": lockedByPayload, }, $push: { @@ -1564,7 +1565,7 @@ export class ExpertBlameService { } /** - * V2: Lock blame case for 15 minutes (blameCases collection). + * V2: Lock blame case for 30 minutes (blameCases collection). * Delegates to {@link assignBlameCaseForReviewV2}; maps conflict to BadRequest for legacy clients. */ async lockRequestV2( @@ -1649,7 +1650,7 @@ export class ExpertBlameService { const lockedAt = request.workflow?.lockedAt; if (lockedAt) { const lockExpiryTime = - new Date(lockedAt).getTime() + this.blameV2LockTtlMs; + new Date(lockedAt).getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS; if (Date.now() > lockExpiryTime) { throw new ForbiddenException( "Your lock time has expired. Please lock the request again.", @@ -1891,11 +1892,11 @@ export class ExpertBlameService { const lockedAt = request.workflow?.lockedAt; const isLockedByCurrentActor = lockedByActorId === actorId; - // Check if lock has expired (15 minutes) + // Check if lock has expired (30 minutes) let isLockExpired = false; if (lockedAt) { const lockExpiryTime = - new Date(lockedAt).getTime() + this.blameV2LockTtlMs; + new Date(lockedAt).getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS; isLockExpired = Date.now() > lockExpiryTime; } @@ -2057,11 +2058,11 @@ export class ExpertBlameService { const lockedAt = request.workflow?.lockedAt; const isLockedByCurrentActor = lockedByActorId === actorId; - // Check if lock has expired (15 minutes) + // Check if lock has expired (30 minutes) let isLockExpired = false; if (lockedAt) { const lockExpiryTime = - new Date(lockedAt).getTime() + this.blameV2LockTtlMs; + new Date(lockedAt).getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS; isLockExpired = Date.now() > lockExpiryTime; } diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 85825d1..7134a87 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -45,6 +45,10 @@ import { buildExpertAssignConflictForOtherReviewer, resolvePersistentReviewAssigneeId, } from "src/helpers/expert-workflow-review-assignee"; +import { + EXPERT_WORKFLOW_LOCK_TTL_MS, + expertWorkflowLockExpiredAt, +} from "src/helpers/expert-workflow-lock"; import { ClaimPerIdRs } from "./dto/claim-list-perId-rs.dto"; import { ClaimListDtoRs } from "./dto/claim-list-rs.dto"; import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service"; @@ -162,9 +166,6 @@ import { canonicalizeResendDocumentKey } from "src/helpers/claim-resend-document 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, @@ -777,7 +778,7 @@ export class ExpertClaimService { throw new BadRequestException("Request has damage expert reply"); } - const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000); + const lockExpiresAt = expertWorkflowLockExpiredAt(); const lockSnapshot = await this.snapshotDamageExpert(actorDetail.sub); await this.claimRequestManagementDbService.findOneAndUpdate( @@ -785,7 +786,7 @@ export class ExpertClaimService { { lockFile: true, claimStatus: ReqClaimStatus.ReviewRequest, - unlockTime: fifteenMinutes, + unlockTime: lockExpiresAt, lockTime: Date.now(), $set: { actorLocked: { @@ -814,7 +815,7 @@ export class ExpertClaimService { idempotencyKey: `claim:${requestId}:checked:${actorDetail.sub}`, }); - this.unlockApi(request, fifteenMinutes.getTime() - Date.now()); + this.unlockApi(request, lockExpiresAt.getTime() - Date.now()); return { _id: requestId, lock: true }; } @@ -2548,7 +2549,7 @@ export class ExpertClaimService { const now = new Date(); const lockSnapshot = await this.snapshotDamageExpert(actor.sub); - const expiredAt = new Date(now.getTime() + this.claimV2WorkflowLockTtlMs); + const expiredAt = new Date(now.getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS); const actorType = isFieldExpertOwner ? "field_expert" : "damage_expert"; const actorRoleLabel = isFieldExpertOwner ? ("field_expert" as const) @@ -4034,7 +4035,7 @@ export class ExpertClaimService { } const la = claim.workflow?.lockedAt as Date | string | undefined; if (!la) return true; - return Date.now() < new Date(la).getTime() + this.claimV2WorkflowLockTtlMs; + return Date.now() < new Date(la).getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS; } /** @@ -4048,7 +4049,7 @@ export class ExpertClaimService { }): Promise { if ((actor as any).role === RoleEnum.FIELD_EXPERT) return; const clientKey = requireActorClientKey(actor); - const lockTtlMs = this.claimV2WorkflowLockTtlMs; + const lockTtlMs = EXPERT_WORKFLOW_LOCK_TTL_MS; const now = new Date(); const candidates = (await this.claimCaseDbService.find( { @@ -4094,7 +4095,7 @@ export class ExpertClaimService { const expiredAt = claim.workflow?.expiredAt; const lockedAt = claim.workflow?.lockedAt; const now = Date.now(); - const ttl = this.claimV2WorkflowLockTtlMs; + const ttl = EXPERT_WORKFLOW_LOCK_TTL_MS; if (expiredAt && now < new Date(expiredAt as Date).getTime()) return false; if ( @@ -4199,7 +4200,7 @@ export class ExpertClaimService { * - Claim must exist and belong to the actor's insurer (`assertClaimCaseForTenant`) * - Allowed when: WAITING_FOR_DAMAGE_EXPERT (queue), or EXPERT_REVIEWING (after lock — same expert must be able to reopen detail), * or factor-validation queue (EXPERT_VALIDATING_REPAIR_FACTORS / legacy 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 + * - If an active workflow lock exists (30 min from lockedAt): only the locking expert; after expiry, any expert (tenant) may view */ async getClaimDetailV2( claimRequestId: string, diff --git a/src/helpers/expert-workflow-lock.ts b/src/helpers/expert-workflow-lock.ts new file mode 100644 index 0000000..b221931 --- /dev/null +++ b/src/helpers/expert-workflow-lock.ts @@ -0,0 +1,8 @@ +/** Expert blame/claim review assign + workflow lock TTL (POST assign, PUT lock). */ +export const EXPERT_WORKFLOW_LOCK_TTL_MS = 30 * 60 * 1000; + +export function expertWorkflowLockExpiredAt( + from: Date = new Date(), +): Date { + return new Date(from.getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS); +} diff --git a/src/request-management/entities/schema/workflow.type.ts b/src/request-management/entities/schema/workflow.type.ts index e1cd915..1264fae 100644 --- a/src/request-management/entities/schema/workflow.type.ts +++ b/src/request-management/entities/schema/workflow.type.ts @@ -42,7 +42,7 @@ export class Workflow { @Prop({ type: Date }) lockedAt?: Date; - /** Lock expiry used by UI countdown (typically lockedAt + 15m). */ + /** Lock expiry used by UI countdown (typically lockedAt + 30m). */ @Prop({ type: Date }) expiredAt?: Date; @@ -50,7 +50,7 @@ export class Workflow { lockedBy?: ActorLock; /** - * First expert who called review assign; kept after the 15m workflow lock expires + * First expert who called review assign; kept after the 30m workflow lock expires * so no other expert can take the case while it remains in the expert queue. */ @Prop({ type: ActorLockSchema })