Centralized damaged parts alongside all their required info

This commit is contained in:
SepehrYahyaee
2026-06-01 11:38:30 +03:30
parent b9e7373225
commit cec349b7c2
4 changed files with 215 additions and 48 deletions

View File

@@ -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,
),
}
: {}),

View File

@@ -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,
},
};
});
}

View File

@@ -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;
};
}

View File

@@ -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<string, unknown> = {
"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.",
};
}