forked from Yara724/api
350 lines
9.7 KiB
TypeScript
350 lines
9.7 KiB
TypeScript
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 };
|
||
}
|