1
0
forked from Yara724/api

merge upstream

This commit is contained in:
2026-04-19 17:23:47 +03:30

View File

@@ -64,6 +64,9 @@ import { resendRequestHasPayload } from "src/helpers/claim-expert-resend";
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,
@@ -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 * V2: Get claim detail for damage expert
* *
* Validations: * Validations:
* - Claim must exist and belong to the actor's insurer (`assertClaimCaseForTenant`) * - 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) * - Allowed when: WAITING_FOR_DAMAGE_EXPERT (queue), or EXPERT_REVIEWING (after lock — same expert must be able to reopen detail),
* - Or when validating factors: WAITING_FOR_INSURER_APPROVAL + UNDER_REVIEW at EXPERT_COST_EVALUATION * 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( async getClaimDetailV2(
claimRequestId: string, claimRequestId: string,
@@ -2572,29 +2590,29 @@ export class ExpertClaimService {
assertClaimCaseForTenant(claim, actor); assertClaimCaseForTenant(claim, actor);
const isPickupReview = const isDamageExpertPhase =
claim.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT; claim.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT ||
claim.status === ClaimCaseStatus.EXPERT_REVIEWING;
const isFactorValidationPending = const isFactorValidationPending =
claim.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL && claim.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL &&
claim.claimStatus === ClaimStatus.UNDER_REVIEW && claim.claimStatus === ClaimStatus.UNDER_REVIEW &&
claim.workflow?.currentStep === ClaimWorkflowStep.EXPERT_COST_EVALUATION; claim.workflow?.currentStep === ClaimWorkflowStep.EXPERT_COST_EVALUATION;
if (!isPickupReview && !isFactorValidationPending) { if (!isDamageExpertPhase && !isFactorValidationPending) {
throw new ForbiddenException( throw new ForbiddenException(
`This claim is not available for expert review. Current status: ${claim.status}`, `This claim is not available for expert review. Current status: ${claim.status}`,
); );
} }
// If locked by someone else, deny access (initial pickup / assessment only) const lockBlocksOthers = this.isClaimV2WorkflowLockCurrentlyEnforced(claim);
if ( if (lockBlocksOthers) {
isPickupReview && const lockerId = claim.workflow?.lockedBy?.actorId?.toString();
claim.workflow?.locked && if (lockerId && lockerId !== actorId) {
claim.workflow.lockedBy?.actorId?.toString() !== actorId
) {
throw new ForbiddenException( throw new ForbiddenException(
`This claim is locked by another expert: ${claim.workflow.lockedBy?.actorName}`, `This claim is locked by another expert: ${claim.workflow.lockedBy?.actorName}`,
); );
} }
}
const hasCapture = (data: any, key: string) => const hasCapture = (data: any, key: string) =>
data && (data instanceof Map ? data.get(key) : data[key]); data && (data instanceof Map ? data.get(key) : data[key]);