Fixed insurer

This commit is contained in:
SepehrYahyaee
2026-06-07 16:10:42 +03:30
parent 34142942e5
commit 249c359898
6 changed files with 184 additions and 121 deletions

View File

@@ -92,7 +92,10 @@ import {
buildMutualAgreementExpertDecision,
enrichBlamePartiesForAgreementView,
} from "src/helpers/blame-party-agreement-decision";
import { resendRequestHasPayload } from "src/helpers/claim-expert-resend";
import {
normalizeResendDocumentKeys,
resendRequestHasPayload,
} from "src/helpers/claim-expert-resend";
import {
claimCaseStatusAfterExpertReplyV2,
classifyV2ExpertPricingParts,
@@ -133,6 +136,7 @@ import { CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN } from "src/constants/repair-amount-li
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { applyListQueryV2 } from "src/helpers/list-query-v2";
import { buildEnrichedDamagedParts } from "./dto/claim-damaged-part.enricher";
import { canonicalizeResendDocumentKey } from "src/helpers/claim-resend-document-keys";
const CLAIM_V2_TOTAL_PAYMENT_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN;
@@ -3842,29 +3846,29 @@ export class ExpertClaimService {
const isResendPending =
claim.status === ClaimCaseStatus.WAITING_FOR_USER_RESEND;
const assignedForReviewById = String(
(claim.workflow as any)?.assignedForReviewBy?.actorId ?? "",
);
const lockEnforced = this.isClaimV2WorkflowLockCurrentlyEnforced(claim);
const lockedById = String(claim.workflow?.lockedBy?.actorId ?? "");
const isAssignedToMe =
!!assignedForReviewById && assignedForReviewById === actorId;
// Gate: must satisfy at least one bucket
if (
!isDamageExpertPhase &&
!isFactorValidationPending &&
!isResendPending
!isResendPending &&
!isAssignedToMe
) {
throw new ForbiddenException(
`This claim is not available for expert review. Current status: ${claim.status}`,
);
}
// Ownership access control — same 4-bucket logic as list
const assignedForReviewById = String(
(claim.workflow as any)?.assignedForReviewBy?.actorId ?? "",
);
const lockEnforced = this.isClaimV2WorkflowLockCurrentlyEnforced(claim);
const lockedById = String(claim.workflow?.lockedBy?.actorId ?? "");
const isAvailable =
isDamageExpertPhase && !assignedForReviewById && !lockEnforced;
const isAssignedToMe =
!!assignedForReviewById && assignedForReviewById === actorId;
const isLockedByMe = lockEnforced && lockedById === actorId;
// Factor validation is open to all tenant experts (no individual ownership)
const isFactorValidationOpen = isFactorValidationPending;
if (
@@ -3888,12 +3892,19 @@ export class ExpertClaimService {
);
}
const hasCapture = (data: any, key: string) =>
data && (data instanceof Map ? data.get(key) : data[key]);
// Build requiredDocuments map
const requiredDocs = claim.requiredDocuments as any;
const requiredDocumentsStatus: Record<string, any> = {};
// Resend documents requested by expert — normalize to canonical keys for comparison
const resendDocumentKeys = new Set(
normalizeResendDocumentKeys(
(claim as any).evaluation?.damageExpertResend?.resendDocuments,
),
);
const resendFulfilled = !!(claim as any).evaluation?.damageExpertResend
?.fulfilledAt;
if (requiredDocs) {
const keys =
requiredDocs instanceof Map
@@ -3902,11 +3913,18 @@ export class ExpertClaimService {
for (const k of keys as string[]) {
const doc =
requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
const canonicalKey = canonicalizeResendDocumentKey(k) ?? k;
const wasResendRequested = resendDocumentKeys.has(canonicalKey);
requiredDocumentsStatus[k] = {
uploaded: !!doc?.uploaded,
fileId: doc?.fileId?.toString(),
fileName: doc?.fileName,
filePath: doc?.filePath ? buildFileLink(doc.filePath) : undefined,
resend: {
wasRequested: wasResendRequested,
isFulfilled: wasResendRequested && resendFulfilled,
},
};
}
}
@@ -3927,7 +3945,7 @@ export class ExpertClaimService {
};
}
// Build damaged parts list (aligned with normalized selected parts and enriched with evaluation context)
// Build enriched damaged parts
const carTypeExpert = claim.vehicle?.carType as
| ClaimVehicleTypeV2
| undefined;
@@ -3938,18 +3956,10 @@ export class ExpertClaimService {
);
const damagedPartsDataExpert = claim.media?.damagedParts as any;
// Extract optimization lookups from the DB document safely
const evaluationBlock = (claim as any).evaluation;
const priceDropLines = evaluationBlock?.priceDrop?.partLines || [];
const objectionParts = evaluationBlock?.objection?.objectionParts || [];
const newObjectionParts = evaluationBlock?.objection?.newParts || [];
// Inside getClaimDetailV2, replace the damagedParts block:
const damagedParts = buildEnrichedDamagedParts({
selectedParts: selectedNormExpert,
damagedPartsData: damagedPartsDataExpert,
evaluationBlock: (claim as any).evaluation, // damageExpertResend lives here already
evaluationBlock: (claim as any).evaluation,
expertAddedParts: (claim.damage as any)?.expertAddedParts ?? [],
getDamagedPartCaptureBlob,
hasDamagedPartCapture,
@@ -3957,7 +3967,7 @@ export class ExpertClaimService {
buildFileLink,
});
// 1. Vehicle payload logic from "upstream"
// Vehicle payload — fall back to blame inquiry if claim vehicle is sparse
let vehiclePayload = claim.vehicle as any;
const hasClaimVehicle =
vehiclePayload &&
@@ -3968,7 +3978,6 @@ export class ExpertClaimService {
let blameLean: any = null;
// 2. Video capture and blame case db queries from "stashed"
const videoCaptureIdRaw = (claim.media as any)?.videoCaptureId;
const blameRequestIdStr = claim.blameRequestId?.toString();
@@ -3991,18 +4000,15 @@ export class ExpertClaimService {
blameLean = blameLeanRows[0] ?? null;
}
// If claim vehicle not found and blameLean exists, replace vehiclePayload
if (!hasClaimVehicle && blameLean) {
const fromBlame = this.damagedPartyVehicleFromBlame(blameLean, claim);
if (fromBlame) vehiclePayload = fromBlame;
}
// 3. Blame file context logic from "upstream"
const blameFileContext = blameLean
? this.blameFileContextForExpert(blameLean)
: {};
// 4. videoCapture logic from "stashed"
let videoCapture: ClaimDetailV2ResponseDto["videoCapture"] = undefined;
if (videoCaptureRow) {
const vc = videoCaptureRow as any;
@@ -4036,28 +4042,12 @@ export class ExpertClaimService {
? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT
: claim.status;
const objection = (claim as any).evaluation?.objection
? {
objectionParts:
(claim as any).evaluation.objection.objectionParts ?? [],
newParts: (claim as any).evaluation.objection.newParts ?? [],
submittedAt: (claim as any).evaluation.objection.submittedAt,
}
: undefined;
const blameCaseForApi = blameCase
? this.sanitizeBlameCaseForClaimDetailApi(
blameCase as Record<string, unknown>,
)
: undefined;
// Enrich the full evaluation block with `signLink`s for every place a user
// signature is referenced, then assemble the response slice.
//
// We always expose owner approval blocks (when present in the DB) because
// the signature URL is needed across all phases of the claim. The expert
// reply payloads are still gated on `isFactorValidationPending` to match
// the historical contract of this endpoint.
const claimEvaluationRaw = (claim as any).evaluation as
| Record<string, unknown>
| undefined;
@@ -4122,7 +4112,6 @@ export class ExpertClaimService {
blameRequestId: claim.blameRequestId?.toString(),
blameRequestNo: claim.blameRequestNo,
money: moneyPayload,
// selectedParts: selectedNormExpert,
otherParts: claim.damage?.otherParts,
requiredDocuments:
Object.keys(requiredDocumentsStatus).length > 0
@@ -4131,10 +4120,9 @@ export class ExpertClaimService {
carAngles,
damagedParts,
awaitingFactorValidation: isFactorValidationPending,
evaluation: enrichedEvaluation as
evaluation: evaluationForApi as
| ClaimDetailV2ResponseDto["evaluation"]
| undefined,
// objection,
videoCapture,
blameCase: blameCaseForApi,
blameExpertDecision,