forked from Yara724/api
Fix damaged-parts flow bug on carAngles
This commit is contained in:
@@ -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,8 +4746,24 @@ 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 = {
|
||||||
path: file.path,
|
path: file.path,
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
74
src/helpers/claim-car-angle-media.ts
Normal file
74
src/helpers/claim-car-angle-media.ts
Normal 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)];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user