diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 8ce7071..bbe8aeb 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -87,8 +87,12 @@ import { import { canonicalizeClaimCarAngleKey, getClaimCarAngleCaptureBlob, + getDamagedPartCaptureBlob, hasClaimCarAngleCapture, + hasDamagedPartCapture, legacyAngleDamagedPartFieldKeysToUnset, + legacyDamagedPartTitleEnFromPartKey, + resolveCanonicalDamagedPartStorageKey, type ClaimCarAngleKey, } from "src/helpers/claim-car-angle-media"; import { @@ -4358,7 +4362,10 @@ export class ClaimRequestManagementService { side, label_fa: sideFa ? `${label.fa} (${sideFa})` : label.fa, label_en: label.en, - captured: !!hasCapture(claimCase.media?.damagedParts as any, partKey), + captured: hasDamagedPartCapture( + claimCase.media?.damagedParts as any, + partKey, + ), }; }); @@ -4809,7 +4816,25 @@ export class ClaimRequestManagementService { } } } else { - updateData[`media.damagedParts.${body.captureKey}`] = captureData; + const canonicalPartKey = resolveCanonicalDamagedPartStorageKey( + body.captureKey, + selectedBefore, + ); + updateData[`media.damagedParts.${canonicalPartKey}`] = captureData; + const raw = String(body.captureKey ?? "").trim(); + const titleLegacy = + legacyDamagedPartTitleEnFromPartKey(canonicalPartKey); + const unsetPartKeys = new Set(); + if (raw && raw !== canonicalPartKey) unsetPartKeys.add(raw); + if (titleLegacy && titleLegacy !== canonicalPartKey) { + unsetPartKeys.add(titleLegacy); + } + if (unsetPartKeys.size) { + updateData.$unset = updateData.$unset || {}; + for (const k of unsetPartKeys) { + updateData.$unset[`media.damagedParts.${k}`] = ""; + } + } } if ( @@ -4864,7 +4889,9 @@ export class ClaimRequestManagementService { k as ClaimCarAngleKey, ), ).length; - const partsCaptured = selectedParts.filter(p => hasCapture(damagedPartsData, p)).length; + const partsCaptured = selectedParts.filter((p) => + hasDamagedPartCapture(damagedPartsData, p), + ).length; const allCapturesComplete = anglesCaptured >= 4 && partsCaptured >= selectedParts.length; @@ -5515,7 +5542,9 @@ export class ClaimRequestManagementService { ...resendPartKeys, ]); for (const p of partKeysForDetails) { - const cap = hasCapture(damagedPartsData, p); + const cap = getDamagedPartCaptureBlob(damagedPartsData, p) as + | { url?: string; path?: string } + | undefined; damagedParts[p] = { label_fa: getPartLabelFa(p), captured: !!cap, diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index d1e39d7..1299000 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -67,6 +67,7 @@ import { import { resendRequestHasPayload } from "src/helpers/claim-expert-resend"; import { getClaimCarAngleCaptureBlob, + getDamagedPartCaptureBlob, type ClaimCarAngleKey, } from "src/helpers/claim-car-angle-media"; import { snapshotFromDamageExpert } from "src/helpers/expert-profile-snapshot"; @@ -3306,7 +3307,9 @@ export class ExpertClaimService { { label_fa: string; captured: boolean; url?: string } > = {}; for (const p of claim.damage?.selectedParts || []) { - const cap = hasCapture(damagedPartsData, p); + const cap = getDamagedPartCaptureBlob(damagedPartsData, p) as + | { url?: string; path?: string } + | undefined; damagedParts[p] = { label_fa: getPartLabelFa(p), captured: !!cap, diff --git a/src/helpers/claim-car-angle-media.ts b/src/helpers/claim-car-angle-media.ts index 3eb3145..de79122 100644 --- a/src/helpers/claim-car-angle-media.ts +++ b/src/helpers/claim-car-angle-media.ts @@ -72,3 +72,71 @@ export function legacyAngleDamagedPartFieldKeysToUnset( } return [...new Set(out)]; } + +/** Same English title derived from snake_case keys as capture-requirements UI (legacy client storage). */ +export function legacyDamagedPartTitleEnFromPartKey(partKey: string): string { + return String(partKey) + .replace(/([a-z])([A-Z])/g, "$1 $2") + .replace(/_/g, " ") + .trim() + .replace(/\b\w/g, (c) => c.toUpperCase()); +} + +function normalizeDamagedPartKeyLoose(s: string): string { + return String(s).toLowerCase().replace(/[\s_-]/g, ""); +} + +export function getDamagedPartCaptureBlob( + damagedPartsData: unknown, + partKey: string, +): unknown { + const pk = String(partKey ?? "").trim(); + if (!pk || damagedPartsData == null) return undefined; + let b = getCaptureBlob(damagedPartsData, pk); + if (b) return b; + const titleEn = legacyDamagedPartTitleEnFromPartKey(pk); + if (titleEn !== pk) { + b = getCaptureBlob(damagedPartsData, titleEn); + if (b) return b; + } + const target = normalizeDamagedPartKeyLoose(pk); + const keys = + damagedPartsData instanceof Map + ? Array.from((damagedPartsData as Map).keys()) + : Object.keys(damagedPartsData as Record); + for (const k of keys) { + if (normalizeDamagedPartKeyLoose(k) === target) { + b = getCaptureBlob(damagedPartsData, k); + if (b) return b; + } + } + return undefined; +} + +/** + * True if a capture exists for catalog/selected part key {@code partKey}, including blobs stored + * under humanized keys (e.g. {@code Left Backfender} vs {@code left_backfender}). + */ +export function hasDamagedPartCapture( + damagedPartsData: unknown, + partKey: string, +): boolean { + return getDamagedPartCaptureBlob(damagedPartsData, partKey) != null; +} + +/** + * Map a client {@code captureKey} to the canonical {@code damage.selectedParts} id when unambiguous. + */ +export function resolveCanonicalDamagedPartStorageKey( + captureKeyRaw: string, + selectedParts: string[], +): string { + const raw = String(captureKeyRaw ?? "").trim(); + if (!raw) return raw; + const parts = (selectedParts ?? []).map((p) => String(p).trim()).filter(Boolean); + if (parts.includes(raw)) return raw; + const n = normalizeDamagedPartKeyLoose(raw); + const matches = parts.filter((p) => normalizeDamagedPartKeyLoose(p) === n); + if (matches.length === 1) return matches[0]; + return raw; +}