updated the fucking mf fanavaran

This commit is contained in:
2026-09-07 16:33:15 +03:30
parent 395e3dbe63
commit b9bffdccc1
5 changed files with 423 additions and 114 deletions

View File

@@ -197,12 +197,17 @@ import {
} from "./fanavaran-accident-vehicle-used"; } from "./fanavaran-accident-vehicle-used";
import { import {
asPartyInquiryRows, asPartyInquiryRows,
collectPersonBirthdays,
collectPersonNationalCodes,
parseJalaliDateParts,
pickPersonBirthday, pickPersonBirthday,
pickPersonNationalCode, pickPersonNationalCode,
selectFanavaranDriverId, selectFanavaranDriverId,
} from "./fanavaran-driver-inquiry"; } from "./fanavaran-driver-inquiry";
import { import {
asObjectRecord, asObjectRecord,
collectPartyVinCandidates,
normalizeVin,
parseFanavaranId, parseFanavaranId,
pickFanavaranPlaqueLookupFields, pickFanavaranPlaqueLookupFields,
pickFanavaranVehicleFromInquiry, pickFanavaranVehicleFromInquiry,
@@ -3934,6 +3939,41 @@ export class ClaimRequestManagementService {
return pickFanavaranVehicleFromInquiry(inquired, vehicleId); return pickFanavaranVehicleFromInquiry(inquired, vehicleId);
} }
/**
* Damaged cars often store chassis on vehicle.vin. Try every VIN-like id
* until Fanavaran returns PlaqueKindId (required on GEN.12).
*/
private async fetchFanavaranVehicleByVinCandidates(
clientKey: FanavaranClientKey,
vins: string[],
): Promise<Record<string, unknown> | null> {
let fallback: Record<string, unknown> | null = null;
for (const vin of vins) {
try {
const vehicle = await this.fetchFanavaranVehicleByVin(clientKey, vin);
if (!vehicle) {
this.logger.warn(
`[buildFanavaranDamageCasePayload] VIN inquiry for ${vin} returned no vehicle`,
);
continue;
}
const plaqueKindId = pickFanavaranPlaqueLookupFields(vehicle).kindId;
this.logger.log(
`[buildFanavaranDamageCasePayload] VIN inquiry ${vin} VehicleId=${pickVehicleId(vehicle) ?? "none"} VehicleKindId=${parseFanavaranId(vehicle.VehicleKindId) ?? "none"} UsedId=${parseFanavaranId(vehicle.UsedId) ?? "none"} PlaqueKindId=${plaqueKindId ?? "none"}`,
);
if (plaqueKindId != null) return vehicle;
if (!fallback) fallback = vehicle;
} catch (error) {
this.logger.warn(
`[buildFanavaranDamageCasePayload] VIN inquiry failed for ${vin}: ${
error instanceof Error ? error.message : error
}`,
);
}
}
return fallback;
}
/** /**
* Fallback when VIN is missing: policy GET → vehicle GET → UsedId/VehicleKindId. * Fallback when VIN is missing: policy GET → vehicle GET → UsedId/VehicleKindId.
*/ */
@@ -4016,43 +4056,9 @@ export class ClaimRequestManagementService {
} }
private parseJalaliBirthday( private parseJalaliBirthday(
birthday: string | number | null | undefined, birthday: string | number | Record<string, unknown> | null | undefined,
): { year: number; month: number; day: number } | null { ): { year: number; month: number; day: number } | null {
if (birthday == null) return null; return parseJalaliDateParts(birthday);
const str = String(birthday).trim();
// Compact format: "13640610" → year=1364, month=06, day=10
if (/^\d{8}$/.test(str)) {
const year = parseInt(str.slice(0, 4), 10);
const month = parseInt(str.slice(4, 6), 10);
const day = parseInt(str.slice(6, 8), 10);
if (isNaN(year) || isNaN(month) || isNaN(day)) {
this.logger.warn(
`parseJalaliBirthday: failed to parse compact format "${str}" - year=${year}, month=${month}, day=${day}`,
);
return null;
}
return { year, month, day };
}
// Delimited formats: "1364/06/10" or "1364-06-10"
const parts = str.split(/[/\-]/);
if (parts.length < 3) {
this.logger.warn(
`parseJalaliBirthday: insufficient parts in "${str}" - got ${parts.length} parts`,
);
return null;
}
const year = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10);
const day = parseInt(parts[2], 10);
if (isNaN(year) || isNaN(month) || isNaN(day)) {
this.logger.warn(
`parseJalaliBirthday: failed to parse delimited format "${str}" - year=${year}, month=${month}, day=${day}`,
);
return null;
}
return { year, month, day };
} }
private async resolveDriverFanavaranId( private async resolveDriverFanavaranId(
@@ -4213,38 +4219,72 @@ export class ClaimRequestManagementService {
]); ]);
const builtYearFromInquiry = builtYearRaw ? Number(builtYearRaw) || null : null; const builtYearFromInquiry = builtYearRaw ? Number(builtYearRaw) || null : null;
const damagedVin = const damagedVinCandidates: string[] = [];
pickVinFromPartyVehicle(damagedParty) ?? const seenVins = new Set<string>();
for (const value of [
...collectPartyVinCandidates(damagedParty),
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [ this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"VIN", "VIN",
"vin", "vin",
"VinNumberField", "VinNumberField",
]); ]),
let apiVehicle: Record<string, unknown> | null = null; this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
if (damagedVin) { "ShsNum",
try { "ChassisNumberField",
apiVehicle = await this.fetchFanavaranVehicleByVin( "shsNam",
input.clientKey, ]),
damagedVin, ]) {
); const vin = normalizeVin(value);
} catch (error) { if (!vin || seenVins.has(vin)) continue;
this.logger.warn( seenVins.add(vin);
`[buildFanavaranDamageCasePayload] VIN inquiry failed for ${damagedVin}: ${ damagedVinCandidates.push(vin);
error instanceof Error ? error.message : error
}`,
);
}
} }
const cachedDamage =
(input.claimCase as any)?.fanavaranSync?.damageCase ?? {};
const lastDamagePayload = asObjectRecord(cachedDamage.lastPayload);
this.logger.log(
`[buildFanavaranDamageCasePayload] VIN candidates=${damagedVinCandidates.join(",") || "none"}`,
);
const apiVehicle = await this.fetchFanavaranVehicleByVinCandidates(
input.clientKey,
damagedVinCandidates,
);
const apiPlaque = pickFanavaranPlaqueLookupFields(apiVehicle); const apiPlaque = pickFanavaranPlaqueLookupFields(apiVehicle);
const inquiryPlaque = pickFanavaranPlaqueLookupFields(
asObjectRecord(inquiryMapped) ?? asObjectRecord(inquiryRaw),
);
const plaqueKindId = this.firstPositiveFanavaranId(
apiPlaque.kindId,
inquiryPlaque.kindId,
cachedDamage.plaqueKindId,
lastDamagePayload?.PlaqueKindId,
);
const plaqueSampleId = this.firstPositiveFanavaranId(
apiPlaque.sampleId,
inquiryPlaque.sampleId,
cachedDamage.plaqueSampleId,
lastDamagePayload?.PlaqueSampleId,
);
if (apiPlaque.kindId == null && plaqueKindId != null) {
this.logger.log(
`[buildFanavaranDamageCasePayload] PlaqueKindId API miss; using DB value ${plaqueKindId}`,
);
}
if (apiPlaque.sampleId == null && plaqueSampleId != null) {
this.logger.log(
`[buildFanavaranDamageCasePayload] PlaqueSampleId API miss; using DB value ${plaqueSampleId}`,
);
}
const builtYear = const builtYear =
parseFanavaranId(apiVehicle?.BuiltYear) ?? builtYearFromInquiry; parseFanavaranId(apiVehicle?.BuiltYear) ?? builtYearFromInquiry;
const carType = input.claimCase?.vehicle?.carType as string | undefined; const carType = input.claimCase?.vehicle?.carType as string | undefined;
const cachedDamage = (input.claimCase as any)?.fanavaranSync?.damageCase ?? {}; let vehicleKindId = this.firstPositiveFanavaranId(
let vehicleKindId = parseFanavaranId(apiVehicle?.VehicleKindId); apiVehicle?.VehicleKindId,
if (vehicleKindId == null && cachedDamage.vehicleKindId != null) { cachedDamage.vehicleKindId,
vehicleKindId = Number(cachedDamage.vehicleKindId); lastDamagePayload?.VehicleKindId,
} );
if (vehicleKindId == null) { if (vehicleKindId == null) {
vehicleKindId = await this.resolveVehicleKindId( vehicleKindId = await this.resolveVehicleKindId(
input.clientKey, input.clientKey,
@@ -4252,20 +4292,23 @@ export class ClaimRequestManagementService {
); );
} }
// Prefer claim-level cache, then party.person.fanavaranDriverId, then live inquiry // Prefer claim-level cache, then party.person.fanavaranDriverId, then live inquiry, then last payload
let driverFanavaranId = let driverFanavaranId = this.firstPositiveFanavaranId(
cachedDamage.driverId ?? person.fanavaranDriverId ?? null; cachedDamage.driverId,
person.fanavaranDriverId,
lastDamagePayload?.DriverId,
);
if (driverFanavaranId) { if (driverFanavaranId) {
this.logger.log( this.logger.log(
`[buildFanavaranDamageCasePayload] Using CACHED DriverId=${driverFanavaranId}`, `[buildFanavaranDamageCasePayload] Using CACHED DriverId=${driverFanavaranId}`,
); );
} else { } else {
driverFanavaranId = await this.resolveDriverFanavaranId( driverFanavaranId = await this.resolveDriverFanavaranIdFromBlameCase(
input.clientKey, input.clientKey,
pickPersonNationalCode(person), input.blameCase?.parties ?? [],
pickPersonBirthday(person), person,
person.driverIsInsurer, input.claimCase,
); );
// Persist the resolved ID back to the blame case party for future use // Persist the resolved ID back to the blame case party for future use
@@ -4386,7 +4429,7 @@ export class ClaimRequestManagementService {
]) ?? (typeof apiVehicle?.MotorNo === "string" ? apiVehicle.MotorNo : null), ]) ?? (typeof apiVehicle?.MotorNo === "string" ? apiVehicle.MotorNo : null),
OwnerId: null, OwnerId: null,
PlaqueCityId: apiPlaque.cityId, PlaqueCityId: apiPlaque.cityId,
PlaqueKindId: apiPlaque.kindId, PlaqueKindId: plaqueKindId,
PlaqueLeftNo: PlaqueLeftNo:
apiPlaque.leftNo ?? (plate ? String(plate.leftDigits) : null), apiPlaque.leftNo ?? (plate ? String(plate.leftDigits) : null),
PlaqueMiddleCodeId: PlaqueMiddleCodeId:
@@ -4398,7 +4441,7 @@ export class ClaimRequestManagementService {
this.formatFanavaranPlateNo(plate), this.formatFanavaranPlateNo(plate),
PlaqueRightNo: PlaqueRightNo:
apiPlaque.rightNo ?? (plate ? String(plate.centerDigits) : null), apiPlaque.rightNo ?? (plate ? String(plate.centerDigits) : null),
PlaqueSampleId: apiPlaque.sampleId, PlaqueSampleId: plaqueSampleId,
PlaqueSerial: apiPlaque.serial ?? (plate ? String(plate.ir) : null), PlaqueSerial: apiPlaque.serial ?? (plate ? String(plate.ir) : null),
PolicyNo: policyNo, PolicyNo: policyNo,
PreviousPolicyEndDate: endDate ?? "", PreviousPolicyEndDate: endDate ?? "",
@@ -4413,11 +4456,20 @@ export class ClaimRequestManagementService {
PolicyCINumber: policyCINumber, PolicyCINumber: policyCINumber,
}; };
const persistedPayload = this.keepPreviousPositiveIds(lastDamagePayload, payload, [
"DriverId",
"PlaqueKindId",
"PlaqueSampleId",
"VehicleKindId",
"AccidentVehicleUsedId",
"InsuranceCorpId",
]);
// Persist Fanavaran-sourced IDs + last payload for reuse on later preview/submit // Persist Fanavaran-sourced IDs + last payload for reuse on later preview/submit
if (input.claimCase?._id) { if (input.claimCase?._id) {
await this.claimCaseDbService.findByIdAndUpdate(String(input.claimCase._id), { await this.claimCaseDbService.findByIdAndUpdate(String(input.claimCase._id), {
$set: { $set: {
"fanavaranSync.damageCase.lastPayload": payload, "fanavaranSync.damageCase.lastPayload": persistedPayload,
"fanavaranSync.damageCase.lastPayloadBuiltAt": new Date(), "fanavaranSync.damageCase.lastPayloadBuiltAt": new Date(),
...(driverFanavaranId != null ...(driverFanavaranId != null
? { "fanavaranSync.damageCase.driverId": driverFanavaranId } ? { "fanavaranSync.damageCase.driverId": driverFanavaranId }
@@ -4425,11 +4477,11 @@ export class ClaimRequestManagementService {
...(vehicleKindId != null ...(vehicleKindId != null
? { "fanavaranSync.damageCase.vehicleKindId": vehicleKindId } ? { "fanavaranSync.damageCase.vehicleKindId": vehicleKindId }
: {}), : {}),
...(apiPlaque.kindId != null ...(plaqueKindId != null
? { "fanavaranSync.damageCase.plaqueKindId": apiPlaque.kindId } ? { "fanavaranSync.damageCase.plaqueKindId": plaqueKindId }
: {}), : {}),
...(apiPlaque.sampleId != null ...(plaqueSampleId != null
? { "fanavaranSync.damageCase.plaqueSampleId": apiPlaque.sampleId } ? { "fanavaranSync.damageCase.plaqueSampleId": plaqueSampleId }
: {}), : {}),
...(accidentVehicleUsedId != null ...(accidentVehicleUsedId != null
? { ? {
@@ -4450,7 +4502,7 @@ export class ClaimRequestManagementService {
}); });
} }
return payload; return persistedPayload;
} }
/** /**
@@ -4909,6 +4961,121 @@ export class ClaimRequestManagementService {
} }
} }
private firstPositiveFanavaranId(...values: unknown[]): number | null {
for (const value of values) {
const id = parseFanavaranId(value);
if (id != null) return id;
}
return null;
}
private keepPreviousPositiveIds(
previous: Record<string, unknown> | null,
next: Record<string, unknown>,
keys: string[],
): Record<string, unknown> {
const merged = { ...previous, ...next };
for (const key of keys) {
if (this.firstPositiveFanavaranId(next[key]) != null) continue;
const previousId = this.firstPositiveFanavaranId(previous?.[key]);
if (previousId != null) {
merged[key] = previousId;
}
}
return merged;
}
private async resolveDriverFanavaranIdFromPerson(
clientKey: FanavaranClientKey,
person: {
driverIsInsurer?: boolean;
nationalCodeOfDriver?: unknown;
nationalCodeOfInsurer?: unknown;
nationalCode?: unknown;
driverBirthday?: unknown;
birthday?: unknown;
insurerBirthday?: unknown;
} | null | undefined,
): Promise<number | null> {
const codes = collectPersonNationalCodes(person);
const birthdays = collectPersonBirthdays(person);
for (const nationalCode of codes) {
for (const birthday of birthdays) {
const driverId = await this.resolveDriverFanavaranId(
clientKey,
nationalCode,
birthday,
person?.driverIsInsurer,
);
if (driverId != null) return driverId;
}
}
return null;
}
private async resolveDriverFanavaranIdFromBlameCase(
clientKey: FanavaranClientKey,
parties: any[],
preferredPerson: any,
claimCase?: any,
): Promise<number | null> {
const people = [
preferredPerson,
...parties.map((party) => party?.person),
].filter(Boolean);
this.logger.log(
`[resolveDriverFanavaranIdFromBlameCase] trying ${people.length} people then leftover codes`,
);
for (const person of people) {
const driverId = await this.resolveDriverFanavaranIdFromPerson(
clientKey,
person,
);
if (driverId != null) return driverId;
}
const leftoverCodes = [
...(claimCase?.money?.nationalCodeOfInsurer
? collectPersonNationalCodes({
nationalCodeOfInsurer: claimCase.money.nationalCodeOfInsurer,
})
: []),
];
const leftoverBirthdays: Array<string | number> = [];
const seenBirthdays = new Set<string>();
for (const person of people) {
for (const birthday of collectPersonBirthdays(person)) {
const parsed = parseJalaliDateParts(birthday);
const key = parsed
? `${parsed.year}-${parsed.month}-${parsed.day}`
: String(birthday);
if (seenBirthdays.has(key)) continue;
seenBirthdays.add(key);
leftoverBirthdays.push(birthday);
}
}
this.logger.log(
`[resolveDriverFanavaranIdFromBlameCase] leftover codes=${leftoverCodes.join(",") || "none"} birthdays=${leftoverBirthdays.join(",") || "none"}`,
);
for (const nationalCode of leftoverCodes) {
for (const birthday of leftoverBirthdays) {
const driverId = await this.resolveDriverFanavaranId(
clientKey,
nationalCode,
birthday,
preferredPerson?.driverIsInsurer,
);
if (driverId != null) return driverId;
}
}
return null;
}
/** /**
* زیان‌دیده for GEN.12: claim owner when linked to a blame party, * زیان‌دیده for GEN.12: claim owner when linked to a blame party,
* else the party that is not the guilty one. * else the party that is not the guilty one.

View File

@@ -1,5 +1,6 @@
import { import {
asPartyInquiryRows, asPartyInquiryRows,
collectPersonNationalCodes,
parseJalaliDateParts, parseJalaliDateParts,
pickPersonBirthday, pickPersonBirthday,
pickPersonNationalCode, pickPersonNationalCode,
@@ -24,8 +25,17 @@ describe("selectFanavaranDriverId", () => {
expect(selectFanavaranDriverId([{ Id: 20, RoleId: 166 }], true)).toBe(20); expect(selectFanavaranDriverId([{ Id: 20, RoleId: 166 }], true)).toBe(20);
}); });
it("unwraps a wrapped Fanavaran payload", () => { it("picks 4773521 from a real Fanavaran unique-identifier response", () => {
expect(selectFanavaranDriverId({ value: rows }, true)).toBe(10); expect(
selectFanavaranDriverId(
[
{ Id: 4773521, RoleId: 161 },
{ Id: 4773521, RoleId: 163 },
{ Id: 4773521, RoleId: 166 },
],
true,
),
).toBe(4773521);
}); });
}); });
@@ -51,11 +61,16 @@ describe("pickPersonNationalCode / birthday", () => {
}); });
describe("asPartyInquiryRows", () => { describe("asPartyInquiryRows", () => {
it("parses compact Jalali birthday 13480313", () => { it("normalizes Persian digits and compact insurer birthday 13640628", () => {
expect(parseJalaliDateParts("13480313")).toEqual({ expect(
year: 1348, collectPersonNationalCodes({
month: 3, nationalCodeOfInsurer: "۰۰۸۰۰۸۶۵۱۹",
day: 13, }),
).toEqual(["0080086519"]);
expect(parseJalaliDateParts(13640628)).toEqual({
year: 1364,
month: 6,
day: 28,
}); });
}); });
}); });

View File

@@ -1,4 +1,7 @@
import { toEnglishDigits } from "src/lookups/fanavaran-last-car-policy";
export const FANAVARAN_INSURER_ROLE_ID = 161; export const FANAVARAN_INSURER_ROLE_ID = 161;
export const FANAVARAN_PLAQUE_OWNER_ROLE_ID = 163;
export const FANAVARAN_DRIVER_ROLE_ID = 166; export const FANAVARAN_DRIVER_ROLE_ID = 166;
export type FanavaranPartyInquiryRow = { export type FanavaranPartyInquiryRow = {
@@ -42,61 +45,109 @@ export function selectFanavaranDriverId(
const parties = asPartyInquiryRows(rows); const parties = asPartyInquiryRows(rows);
if (parties.length === 0) return null; if (parties.length === 0) return null;
const preferredRole = driverIsInsurer const preferredRoles = driverIsInsurer
? FANAVARAN_INSURER_ROLE_ID ? [FANAVARAN_INSURER_ROLE_ID, FANAVARAN_DRIVER_ROLE_ID, FANAVARAN_PLAQUE_OWNER_ROLE_ID]
: FANAVARAN_DRIVER_ROLE_ID; : [FANAVARAN_DRIVER_ROLE_ID, FANAVARAN_INSURER_ROLE_ID, FANAVARAN_PLAQUE_OWNER_ROLE_ID];
const preferred = parties.find(
(row) => asPositiveId(row.RoleId) === preferredRole,
);
if (preferred) return asPositiveId(preferred.Id);
const fallbackRole = driverIsInsurer for (const roleId of preferredRoles) {
? FANAVARAN_DRIVER_ROLE_ID const match = parties.find((row) => asPositiveId(row.RoleId) === roleId);
: FANAVARAN_INSURER_ROLE_ID; if (match) return asPositiveId(match.Id);
const fallback = parties.find( }
(row) => asPositiveId(row.RoleId) === fallbackRole,
);
if (fallback) return asPositiveId(fallback.Id);
return asPositiveId(parties[0].Id); return asPositiveId(parties[0].Id);
} }
export function normalizeNationalCode(value: unknown): string | null {
const digits = toEnglishDigits(value).replace(/\D/g, "");
if (digits.length === 10) return digits;
if (digits.length === 8 || digits.length === 9) return digits.padStart(10, "0");
return digits.length > 0 ? digits : null;
}
export function pickPersonNationalCode(person: { export function pickPersonNationalCode(person: {
driverIsInsurer?: boolean; driverIsInsurer?: boolean;
nationalCodeOfDriver?: unknown; nationalCodeOfDriver?: unknown;
nationalCodeOfInsurer?: unknown; nationalCodeOfInsurer?: unknown;
nationalCode?: unknown;
} | null | undefined): string | null { } | null | undefined): string | null {
if (!person) return null; if (!person) return null;
const driver = String(person.nationalCodeOfDriver ?? "").trim(); const driver = normalizeNationalCode(person.nationalCodeOfDriver);
const insurer = String(person.nationalCodeOfInsurer ?? "").trim(); const insurer = normalizeNationalCode(person.nationalCodeOfInsurer);
const generic = normalizeNationalCode(person.nationalCode);
if (person.driverIsInsurer) { if (person.driverIsInsurer) {
return insurer || driver || null; return insurer || driver || generic;
} }
return driver || insurer || null; return driver || insurer || generic;
} }
export function pickPersonBirthday(person: { export function pickPersonBirthday(person: {
driverIsInsurer?: boolean;
driverBirthday?: unknown; driverBirthday?: unknown;
birthday?: unknown; birthday?: unknown;
insurerBirthday?: unknown; insurerBirthday?: unknown;
} | null | undefined): string | number | null { } | null | undefined): string | number | null {
if (!person) return null; if (!person) return null;
for (const value of [ const ordered = person.driverIsInsurer
person.driverBirthday, ? [person.insurerBirthday, person.birthday, person.driverBirthday]
person.birthday, : [person.driverBirthday, person.birthday, person.insurerBirthday];
person.insurerBirthday, for (const value of ordered) {
]) {
if (value == null || String(value).trim() === "") continue; if (value == null || String(value).trim() === "") continue;
return value as string | number; if (parseJalaliDateParts(value)) return value as string | number;
} }
return null; return null;
} }
export function collectPersonNationalCodes(
person: {
nationalCodeOfDriver?: unknown;
nationalCodeOfInsurer?: unknown;
nationalCode?: unknown;
} | null | undefined,
): string[] {
const codes = [
normalizeNationalCode(person?.nationalCodeOfDriver),
normalizeNationalCode(person?.nationalCodeOfInsurer),
normalizeNationalCode(person?.nationalCode),
].filter((value): value is string => !!value);
return [...new Set(codes)];
}
export function collectPersonBirthdays(person: {
driverBirthday?: unknown;
birthday?: unknown;
insurerBirthday?: unknown;
} | null | undefined): Array<string | number> {
const values = [
person?.insurerBirthday,
person?.birthday,
person?.driverBirthday,
].filter((value) => value != null && String(value).trim() !== "");
const unique: Array<string | number> = [];
const seen = new Set<string>();
for (const value of values) {
const parsed = parseJalaliDateParts(value);
const key = parsed
? `${parsed.year}-${parsed.month}-${parsed.day}`
: String(value);
if (seen.has(key)) continue;
seen.add(key);
unique.push(value as string | number);
}
return unique;
}
export function parseJalaliDateParts( export function parseJalaliDateParts(
birthday: string | number | null | undefined, birthday: unknown,
): { year: number; month: number; day: number } | null { ): { year: number; month: number; day: number } | null {
if (birthday == null) return null; if (birthday == null) return null;
const str = String(birthday).trim(); if (typeof birthday === "object") {
const year = Number(toEnglishDigits((birthday as any).year ?? (birthday as any).BirthYear));
const month = Number(toEnglishDigits((birthday as any).month ?? (birthday as any).BirthMonth));
const day = Number(toEnglishDigits((birthday as any).day ?? (birthday as any).BirthDay));
if (year > 0 && month > 0 && day > 0) return { year, month, day };
return null;
}
const str = toEnglishDigits(birthday).trim();
if (/^\d{8}$/.test(str)) { if (/^\d{8}$/.test(str)) {
const year = parseInt(str.slice(0, 4), 10); const year = parseInt(str.slice(0, 4), 10);
const month = parseInt(str.slice(4, 6), 10); const month = parseInt(str.slice(4, 6), 10);

View File

@@ -1,4 +1,5 @@
import { import {
collectPartyVinCandidates,
filterPoliciesByLine, filterPoliciesByLine,
insuranceLineLabel, insuranceLineLabel,
parseLastCarPolicyInput, parseLastCarPolicyInput,
@@ -114,6 +115,39 @@ describe("fanavaran last car policy", () => {
).toBe("NAS451100P4934023"); ).toBe("NAS451100P4934023");
}); });
it("does not let chassis stored on vehicle.vin beat the inquiry VIN", () => {
const party = {
vehicle: {
vin: "NAAN01CA9BK493683",
inquiry: {
mapped: {
VIN: "IRFC901V119493683",
ChassisNo: "NAAN01CA9BK493683",
},
},
},
};
expect(pickVinFromPartyVehicle(party)).toBe("IRFC901V119493683");
expect(collectPartyVinCandidates(party)).toEqual([
"IRFC901V119493683",
"NAAN01CA9BK493683",
]);
});
it("prefers the VIN-inquiry row that already has PlaqueKindId", () => {
expect(
pickFanavaranVehicleFromInquiry([
{ Id: 1, VIN: "IRFC901V119493683" },
{ Id: 2, VIN: "IRFC901V119493683", PlaqueKindId: 15, PlaqueSampleId: 24 },
]),
).toEqual({
Id: 2,
VIN: "IRFC901V119493683",
PlaqueKindId: 15,
PlaqueSampleId: 24,
});
});
it("reads plaque kind and sample from VIN inquiry for the lookup filter", () => { it("reads plaque kind and sample from VIN inquiry for the lookup filter", () => {
expect( expect(
pickFanavaranPlaqueLookupFields({ pickFanavaranPlaqueLookupFields({

View File

@@ -243,6 +243,12 @@ export function pickFanavaranVehicleFromInquiry(
const match = inquired.find((row) => pickVehicleId(row) === vehicleId); const match = inquired.find((row) => pickVehicleId(row) === vehicleId);
if (match) return asObjectRecord(match); 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[0]);
} }
return asObjectRecord(inquired); return asObjectRecord(inquired);
@@ -251,15 +257,51 @@ export function pickFanavaranVehicleFromInquiry(
export function pickVinFromPartyVehicle(party: { export function pickVinFromPartyVehicle(party: {
vehicle?: { vin?: unknown; inquiry?: unknown }; vehicle?: { vin?: unknown; inquiry?: unknown };
} | null | undefined): string | null { } | null | undefined): string | null {
const direct = normalizeVin(party?.vehicle?.vin); return collectPartyVinCandidates(party)[0] ?? null;
if (direct) return direct; }
const inquiry = party?.vehicle?.inquiry; /**
const nested = asObjectRecord(inquiry); * VIN first (mapped/raw VIN fields), then party.vehicle.vin, then chassis.
const mapped = pickVehicleVin(asObjectRecord(nested?.mapped)); * Damaged cars often store chassis on `vehicle.vin` which must not win over the real VIN.
if (mapped) return mapped; */
const raw = pickVehicleVin(asObjectRecord(nested?.raw ?? inquiry)); export function collectPartyVinCandidates(party: {
return raw || null; 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 = { export type FanavaranPlaqueLookupFields = {