diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index a024567..766c2cf 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -3985,7 +3985,10 @@ export class ClaimRequestManagementService { ...(currentUserParty.person?.clientId ? { clientId: new Types.ObjectId( - currentUserParty.person.clientId as any, + String(blameRequest?.expert?.decision?.guiltyPartyId) === + String(blameRequest?.parties[0]?.person?.userId) + ? blameRequest?.parties[0]?.person?.clientId + : blameRequest?.parties[1]?.person?.clientId, ), } : {}), diff --git a/src/expert-claim/dto/claim-damaged-part.enricher.ts b/src/expert-claim/dto/claim-damaged-part.enricher.ts new file mode 100644 index 0000000..99d8741 --- /dev/null +++ b/src/expert-claim/dto/claim-damaged-part.enricher.ts @@ -0,0 +1,138 @@ +// claim-damaged-part.enricher.ts + +import { + DamagedPartCaptureStatus, + EnrichedDamagedPart, +} from "./claim-damaged-part.types"; + +export function buildEnrichedDamagedParts(params: { + selectedParts: any[]; + damagedPartsData: any; + evaluationBlock: any; + expertAddedParts?: any[]; + getDamagedPartCaptureBlob: (data: any, key: string, parts: any[]) => any; + hasDamagedPartCapture: (data: any, key: string, parts: any[]) => boolean; + catalogLikeKeyFromPart: (part: any) => string; + buildFileLink: (path: string) => string; +}): EnrichedDamagedPart[] { + const { + selectedParts, + damagedPartsData, + evaluationBlock, + expertAddedParts = [], + getDamagedPartCaptureBlob, + hasDamagedPartCapture, + catalogLikeKeyFromPart, + buildFileLink, + } = params; + + const priceDropLines: any[] = evaluationBlock?.priceDrop?.partLines ?? []; + const objectionParts: any[] = + evaluationBlock?.objection?.objectionParts ?? []; + const newObjectionParts: any[] = evaluationBlock?.objection?.newParts ?? []; + + // Resend block — lives at evaluation.damageExpertResend + const damageExpertResend = evaluationBlock?.damageExpertResend; + const resendCarParts: any[] = damageExpertResend?.resendCarParts ?? []; + const resendFulfilledAt: Date | undefined = damageExpertResend?.fulfilledAt; + + function isPartInResend(sp: any): boolean { + return resendCarParts.some( + (r: any) => + r.id === sp.id || r.name === sp.name || r.catalogKey === sp.catalogKey, + ); + } + + function resolveOrigin(sp: any): EnrichedDamagedPart["origin"] { + if ( + newObjectionParts.some((p: any) => p.id === sp.id || p.name === sp.name) + ) { + return "added_by_objection"; + } + if ( + expertAddedParts.some((p: any) => p.id === sp.id || p.name === sp.name) + ) { + return "added_by_expert"; + } + return "user_selected"; + } + + function resolveCaptureStatus( + sp: any, + captured: boolean, + ): DamagedPartCaptureStatus { + const inResend = isPartInResend(sp); + if (!captured) { + return inResend ? "resend_requested" : "pending"; + } + // captured=true + was in a resend request that is now fulfilled → "resent" + return inResend && resendFulfilledAt ? "resent" : "uploaded"; + } + + return selectedParts.map((sp, index) => { + const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp); + + const cap = getDamagedPartCaptureBlob( + damagedPartsData, + ck, + selectedParts, + ) as { url?: string; path?: string } | undefined; + const captured = hasDamagedPartCapture(damagedPartsData, ck, selectedParts); + const url = cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined); + + const matchingPriceDrop = priceDropLines.find( + (line: any) => + line.partId === sp.id || + line.priceDropPartKey?.toLowerCase() === sp.name?.toLowerCase(), + ); + + const isObjected = objectionParts.some( + (p: any) => p.id === sp.id || p.name === sp.name, + ); + const isNewlyAdded = newObjectionParts.some( + (p: any) => p.id === sp.id || p.name === sp.name, + ); + const isNewByExpert = expertAddedParts.some( + (p: any) => p.id === sp.id || p.name === sp.name, + ); + const inResend = isPartInResend(sp); + + return { + index, + partId: sp.id ?? null, + id: sp.id ?? null, + name: sp.name, + side: sp.side, + label_fa: sp.label_fa, + ...(sp.catalogKey ? { catalogKey: sp.catalogKey } : {}), + + origin: resolveOrigin(sp), + captureStatus: resolveCaptureStatus(sp, captured), + captured, + url, + + objection: { + isObjected, + isNewlyAdded, + ...(isNewlyAdded ? { addedBy: isNewByExpert ? "expert" : "user" } : {}), + }, + + resend: { + wasRequested: inResend, + isFulfilled: inResend && !!resendFulfilledAt, + ...(inResend && damageExpertResend?.requestedAt + ? { requestedAt: damageExpertResend.requestedAt } + : {}), + ...(inResend && resendFulfilledAt + ? { fulfilledAt: resendFulfilledAt } + : {}), + }, + + pricing: { + hasPriceDrop: !!matchingPriceDrop, + severity: matchingPriceDrop?.severity ?? null, + coefficient: matchingPriceDrop?.coefficient ?? null, + }, + }; + }); +} diff --git a/src/expert-claim/dto/claim-damaged-part.types.ts b/src/expert-claim/dto/claim-damaged-part.types.ts new file mode 100644 index 0000000..99cccd6 --- /dev/null +++ b/src/expert-claim/dto/claim-damaged-part.types.ts @@ -0,0 +1,40 @@ +// claim-damaged-part.types.ts — update the type +export type DamagedPartCaptureStatus = + | "pending" // never uploaded + | "uploaded" // uploaded, no issues + | "resend_requested" // expert asked to redo this part's photo + | "resent"; // owner re-uploaded after resend request + +export interface EnrichedDamagedPart { + index: number; + partId: number | null; + id: number | null; + name: string; + side: string; + label_fa: string; + catalogKey?: string; + + origin: "user_selected" | "added_by_objection" | "added_by_expert"; + captureStatus: DamagedPartCaptureStatus; + captured: boolean; + url?: string; + + objection: { + isObjected: boolean; + isNewlyAdded: boolean; + addedBy?: "user" | "expert"; + }; + + resend: { + wasRequested: boolean; // expert included this part in a resend request + isFulfilled: boolean; // owner completed the resend + requestedAt?: Date; + fulfilledAt?: Date; + }; + + pricing: { + hasPriceDrop: boolean; + severity: string | null; + coefficient: number | null; + }; +} diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 6d040ef..d3aed24 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -132,6 +132,7 @@ import { import { CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN } from "src/constants/repair-amount-limits"; 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"; const CLAIM_V2_TOTAL_PAYMENT_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN; @@ -3867,56 +3868,19 @@ export class ExpertClaimService { const objectionParts = evaluationBlock?.objection?.objectionParts || []; const newObjectionParts = evaluationBlock?.objection?.newParts || []; - const damagedParts = selectedNormExpert.map((sp, index) => { - const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp); - const cap = getDamagedPartCaptureBlob( - damagedPartsDataExpert, - ck, - selectedNormExpert, - ) as { url?: string; path?: string } | undefined; + // Inside getClaimDetailV2, replace the damagedParts block: - // 1. Cross-reference Price Drop items via partId or name match - const matchingPriceDrop = priceDropLines.find( - (line: any) => - line.partId === sp.id || - line.priceDropPartKey?.toLowerCase() === sp.name?.toLowerCase(), - ); - - // 2. Evaluate if this specific part is flag-linked within an active user objection - const isObjected = objectionParts.some( - (objPart: any) => objPart.id === sp.id || objPart.name === sp.name, - ); - const isNewAddedPart = newObjectionParts.some( - (newPart: any) => newPart.id === sp.id || newPart.name === sp.name, - ); - - return { - index, - partId: catalogPartIdFromSelectedPart(sp), - id: sp.id, - name: sp.name, - side: sp.side, - label_fa: sp.label_fa, - captured: hasDamagedPartCapture( - damagedPartsDataExpert, - ck, - selectedNormExpert, - ), - url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined), - - // --- Added Consolidated Production Enrichments --- - severity: matchingPriceDrop ? matchingPriceDrop.severity : null, - coefficient: matchingPriceDrop ? matchingPriceDrop.coefficient : null, - objectionStatus: { - hasObjection: isObjected, - isNewlyAddedByObjection: isNewAddedPart, - // You can map extra parameters here if the front-end requires reason texts - }, - }; + const damagedParts = buildEnrichedDamagedParts({ + selectedParts: selectedNormExpert, + damagedPartsData: damagedPartsDataExpert, + evaluationBlock: (claim as any).evaluation, // damageExpertResend lives here already + expertAddedParts: (claim.damage as any)?.expertAddedParts ?? [], + getDamagedPartCaptureBlob, + hasDamagedPartCapture, + catalogLikeKeyFromPart, + buildFileLink, }); - // --- Combine both branches of the conflict, preserving necessary logic from both --- - // 1. Vehicle payload logic from "upstream" let vehiclePayload = claim.vehicle as any; const hasClaimVehicle = @@ -4377,9 +4341,29 @@ export class ExpertClaimService { ? [...(claim as any).damage.selectedParts] : []; + // Diff to find parts the expert is adding that weren't in the original selection + const previousPartIds = new Set( + previousNorm.map((p) => p.id ?? `${p.name}::${p.side}`), + ); + const expertAddedNow = nextNorm.filter( + (p) => !previousPartIds.has(p.id ?? `${p.name}::${p.side}`), + ); + + // Merge with parts the expert added in any previous edits on this claim (idempotent) + const existingExpertAdded: any[] = + (claim.damage as any)?.expertAddedParts ?? []; + const existingExpertAddedIds = new Set( + existingExpertAdded.map((p: any) => p.id ?? `${p.name}::${p.side}`), + ); + const expertAddedToAppend = expertAddedNow.filter( + (p) => !existingExpertAddedIds.has(p.id ?? `${p.name}::${p.side}`), + ); + const mergedExpertAdded = [...existingExpertAdded, ...expertAddedToAppend]; + const $set: Record = { "damage.selectedParts": nextNorm, "media.damagedParts": nextMedia, + "damage.expertAddedParts": mergedExpertAdded, }; await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { @@ -4396,6 +4380,7 @@ export class ExpertClaimService { metadata: { previousSelectedParts: previous, selectedParts: nextNorm, + expertAddedParts: mergedExpertAdded, ...(damagedPartsEditSnapshot && { expertProfileSnapshot: damagedPartsEditSnapshot, }), @@ -4408,6 +4393,7 @@ export class ExpertClaimService { claimRequestId: claim._id.toString(), selectedParts: nextNorm, previousSelectedParts: previous, + expertAddedParts: mergedExpertAdded, message: "Damaged parts updated successfully.", }; }