feat: add car inquiries with resilience, ownership privacy, and retry visibility

Introduce plate, chassis, and third-party car endpoints, a wall-clock inquiry
deadline with transport-only retries, and ownership-safe no-match errors so
another person's identity never leaks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 17:21:16 +03:30
parent b4be98b156
commit 61098f1bf4
40 changed files with 2595 additions and 374 deletions

View File

@@ -0,0 +1,323 @@
import { CarPlateSoapFields } from '../../providers/shared/centinsur-car-policy.client';
export interface NormalizedCarPolicy {
raw: Record<string, string>;
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<string, string>,
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<Record<string, string> | null>;
byNationalCode?: () => Promise<Record<string, string> | 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<string, string>, 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);
}