Files
yara724api/src/helpers/claim-car-angle-media.ts

182 lines
5.6 KiB
TypeScript

/**
* 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.
*/
import type { DamageSelectedPartV2 } from "src/helpers/outer-damage-parts";
import {
damageSelectedPartsToLookupStrings,
mediaRowHasCapture,
resolvePartCaptureIndex,
} from "src/helpers/outer-damage-parts";
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: unknown,
carType?: import("src/static/outer-car-parts-catalog").ClaimVehicleTypeV2,
selectedOuterPartsLegacy?: unknown,
): string[] {
const selected = new Set(
damageSelectedPartsToLookupStrings(
selectedParts,
carType,
selectedOuterPartsLegacy,
),
);
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)];
}
/** 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,
selectedNormalized?: DamageSelectedPartV2[],
): unknown {
const pk = String(partKey ?? "").trim();
if (!pk || damagedPartsData == null) return undefined;
if (Array.isArray(damagedPartsData)) {
const norm = selectedNormalized ?? [];
const idx = resolvePartCaptureIndex(
pk,
norm,
damagedPartsData as Record<string, unknown>[],
);
if (idx >= 0) {
const row = (damagedPartsData as unknown[])[idx];
if (mediaRowHasCapture(row)) return row;
}
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<string, unknown>).keys())
: Object.keys(damagedPartsData as Record<string, unknown>);
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,
selectedNormalized?: DamageSelectedPartV2[],
): boolean {
return (
getDamagedPartCaptureBlob(damagedPartsData, partKey, selectedNormalized) !=
null
);
}
/**
* Map a client {@code captureKey} to the canonical {@code damage.selectedParts} id when unambiguous.
*/
export function resolveCanonicalDamagedPartStorageKey(
captureKeyRaw: string,
selectedParts: unknown,
carType?: import("src/static/outer-car-parts-catalog").ClaimVehicleTypeV2,
selectedOuterPartsLegacy?: unknown,
): string {
const raw = String(captureKeyRaw ?? "").trim();
if (!raw) return raw;
const parts = damageSelectedPartsToLookupStrings(
selectedParts,
carType,
selectedOuterPartsLegacy,
);
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;
}