YARA-883 + Side ID fixes

This commit is contained in:
SepehrYahyaee
2026-05-18 17:14:45 +03:30
parent e1954cdb37
commit cef684e37f
12 changed files with 1234 additions and 119 deletions

View File

@@ -0,0 +1,349 @@
import {
ClaimVehicleTypeV2,
} from "src/static/outer-car-parts-catalog";
import {
catalogPartIdFromSelectedPart,
parseCatalogPartIdInput,
resolvePartForExpertReply,
type DamageSelectedPartV2,
} from "src/helpers/outer-damage-parts";
/** Canonical severity values for expert input (API). */
export type PriceDropSeverity = "Minor" | "Moderate" | "Severe";
export const PRICE_DROP_SEVERITY_FA: Record<PriceDropSeverity, string> = {
Minor: "جزیی",
Moderate: "متوسط",
Severe: "شدید",
};
export const PRICE_DROP_SEVERITY_OPTIONS = (
Object.entries(PRICE_DROP_SEVERITY_FA) as [PriceDropSeverity, string][]
).map(([value, label_fa]) => ({ value, label_fa }));
/** Part → severity → coefficient (same table as V1 expert-claim). */
export const PRICE_DROP_PART_TABLE: Record<
string,
Partial<Record<string, number>>
> = {
backFender: { Minor: 2, Moderate: 3, Severe: 5 },
backDoor: { Minor: 1, Moderate: 2, Severe: 3 },
frontDoor: { Minor: 1, Moderate: 2, Severe: 3 },
frontFender: { Minor: 1, Moderate: 2, Severe: 3 },
frontBumper: { Minor: 1, Moderate: 2, Severe: 3 },
Hood: { Minor: 2, moderate: 3, Severe: 4 },
Trunk: { minor: 1, moderate: 3, Severe: 5 },
Roof: { Minor: 3, Moderate: 5, Severe: 7 },
coil: { Minor: 2, Moderate: 3, Severe: 4 },
column: { Minor: 2, Moderate: 3, Severe: 4 },
frontTray: { Minor: 1, Moderate: 2, Severe: 3 },
backTray: { Minor: 2, moderate: 4, Severe: 5 },
frontChassis: { Minor: 3, Moderate: 5, Severe: 7 },
backChassis: { Minor: 2, Moderate: 4, Severe: 6 },
carFootrest: { Minor: 1, Moderate: 2, Severe: 3 },
carFloor: { Minor: 4, moderate: 6, Severe: 8 },
};
/** Jalali model year → year coefficient (V1). */
export const PRICE_DROP_YEAR_COEFFICIENT: Record<number, number> = {
1393: 2.05,
1394: 2.1,
1395: 2.2,
1396: 2.3,
1397: 2.4,
1398: 2.5,
1399: 2.6,
1400: 2.7,
1401: 2.8,
1402: 2.9,
1403: 3,
1404: 3,
};
const PRICE_DROP_PART_LABEL_FA: Record<string, string> = {
backFender: "گلگیر عقب",
backDoor: "در عقب",
frontDoor: "در جلو",
frontFender: "گلگیر جلو",
frontBumper: "سپر جلو",
Hood: "کاپوت",
Trunk: "صندوق عقب",
Roof: "سقف",
coil: "لوله",
column: "ستون",
frontTray: "سینی جلو",
backTray: "سینی عقب",
frontChassis: "شاسی جلو",
backChassis: "شاسی عقب",
carFootrest: "پا رکاب",
carFloor: "کف خودرو",
};
/** Catalog / damage `name` segments → price-drop part key. */
const SEGMENT_ALIASES: Record<string, string> = {
backfender: "backFender",
backdoor: "backDoor",
frontdoor: "frontDoor",
frontfender: "frontFender",
frontbumper: "frontBumper",
backbumper: "frontBumper",
carhood: "Hood",
hood: "Hood",
cartrunk: "Trunk",
trunk: "Trunk",
roof: "Roof",
coil: "coil",
column: "column",
fronttray: "frontTray",
backtray: "backTray",
frontchassis: "frontChassis",
backchassis: "backChassis",
carfootrest: "carFootrest",
carfloor: "carFloor",
};
const NORM_TO_PART_KEY = new Map<string, string>();
for (const key of Object.keys(PRICE_DROP_PART_TABLE)) {
NORM_TO_PART_KEY.set(normalizePriceDropKey(key), key);
}
for (const [seg, key] of Object.entries(SEGMENT_ALIASES)) {
NORM_TO_PART_KEY.set(seg, key);
}
export function normalizePriceDropKey(str: string): string {
return String(str ?? "")
.toLowerCase()
.replace(/[^a-z0-9]/g, "");
}
export function parsePriceDropNumber(input: number | string): number {
if (typeof input === "number" && Number.isFinite(input)) return input;
return Number(
String(input)
.replace(/[۰-۹]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 1728))
.replace(/,/g, "")
.trim(),
);
}
export function resolveSeverityCoefficient(
partKey: string,
severity: string,
): number | undefined {
const row = PRICE_DROP_PART_TABLE[partKey];
if (!row) return undefined;
const sev = String(severity ?? "").trim();
if (row[sev] !== undefined) return row[sev];
const cap = sev.charAt(0).toUpperCase() + sev.slice(1).toLowerCase();
if (row[cap] !== undefined) return row[cap];
const low = sev.toLowerCase();
if (row[low] !== undefined) return row[low];
return undefined;
}
export function calculateClaimPriceDrop(
carPrice: number | string,
carModelYear: number,
coefficients: number[],
): {
carPrice: number;
carModel: number;
carValue: number[];
sumOfSeverity: number;
coefficientYear: number;
total: number;
} {
const normalizePrice = parsePriceDropNumber(carPrice);
const coefficient = PRICE_DROP_YEAR_COEFFICIENT[carModelYear] ?? 1;
const carValue = coefficients.filter((n) => Number.isFinite(n));
const sumOfSeverity = carValue.reduce((a, c) => a + c, 0);
const total = (normalizePrice * coefficient * sumOfSeverity) / 400;
return {
carPrice: normalizePrice,
carModel: carModelYear,
carValue,
sumOfSeverity,
coefficientYear: coefficient,
total: total || 0,
};
}
export function buildPriceDropCatalogForApi(): Array<{
key: string;
label_fa: string;
severities: Record<PriceDropSeverity, number | undefined>;
}> {
return Object.keys(PRICE_DROP_PART_TABLE).map((key) => {
const row = PRICE_DROP_PART_TABLE[key];
return {
key,
label_fa: PRICE_DROP_PART_LABEL_FA[key] ?? key,
severities: {
Minor: resolveSeverityCoefficient(key, "Minor"),
Moderate: resolveSeverityCoefficient(key, "Moderate"),
Severe: resolveSeverityCoefficient(key, "Severe"),
},
};
});
}
export function resolvePriceDropPartKeyFromDamagePart(part: {
name?: string;
catalogKey?: string;
}): string | null {
const candidates: string[] = [];
if (part.catalogKey) {
for (const seg of String(part.catalogKey).split("_")) {
if (seg) candidates.push(seg);
}
}
if (part.name) candidates.push(String(part.name));
for (const raw of candidates) {
const norm = normalizePriceDropKey(raw);
const hit = NORM_TO_PART_KEY.get(norm);
if (hit && PRICE_DROP_PART_TABLE[hit]) return hit;
}
return null;
}
export function buildDamagedPartsPriceDropLines(
selectedParts: DamageSelectedPartV2[],
): Array<{
partId: number | null;
name: string;
side: string;
label_fa: string;
priceDropPartKey: string | null;
mappable: boolean;
}> {
return selectedParts.map((sp) => {
const priceDropPartKey = resolvePriceDropPartKeyFromDamagePart(sp);
return {
partId: catalogPartIdFromSelectedPart(sp),
name: sp.name,
side: sp.side,
label_fa: sp.label_fa,
priceDropPartKey,
mappable: priceDropPartKey != null,
};
});
}
/** Resolve Jalali year from linked blame party inquiry (owner party preferred). */
export function resolveVehicleModelYearFromBlame(
claim: { owner?: { userId?: unknown } },
blame: {
parties?: Array<{
role?: string;
person?: { userId?: unknown };
vehicle?: {
inquiry?: {
mapped?: { ModelField?: unknown; ModelCii?: unknown };
ModelField?: unknown;
ModelCii?: unknown;
};
};
}>;
} | null,
): number | null {
if (!blame?.parties?.length) return null;
const ownerId =
claim.owner?.userId != null ? String(claim.owner.userId) : "";
let party =
blame.parties.find(
(p) =>
ownerId &&
p.person?.userId != null &&
String(p.person.userId) === ownerId,
) ?? blame.parties.find((p) => p.role === "FIRST");
if (!party) party = blame.parties[0];
const inquiry = party?.vehicle?.inquiry;
const mapped = inquiry?.mapped ?? inquiry;
const raw =
(mapped as { ModelField?: unknown })?.ModelField ??
(mapped as { ModelCii?: unknown })?.ModelCii;
if (raw == null || raw === "") return null;
const n =
typeof raw === "number" ? raw : parseInt(String(raw).trim(), 10);
return Number.isFinite(n) ? n : null;
}
export interface PriceDropPartSeverityInput {
partId: number | string;
severity: PriceDropSeverity;
}
export function buildCoefficientsFromPartSeverities(
selectedParts: DamageSelectedPartV2[],
partSeverities: PriceDropPartSeverityInput[],
carType?: ClaimVehicleTypeV2,
): {
coefficients: number[];
lines: Array<{
partId: number;
priceDropPartKey: string;
label_fa: string;
severity: PriceDropSeverity;
coefficient: number;
}>;
errors: string[];
} {
const coefficients: number[] = [];
const lines: Array<{
partId: number;
priceDropPartKey: string;
label_fa: string;
severity: PriceDropSeverity;
coefficient: number;
}> = [];
const errors: string[] = [];
for (const row of partSeverities) {
const catalogId = parseCatalogPartIdInput(row.partId);
if (catalogId == null) {
errors.push(`Unknown partId "${row.partId}".`);
continue;
}
const sp = resolvePartForExpertReply(
catalogId,
selectedParts,
carType,
);
if (!sp) {
errors.push(`Unknown partId "${row.partId}".`);
continue;
}
const partKey = resolvePriceDropPartKeyFromDamagePart(sp);
if (!partKey) {
errors.push(
`Part "${row.partId}" (${sp.label_fa}) has no price-drop mapping.`,
);
continue;
}
const coef = resolveSeverityCoefficient(partKey, row.severity);
if (coef === undefined) {
errors.push(
`Invalid severity "${row.severity}" for part key "${partKey}".`,
);
continue;
}
coefficients.push(coef);
const linePartId = catalogPartIdFromSelectedPart(sp);
if (linePartId == null) {
errors.push(`Part "${row.partId}" has no catalog id.`);
continue;
}
lines.push({
partId: linePartId,
priceDropPartKey: partKey,
label_fa: sp.label_fa,
severity: row.severity,
coefficient: coef,
});
}
return { coefficients, lines, errors };
}