YARA-1045

This commit is contained in:
SepehrYahyaee
2026-06-23 13:31:04 +03:30
parent e2b879d943
commit cca3ed01a4
5 changed files with 40 additions and 30 deletions

View File

@@ -57,7 +57,7 @@ export class ClaimWorkflow {
@Prop({ type: Date }) @Prop({ type: Date })
lockedAt?: Date; lockedAt?: Date;
/** Lock expiry used by UI countdown (typically lockedAt + 15m). */ /** Lock expiry used by UI countdown (typically lockedAt + 30m). */
@Prop({ type: Date }) @Prop({ type: Date })
expiredAt?: Date; expiredAt?: Date;
@@ -68,7 +68,7 @@ export class ClaimWorkflow {
preLockQueueSnapshot?: ClaimPreLockQueueSnapshot; 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. * so no other expert can take the case while it remains in the expert queue.
*/ */
@Prop({ type: ClaimActorLockSchema }) @Prop({ type: ClaimActorLockSchema })

View File

@@ -45,6 +45,10 @@ import {
buildExpertAssignConflictForOtherReviewer, buildExpertAssignConflictForOtherReviewer,
resolvePersistentReviewAssigneeId, resolvePersistentReviewAssigneeId,
} from "src/helpers/expert-workflow-review-assignee"; } 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 { 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 { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import { import {
@@ -110,9 +114,6 @@ function statementToFormKey(statement?: {
export class ExpertBlameService { export class ExpertBlameService {
private readonly logger = new Logger(ExpertBlameService.name); 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( constructor(
private readonly requestManagementDbService: RequestManagementDbService, private readonly requestManagementDbService: RequestManagementDbService,
private readonly blameRequestDbService: BlameRequestDbService, private readonly blameRequestDbService: BlameRequestDbService,
@@ -743,11 +744,11 @@ export class ExpertBlameService {
} }
const la = doc.workflow.lockedAt; const la = doc.workflow.lockedAt;
if (!la) return true; 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. * V1 uses in-memory `scheduleUnlock`; V2 only stored `workflow.lockedAt`, so locks never cleared until now.
*/ */
private async expireBlameCaseWorkflowLockV2IfStale( private async expireBlameCaseWorkflowLockV2IfStale(
@@ -765,7 +766,7 @@ export class ExpertBlameService {
} }
if ( if (
lockedAt && 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; return;
} }
@@ -1244,7 +1245,7 @@ export class ExpertBlameService {
throw new BadRequestException("Request is locked by another expert"); 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 lockSnapshot = await this.snapshotFieldExpert(actorDetail.sub);
const updateResult = await this.requestManagementDbService.findOneAndUpdate( const updateResult = await this.requestManagementDbService.findOneAndUpdate(
@@ -1256,7 +1257,7 @@ export class ExpertBlameService {
$set: { $set: {
lockFile: true, lockFile: true,
blameStatus: ReqBlameStatus.ReviewRequest, blameStatus: ReqBlameStatus.ReviewRequest,
unlockTime: fifteenMinutes, unlockTime: lockExpiresAt,
lockTime: new Date(), lockTime: new Date(),
actorLocked: { actorLocked: {
fullName: actorDetail.fullName, fullName: actorDetail.fullName,
@@ -1433,7 +1434,7 @@ export class ExpertBlameService {
$set: { $set: {
"workflow.locked": true, "workflow.locked": true,
"workflow.lockedAt": now, "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, "workflow.lockedBy": lockedByPayload,
}, },
$push: { $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. * Delegates to {@link assignBlameCaseForReviewV2}; maps conflict to BadRequest for legacy clients.
*/ */
async lockRequestV2( async lockRequestV2(
@@ -1649,7 +1650,7 @@ export class ExpertBlameService {
const lockedAt = request.workflow?.lockedAt; const lockedAt = request.workflow?.lockedAt;
if (lockedAt) { if (lockedAt) {
const lockExpiryTime = const lockExpiryTime =
new Date(lockedAt).getTime() + this.blameV2LockTtlMs; new Date(lockedAt).getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS;
if (Date.now() > lockExpiryTime) { if (Date.now() > lockExpiryTime) {
throw new ForbiddenException( throw new ForbiddenException(
"Your lock time has expired. Please lock the request again.", "Your lock time has expired. Please lock the request again.",
@@ -1891,11 +1892,11 @@ export class ExpertBlameService {
const lockedAt = request.workflow?.lockedAt; const lockedAt = request.workflow?.lockedAt;
const isLockedByCurrentActor = lockedByActorId === actorId; const isLockedByCurrentActor = lockedByActorId === actorId;
// Check if lock has expired (15 minutes) // Check if lock has expired (30 minutes)
let isLockExpired = false; let isLockExpired = false;
if (lockedAt) { if (lockedAt) {
const lockExpiryTime = const lockExpiryTime =
new Date(lockedAt).getTime() + this.blameV2LockTtlMs; new Date(lockedAt).getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS;
isLockExpired = Date.now() > lockExpiryTime; isLockExpired = Date.now() > lockExpiryTime;
} }
@@ -2057,11 +2058,11 @@ export class ExpertBlameService {
const lockedAt = request.workflow?.lockedAt; const lockedAt = request.workflow?.lockedAt;
const isLockedByCurrentActor = lockedByActorId === actorId; const isLockedByCurrentActor = lockedByActorId === actorId;
// Check if lock has expired (15 minutes) // Check if lock has expired (30 minutes)
let isLockExpired = false; let isLockExpired = false;
if (lockedAt) { if (lockedAt) {
const lockExpiryTime = const lockExpiryTime =
new Date(lockedAt).getTime() + this.blameV2LockTtlMs; new Date(lockedAt).getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS;
isLockExpired = Date.now() > lockExpiryTime; isLockExpired = Date.now() > lockExpiryTime;
} }

View File

@@ -45,6 +45,10 @@ import {
buildExpertAssignConflictForOtherReviewer, buildExpertAssignConflictForOtherReviewer,
resolvePersistentReviewAssigneeId, resolvePersistentReviewAssigneeId,
} from "src/helpers/expert-workflow-review-assignee"; } 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 { ClaimPerIdRs } from "./dto/claim-list-perId-rs.dto";
import { ClaimListDtoRs } from "./dto/claim-list-rs.dto"; import { ClaimListDtoRs } from "./dto/claim-list-rs.dto";
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service"; 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 { export class ExpertClaimService {
private readonly logger = new Logger(ExpertClaimService.name); 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 = { private readonly priceDropPart = {
backFender: { backFender: {
Minor: 2, Minor: 2,
@@ -777,7 +778,7 @@ export class ExpertClaimService {
throw new BadRequestException("Request has damage expert reply"); 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); const lockSnapshot = await this.snapshotDamageExpert(actorDetail.sub);
await this.claimRequestManagementDbService.findOneAndUpdate( await this.claimRequestManagementDbService.findOneAndUpdate(
@@ -785,7 +786,7 @@ export class ExpertClaimService {
{ {
lockFile: true, lockFile: true,
claimStatus: ReqClaimStatus.ReviewRequest, claimStatus: ReqClaimStatus.ReviewRequest,
unlockTime: fifteenMinutes, unlockTime: lockExpiresAt,
lockTime: Date.now(), lockTime: Date.now(),
$set: { $set: {
actorLocked: { actorLocked: {
@@ -814,7 +815,7 @@ export class ExpertClaimService {
idempotencyKey: `claim:${requestId}:checked:${actorDetail.sub}`, 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 }; return { _id: requestId, lock: true };
} }
@@ -2548,7 +2549,7 @@ export class ExpertClaimService {
const now = new Date(); const now = new Date();
const lockSnapshot = await this.snapshotDamageExpert(actor.sub); 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 actorType = isFieldExpertOwner ? "field_expert" : "damage_expert";
const actorRoleLabel = isFieldExpertOwner const actorRoleLabel = isFieldExpertOwner
? ("field_expert" as const) ? ("field_expert" as const)
@@ -4034,7 +4035,7 @@ export class ExpertClaimService {
} }
const la = claim.workflow?.lockedAt as Date | string | undefined; const la = claim.workflow?.lockedAt as Date | string | undefined;
if (!la) return true; 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<void> { }): Promise<void> {
if ((actor as any).role === RoleEnum.FIELD_EXPERT) return; if ((actor as any).role === RoleEnum.FIELD_EXPERT) return;
const clientKey = requireActorClientKey(actor); const clientKey = requireActorClientKey(actor);
const lockTtlMs = this.claimV2WorkflowLockTtlMs; const lockTtlMs = EXPERT_WORKFLOW_LOCK_TTL_MS;
const now = new Date(); const now = new Date();
const candidates = (await this.claimCaseDbService.find( const candidates = (await this.claimCaseDbService.find(
{ {
@@ -4094,7 +4095,7 @@ export class ExpertClaimService {
const expiredAt = claim.workflow?.expiredAt; const expiredAt = claim.workflow?.expiredAt;
const lockedAt = claim.workflow?.lockedAt; const lockedAt = claim.workflow?.lockedAt;
const now = Date.now(); 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 (expiredAt && now < new Date(expiredAt as Date).getTime()) return false;
if ( if (
@@ -4199,7 +4200,7 @@ export class ExpertClaimService {
* - Claim must exist and belong to the actor's insurer (`assertClaimCaseForTenant`) * - 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), * - 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) * 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( async getClaimDetailV2(
claimRequestId: string, claimRequestId: string,

View File

@@ -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);
}

View File

@@ -42,7 +42,7 @@ export class Workflow {
@Prop({ type: Date }) @Prop({ type: Date })
lockedAt?: Date; lockedAt?: Date;
/** Lock expiry used by UI countdown (typically lockedAt + 15m). */ /** Lock expiry used by UI countdown (typically lockedAt + 30m). */
@Prop({ type: Date }) @Prop({ type: Date })
expiredAt?: Date; expiredAt?: Date;
@@ -50,7 +50,7 @@ export class Workflow {
lockedBy?: ActorLock; 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. * so no other expert can take the case while it remains in the expert queue.
*/ */
@Prop({ type: ActorLockSchema }) @Prop({ type: ActorLockSchema })