diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 1a6ef96..c70de92 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -82,6 +82,8 @@ import { coerceDamagedPartsMediaToArray, normalizeCarPartDamageForExpertReply, normalizeDamageSelectedParts, + partIdentityKey, + partIdentityKeyFromCarPartDamage, } from "src/helpers/outer-damage-parts"; import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog"; import { snapshotFromDamageExpert } from "src/helpers/expert-profile-snapshot"; @@ -2639,6 +2641,17 @@ export class ExpertClaimService { reply.parts?.length > 0 ? this.validateAndNormalizeDaghiForExpertReplyV2(reply.parts) : []; + + const ownerSelectedParts = normalizeDamageSelectedParts( + (claim as any)?.damage?.selectedParts, + carTypeSubmit, + (claim as any)?.damage?.selectedOuterParts, + ); + const ownerSelectedIdentitySet = new Set( + ownerSelectedParts.map((p) => partIdentityKey(p)), + ); + const seenIdentities = new Set(); + const processedParts = daghiNormalized.map((p) => { let carPartDamage: Record; try { @@ -2653,7 +2666,31 @@ export class ExpertClaimService { }`, ); } - return { ...p, carPartDamage }; + + const identityKey = partIdentityKeyFromCarPartDamage(carPartDamage); + if (!identityKey) { + throw new BadRequestException( + `Invalid carPartDamage for part ${p.partId}: cannot derive a stable identity (need a catalog id or both name and side).`, + ); + } + + if ( + ownerSelectedIdentitySet.size > 0 && + !ownerSelectedIdentitySet.has(identityKey) + ) { + throw new BadRequestException( + `Part "${identityKey}" is not in the owner's selected damaged parts; only parts the user picked (or added via objection) can be priced.`, + ); + } + + if (seenIdentities.has(identityKey)) { + throw new BadRequestException( + `Duplicate part submitted in expert reply: ${identityKey}.`, + ); + } + seenIdentities.add(identityKey); + + return { ...p, partId: identityKey, carPartDamage }; }); const { mixedFactorAndPrice, allFactorLines } = classifyV2ExpertPricingParts( diff --git a/src/helpers/outer-damage-parts.ts b/src/helpers/outer-damage-parts.ts index fe904dd..27dd684 100644 --- a/src/helpers/outer-damage-parts.ts +++ b/src/helpers/outer-damage-parts.ts @@ -298,6 +298,40 @@ export function partIdentityKey(p: DamageSelectedPartV2): string { return `${p.side}|${p.name}`; } +/** + * Same canonical identity as `partIdentityKey`, but derived directly from a + * unified `carPartDamage` snapshot (the shape we persist on + * `evaluation.damageExpertReply*.parts[].carPartDamage`). + * + * Returns `null` when the input cannot produce an identity (no `id`, no + * `name`). Callers should treat that as an invalid expert reply line. + */ +export function partIdentityKeyFromCarPartDamage( + carPartDamage: unknown, +): string | null { + if ( + !carPartDamage || + typeof carPartDamage !== "object" || + Array.isArray(carPartDamage) + ) { + return null; + } + const o = carPartDamage as Record; + const idRaw = o.id; + const id = + typeof idRaw === "number" && Number.isFinite(idRaw) + ? idRaw + : typeof idRaw === "string" && + idRaw.trim() && + Number.isFinite(Number(idRaw)) + ? Number(idRaw) + : null; + const name = typeof o.name === "string" ? o.name.trim() : ""; + const side = typeof o.side === "string" ? o.side.trim() : ""; + if (id == null && !name) return null; + return partIdentityKey({ id, name, side, label_fa: "" }); +} + function normPartSegment(s: string): string { return String(s ?? "").replace(/[^a-z0-9]/gi, "").toLowerCase(); }