forked from Yara724/api
YARA-1045
This commit is contained in:
@@ -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 })
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
8
src/helpers/expert-workflow-lock.ts
Normal file
8
src/helpers/expert-workflow-lock.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -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 })
|
||||||
|
|||||||
Reference in New Issue
Block a user