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; vehicle: Record | null; }; /** Same Fanavaran middle-letter codes used on claim submit. */ export const FANAVARAN_PLATE_LETTER_CODE: Record = { الف: 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 = { "۰": "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 | 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 | null { if (!value || typeof value !== "object") { return null; } if (Array.isArray(value)) { return asObjectRecord(value[0]); } return value as Record; } 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 | 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 | 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 | 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, ): 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 | 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 | 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 } : {}), }; }