From d9dc4ecdffb613a015eed7662cf125e5072828d8 Mon Sep 17 00:00:00 2001 From: "s.yahyaee" Date: Fri, 1 May 2026 14:44:28 +0330 Subject: [PATCH] Fix damaged-parts flow bug on carAngles --- .../claim-request-management.service.ts | 66 +++++++++++++++-- src/expert-claim/expert-claim.service.ts | 11 ++- src/helpers/claim-car-angle-media.ts | 74 +++++++++++++++++++ 3 files changed, 143 insertions(+), 8 deletions(-) create mode 100644 src/helpers/claim-car-angle-media.ts diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 228c722..8ce7071 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -84,6 +84,13 @@ import { normalizeResendPartKeys, partKeyAllowedForExpertResend, } from "src/helpers/claim-expert-resend"; +import { + canonicalizeClaimCarAngleKey, + getClaimCarAngleCaptureBlob, + hasClaimCarAngleCapture, + legacyAngleDamagedPartFieldKeysToUnset, + type ClaimCarAngleKey, +} from "src/helpers/claim-car-angle-media"; import { ClaimVehicleTypeV2, OUTER_PARTS_BY_CAR_TYPE, @@ -4307,7 +4314,11 @@ export class ClaimRequestManagementService { key: angle.key, label_fa: angle.label_fa, label_en: angle.label_en, - captured: !!hasCapture(claimCase.media?.carAngles as any, angle.key), + captured: hasClaimCarAngleCapture( + claimCase.media?.carAngles as any, + claimCase.media?.damagedParts as any, + angle.key as ClaimCarAngleKey, + ), })); // Damaged parts from selected parts @@ -4735,8 +4746,24 @@ export class ClaimRequestManagementService { `Part "${body.captureKey}" was not requested by the damage expert for this resend.`, ); } + if (canonicalizeClaimCarAngleKey(body.captureKey)) { + throw new BadRequestException( + "During expert resend only damaged-part photos are allowed, not car angles (front/back/left/right).", + ); + } } + const angleCanon = canonicalizeClaimCarAngleKey(body.captureKey); + if (body.captureType === "angle" && !angleCanon) { + throw new BadRequestException( + `Invalid angle captureKey "${body.captureKey}". Use one of: front, back, left, right.`, + ); + } + const treatAsAngle = + !isResendCapture && + (body.captureType === "angle" || + (body.captureType === "part" && angleCanon !== null)); + const fileUrl = buildFileLink(file.path); const captureData = { path: file.path, @@ -4748,7 +4775,7 @@ export class ClaimRequestManagementService { const updateData: any = { $push: { history: { - type: body.captureType === 'angle' ? 'ANGLE_CAPTURED' : 'PART_CAPTURED', + type: treatAsAngle ? "ANGLE_CAPTURED" : "PART_CAPTURED", actor: { actorId: new Types.ObjectId(currentUserId), actorName: claimCase.owner?.fullName || 'User', @@ -4758,19 +4785,33 @@ export class ClaimRequestManagementService { metadata: { captureType: body.captureType, captureKey: body.captureKey, + ...(treatAsAngle && angleCanon + ? { resolvedCarAngleKey: angleCanon } + : {}), fileName: file.filename, }, }, }, }; - if (body.captureType === 'angle') { - updateData[`media.carAngles.${body.captureKey}`] = captureData; + const selectedBefore = (claimCase.damage?.selectedParts || []) as string[]; + if (treatAsAngle && angleCanon) { + updateData[`media.carAngles.${angleCanon}`] = captureData; + const unsetFieldKeys = legacyAngleDamagedPartFieldKeysToUnset( + angleCanon, + body.captureKey, + selectedBefore, + ); + if (unsetFieldKeys.length) { + updateData.$unset = {}; + for (const k of unsetFieldKeys) { + updateData.$unset[`media.damagedParts.${k}`] = ""; + } + } } else { updateData[`media.damagedParts.${body.captureKey}`] = captureData; } - const selectedBefore = (claimCase.damage?.selectedParts || []) as string[]; if ( isResendCapture && body.captureType === "part" && @@ -4816,7 +4857,13 @@ export class ClaimRequestManagementService { const damagedPartsData = updatedClaim?.media?.damagedParts as any; const selectedParts = updatedClaim?.damage?.selectedParts || []; - const anglesCaptured = anglesKeys.filter(k => hasCapture(carAnglesData, k)).length; + const anglesCaptured = anglesKeys.filter((k) => + hasClaimCarAngleCapture( + carAnglesData, + damagedPartsData, + k as ClaimCarAngleKey, + ), + ).length; const partsCaptured = selectedParts.filter(p => hasCapture(damagedPartsData, p)).length; const allCapturesComplete = @@ -5422,9 +5469,14 @@ export class ClaimRequestManagementService { } const carAnglesData = claim.media?.carAngles as any; + const damagedPartsForAngles = claim.media?.damagedParts as any; const carAngles: Record = {}; for (const k of ['front', 'back', 'left', 'right']) { - const cap = hasCapture(carAnglesData, k); + const cap = getClaimCarAngleCaptureBlob( + carAnglesData, + damagedPartsForAngles, + k as ClaimCarAngleKey, + ) as { url?: string; path?: string } | undefined; carAngles[k] = { captured: !!cap, url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined), diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index d1c3b3a..f960a0f 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -65,6 +65,10 @@ import { enrichBlamePartiesForAgreementView, } from "src/helpers/blame-party-agreement-decision"; import { resendRequestHasPayload } from "src/helpers/claim-expert-resend"; +import { + getClaimCarAngleCaptureBlob, + type ClaimCarAngleKey, +} from "src/helpers/claim-car-angle-media"; import { snapshotFromDamageExpert } from "src/helpers/expert-profile-snapshot"; import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema"; import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service"; @@ -3263,9 +3267,14 @@ export class ExpertClaimService { // Build car angles map const carAnglesData = claim.media?.carAngles as any; + const damagedPartsDataForAngles = claim.media?.damagedParts as any; const carAngles: Record = {}; for (const k of ['front', 'back', 'left', 'right']) { - const cap = hasCapture(carAnglesData, k); + const cap = getClaimCarAngleCaptureBlob( + carAnglesData, + damagedPartsDataForAngles, + k as ClaimCarAngleKey, + ) as { url?: string; path?: string } | undefined; carAngles[k] = { captured: !!cap, url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined), diff --git a/src/helpers/claim-car-angle-media.ts b/src/helpers/claim-car-angle-media.ts new file mode 100644 index 0000000..3eb3145 --- /dev/null +++ b/src/helpers/claim-car-angle-media.ts @@ -0,0 +1,74 @@ +/** + * Car-angle captures must live under {@code media.carAngles} with lowercase keys + * {@code front | back | left | right}. Clients sometimes send Title Case keys or + * mistakenly use {@code captureType: "part"} for angles, which previously wrote + * into {@code media.damagedParts} and broke step-completion checks. + */ + +export const CLAIM_CAR_ANGLE_KEYS = ["front", "back", "left", "right"] as const; +export type ClaimCarAngleKey = (typeof CLAIM_CAR_ANGLE_KEYS)[number]; + +export function canonicalizeClaimCarAngleKey( + raw: string | undefined | null, +): ClaimCarAngleKey | null { + const t = String(raw ?? "").trim().toLowerCase(); + return (CLAIM_CAR_ANGLE_KEYS as readonly string[]).includes(t) + ? (t as ClaimCarAngleKey) + : null; +} + +function getCaptureBlob(data: unknown, key: string): unknown { + if (data == null) return undefined; + if (data instanceof Map) return (data as Map).get(key); + return (data as Record)[key]; +} + +/** Prefer carAngles; fall back to legacy mis-placed blobs under damagedParts. */ +export function getClaimCarAngleCaptureBlob( + carAnglesData: unknown, + damagedPartsData: unknown, + angle: ClaimCarAngleKey, +): unknown { + return ( + getCaptureBlob(carAnglesData, angle) ?? + getCaptureBlob(damagedPartsData, angle) ?? + getCaptureBlob( + damagedPartsData, + angle.charAt(0).toUpperCase() + angle.slice(1), + ) + ); +} + +export function hasClaimCarAngleCapture( + carAnglesData: unknown, + damagedPartsData: unknown, + angle: ClaimCarAngleKey, +): boolean { + return getClaimCarAngleCaptureBlob(carAnglesData, damagedPartsData, angle) != null; +} + +/** + * Field names under media.damagedParts that should be removed when we persist the + * same shot under media.carAngles (migration). Skips keys that are also real selected part ids. + */ +export function legacyAngleDamagedPartFieldKeysToUnset( + canonical: ClaimCarAngleKey, + rawCaptureKey: string, + selectedParts: string[], +): string[] { + const selected = new Set((selectedParts ?? []).map((p) => String(p))); + const variants = new Set( + [ + rawCaptureKey, + canonical, + canonical.charAt(0).toUpperCase() + canonical.slice(1), + ].filter(Boolean), + ); + const out: string[] = []; + for (const k of variants) { + if (canonicalizeClaimCarAngleKey(k) === null) continue; + if (selected.has(k)) continue; + out.push(k); + } + return [...new Set(out)]; +}