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

@@ -23,6 +23,7 @@ import { UsersModule } from "./users/users.module";
import { applyIranFaTimestampPlugin } from "./helpers/mongoose-fa-timestamps.plugin"; import { applyIranFaTimestampPlugin } from "./helpers/mongoose-fa-timestamps.plugin";
import { CronModule } from "./utils/cron/cron.module"; import { CronModule } from "./utils/cron/cron.module";
import { WorkflowStepManagementModule } from "./workflow-step-management/workflow-step-management.module"; import { WorkflowStepManagementModule } from "./workflow-step-management/workflow-step-management.module";
import { ExpertInitiatedModule } from "./expert-initiated/expert-initiated.module";
import * as Joi from "joi"; import * as Joi from "joi";
@Module({ @Module({
@@ -112,6 +113,7 @@ import * as Joi from "joi";
ExpertInsurerModule, ExpertInsurerModule,
LookupsModule, LookupsModule,
WorkflowStepManagementModule, WorkflowStepManagementModule,
// ExpertInitiatedModule,
], ],
controllers: [], controllers: [],
providers: [ providers: [

View File

@@ -117,6 +117,7 @@ export class UserAuthService {
if (!userExist) { if (!userExist) {
await this.smsSender(otp, canonicalMobile); await this.smsSender(otp, canonicalMobile);
// console.log(`OTP for ${canonicalMobile}: ${otp}`);
const newUser = await this.userDbService.createUser({ const newUser = await this.userDbService.createUser({
mobile: canonicalMobile, mobile: canonicalMobile,
username: canonicalMobile, username: canonicalMobile,
@@ -144,6 +145,7 @@ export class UserAuthService {
} }
await this.smsSender(otp, canonicalMobile); await this.smsSender(otp, canonicalMobile);
// console.log(`OTP for ${canonicalMobile}: ${otp}`);
await this.userDbService.findOneAndUpdate( await this.userDbService.findOneAndUpdate(
buildUserLookupByPhone(canonicalMobile), buildUserLookupByPhone(canonicalMobile),
{ {

View File

@@ -5156,7 +5156,6 @@ export class ClaimRequestManagementService {
): Promise<UploadRequiredDocumentV2ResponseDto> { ): Promise<UploadRequiredDocumentV2ResponseDto> {
try { try {
const claimCase = await this.claimCaseDbService.findById(claimRequestId); const claimCase = await this.claimCaseDbService.findById(claimRequestId);
if (!claimCase) { if (!claimCase) {
throw new NotFoundException( throw new NotFoundException(
`Claim case with ID ${claimRequestId} not found`, `Claim case with ID ${claimRequestId} not found`,
@@ -5214,6 +5213,7 @@ export class ClaimRequestManagementService {
); );
} }
} }
if (isResendUpload) { if (isResendUpload) {
if (claimCase.status !== ClaimCaseStatus.WAITING_FOR_USER_RESEND) { if (claimCase.status !== ClaimCaseStatus.WAITING_FOR_USER_RESEND) {
throw new BadRequestException( throw new BadRequestException(
@@ -5262,7 +5262,6 @@ export class ClaimRequestManagementService {
const fileUrl = buildFileLink(file.path); const fileUrl = buildFileLink(file.path);
// Create document reference in claim-required-documents collection
const docRef = await this.claimRequiredDocumentDbService.create({ const docRef = await this.claimRequiredDocumentDbService.create({
path: file.path, path: file.path,
fileName: file.filename, fileName: file.filename,
@@ -5271,17 +5270,8 @@ export class ClaimRequestManagementService {
uploadedAt: new Date(), uploadedAt: new Date(),
}); });
// Update claim case // Base history entry for this upload
const updateData: any = { const uploadHistoryEntry = {
[`requiredDocuments.${body.documentKey}`]: {
fileId: docRef._id,
filePath: file.path,
fileName: file.filename,
uploaded: true,
uploadedAt: new Date(),
},
$push: {
history: {
type: "DOCUMENT_UPLOADED", type: "DOCUMENT_UPLOADED",
actor: { actor: {
actorId: new Types.ObjectId(currentUserId), actorId: new Types.ObjectId(currentUserId),
@@ -5294,19 +5284,27 @@ export class ClaimRequestManagementService {
fileName: file.filename, fileName: file.filename,
fileId: docRef._id.toString(), fileId: docRef._id.toString(),
}, },
}, };
// Base $set — always applied
const $set: Record<string, unknown> = {
[`requiredDocuments.${body.documentKey}`]: {
fileId: docRef._id,
filePath: file.path,
fileName: file.filename,
uploaded: true,
uploadedAt: new Date(),
}, },
}; };
// History entries to push — always at least the upload entry
const historyEntries: unknown[] = [uploadHistoryEntry];
// completedSteps entries to push
const completedStepsEntries: string[] = [];
let allDocumentsUploaded = false; let allDocumentsUploaded = false;
let remaining = 0; let remaining = 0;
// Will be true when this upload also completes capture-phase docs AND
// every angle + every selected damaged part has already been captured.
// In that case we advance the workflow exactly like `capturePartV2` does
// when the user finishes the last capture — otherwise the user would be
// stuck in CAPTURE_PART_DAMAGES with all captures done but no more
// captures to upload to trigger the transition.
let captureStepCompletedOnThisUpload = false; let captureStepCompletedOnThisUpload = false;
if (!isResendUpload) { if (!isResendUpload) {
@@ -5317,7 +5315,6 @@ export class ClaimRequestManagementService {
remaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter( remaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter(
(k) => !afterThis(k), (k) => !afterThis(k),
).length; ).length;
allDocumentsUploaded = false;
if (remaining === 0) { if (remaining === 0) {
const progressAfterDoc = getClaimCaptureProgress(claimCase, { const progressAfterDoc = getClaimCaptureProgress(claimCase, {
@@ -5330,15 +5327,16 @@ export class ClaimRequestManagementService {
progressAfterDoc.capturePhaseDocsComplete progressAfterDoc.capturePhaseDocsComplete
) { ) {
captureStepCompletedOnThisUpload = true; captureStepCompletedOnThisUpload = true;
updateData["status"] = $set["status"] = ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS;
ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS; $set["workflow.currentStep"] =
updateData["workflow.currentStep"] =
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS; ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS;
updateData["workflow.nextStep"] = $set["workflow.nextStep"] =
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE; ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
updateData.$push.history = [
updateData.$push.history, completedStepsEntries.push(
{ ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
);
historyEntries.push({
type: "STEP_COMPLETED", type: "STEP_COMPLETED",
actor: { actor: {
actorId: new Types.ObjectId(currentUserId), actorId: new Types.ObjectId(currentUserId),
@@ -5351,10 +5349,7 @@ export class ClaimRequestManagementService {
description: description:
"Angles, damaged parts, and capture-phase vehicle evidence (chassis, engine, metal plate) are complete. Please upload remaining required documents.", "Angles, damaged parts, and capture-phase vehicle evidence (chassis, engine, metal plate) are complete. Please upload remaining required documents.",
}, },
}, });
];
updateData.$push["workflow.completedSteps"] =
ClaimWorkflowStep.CAPTURE_PART_DAMAGES;
} }
} }
} else { } else {
@@ -5370,14 +5365,17 @@ export class ClaimRequestManagementService {
); );
if (allDocumentsUploaded) { if (allDocumentsUploaded) {
updateData["status"] = ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT; $set["status"] = ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT;
updateData["claimStatus"] = ClaimStatus.PENDING; $set["claimStatus"] = ClaimStatus.PENDING;
updateData["workflow.currentStep"] = $set["workflow.currentStep"] =
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE; ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
updateData["workflow.nextStep"] = $set["workflow.nextStep"] =
ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT; ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
updateData.$push.history = [
updateData.$push.history, completedStepsEntries.push(
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
);
historyEntries.push(
{ {
type: "STEP_COMPLETED", type: "STEP_COMPLETED",
actor: { actor: {
@@ -5405,16 +5403,30 @@ export class ClaimRequestManagementService {
"User submission complete. Claim ready for damage expert review.", "User submission complete. Claim ready for damage expert review.",
}, },
}, },
]; );
updateData.$push["workflow.completedSteps"] =
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS;
} }
} }
} }
// Build the final MongoDB update — all operators explicit and clean
const updatePayload: Record<string, unknown> = { $set };
const $push: Record<string, unknown> = {
history:
historyEntries.length === 1
? historyEntries[0]
: { $each: historyEntries },
};
if (completedStepsEntries.length === 1) {
$push["workflow.completedSteps"] = completedStepsEntries[0];
} else if (completedStepsEntries.length > 1) {
$push["workflow.completedSteps"] = { $each: completedStepsEntries };
}
updatePayload.$push = $push;
await this.claimCaseDbService.findByIdAndUpdate( await this.claimCaseDbService.findByIdAndUpdate(
claimRequestId, claimRequestId,
updateData, updatePayload,
); );
if (isResendUpload) { if (isResendUpload) {

View File

@@ -92,7 +92,10 @@ import {
buildMutualAgreementExpertDecision, buildMutualAgreementExpertDecision,
enrichBlamePartiesForAgreementView, enrichBlamePartiesForAgreementView,
} from "src/helpers/blame-party-agreement-decision"; } 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 { import {
claimCaseStatusAfterExpertReplyV2, claimCaseStatusAfterExpertReplyV2,
classifyV2ExpertPricingParts, 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 { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { applyListQueryV2 } from "src/helpers/list-query-v2"; import { applyListQueryV2 } from "src/helpers/list-query-v2";
import { buildEnrichedDamagedParts } from "./dto/claim-damaged-part.enricher"; 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; const CLAIM_V2_TOTAL_PAYMENT_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN;
@@ -3842,29 +3846,29 @@ export class ExpertClaimService {
const isResendPending = const isResendPending =
claim.status === ClaimCaseStatus.WAITING_FOR_USER_RESEND; 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 ( if (
!isDamageExpertPhase && !isDamageExpertPhase &&
!isFactorValidationPending && !isFactorValidationPending &&
!isResendPending !isResendPending &&
!isAssignedToMe
) { ) {
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}`,
); );
} }
// 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 = const isAvailable =
isDamageExpertPhase && !assignedForReviewById && !lockEnforced; isDamageExpertPhase && !assignedForReviewById && !lockEnforced;
const isAssignedToMe =
!!assignedForReviewById && assignedForReviewById === actorId;
const isLockedByMe = lockEnforced && lockedById === actorId; const isLockedByMe = lockEnforced && lockedById === actorId;
// Factor validation is open to all tenant experts (no individual ownership)
const isFactorValidationOpen = isFactorValidationPending; const isFactorValidationOpen = isFactorValidationPending;
if ( 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 // Build requiredDocuments map
const requiredDocs = claim.requiredDocuments as any; const requiredDocs = claim.requiredDocuments as any;
const requiredDocumentsStatus: Record<string, 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) { if (requiredDocs) {
const keys = const keys =
requiredDocs instanceof Map requiredDocs instanceof Map
@@ -3902,11 +3913,18 @@ export class ExpertClaimService {
for (const k of keys as string[]) { for (const k of keys as string[]) {
const doc = const doc =
requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k]; requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
const canonicalKey = canonicalizeResendDocumentKey(k) ?? k;
const wasResendRequested = resendDocumentKeys.has(canonicalKey);
requiredDocumentsStatus[k] = { requiredDocumentsStatus[k] = {
uploaded: !!doc?.uploaded, uploaded: !!doc?.uploaded,
fileId: doc?.fileId?.toString(), fileId: doc?.fileId?.toString(),
fileName: doc?.fileName, fileName: doc?.fileName,
filePath: doc?.filePath ? buildFileLink(doc.filePath) : undefined, 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 const carTypeExpert = claim.vehicle?.carType as
| ClaimVehicleTypeV2 | ClaimVehicleTypeV2
| undefined; | undefined;
@@ -3938,18 +3956,10 @@ export class ExpertClaimService {
); );
const damagedPartsDataExpert = claim.media?.damagedParts as any; 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({ const damagedParts = buildEnrichedDamagedParts({
selectedParts: selectedNormExpert, selectedParts: selectedNormExpert,
damagedPartsData: damagedPartsDataExpert, damagedPartsData: damagedPartsDataExpert,
evaluationBlock: (claim as any).evaluation, // damageExpertResend lives here already evaluationBlock: (claim as any).evaluation,
expertAddedParts: (claim.damage as any)?.expertAddedParts ?? [], expertAddedParts: (claim.damage as any)?.expertAddedParts ?? [],
getDamagedPartCaptureBlob, getDamagedPartCaptureBlob,
hasDamagedPartCapture, hasDamagedPartCapture,
@@ -3957,7 +3967,7 @@ export class ExpertClaimService {
buildFileLink, 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; let vehiclePayload = claim.vehicle as any;
const hasClaimVehicle = const hasClaimVehicle =
vehiclePayload && vehiclePayload &&
@@ -3968,7 +3978,6 @@ export class ExpertClaimService {
let blameLean: any = null; let blameLean: any = null;
// 2. Video capture and blame case db queries from "stashed"
const videoCaptureIdRaw = (claim.media as any)?.videoCaptureId; const videoCaptureIdRaw = (claim.media as any)?.videoCaptureId;
const blameRequestIdStr = claim.blameRequestId?.toString(); const blameRequestIdStr = claim.blameRequestId?.toString();
@@ -3991,18 +4000,15 @@ export class ExpertClaimService {
blameLean = blameLeanRows[0] ?? null; blameLean = blameLeanRows[0] ?? null;
} }
// If claim vehicle not found and blameLean exists, replace vehiclePayload
if (!hasClaimVehicle && blameLean) { if (!hasClaimVehicle && blameLean) {
const fromBlame = this.damagedPartyVehicleFromBlame(blameLean, claim); const fromBlame = this.damagedPartyVehicleFromBlame(blameLean, claim);
if (fromBlame) vehiclePayload = fromBlame; if (fromBlame) vehiclePayload = fromBlame;
} }
// 3. Blame file context logic from "upstream"
const blameFileContext = blameLean const blameFileContext = blameLean
? this.blameFileContextForExpert(blameLean) ? this.blameFileContextForExpert(blameLean)
: {}; : {};
// 4. videoCapture logic from "stashed"
let videoCapture: ClaimDetailV2ResponseDto["videoCapture"] = undefined; let videoCapture: ClaimDetailV2ResponseDto["videoCapture"] = undefined;
if (videoCaptureRow) { if (videoCaptureRow) {
const vc = videoCaptureRow as any; const vc = videoCaptureRow as any;
@@ -4036,28 +4042,12 @@ export class ExpertClaimService {
? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT ? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT
: claim.status; : 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 const blameCaseForApi = blameCase
? this.sanitizeBlameCaseForClaimDetailApi( ? this.sanitizeBlameCaseForClaimDetailApi(
blameCase as Record<string, unknown>, blameCase as Record<string, unknown>,
) )
: undefined; : 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 const claimEvaluationRaw = (claim as any).evaluation as
| Record<string, unknown> | Record<string, unknown>
| undefined; | undefined;
@@ -4122,7 +4112,6 @@ export class ExpertClaimService {
blameRequestId: claim.blameRequestId?.toString(), blameRequestId: claim.blameRequestId?.toString(),
blameRequestNo: claim.blameRequestNo, blameRequestNo: claim.blameRequestNo,
money: moneyPayload, money: moneyPayload,
// selectedParts: selectedNormExpert,
otherParts: claim.damage?.otherParts, otherParts: claim.damage?.otherParts,
requiredDocuments: requiredDocuments:
Object.keys(requiredDocumentsStatus).length > 0 Object.keys(requiredDocumentsStatus).length > 0
@@ -4131,10 +4120,9 @@ export class ExpertClaimService {
carAngles, carAngles,
damagedParts, damagedParts,
awaitingFactorValidation: isFactorValidationPending, awaitingFactorValidation: isFactorValidationPending,
evaluation: enrichedEvaluation as evaluation: evaluationForApi as
| ClaimDetailV2ResponseDto["evaluation"] | ClaimDetailV2ResponseDto["evaluation"]
| undefined, | undefined,
// objection,
videoCapture, videoCapture,
blameCase: blameCaseForApi, blameCase: blameCaseForApi,
blameExpertDecision, blameExpertDecision,

View File

@@ -1099,6 +1099,8 @@ export class ExpertInsurerService {
async retrieveAllFilesOfClient(insurerId: string) { async retrieveAllFilesOfClient(insurerId: string) {
const id = this.getClientId(insurerId); const id = this.getClientId(insurerId);
const idStr = String(id);
const [blameFiles, claimFiles] = await Promise.all([ const [blameFiles, claimFiles] = await Promise.all([
this.getClientBlameFiles(id), this.getClientBlameFiles(id),
this.getClientClaimFiles(id), this.getClientClaimFiles(id),
@@ -1112,11 +1114,7 @@ export class ExpertInsurerService {
parties?: Array<{ parties?: Array<{
role?: string; role?: string;
fullName?: string; fullName?: string;
vehicle?: { vehicle?: { carName?: string; carModel?: string; plate?: string };
carName?: string;
carModel?: string;
plate?: string;
};
}>; }>;
blame?: { blame?: {
requestId?: string; requestId?: string;
@@ -1125,11 +1123,7 @@ export class ExpertInsurerService {
parties?: Array<{ parties?: Array<{
role?: string; role?: string;
fullName?: string; fullName?: string;
vehicle?: { vehicle?: { carName?: string; carModel?: string; plate?: string };
carName?: string;
carModel?: string;
plate?: string;
};
}>; }>;
expertNames?: string[]; expertNames?: string[];
createdAt?: Date | string; createdAt?: Date | string;
@@ -1150,6 +1144,7 @@ export class ExpertInsurerService {
} }
>(); >();
// Index blame files first
for (const b of blameFiles) { for (const b of blameFiles) {
const key = String((b as any).publicId || (b as any)._id || ""); const key = String((b as any).publicId || (b as any)._id || "");
if (!key) continue; if (!key) continue;
@@ -1163,7 +1158,7 @@ export class ExpertInsurerService {
(b as any).requestNo || String((b as any).requestNumber || ""), (b as any).requestNo || String((b as any).requestNumber || ""),
status: (b as any).status, status: (b as any).status,
parties: partiesPreview, parties: partiesPreview,
expertNames: extractExpertNamesFromBlame(b), // ← add expertNames: extractExpertNamesFromBlame(b),
createdAt: (b as any).createdAt, createdAt: (b as any).createdAt,
updatedAt: (b as any).updatedAt, updatedAt: (b as any).updatedAt,
}; };
@@ -1173,10 +1168,61 @@ export class ExpertInsurerService {
byPublicId.set(key, prev); byPublicId.set(key, prev);
} }
// For claim files whose blame wasn't in blameFiles (different insurer's blame),
// fetch the linked blame directly so we can get vehicle + parties data
const claimOnlyBlameIds = claimFiles
.filter((c) => {
const key = String((c as any).publicId || (c as any)._id || "");
return key && !byPublicId.has(key) && (c as any).blameRequestId;
})
.map((c) => String((c as any).blameRequestId));
const uniqueBlameIds = [...new Set(claimOnlyBlameIds)].filter(Boolean);
const linkedBlames =
uniqueBlameIds.length > 0
? ((await this.blameRequestDbService.find(
{
_id: {
$in: uniqueBlameIds.map((id) => new Types.ObjectId(id)),
},
},
{ lean: true },
)) as any[])
: [];
const linkedBlameByPublicId = new Map<string, any>(
linkedBlames.map((b) => [String(b.publicId), b]),
);
// Index claim files
for (const c of claimFiles) { for (const c of claimFiles) {
const key = String((c as any).publicId || (c as any)._id || ""); const key = String((c as any).publicId || (c as any)._id || "");
if (!key) continue; if (!key) continue;
const prev = byPublicId.get(key) ?? { publicId: key }; const prev = byPublicId.get(key) ?? { publicId: key };
// If no blame entry yet, use the linked blame for parties/vehicle/fileType
if (!prev.blame) {
const linkedBlame = linkedBlameByPublicId.get(key);
if (linkedBlame) {
const partiesFromBlame = this.buildPartiesPreview(
linkedBlame.parties,
);
prev.fileType = prev.fileType ?? linkedBlame.type;
prev.blame = {
requestId: String(linkedBlame._id),
requestNo: linkedBlame.requestNo || "",
status: linkedBlame.status,
parties: partiesFromBlame,
expertNames: extractExpertNamesFromBlame(linkedBlame),
createdAt: linkedBlame.createdAt,
updatedAt: linkedBlame.updatedAt,
};
prev.parties = prev.parties ?? partiesFromBlame;
prev.createdAt = prev.createdAt ?? linkedBlame.createdAt;
}
}
const claimPartiesPreview = this.buildPartiesPreview( const claimPartiesPreview = this.buildPartiesPreview(
(c as any)?.snapshot?.parties, (c as any)?.snapshot?.parties,
); );
@@ -1189,7 +1235,7 @@ export class ExpertInsurerService {
status: (c as any).status, status: (c as any).status,
claimStatus: (c as any).claimStatus, claimStatus: (c as any).claimStatus,
currentStep: (c as any).currentStep, currentStep: (c as any).currentStep,
expertNames: extractExpertNamesFromClaim(c), // ← add expertNames: extractExpertNamesFromClaim(c),
createdAt: (c as any).createdAt, createdAt: (c as any).createdAt,
updatedAt: (c as any).updatedAt, updatedAt: (c as any).updatedAt,
}; };

View File

@@ -260,6 +260,19 @@ export function canFinalizeExpertResend(claim: any): boolean {
claim?.damage?.selectedParts, claim?.damage?.selectedParts,
); );
console.log("[canFinalizeExpertResend]", {
docKeys,
partKeys,
docResults: docKeys.map((k) => ({
k,
uploaded: isRequiredDocumentUploaded(claim, k),
})),
partResults: partKeys.map((k) => ({
k,
captured: hasDamagedPartCapture(claim, k),
})),
});
if (docKeys.length === 0 && partKeys.length === 0) { if (docKeys.length === 0 && partKeys.length === 0) {
return String(r.resendDescription || "").trim() !== ""; return String(r.resendDescription || "").trim() !== "";
} }