1
0
forked from Yara724/api

Merge pull request 'Fix damaged-parts flow bug on carAngles' (#51) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#51
This commit is contained in:
2026-05-01 14:45:39 +03:30
3 changed files with 143 additions and 8 deletions

View File

@@ -84,6 +84,13 @@ import {
normalizeResendPartKeys, normalizeResendPartKeys,
partKeyAllowedForExpertResend, partKeyAllowedForExpertResend,
} from "src/helpers/claim-expert-resend"; } from "src/helpers/claim-expert-resend";
import {
canonicalizeClaimCarAngleKey,
getClaimCarAngleCaptureBlob,
hasClaimCarAngleCapture,
legacyAngleDamagedPartFieldKeysToUnset,
type ClaimCarAngleKey,
} from "src/helpers/claim-car-angle-media";
import { import {
ClaimVehicleTypeV2, ClaimVehicleTypeV2,
OUTER_PARTS_BY_CAR_TYPE, OUTER_PARTS_BY_CAR_TYPE,
@@ -4307,7 +4314,11 @@ export class ClaimRequestManagementService {
key: angle.key, key: angle.key,
label_fa: angle.label_fa, label_fa: angle.label_fa,
label_en: angle.label_en, 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 // Damaged parts from selected parts
@@ -4735,7 +4746,23 @@ export class ClaimRequestManagementService {
`Part "${body.captureKey}" was not requested by the damage expert for this resend.`, `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 fileUrl = buildFileLink(file.path);
const captureData = { const captureData = {
@@ -4748,7 +4775,7 @@ export class ClaimRequestManagementService {
const updateData: any = { const updateData: any = {
$push: { $push: {
history: { history: {
type: body.captureType === 'angle' ? 'ANGLE_CAPTURED' : 'PART_CAPTURED', type: treatAsAngle ? "ANGLE_CAPTURED" : "PART_CAPTURED",
actor: { actor: {
actorId: new Types.ObjectId(currentUserId), actorId: new Types.ObjectId(currentUserId),
actorName: claimCase.owner?.fullName || 'User', actorName: claimCase.owner?.fullName || 'User',
@@ -4758,19 +4785,33 @@ export class ClaimRequestManagementService {
metadata: { metadata: {
captureType: body.captureType, captureType: body.captureType,
captureKey: body.captureKey, captureKey: body.captureKey,
...(treatAsAngle && angleCanon
? { resolvedCarAngleKey: angleCanon }
: {}),
fileName: file.filename, fileName: file.filename,
}, },
}, },
}, },
}; };
if (body.captureType === 'angle') { const selectedBefore = (claimCase.damage?.selectedParts || []) as string[];
updateData[`media.carAngles.${body.captureKey}`] = captureData; 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 { } else {
updateData[`media.damagedParts.${body.captureKey}`] = captureData; updateData[`media.damagedParts.${body.captureKey}`] = captureData;
} }
const selectedBefore = (claimCase.damage?.selectedParts || []) as string[];
if ( if (
isResendCapture && isResendCapture &&
body.captureType === "part" && body.captureType === "part" &&
@@ -4816,7 +4857,13 @@ export class ClaimRequestManagementService {
const damagedPartsData = updatedClaim?.media?.damagedParts as any; const damagedPartsData = updatedClaim?.media?.damagedParts as any;
const selectedParts = updatedClaim?.damage?.selectedParts || []; 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 partsCaptured = selectedParts.filter(p => hasCapture(damagedPartsData, p)).length;
const allCapturesComplete = const allCapturesComplete =
@@ -5422,9 +5469,14 @@ export class ClaimRequestManagementService {
} }
const carAnglesData = claim.media?.carAngles as any; const carAnglesData = claim.media?.carAngles as any;
const damagedPartsForAngles = claim.media?.damagedParts as any;
const carAngles: Record<string, { captured: boolean; url?: string }> = {}; const carAngles: Record<string, { captured: boolean; url?: string }> = {};
for (const k of ['front', 'back', 'left', 'right']) { 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] = { carAngles[k] = {
captured: !!cap, captured: !!cap,
url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined), url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined),

View File

@@ -65,6 +65,10 @@ import {
enrichBlamePartiesForAgreementView, enrichBlamePartiesForAgreementView,
} from "src/helpers/blame-party-agreement-decision"; } from "src/helpers/blame-party-agreement-decision";
import { resendRequestHasPayload } from "src/helpers/claim-expert-resend"; 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 { snapshotFromDamageExpert } from "src/helpers/expert-profile-snapshot";
import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema"; import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema";
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service"; import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
@@ -3263,9 +3267,14 @@ export class ExpertClaimService {
// Build car angles map // Build car angles map
const carAnglesData = claim.media?.carAngles as any; const carAnglesData = claim.media?.carAngles as any;
const damagedPartsDataForAngles = claim.media?.damagedParts as any;
const carAngles: Record<string, { captured: boolean; url?: string }> = {}; const carAngles: Record<string, { captured: boolean; url?: string }> = {};
for (const k of ['front', 'back', 'left', 'right']) { 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] = { carAngles[k] = {
captured: !!cap, captured: !!cap,
url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined), url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined),

View File

@@ -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<string, unknown>).get(key);
return (data as Record<string, unknown>)[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)];
}