From 249c35989827458f29cf2d77aea04e9aa7a42a17 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sun, 7 Jun 2026 16:10:42 +0330 Subject: [PATCH] Fixed insurer --- src/app.module.ts | 2 + src/auth/auth-services/user.auth.service.ts | 2 + .../claim-request-management.service.ts | 132 ++++++++++-------- src/expert-claim/expert-claim.service.ts | 86 +++++------- src/expert-insurer/expert-insurer.service.ts | 70 ++++++++-- src/helpers/claim-expert-resend.ts | 13 ++ 6 files changed, 184 insertions(+), 121 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 6617ee0..3ab2b03 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,6 +23,7 @@ import { UsersModule } from "./users/users.module"; import { applyIranFaTimestampPlugin } from "./helpers/mongoose-fa-timestamps.plugin"; import { CronModule } from "./utils/cron/cron.module"; import { WorkflowStepManagementModule } from "./workflow-step-management/workflow-step-management.module"; +import { ExpertInitiatedModule } from "./expert-initiated/expert-initiated.module"; import * as Joi from "joi"; @Module({ @@ -112,6 +113,7 @@ import * as Joi from "joi"; ExpertInsurerModule, LookupsModule, WorkflowStepManagementModule, + // ExpertInitiatedModule, ], controllers: [], providers: [ diff --git a/src/auth/auth-services/user.auth.service.ts b/src/auth/auth-services/user.auth.service.ts index abf038f..3555f6f 100644 --- a/src/auth/auth-services/user.auth.service.ts +++ b/src/auth/auth-services/user.auth.service.ts @@ -117,6 +117,7 @@ export class UserAuthService { if (!userExist) { await this.smsSender(otp, canonicalMobile); + // console.log(`OTP for ${canonicalMobile}: ${otp}`); const newUser = await this.userDbService.createUser({ mobile: canonicalMobile, username: canonicalMobile, @@ -144,6 +145,7 @@ export class UserAuthService { } await this.smsSender(otp, canonicalMobile); + // console.log(`OTP for ${canonicalMobile}: ${otp}`); await this.userDbService.findOneAndUpdate( buildUserLookupByPhone(canonicalMobile), { diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 6eb088e..a6ff6f8 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -5156,7 +5156,6 @@ export class ClaimRequestManagementService { ): Promise { try { const claimCase = await this.claimCaseDbService.findById(claimRequestId); - if (!claimCase) { throw new NotFoundException( `Claim case with ID ${claimRequestId} not found`, @@ -5214,6 +5213,7 @@ export class ClaimRequestManagementService { ); } } + if (isResendUpload) { if (claimCase.status !== ClaimCaseStatus.WAITING_FOR_USER_RESEND) { throw new BadRequestException( @@ -5262,7 +5262,6 @@ export class ClaimRequestManagementService { const fileUrl = buildFileLink(file.path); - // Create document reference in claim-required-documents collection const docRef = await this.claimRequiredDocumentDbService.create({ path: file.path, fileName: file.filename, @@ -5271,8 +5270,24 @@ export class ClaimRequestManagementService { uploadedAt: new Date(), }); - // Update claim case - const updateData: any = { + // Base history entry for this upload + const uploadHistoryEntry = { + type: "DOCUMENT_UPLOADED", + actor: { + actorId: new Types.ObjectId(currentUserId), + actorName: claimCase.owner?.fullName || "User", + actorType: "user", + }, + timestamp: new Date(), + metadata: { + documentKey: body.documentKey, + fileName: file.filename, + fileId: docRef._id.toString(), + }, + }; + + // Base $set — always applied + const $set: Record = { [`requiredDocuments.${body.documentKey}`]: { fileId: docRef._id, filePath: file.path, @@ -5280,33 +5295,16 @@ export class ClaimRequestManagementService { uploaded: true, uploadedAt: new Date(), }, - $push: { - history: { - type: "DOCUMENT_UPLOADED", - actor: { - actorId: new Types.ObjectId(currentUserId), - actorName: claimCase.owner?.fullName || "User", - actorType: "user", - }, - timestamp: new Date(), - metadata: { - documentKey: body.documentKey, - fileName: file.filename, - fileId: docRef._id.toString(), - }, - }, - }, }; + // 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 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; if (!isResendUpload) { @@ -5317,7 +5315,6 @@ export class ClaimRequestManagementService { remaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter( (k) => !afterThis(k), ).length; - allDocumentsUploaded = false; if (remaining === 0) { const progressAfterDoc = getClaimCaptureProgress(claimCase, { @@ -5330,31 +5327,29 @@ export class ClaimRequestManagementService { progressAfterDoc.capturePhaseDocsComplete ) { captureStepCompletedOnThisUpload = true; - updateData["status"] = - ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS; - updateData["workflow.currentStep"] = + $set["status"] = ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS; + $set["workflow.currentStep"] = ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS; - updateData["workflow.nextStep"] = + $set["workflow.nextStep"] = ClaimWorkflowStep.USER_SUBMISSION_COMPLETE; - updateData.$push.history = [ - updateData.$push.history, - { - type: "STEP_COMPLETED", - actor: { - actorId: new Types.ObjectId(currentUserId), - actorName: claimCase.owner?.fullName || "User", - actorType: "user", - }, - timestamp: new Date(), - metadata: { - stepKey: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, - description: - "Angles, damaged parts, and capture-phase vehicle evidence (chassis, engine, metal plate) are complete. Please upload remaining required documents.", - }, + + completedStepsEntries.push( + ClaimWorkflowStep.CAPTURE_PART_DAMAGES, + ); + historyEntries.push({ + type: "STEP_COMPLETED", + actor: { + actorId: new Types.ObjectId(currentUserId), + actorName: claimCase.owner?.fullName || "User", + actorType: "user", }, - ]; - updateData.$push["workflow.completedSteps"] = - ClaimWorkflowStep.CAPTURE_PART_DAMAGES; + timestamp: new Date(), + metadata: { + stepKey: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, + description: + "Angles, damaged parts, and capture-phase vehicle evidence (chassis, engine, metal plate) are complete. Please upload remaining required documents.", + }, + }); } } } else { @@ -5370,14 +5365,17 @@ export class ClaimRequestManagementService { ); if (allDocumentsUploaded) { - updateData["status"] = ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT; - updateData["claimStatus"] = ClaimStatus.PENDING; - updateData["workflow.currentStep"] = + $set["status"] = ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT; + $set["claimStatus"] = ClaimStatus.PENDING; + $set["workflow.currentStep"] = ClaimWorkflowStep.USER_SUBMISSION_COMPLETE; - updateData["workflow.nextStep"] = + $set["workflow.nextStep"] = ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT; - updateData.$push.history = [ - updateData.$push.history, + + completedStepsEntries.push( + ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, + ); + historyEntries.push( { type: "STEP_COMPLETED", actor: { @@ -5405,16 +5403,30 @@ export class ClaimRequestManagementService { "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 = { $set }; + + const $push: Record = { + 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( claimRequestId, - updateData, + updatePayload, ); if (isResendUpload) { diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index a69f8cf..87aaf9f 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -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 = {}; + + // 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, ) : 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 | 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, diff --git a/src/expert-insurer/expert-insurer.service.ts b/src/expert-insurer/expert-insurer.service.ts index e5e3571..a3c3315 100644 --- a/src/expert-insurer/expert-insurer.service.ts +++ b/src/expert-insurer/expert-insurer.service.ts @@ -1099,6 +1099,8 @@ export class ExpertInsurerService { async retrieveAllFilesOfClient(insurerId: string) { const id = this.getClientId(insurerId); + const idStr = String(id); + const [blameFiles, claimFiles] = await Promise.all([ this.getClientBlameFiles(id), this.getClientClaimFiles(id), @@ -1112,11 +1114,7 @@ export class ExpertInsurerService { parties?: Array<{ role?: string; fullName?: string; - vehicle?: { - carName?: string; - carModel?: string; - plate?: string; - }; + vehicle?: { carName?: string; carModel?: string; plate?: string }; }>; blame?: { requestId?: string; @@ -1125,11 +1123,7 @@ export class ExpertInsurerService { parties?: Array<{ role?: string; fullName?: string; - vehicle?: { - carName?: string; - carModel?: string; - plate?: string; - }; + vehicle?: { carName?: string; carModel?: string; plate?: string }; }>; expertNames?: string[]; createdAt?: Date | string; @@ -1150,6 +1144,7 @@ export class ExpertInsurerService { } >(); + // Index blame files first for (const b of blameFiles) { const key = String((b as any).publicId || (b as any)._id || ""); if (!key) continue; @@ -1163,7 +1158,7 @@ export class ExpertInsurerService { (b as any).requestNo || String((b as any).requestNumber || ""), status: (b as any).status, parties: partiesPreview, - expertNames: extractExpertNamesFromBlame(b), // ← add + expertNames: extractExpertNamesFromBlame(b), createdAt: (b as any).createdAt, updatedAt: (b as any).updatedAt, }; @@ -1173,10 +1168,61 @@ export class ExpertInsurerService { 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( + linkedBlames.map((b) => [String(b.publicId), b]), + ); + + // Index claim files for (const c of claimFiles) { const key = String((c as any).publicId || (c as any)._id || ""); if (!key) continue; 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( (c as any)?.snapshot?.parties, ); @@ -1189,7 +1235,7 @@ export class ExpertInsurerService { status: (c as any).status, claimStatus: (c as any).claimStatus, currentStep: (c as any).currentStep, - expertNames: extractExpertNamesFromClaim(c), // ← add + expertNames: extractExpertNamesFromClaim(c), createdAt: (c as any).createdAt, updatedAt: (c as any).updatedAt, }; diff --git a/src/helpers/claim-expert-resend.ts b/src/helpers/claim-expert-resend.ts index ecf700a..78373e0 100644 --- a/src/helpers/claim-expert-resend.ts +++ b/src/helpers/claim-expert-resend.ts @@ -260,6 +260,19 @@ export function canFinalizeExpertResend(claim: any): boolean { 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) { return String(r.resendDescription || "").trim() !== ""; }