forked from Yara724/api
YARA-883 + Side ID fixes
This commit is contained in:
349
src/helpers/claim-price-drop.ts
Normal file
349
src/helpers/claim-price-drop.ts
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user