1
0
forked from Yara724/api
Files
yara724-api/src/helpers/claim-expert-resend.ts
2026-06-03 11:34:21 +03:30

324 lines
10 KiB
TypeScript

import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
import { getDamagedPartCaptureBlob } from "./claim-car-angle-media";
import {
catalogLikeKeyFromPart,
DamageSelectedPartV2,
normalizeDamageSelectedParts,
partLookupKey,
} from "./outer-damage-parts";
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
import { canonicalizeResendDocumentKey } from "./claim-resend-document-keys";
export type ExpertResendCarPartV2 = {
id?: number | null;
name: string;
side: string;
label_fa: string;
label_en?: string;
catalogKey?: string;
/** Same as `catalogKey` — kept for clients that use `key` on capture/resend APIs. */
key?: string;
captured: boolean;
url?: string;
path?: string;
fileName?: string;
};
export function resendRequestHasPayload(r: {
resendDescription?: string;
resendDocuments?: unknown[];
resendCarParts?: unknown[];
}): boolean {
if (!r) return false;
if (String(r.resendDescription || "").trim() !== "") return true;
if (Array.isArray(r.resendDocuments) && r.resendDocuments.length > 0)
return true;
if (Array.isArray(r.resendCarParts) && r.resendCarParts.length > 0)
return true;
return false;
}
/** Normalize expert-requested document keys (strings or `{ key }`) to canonical enum strings. */
export function normalizeResendDocumentKeys(
docs: unknown[] | undefined,
): string[] {
if (!Array.isArray(docs)) return [];
const keys: string[] = [];
for (const d of docs) {
if (typeof d === "string" && d.trim()) {
const c = canonicalizeResendDocumentKey(d.trim());
if (c) keys.push(c);
} else if (d && typeof d === "object" && "key" in (d as object)) {
const k = String((d as { key?: string }).key || "").trim();
const c = canonicalizeResendDocumentKey(k);
if (c) keys.push(c);
}
}
return [...new Set(keys)];
}
/** Capture / finalize lookup keys for expert-requested damaged parts. */
export function normalizeResendPartKeys(
parts: unknown[] | undefined,
carType?: ClaimVehicleTypeV2,
selectedParts?: unknown,
): string[] {
if (!Array.isArray(parts)) return [];
const selectedNorm = normalizeDamageSelectedParts(selectedParts, carType);
const keys: string[] = [];
for (const p of parts) {
const enriched =
normalizeDamageSelectedParts([p], carType)[0] ||
matchResendPartFromSelected(p, selectedNorm);
if (enriched) {
const ck =
enriched.catalogKey?.trim() || catalogLikeKeyFromPart(enriched);
if (ck) keys.push(ck);
continue;
}
if (typeof p === "string" && p.trim()) keys.push(p.trim());
else if (p && typeof p === "object" && "key" in (p as object)) {
const k = String((p as { key?: string }).key || "").trim();
if (k) keys.push(k);
}
}
return [...new Set(keys)];
}
function matchResendPartFromSelected(
raw: unknown,
selectedNorm: DamageSelectedPartV2[],
): DamageSelectedPartV2 | undefined {
if (!raw || typeof raw !== "object" || !selectedNorm.length) return undefined;
const o = raw as { id?: number };
if (typeof o.id === "number" && Number.isFinite(o.id)) {
return selectedNorm.find((sp) => sp.id === Math.trunc(o.id));
}
return undefined;
}
function resolveResendPartRow(
raw: unknown,
carType?: ClaimVehicleTypeV2,
selectedNorm?: DamageSelectedPartV2[],
): DamageSelectedPartV2 {
const fromRaw = normalizeDamageSelectedParts([raw], carType)[0];
if (fromRaw?.label_fa && fromRaw.name) return fromRaw;
const fromSelected = matchResendPartFromSelected(raw, selectedNorm ?? []);
if (fromSelected) return fromSelected;
if (fromRaw) return fromRaw;
const o = (raw && typeof raw === "object" ? raw : {}) as Record<
string,
unknown
>;
const fallbackKey =
typeof o.key === "string" && o.key.trim()
? o.key.trim()
: typeof raw === "string"
? String(raw).trim()
: "unknown";
return {
id: typeof o.id === "number" ? Math.trunc(o.id) : null,
name: typeof o.name === "string" ? o.name : fallbackKey,
side: typeof o.side === "string" ? o.side : "",
label_fa:
typeof o.label_fa === "string" && o.label_fa.trim()
? o.label_fa.trim()
: fallbackKey,
catalogKey: typeof o.catalogKey === "string" ? o.catalogKey : undefined,
};
}
/** Enrich stored resend rows for claim detail / capture UIs (label_fa, catalogKey, capture state). */
export function mapExpertResendCarPartsForClient(
rawParts: unknown[] | undefined,
options: {
carType?: ClaimVehicleTypeV2;
selectedParts?: unknown;
damagedPartsData?: unknown;
buildUrl?: (path: string) => string;
} = {},
): ExpertResendCarPartV2[] {
if (!Array.isArray(rawParts) || rawParts.length === 0) return [];
const selectedNorm = normalizeDamageSelectedParts(
options.selectedParts,
options.carType,
);
return rawParts.map((raw) => {
const stored =
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
const sp = resolveResendPartRow(raw, options.carType, selectedNorm);
const catalogKey = sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp);
const captureKey = catalogKey || partLookupKey(sp);
const cap = getDamagedPartCaptureBlob(
options.damagedPartsData,
captureKey,
selectedNorm,
) as { url?: string; path?: string; fileName?: string } | undefined;
const capturedFromStore =
typeof stored.captured === "boolean" ? stored.captured : undefined;
const url =
cap?.url ||
(cap?.path && options.buildUrl ? options.buildUrl(cap.path) : undefined);
return {
id: sp.id,
name: sp.name,
side: sp.side,
label_fa: sp.label_fa,
...(typeof stored.label_en === "string" && stored.label_en.trim()
? { label_en: stored.label_en.trim() }
: {}),
catalogKey,
key: catalogKey,
captured: capturedFromStore ?? !!cap,
...(url ? { url } : {}),
...(cap?.path ? { path: cap.path } : {}),
...(cap?.fileName ? { fileName: cap.fileName } : {}),
};
});
}
/** Persistable snapshot for `evaluation.damageExpertResend.resendCarParts`. */
export function normalizeResendCarPartsForStorage(
rawParts: unknown[] | undefined,
carType?: ClaimVehicleTypeV2,
selectedParts?: unknown,
): Array<Record<string, unknown>> {
if (!Array.isArray(rawParts)) return [];
const selectedNorm = normalizeDamageSelectedParts(selectedParts, carType);
return rawParts.map((raw) => {
const stored =
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
const sp = resolveResendPartRow(raw, carType, selectedNorm);
const catalogKey = sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp);
const row: Record<string, unknown> = {
id: sp.id,
name: sp.name,
side: sp.side,
label_fa: sp.label_fa,
catalogKey,
key: catalogKey,
captured: false,
};
if (typeof stored.label_en === "string" && stored.label_en.trim()) {
row.label_en = stored.label_en.trim();
}
return row;
});
}
function getRequiredDocEntry(claim: any, documentKey: string): unknown {
const rd = claim?.requiredDocuments;
if (!rd) return undefined;
return rd instanceof Map ? rd.get(documentKey) : rd[documentKey];
}
export function isRequiredDocumentUploaded(
claim: any,
documentKey: string,
): boolean {
const doc = getRequiredDocEntry(claim, documentKey) as
| { uploaded?: boolean }
| undefined;
return !!doc?.uploaded;
}
export function hasDamagedPartCapture(claim: any, partKey: string): boolean {
const data = claim?.media?.damagedParts;
if (!data) return false;
const carType = claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
const norm = normalizeDamageSelectedParts(
claim?.damage?.selectedParts,
carType,
claim?.damage?.selectedOuterParts,
);
return getDamagedPartCaptureBlob(data, partKey, norm) != null;
}
/**
* True when the user has satisfied every document and part the damage expert asked for,
* or when the expert only left instructions (no doc/part list) and there is a description.
*/
export function canFinalizeExpertResend(claim: any): boolean {
if (!claim) return false;
if (claim.status !== ClaimCaseStatus.WAITING_FOR_USER_RESEND) return false;
if (claim.workflow?.currentStep !== ClaimWorkflowStep.USER_EXPERT_RESEND)
return false;
const r = claim.evaluation?.damageExpertResend;
if (!r || r.fulfilledAt) return false;
const carType = claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
const docKeys = normalizeResendDocumentKeys(r.resendDocuments);
const partKeys = normalizeResendPartKeys(
r.resendCarParts,
carType,
claim?.damage?.selectedParts,
);
if (docKeys.length === 0 && partKeys.length === 0) {
return String(r.resendDescription || "").trim() !== "";
}
for (const k of docKeys) {
if (!isRequiredDocumentUploaded(claim, k)) return false;
}
for (const k of partKeys) {
if (!hasDamagedPartCapture(claim, k)) return false;
}
return true;
}
export function documentKeyAllowedForExpertResend(
claim: any,
documentKey: string,
): boolean {
const r = claim?.evaluation?.damageExpertResend;
if (!r || r.fulfilledAt) return false;
const allowed = new Set(normalizeResendDocumentKeys(r.resendDocuments));
const normalized =
canonicalizeResendDocumentKey(documentKey) ?? documentKey.trim();
return allowed.has(normalized);
}
export function partKeyAllowedForExpertResend(
claim: any,
partKey: string,
): boolean {
const r = claim?.evaluation?.damageExpertResend;
if (!r || r.fulfilledAt) return false;
const carType = claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
const selectedParts = claim?.damage?.selectedParts;
// Normalize the stored resend parts to their canonical catalog keys
const allowedKeys = new Set(
normalizeResendPartKeys(r.resendCarParts, carType, selectedParts),
);
// Normalize the incoming partKey the same way so the comparison is apples-to-apples
const resolvedKeys = normalizeResendPartKeys(
[partKey],
carType,
selectedParts,
);
const resolvedKey = resolvedKeys[0];
// Also try direct catalogKey/name match against the stored objects as fallback
const directMatch = ((r.resendCarParts as any[]) ?? []).some((p: any) => {
if (!p || typeof p !== "object") return String(p) === partKey;
return (
p.catalogKey === partKey ||
p.key === partKey ||
p.name === partKey ||
(p.id != null && String(p.id) === partKey)
);
});
return directMatch || (resolvedKey != null && allowedKeys.has(resolvedKey));
}