forked from Yara724/api
Merge pull request 'Centralized damaged parts alongside all their required info' (#98) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#98
This commit is contained in:
@@ -3985,7 +3985,10 @@ export class ClaimRequestManagementService {
|
|||||||
...(currentUserParty.person?.clientId
|
...(currentUserParty.person?.clientId
|
||||||
? {
|
? {
|
||||||
clientId: new Types.ObjectId(
|
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,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
|||||||
138
src/expert-claim/dto/claim-damaged-part.enricher.ts
Normal file
138
src/expert-claim/dto/claim-damaged-part.enricher.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
40
src/expert-claim/dto/claim-damaged-part.types.ts
Normal file
40
src/expert-claim/dto/claim-damaged-part.types.ts
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -132,6 +132,7 @@ import {
|
|||||||
import { CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN } from "src/constants/repair-amount-limits";
|
import { CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN } from "src/constants/repair-amount-limits";
|
||||||
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";
|
||||||
|
|
||||||
const CLAIM_V2_TOTAL_PAYMENT_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN;
|
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 objectionParts = evaluationBlock?.objection?.objectionParts || [];
|
||||||
const newObjectionParts = evaluationBlock?.objection?.newParts || [];
|
const newObjectionParts = evaluationBlock?.objection?.newParts || [];
|
||||||
|
|
||||||
const damagedParts = selectedNormExpert.map((sp, index) => {
|
// Inside getClaimDetailV2, replace the damagedParts block:
|
||||||
const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp);
|
|
||||||
const cap = getDamagedPartCaptureBlob(
|
|
||||||
damagedPartsDataExpert,
|
|
||||||
ck,
|
|
||||||
selectedNormExpert,
|
|
||||||
) as { url?: string; path?: string } | undefined;
|
|
||||||
|
|
||||||
// 1. Cross-reference Price Drop items via partId or name match
|
const damagedParts = buildEnrichedDamagedParts({
|
||||||
const matchingPriceDrop = priceDropLines.find(
|
selectedParts: selectedNormExpert,
|
||||||
(line: any) =>
|
damagedPartsData: damagedPartsDataExpert,
|
||||||
line.partId === sp.id ||
|
evaluationBlock: (claim as any).evaluation, // damageExpertResend lives here already
|
||||||
line.priceDropPartKey?.toLowerCase() === sp.name?.toLowerCase(),
|
expertAddedParts: (claim.damage as any)?.expertAddedParts ?? [],
|
||||||
);
|
getDamagedPartCaptureBlob,
|
||||||
|
hasDamagedPartCapture,
|
||||||
// 2. Evaluate if this specific part is flag-linked within an active user objection
|
catalogLikeKeyFromPart,
|
||||||
const isObjected = objectionParts.some(
|
buildFileLink,
|
||||||
(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
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Combine both branches of the conflict, preserving necessary logic from both ---
|
|
||||||
|
|
||||||
// 1. Vehicle payload logic from "upstream"
|
// 1. Vehicle payload logic from "upstream"
|
||||||
let vehiclePayload = claim.vehicle as any;
|
let vehiclePayload = claim.vehicle as any;
|
||||||
const hasClaimVehicle =
|
const hasClaimVehicle =
|
||||||
@@ -4377,9 +4341,29 @@ export class ExpertClaimService {
|
|||||||
? [...(claim as any).damage.selectedParts]
|
? [...(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> = {
|
const $set: Record<string, unknown> = {
|
||||||
"damage.selectedParts": nextNorm,
|
"damage.selectedParts": nextNorm,
|
||||||
"media.damagedParts": nextMedia,
|
"media.damagedParts": nextMedia,
|
||||||
|
"damage.expertAddedParts": mergedExpertAdded,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
@@ -4396,6 +4380,7 @@ export class ExpertClaimService {
|
|||||||
metadata: {
|
metadata: {
|
||||||
previousSelectedParts: previous,
|
previousSelectedParts: previous,
|
||||||
selectedParts: nextNorm,
|
selectedParts: nextNorm,
|
||||||
|
expertAddedParts: mergedExpertAdded,
|
||||||
...(damagedPartsEditSnapshot && {
|
...(damagedPartsEditSnapshot && {
|
||||||
expertProfileSnapshot: damagedPartsEditSnapshot,
|
expertProfileSnapshot: damagedPartsEditSnapshot,
|
||||||
}),
|
}),
|
||||||
@@ -4408,6 +4393,7 @@ export class ExpertClaimService {
|
|||||||
claimRequestId: claim._id.toString(),
|
claimRequestId: claim._id.toString(),
|
||||||
selectedParts: nextNorm,
|
selectedParts: nextNorm,
|
||||||
previousSelectedParts: previous,
|
previousSelectedParts: previous,
|
||||||
|
expertAddedParts: mergedExpertAdded,
|
||||||
message: "Damaged parts updated successfully.",
|
message: "Damaged parts updated successfully.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user