Files
yara724api/src/helpers/claim-price-drop.ts
2026-05-18 17:14:45 +03:30

350 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
}