Files
yara724api/src/lookups/fanavaran-last-car-policy.ts
2026-09-07 15:08:08 +03:30

487 lines
13 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 { jalaliToGregorianDate } from "src/helpers/date-jalali";
import { gregorianDateInIran } from "src/helpers/iran-datetime";
import { normalizePlateText } from "src/utils/plate-normalizer/plate-normalizer.service";
export const FANAVARAN_THIRD_PARTY_LINE_ID = 5;
export const FANAVARAN_CAR_BODY_LINE_ID = 4;
export type FanavaranCarPolicyProduct = "third-party" | "car-body";
export type FanavaranPlaqueParts = {
left: string;
letter: string;
right: string;
serial: string;
};
export type FanavaranCarMatchInput = {
vin?: string;
plaque?: FanavaranPlaqueParts;
};
export type FanavaranPolicyListRow = {
PolicyId?: unknown;
InsuranceLineId?: unknown;
BeginDate?: unknown;
EndDate?: unknown;
};
export type HydratedFanavaranCarPolicy = {
policyId: number;
beginDate?: unknown;
endDate?: unknown;
vehicleId?: number | null;
policy: Record<string, unknown>;
vehicle: Record<string, unknown> | null;
};
/** Same Fanavaran middle-letter codes used on claim submit. */
export const FANAVARAN_PLATE_LETTER_CODE: Record<string, number> = {
الف: 1,
ب: 2,
پ: 3,
ج: 4,
د: 5,
س: 6,
ص: 7,
ط: 8,
ع: 9,
ق: 10,
ل: 11,
م: 12,
ن: 13,
و: 14,
ه: 15,
ی: 16,
ک: 17,
ژ: 18,
ت: 19,
ث: 20,
ز: 21,
ش: 22,
ف: 23,
گ: 24,
};
const PERSIAN_AND_ARABIC_DIGITS: Record<string, string> = {
"۰": "0",
"۱": "1",
"۲": "2",
"۳": "3",
"۴": "4",
"۵": "5",
"۶": "6",
"۷": "7",
"۸": "8",
"۹": "9",
"٠": "0",
"١": "1",
"٢": "2",
"٣": "3",
"٤": "4",
"٥": "5",
"٦": "6",
"٧": "7",
"٨": "8",
"٩": "9",
};
export function insuranceLineIdForProduct(
product: FanavaranCarPolicyProduct,
): number {
return product === "car-body"
? FANAVARAN_CAR_BODY_LINE_ID
: FANAVARAN_THIRD_PARTY_LINE_ID;
}
export function insuranceLineLabel(
insuranceLineId: number,
): "CAR_BODY" | "THIRD_PARTY" | null {
if (insuranceLineId === FANAVARAN_CAR_BODY_LINE_ID) return "CAR_BODY";
if (insuranceLineId === FANAVARAN_THIRD_PARTY_LINE_ID) return "THIRD_PARTY";
return null;
}
export function plaqueLetterFromMiddleCode(code: unknown): string | null {
const id = parseFanavaranId(code);
if (id === null) return null;
const found = Object.entries(FANAVARAN_PLATE_LETTER_CODE).find(
([, value]) => value === id,
);
return found?.[0] ?? null;
}
export type AppPlaque = {
leftTwoDigits: string;
serialLetter: string;
threeDigits: string;
rightTwoDigits: string;
};
export function toAppPlaque(
vehicle: Record<string, unknown> | null,
): AppPlaque | null {
if (!vehicle) return null;
const leftTwoDigits = normalizePlaquePart(
vehicle.PlaqueLeftNo ?? vehicle.plaqueLeftNo,
);
const threeDigits = normalizePlaquePart(
vehicle.PlaqueRightNo ?? vehicle.plaqueRightNo,
);
const rightTwoDigits = normalizePlaquePart(
vehicle.PlaqueSerial ?? vehicle.plaqueSerial,
);
const serialLetter = plaqueLetterFromMiddleCode(
vehicle.PlaqueMiddleCodeId ?? vehicle.plaqueMiddleCodeId,
);
if (!leftTwoDigits || !serialLetter || !threeDigits || !rightTwoDigits) {
return null;
}
return { leftTwoDigits, serialLetter, threeDigits, rightTwoDigits };
}
export function toEnglishDigits(value: unknown): string {
return String(value ?? "")
.split("")
.map((char) => PERSIAN_AND_ARABIC_DIGITS[char] ?? char)
.join("");
}
export function normalizeVin(value: unknown): string {
return toEnglishDigits(value).trim().toUpperCase();
}
export function normalizePlaquePart(value: unknown): string {
return toEnglishDigits(value).replace(/\s+/g, "").trim();
}
export function parseFanavaranId(value: unknown): number | null {
if (value === null || value === undefined) return null;
if (typeof value === "string" && value.trim() === "") return null;
const id = Number(toEnglishDigits(value).trim());
return Number.isFinite(id) && id > 0 ? id : null;
}
export function asObjectRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object") {
return null;
}
if (Array.isArray(value)) {
return asObjectRecord(value[0]);
}
return value as Record<string, unknown>;
}
export function asPolicyList(data: unknown): FanavaranPolicyListRow[] {
if (Array.isArray(data)) {
return data.filter(
(row): row is FanavaranPolicyListRow =>
!!row && typeof row === "object" && !Array.isArray(row),
);
}
const record = asObjectRecord(data);
if (!record) return [];
for (const key of ["value", "Value", "items", "Items", "data", "Data"]) {
const nested = record[key];
if (Array.isArray(nested)) {
return asPolicyList(nested);
}
}
return [];
}
export function filterPoliciesByLine(
policies: unknown,
insuranceLineId: number,
): FanavaranPolicyListRow[] {
return asPolicyList(policies).filter((row) => {
const line = parseFanavaranId(row.InsuranceLineId);
return line === null || line === insuranceLineId;
});
}
export function sortPoliciesNewestEndDateFirst(
policies: FanavaranPolicyListRow[],
): FanavaranPolicyListRow[] {
return [...policies].sort((left, right) => {
const leftEnd = toGregorianDate(left.EndDate) ?? "";
const rightEnd = toGregorianDate(right.EndDate) ?? "";
return rightEnd.localeCompare(leftEnd);
});
}
export function toGregorianDate(value: unknown): string | null {
return jalaliToGregorianDate(toEnglishDigits(value).trim() || null);
}
export function pickVehicleVin(vehicle: Record<string, unknown> | null): string {
if (!vehicle) return "";
for (const key of ["VIN", "Vin", "VinNo", "vin", "ChassisNo", "chassisNo"]) {
const vin = normalizeVin(vehicle[key]);
if (vin) return vin;
}
return "";
}
export function pickVehicleId(source: unknown): number | null {
const record = asObjectRecord(source);
if (!record) return parseFanavaranId(source);
for (const key of ["VehicleId", "vehicleId", "Id", "id"]) {
const id = parseFanavaranId(record[key]);
if (id !== null) return id;
}
return null;
}
/** Prefer the inquiry row whose Id matches the policy VehicleId. */
export function pickFanavaranVehicleFromInquiry(
inquired: unknown,
vehicleId?: number | null,
): Record<string, unknown> | null {
if (Array.isArray(inquired)) {
if (vehicleId != null) {
const match = inquired.find((row) => pickVehicleId(row) === vehicleId);
if (match) return asObjectRecord(match);
}
return asObjectRecord(inquired[0]);
}
return asObjectRecord(inquired);
}
export function pickVinFromPartyVehicle(party: {
vehicle?: { vin?: unknown; inquiry?: unknown };
} | null | undefined): string | null {
const direct = normalizeVin(party?.vehicle?.vin);
if (direct) return direct;
const inquiry = party?.vehicle?.inquiry;
const nested = asObjectRecord(inquiry);
const mapped = pickVehicleVin(asObjectRecord(nested?.mapped));
if (mapped) return mapped;
const raw = pickVehicleVin(asObjectRecord(nested?.raw ?? inquiry));
return raw || null;
}
export type FanavaranPlaqueLookupFields = {
kindId: number | null;
sampleId: number | null;
cityId: number | null;
leftNo: string | null;
middleCodeId: number | null;
rightNo: string | null;
serial: string | null;
plaqueNo: string | null;
};
function asPlaqueText(value: unknown): string | null {
const text = normalizePlaquePart(value);
return text || null;
}
/** Plaque lookup-filter fields from VIN inquiry / vehicle GET. */
export function pickFanavaranPlaqueLookupFields(
vehicle: Record<string, unknown> | null,
): FanavaranPlaqueLookupFields {
return {
kindId: parseFanavaranId(vehicle?.PlaqueKindId ?? vehicle?.plaqueKindId),
sampleId: parseFanavaranId(
vehicle?.PlaqueSampleId ?? vehicle?.plaqueSampleId,
),
cityId: parseFanavaranId(vehicle?.PlaqueCityId ?? vehicle?.plaqueCityId),
leftNo: asPlaqueText(vehicle?.PlaqueLeftNo ?? vehicle?.plaqueLeftNo),
middleCodeId: parseFanavaranId(
vehicle?.PlaqueMiddleCodeId ?? vehicle?.plaqueMiddleCodeId,
),
rightNo: asPlaqueText(vehicle?.PlaqueRightNo ?? vehicle?.plaqueRightNo),
serial: asPlaqueText(vehicle?.PlaqueSerial ?? vehicle?.plaqueSerial),
plaqueNo: asPlaqueText(vehicle?.PlaqueNo ?? vehicle?.plaqueNo),
};
}
function plaqueNumberEquals(left: unknown, right: unknown): boolean {
const a = normalizePlaquePart(left);
const b = normalizePlaquePart(right);
if (!a || !b) return false;
const aNum = Number(a);
const bNum = Number(b);
if (Number.isFinite(aNum) && Number.isFinite(bNum)) {
return aNum === bNum;
}
return a === b;
}
function plaqueLetterEquals(
inputLetter: string,
vehicle: Record<string, unknown>,
): boolean {
const normalizedInput = normalizePlateText(toEnglishDigits(inputLetter).trim());
if (!normalizedInput) return false;
const vehicleLetterRaw = [
vehicle.PlaqueMiddleCodeCaption,
vehicle.PlaqueLetter,
vehicle.MiddleCodeCaption,
vehicle.PlaqueMiddleCode,
]
.map((value) => normalizePlateText(String(value ?? "").trim()))
.find((value) => value && Number.isNaN(Number(value)));
if (vehicleLetterRaw && normalizePlateText(vehicleLetterRaw) === normalizedInput) {
return true;
}
const inputCode =
FANAVARAN_PLATE_LETTER_CODE[normalizedInput] ??
(Number.isFinite(Number(normalizedInput))
? Number(normalizedInput)
: null);
const vehicleCode = parseFanavaranId(
vehicle.PlaqueMiddleCodeId ?? vehicle.plaqueMiddleCodeId,
);
if (inputCode !== null && vehicleCode !== null) {
return inputCode === vehicleCode;
}
return false;
}
export function plaqueMatchesVehicle(
plaque: FanavaranPlaqueParts,
vehicle: Record<string, unknown> | null,
): boolean {
if (!vehicle) return false;
return (
plaqueNumberEquals(plaque.left, vehicle.PlaqueLeftNo ?? vehicle.plaqueLeftNo) &&
plaqueLetterEquals(plaque.letter, vehicle) &&
plaqueNumberEquals(
plaque.right,
vehicle.PlaqueRightNo ?? vehicle.plaqueRightNo,
) &&
plaqueNumberEquals(
plaque.serial,
vehicle.PlaqueSerial ?? vehicle.plaqueSerial,
)
);
}
export function vehicleMatchesCar(
vehicle: Record<string, unknown> | null,
input: FanavaranCarMatchInput,
vinVehicleId?: number | null,
): boolean {
const vin = normalizeVin(input.vin);
if (vin) {
const vehicleId = pickVehicleId(vehicle);
if (
vinVehicleId != null &&
vehicleId != null &&
vinVehicleId === vehicleId
) {
return true;
}
return pickVehicleVin(vehicle) === vin;
}
if (input.plaque) {
return plaqueMatchesVehicle(input.plaque, vehicle);
}
return false;
}
export function isPolicyActiveOn(
policy: { beginDate?: unknown; endDate?: unknown },
todayGregorian: string,
): boolean {
const begin = toGregorianDate(policy.beginDate);
const end = toGregorianDate(policy.endDate);
if (!end) return false;
if (end < todayGregorian) return false;
if (begin && begin > todayGregorian) return false;
return true;
}
/**
* Same car, several renewals: keep that car only.
* Active (EndDate overlaps today) → newest BeginDate.
* Else → latest EndDate.
*/
export function selectLastAmongCarMatches(
matches: HydratedFanavaranCarPolicy[],
todayGregorian: string = gregorianDateInIran(new Date()),
): HydratedFanavaranCarPolicy | null {
if (matches.length === 0) return null;
const active = matches.filter((match) => isPolicyActiveOn(match, todayGregorian));
const pool = active.length > 0 ? active : matches;
const useBeginDate = active.length > 0;
return pool.reduce((current, candidate) => {
const currentKey = useBeginDate
? toGregorianDate(current.beginDate) ?? ""
: toGregorianDate(current.endDate) ?? "";
const candidateKey = useBeginDate
? toGregorianDate(candidate.beginDate) ?? ""
: toGregorianDate(candidate.endDate) ?? "";
return candidateKey > currentKey ? candidate : current;
});
}
export function completePlaqueParts(input: {
plaqueLeft?: string;
plaqueLetter?: string;
plaqueRight?: string;
plaqueSerial?: string;
}): FanavaranPlaqueParts | null {
const left = normalizePlaquePart(input.plaqueLeft);
const letter = normalizePlateText(toEnglishDigits(input.plaqueLetter).trim());
const right = normalizePlaquePart(input.plaqueRight);
const serial = normalizePlaquePart(input.plaqueSerial);
if (!left || !letter || !right || !serial) {
return null;
}
return { left, letter, right, serial };
}
export function parseLastCarPolicyInput(query: {
nationalCode?: string;
vin?: string;
plaqueLeft?: string;
plaqueLetter?: string;
plaqueRight?: string;
plaqueSerial?: string;
}): {
nationalCode: string;
vin?: string;
plaque?: FanavaranPlaqueParts;
} | { error: string } {
const nationalCode = toEnglishDigits(query.nationalCode).replace(/\D/g, "");
if (!nationalCode) {
return { error: "nationalCode is required" };
}
const vin = normalizeVin(query.vin);
const plaque = completePlaqueParts(query);
const anyPlaquePart = [
query.plaqueLeft,
query.plaqueLetter,
query.plaqueRight,
query.plaqueSerial,
].some((part) => String(part ?? "").trim() !== "");
if (!vin && !plaque) {
if (anyPlaquePart) {
return {
error:
"plaque requires all four parts: plaqueLeft, plaqueLetter, plaqueRight, plaqueSerial",
};
}
return { error: "vin and/or a complete plaque is required" };
}
return {
nationalCode,
...(vin ? { vin } : {}),
...(plaque ? { plaque } : {}),
};
}