import { CarPlateSoapFields } from '../../providers/shared/centinsur-car-policy.client'; export interface NormalizedCarPolicy { raw: Record; nationalCode: string; plateKey: string; chassisNo: string; motorNo: string; vin: string; startDate?: Date; endDate?: Date; vehicleGroup: string; insuranceType: string; isZeroKm: boolean; isActive: boolean; } export interface CarPlateInput extends CarPlateSoapFields { nationalCode: string; } export function getCurrentJalaliYear(now = new Date()): number { const parts = new Intl.DateTimeFormat('en-u-ca-persian', { year: 'numeric' }).formatToParts(now); const yearPart = parts.find((part) => part.type === 'year'); return Number(yearPart?.value ?? new Date().getFullYear() - 621); } /** Persian plate letter must be present and not a placeholder. */ export function isValidPlateLetter(letter: string): boolean { const trimmed = letter.trim(); if (!trimmed || trimmed === '-' || trimmed === '0') { return false; } return /[\u0600-\u06FF]/.test(trimmed); } export function nationalIdsMatch(requested: string, fromPolicy: string): boolean { return requested.trim().padStart(10, '0') === fromPolicy.trim().padStart(10, '0'); } export function normalizeSoapCarPolicy( policy: Record, nationalCode: string, ): NormalizedCarPolicy { const plk1 = pickField(policy, ['Plk1', 'PLK1']); const plk2 = pickField(policy, ['Plk2', 'PLK2']); const plk3 = pickField(policy, ['Plk3', 'PLK3']); const plksrl = pickField(policy, ['PlkSrl', 'PlkSRL', 'Plk4']); const chassisNo = pickField(policy, ['ChassisNo', 'ShasiNo', 'VIN', 'Vin']); const motorNo = pickField(policy, ['MotorNo', 'Motor']); const vin = pickField(policy, ['VIN', 'Vin', 'ChassisNo']); const vehicleGroup = pickField(policy, [ 'CarGrpCod', 'VhcleGrpCd', 'VhcleGrp', 'KbTyp', 'VehicleGroup', ]); const insuranceType = pickField(policy, ['InsurTy', 'InsurTyp', 'PlcyTyp', 'InsuranceType']); const policyNationalCode = pickField(policy, ['NtnlId', 'NationalId', 'NationalCode']) || nationalCode; const startDate = parsePolicyDate( pickField(policy, [ 'HBgnDte', 'PlcyBegnDt', 'BgnDt', 'PolicyBeginDate', 'StartDate', ]), ); const endDate = parsePolicyDate( pickField(policy, ['HEndDte', 'PlcyEndDt', 'EndDt', 'PolicyEndDate', 'EndDate']), ); const plateKey = [plk1, plk2, plk3, plksrl].filter(Boolean).join('-'); const isZeroKm = isZeroKmPlate(plk1, plk2, plk3, plksrl); const now = new Date(); return { raw: policy, nationalCode: policyNationalCode, plateKey, chassisNo, motorNo, vin, startDate, endDate, vehicleGroup, insuranceType, isZeroKm, isActive: Boolean(startDate && endDate && startDate <= now && endDate >= now), }; } export function isPassengerVehicleGroup(group: string): boolean { const normalized = group.trim(); return normalized === '2' || normalized === '3' || normalized === '02' || normalized === '03'; } export function overlapsCurrentJalaliYear( policy: NormalizedCarPolicy, jalaliYear = getCurrentJalaliYear(), ): boolean { if (!policy.startDate || !policy.endDate) { return true; } const yearStart = jalaliYearStartGregorian(jalaliYear); const yearEnd = jalaliYearStartGregorian(jalaliYear + 1); return policy.startDate < yearEnd && policy.endDate >= yearStart; } export function isAcceptableThirdPartyPolicy( policy: NormalizedCarPolicy, requestedNationalCode: string, ): boolean { if (!nationalIdsMatch(requestedNationalCode, policy.nationalCode)) { return false; } if (policy.vehicleGroup && !isPassengerVehicleGroup(policy.vehicleGroup)) { return false; } if (!overlapsCurrentJalaliYear(policy)) { return false; } return true; } export function sameVehicleIdentity(a: NormalizedCarPolicy, b: NormalizedCarPolicy): boolean { const aChassis = normalizeIdentity(a.chassisNo || a.vin); const bChassis = normalizeIdentity(b.chassisNo || b.vin); if (aChassis && bChassis) { return aChassis === bChassis; } const aMotor = normalizeIdentity(a.motorNo); const bMotor = normalizeIdentity(b.motorNo); if (aMotor && bMotor) { return aMotor === bMotor; } return a.plateKey !== '' && a.plateKey === b.plateKey; } export function selectBestThirdPartyPolicy( policies: NormalizedCarPolicy[], requestedNationalCode: string, ): NormalizedCarPolicy | null { const acceptable = policies.filter((policy) => isAcceptableThirdPartyPolicy(policy, requestedNationalCode), ); if (acceptable.length === 0) { return null; } const active = acceptable.filter((policy) => policy.isActive); const pool = active.length > 0 ? active : acceptable; return pool.sort((left, right) => { const leftStart = left.startDate?.getTime() ?? 0; const rightStart = right.startDate?.getTime() ?? 0; return rightStart - leftStart; })[0]!; } export function mergePlateAndNationalPolicies( platePolicy: NormalizedCarPolicy | null, nationalPolicy: NormalizedCarPolicy | null, ): NormalizedCarPolicy | null { if (!platePolicy && !nationalPolicy) { return null; } if (!platePolicy) { return nationalPolicy; } if (!nationalPolicy) { return platePolicy; } if (sameVehicleIdentity(platePolicy, nationalPolicy)) { return platePolicy.startDate && nationalPolicy.startDate ? platePolicy.startDate >= nationalPolicy.startDate ? platePolicy : nationalPolicy : platePolicy; } if (nationalPolicy.isZeroKm) { return nationalPolicy.startDate && platePolicy.startDate ? nationalPolicy.startDate >= platePolicy.startDate ? nationalPolicy : platePolicy : nationalPolicy; } return platePolicy; } export interface ThirdPartyCarLookup { byPlate?: () => Promise | null>; byNationalCode?: () => Promise | null>; } export async function resolveThirdPartyCarPolicy( input: CarPlateInput, lookup: ThirdPartyCarLookup, ): Promise<{ policy: NormalizedCarPolicy; sources: string[] } | null> { const plateLetterValid = isValidPlateLetter(input.plk2); const sources: string[] = []; const platePromise = plateLetterValid && lookup.byPlate ? lookup.byPlate().catch(() => null) : Promise.resolve(null); const nationalPromise = lookup.byNationalCode ? lookup.byNationalCode().catch(() => null) : Promise.resolve(null); // Parallel so dual SOAP inquiries share wall-clock time under the inquiry deadline. const [plateRaw, nationalRaw] = await Promise.all([platePromise, nationalPromise]); const platePolicy = plateRaw ? normalizeSoapCarPolicy(plateRaw, input.nationalCode) : null; const nationalPolicy = nationalRaw ? normalizeSoapCarPolicy(nationalRaw, input.nationalCode) : null; if (platePolicy) { sources.push('plate'); } if (nationalPolicy) { sources.push('nationalCode'); } let merged = mergePlateAndNationalPolicies( platePolicy && isAcceptableThirdPartyPolicy(platePolicy, input.nationalCode) ? platePolicy : null, nationalPolicy && isAcceptableThirdPartyPolicy(nationalPolicy, input.nationalCode) ? nationalPolicy : null, ); if (!merged && plateLetterValid && lookup.byPlate && !platePolicy) { try { const raw = await lookup.byPlate(); if (raw) { const fallback = normalizeSoapCarPolicy(raw, input.nationalCode); merged = selectBestThirdPartyPolicy([fallback], input.nationalCode); if (merged) { sources.push('plateFallback'); } } } catch { merged = null; } } if (!merged) { return null; } return { policy: merged, sources }; } function pickField(policy: Record, keys: string[]): string { for (const key of keys) { const value = policy[key]?.trim(); if (value) { return value; } } return ''; } function parsePolicyDate(value: string): Date | undefined { const trimmed = value.trim(); if (!trimmed) { return undefined; } // Jalali dates from CentInsur: 1405/05/23 const jalaliSlash = /^(\d{4})\/(\d{1,2})\/(\d{1,2})$/.exec(trimmed); if (jalaliSlash) { const year = Number(jalaliSlash[1]); const month = Number(jalaliSlash[2]); const day = Number(jalaliSlash[3]); // Approximate Gregorian for overlap checks (Jalali year + 621, month/day as-is). // Good enough for "overlaps current Jalali year" when both ends use the same scheme. if (year > 1300 && year < 1600) { const date = new Date(year + 621, month - 1, day); return Number.isNaN(date.getTime()) ? undefined : date; } } const compact = trimmed.replace(/[^\d]/g, ''); if (compact.length === 8) { const year = Number(compact.slice(0, 4)); const month = Number(compact.slice(4, 6)); const day = Number(compact.slice(6, 8)); if (year > 1300 && year < 1600) { const date = new Date(year + 621, month - 1, day); return Number.isNaN(date.getTime()) ? undefined : date; } const date = new Date(year, month - 1, day); return Number.isNaN(date.getTime()) ? undefined : date; } const parsed = new Date(trimmed); return Number.isNaN(parsed.getTime()) ? undefined : parsed; } function isZeroKmPlate(plk1: string, plk2: string, plk3: string, plksrl: string): boolean { return !plk1.trim() && !plk2.trim() && !plk3.trim() && !plksrl.trim(); } function normalizeIdentity(value: string): string { return value.trim().toUpperCase(); } function jalaliYearStartGregorian(jalaliYear: number): Date { const march = 20; const gregorianYear = jalaliYear + 621; return new Date(gregorianYear, 2, march); }