Compare commits

..

4 Commits

View File

@@ -64,6 +64,9 @@ import { resendRequestHasPayload } from "src/helpers/claim-expert-resend";
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,
@@ -2551,13 +2554,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,
@@ -2572,28 +2590,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) =>