forked from Yara724/api
773 lines
22 KiB
TypeScript
773 lines
22 KiB
TypeScript
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;
|
||
}
|
||
|
||
/** Live Fanavaran / cache id wins; tenant `fanavaranClientConfigs.defaults` if the API returned nothing. */
|
||
export function fanavaranIdOrConfigDefault(
|
||
resolved: unknown,
|
||
configDefault: number,
|
||
): number {
|
||
return parseFanavaranId(resolved) ?? configDefault;
|
||
}
|
||
|
||
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);
|
||
}
|
||
const withPlaqueKind = inquired.find(
|
||
(row) =>
|
||
parseFanavaranId(asObjectRecord(row)?.PlaqueKindId) != null ||
|
||
parseFanavaranId(asObjectRecord(row)?.plaqueKindId) != null,
|
||
);
|
||
if (withPlaqueKind) return asObjectRecord(withPlaqueKind);
|
||
return asObjectRecord(inquired[0]);
|
||
}
|
||
return asObjectRecord(inquired);
|
||
}
|
||
|
||
export type FanavaranDamagedVehicleIdentity = {
|
||
vin?: string | null;
|
||
chassis?: string | null;
|
||
plaque?: FanavaranPlaqueParts | null;
|
||
policyCINumber?: string | null;
|
||
vehicleKindId?: number | null;
|
||
};
|
||
|
||
export function asVehicleInquiryRows(
|
||
inquired: unknown,
|
||
): Record<string, unknown>[] {
|
||
if (Array.isArray(inquired)) {
|
||
return inquired
|
||
.map((row) => asObjectRecord(row))
|
||
.filter((row): row is Record<string, unknown> => row != null);
|
||
}
|
||
const one = asObjectRecord(inquired);
|
||
return one ? [one] : [];
|
||
}
|
||
|
||
export function normalizePolicyCINumber(value: unknown): string {
|
||
return toEnglishDigits(value).replace(/\s+/g, "").trim();
|
||
}
|
||
|
||
function vehicleHasPlaqueParts(vehicle: Record<string, unknown>): boolean {
|
||
return Boolean(
|
||
asPlaqueText(vehicle.PlaqueLeftNo ?? vehicle.plaqueLeftNo) &&
|
||
(parseFanavaranId(
|
||
vehicle.PlaqueMiddleCodeId ?? vehicle.plaqueMiddleCodeId,
|
||
) != null ||
|
||
String(vehicle.PlaqueLetter ?? vehicle.plaqueLetter ?? "").trim()) &&
|
||
asPlaqueText(vehicle.PlaqueRightNo ?? vehicle.plaqueRightNo) &&
|
||
asPlaqueText(vehicle.PlaqueSerial ?? vehicle.plaqueSerial),
|
||
);
|
||
}
|
||
|
||
function vinEquals(left: unknown, right: unknown): boolean {
|
||
const a = normalizeVin(left);
|
||
const b = normalizeVin(right);
|
||
return Boolean(a && b && a === b);
|
||
}
|
||
|
||
/**
|
||
* Same physical car: VIN and plaque must not contradict the damaged-party
|
||
* inquiry. A row that only "has PlaqueKindId" is not enough.
|
||
*/
|
||
export function fanavaranVehicleMatchesDamagedIdentity(
|
||
vehicle: Record<string, unknown> | null,
|
||
identity: FanavaranDamagedVehicleIdentity,
|
||
): boolean {
|
||
if (!vehicle) return false;
|
||
|
||
const identityVin = normalizeVin(identity.vin);
|
||
const identityChassis = normalizeVin(identity.chassis);
|
||
const vehicleVin = pickVehicleVin(vehicle);
|
||
const vehicleChassis = normalizeVin(
|
||
vehicle.ChassisNo ?? vehicle.chassisNo ?? vehicle.ShsNum,
|
||
);
|
||
|
||
const vinMatched = Boolean(
|
||
identityVin &&
|
||
(vinEquals(vehicleVin, identityVin) ||
|
||
vinEquals(vehicleChassis, identityVin)),
|
||
);
|
||
const chassisMatched = Boolean(
|
||
identityChassis &&
|
||
(vinEquals(vehicleVin, identityChassis) ||
|
||
vinEquals(vehicleChassis, identityChassis)),
|
||
);
|
||
|
||
if (identityVin && vehicleVin && !vinMatched && !chassisMatched) {
|
||
return false;
|
||
}
|
||
|
||
const plaqueMatched = Boolean(
|
||
identity.plaque && plaqueMatchesVehicle(identity.plaque, vehicle),
|
||
);
|
||
if (identity.plaque && vehicleHasPlaqueParts(vehicle) && !plaqueMatched) {
|
||
return false;
|
||
}
|
||
|
||
return vinMatched || chassisMatched || plaqueMatched;
|
||
}
|
||
|
||
export function fanavaranPayloadMatchesDamagedIdentity(
|
||
payload: Record<string, unknown> | null,
|
||
identity: FanavaranDamagedVehicleIdentity,
|
||
): boolean {
|
||
if (!payload) return false;
|
||
|
||
const payloadVin = normalizeVin(payload.VIN ?? payload.vin);
|
||
const identityVin = normalizeVin(identity.vin);
|
||
if (payloadVin && identityVin && payloadVin !== identityVin) return false;
|
||
|
||
const payloadChassis = normalizeVin(
|
||
payload.ChassisNo ?? payload.chassisNo,
|
||
);
|
||
const identityChassis = normalizeVin(identity.chassis);
|
||
if (payloadChassis && identityChassis && payloadChassis !== identityChassis) {
|
||
return false;
|
||
}
|
||
|
||
const payloadCI = normalizePolicyCINumber(payload.PolicyCINumber);
|
||
const identityCI = normalizePolicyCINumber(identity.policyCINumber);
|
||
if (payloadCI && identityCI && payloadCI !== identityCI) return false;
|
||
|
||
if (identity.plaque && vehicleHasPlaqueParts(payload)) {
|
||
if (!plaqueMatchesVehicle(identity.plaque, payload)) return false;
|
||
}
|
||
|
||
const vinMatched = Boolean(payloadVin && identityVin && payloadVin === identityVin);
|
||
const chassisMatched = Boolean(
|
||
payloadChassis && identityChassis && payloadChassis === identityChassis,
|
||
);
|
||
const ciMatched = Boolean(payloadCI && identityCI && payloadCI === identityCI);
|
||
const plaqueMatched = Boolean(
|
||
identity.plaque && plaqueMatchesVehicle(identity.plaque, payload),
|
||
);
|
||
return vinMatched || chassisMatched || ciMatched || plaqueMatched;
|
||
}
|
||
|
||
export function canReuseCachedFanavaranVehicleIds(source: unknown): boolean {
|
||
return source === "vehicle-inquiry" || source === "vin-inquiry" || source === "vehicle-get";
|
||
}
|
||
|
||
/** Proven Parsian national (چهار تکه) plaque codebook ids. */
|
||
export const FANAVARAN_NATIONAL_PLAQUE_KIND_ID = 8;
|
||
export const FANAVARAN_NATIONAL_PLAQUE_SAMPLE_ID = 10;
|
||
|
||
export const FANAVARAN_DAMAGE_MANUAL_OVERRIDE_KEYS = [
|
||
"Desc",
|
||
"EstimateAmount",
|
||
"LicenceNo",
|
||
"LicenceIssuDate",
|
||
"DriverIsOwner",
|
||
] as const;
|
||
|
||
export const FANAVARAN_EXPERTISE_MANUAL_OVERRIDE_KEYS = [
|
||
"RepairWage",
|
||
"ComponentReplacementCost",
|
||
"WasteValue",
|
||
"DmgAssessmentDate",
|
||
"InspectionTime",
|
||
"DamagedVehicleCurrentPrice",
|
||
"DropAmountAdditionsDeductions",
|
||
] as const;
|
||
|
||
export const FANAVARAN_BASE_MANUAL_OVERRIDE_KEYS = [
|
||
"EstimateAmount",
|
||
"CulpritLicenceNo",
|
||
"CulpritLicenceIssuDate",
|
||
"AccidentLocationAddress",
|
||
"AccidentDate",
|
||
"AnnouncementDate",
|
||
"DocReceivedDate",
|
||
"AccidentTime",
|
||
"PoliceReportDesc",
|
||
"PoliceReportSeri",
|
||
"PoliceReportSerial",
|
||
] as const;
|
||
|
||
/**
|
||
* Swagger POST body is optional user-entry tweaks only.
|
||
* Inquiry / lookup-filter ids always come from the live rebuilt payload.
|
||
*/
|
||
export function applyFanavaranManualPayloadOverrides(
|
||
built: Record<string, unknown>,
|
||
body: Record<string, unknown> | null | undefined,
|
||
keys: readonly string[],
|
||
): Record<string, unknown> {
|
||
if (!body) return built;
|
||
const next = { ...built };
|
||
for (const key of keys) {
|
||
if (!Object.prototype.hasOwnProperty.call(body, key)) continue;
|
||
if (body[key] === undefined) continue;
|
||
next[key] = body[key];
|
||
}
|
||
return next;
|
||
}
|
||
|
||
/**
|
||
* Plaque kind/sample only from a matched Fanavaran vehicle record.
|
||
* CII/ESG inquiry, unsourced cache, and tenant defaults of 15 must not win.
|
||
* National 4-part plates fall back to 8/10 (lookup-filter safe on Parsian).
|
||
*/
|
||
export function resolveDamageCasePlaqueLookupIds(matchedVehicle: Record<
|
||
string,
|
||
unknown
|
||
> | null): {
|
||
kindId: number;
|
||
sampleId: number;
|
||
source: "vehicle-inquiry" | "national-plate-default";
|
||
} {
|
||
const plaque = pickFanavaranPlaqueLookupFields(matchedVehicle);
|
||
if (plaque.kindId != null) {
|
||
return {
|
||
kindId: plaque.kindId,
|
||
sampleId: plaque.sampleId ?? FANAVARAN_NATIONAL_PLAQUE_SAMPLE_ID,
|
||
source: "vehicle-inquiry",
|
||
};
|
||
}
|
||
return {
|
||
kindId: FANAVARAN_NATIONAL_PLAQUE_KIND_ID,
|
||
sampleId: FANAVARAN_NATIONAL_PLAQUE_SAMPLE_ID,
|
||
source: "national-plate-default",
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Pick the VIN-inquiry / vehicle-GET row that is the damaged car.
|
||
* Newest VersionNo wins among matches; PlaqueKindId is a tie-break only.
|
||
*/
|
||
export function selectFanavaranVehicleForDamageCase(
|
||
inquired: unknown,
|
||
identity: FanavaranDamagedVehicleIdentity,
|
||
): Record<string, unknown> | null {
|
||
const matches = asVehicleInquiryRows(inquired).filter((row) =>
|
||
fanavaranVehicleMatchesDamagedIdentity(row, identity),
|
||
);
|
||
if (matches.length === 0) return null;
|
||
|
||
return matches.reduce((current, candidate) => {
|
||
const currentVersion = parseFanavaranId(current.VersionNo) ?? 0;
|
||
const candidateVersion = parseFanavaranId(candidate.VersionNo) ?? 0;
|
||
if (candidateVersion !== currentVersion) {
|
||
return candidateVersion > currentVersion ? candidate : current;
|
||
}
|
||
const currentHasKind =
|
||
pickFanavaranPlaqueLookupFields(current).kindId != null;
|
||
const candidateHasKind =
|
||
pickFanavaranPlaqueLookupFields(candidate).kindId != null;
|
||
if (candidateHasKind && !currentHasKind) return candidate;
|
||
return current;
|
||
});
|
||
}
|
||
|
||
export function pickVinFromPartyVehicle(party: {
|
||
vehicle?: { vin?: unknown; inquiry?: unknown };
|
||
} | null | undefined): string | null {
|
||
return collectPartyVinCandidates(party)[0] ?? null;
|
||
}
|
||
|
||
/**
|
||
* VIN first (mapped/raw VIN fields), then party.vehicle.vin, then chassis.
|
||
* Damaged cars often store chassis on `vehicle.vin` which must not win over the real VIN.
|
||
*/
|
||
export function collectPartyVinCandidates(party: {
|
||
vehicle?: { vin?: unknown; inquiry?: unknown };
|
||
} | null | undefined): string[] {
|
||
const inquiry = asObjectRecord(party?.vehicle?.inquiry);
|
||
const mapped = asObjectRecord(inquiry?.mapped);
|
||
const rawData = asObjectRecord(
|
||
asObjectRecord(inquiry?.raw)?.data ?? inquiry?.raw ?? inquiry,
|
||
);
|
||
const values = [
|
||
mapped?.VIN,
|
||
mapped?.Vin,
|
||
mapped?.vin,
|
||
mapped?.VinNumberField,
|
||
rawData?.VIN,
|
||
rawData?.Vin,
|
||
rawData?.vin,
|
||
rawData?.VinNumberField,
|
||
party?.vehicle?.vin,
|
||
mapped?.ChassisNo,
|
||
mapped?.chassisNo,
|
||
mapped?.ShsNum,
|
||
mapped?.shsNam,
|
||
mapped?.ChassisNumberField,
|
||
rawData?.ChassisNo,
|
||
rawData?.chassisNo,
|
||
rawData?.ShsNum,
|
||
rawData?.shsNam,
|
||
rawData?.ChassisNumberField,
|
||
];
|
||
const unique: string[] = [];
|
||
const seen = new Set<string>();
|
||
for (const value of values) {
|
||
const vin = normalizeVin(value);
|
||
if (!vin || seen.has(vin)) continue;
|
||
seen.add(vin);
|
||
unique.push(vin);
|
||
}
|
||
return unique;
|
||
}
|
||
|
||
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 } : {}),
|
||
};
|
||
}
|