diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 7566dae..2b9c16d 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -197,12 +197,17 @@ import { } from "./fanavaran-accident-vehicle-used"; import { asPartyInquiryRows, + collectPersonBirthdays, + collectPersonNationalCodes, + parseJalaliDateParts, pickPersonBirthday, pickPersonNationalCode, selectFanavaranDriverId, } from "./fanavaran-driver-inquiry"; import { asObjectRecord, + collectPartyVinCandidates, + normalizeVin, parseFanavaranId, pickFanavaranPlaqueLookupFields, pickFanavaranVehicleFromInquiry, @@ -3934,6 +3939,41 @@ export class ClaimRequestManagementService { 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 | null> { + let fallback: Record | 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. */ @@ -4016,43 +4056,9 @@ export class ClaimRequestManagementService { } private parseJalaliBirthday( - birthday: string | number | null | undefined, + birthday: string | number | Record | null | undefined, ): { year: number; month: number; day: number } | null { - if (birthday == null) return null; - 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 }; + return parseJalaliDateParts(birthday); } private async resolveDriverFanavaranId( @@ -4213,38 +4219,72 @@ export class ClaimRequestManagementService { ]); const builtYearFromInquiry = builtYearRaw ? Number(builtYearRaw) || null : null; - const damagedVin = - pickVinFromPartyVehicle(damagedParty) ?? + const damagedVinCandidates: string[] = []; + const seenVins = new Set(); + for (const value of [ + ...collectPartyVinCandidates(damagedParty), this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [ "VIN", "vin", "VinNumberField", - ]); - let apiVehicle: Record | null = null; - if (damagedVin) { - try { - apiVehicle = await this.fetchFanavaranVehicleByVin( - input.clientKey, - damagedVin, - ); - } catch (error) { - this.logger.warn( - `[buildFanavaranDamageCasePayload] VIN inquiry failed for ${damagedVin}: ${ - error instanceof Error ? error.message : error - }`, - ); - } + ]), + this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [ + "ShsNum", + "ChassisNumberField", + "shsNam", + ]), + ]) { + const vin = normalizeVin(value); + if (!vin || seenVins.has(vin)) continue; + seenVins.add(vin); + damagedVinCandidates.push(vin); } + 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 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 = parseFanavaranId(apiVehicle?.BuiltYear) ?? builtYearFromInquiry; const carType = input.claimCase?.vehicle?.carType as string | undefined; - const cachedDamage = (input.claimCase as any)?.fanavaranSync?.damageCase ?? {}; - let vehicleKindId = parseFanavaranId(apiVehicle?.VehicleKindId); - if (vehicleKindId == null && cachedDamage.vehicleKindId != null) { - vehicleKindId = Number(cachedDamage.vehicleKindId); - } + let vehicleKindId = this.firstPositiveFanavaranId( + apiVehicle?.VehicleKindId, + cachedDamage.vehicleKindId, + lastDamagePayload?.VehicleKindId, + ); if (vehicleKindId == null) { vehicleKindId = await this.resolveVehicleKindId( input.clientKey, @@ -4252,20 +4292,23 @@ export class ClaimRequestManagementService { ); } - // Prefer claim-level cache, then party.person.fanavaranDriverId, then live inquiry - let driverFanavaranId = - cachedDamage.driverId ?? person.fanavaranDriverId ?? null; + // Prefer claim-level cache, then party.person.fanavaranDriverId, then live inquiry, then last payload + let driverFanavaranId = this.firstPositiveFanavaranId( + cachedDamage.driverId, + person.fanavaranDriverId, + lastDamagePayload?.DriverId, + ); if (driverFanavaranId) { this.logger.log( `[buildFanavaranDamageCasePayload] Using CACHED DriverId=${driverFanavaranId}`, ); } else { - driverFanavaranId = await this.resolveDriverFanavaranId( + driverFanavaranId = await this.resolveDriverFanavaranIdFromBlameCase( input.clientKey, - pickPersonNationalCode(person), - pickPersonBirthday(person), - person.driverIsInsurer, + input.blameCase?.parties ?? [], + person, + input.claimCase, ); // 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), OwnerId: null, PlaqueCityId: apiPlaque.cityId, - PlaqueKindId: apiPlaque.kindId, + PlaqueKindId: plaqueKindId, PlaqueLeftNo: apiPlaque.leftNo ?? (plate ? String(plate.leftDigits) : null), PlaqueMiddleCodeId: @@ -4398,7 +4441,7 @@ export class ClaimRequestManagementService { this.formatFanavaranPlateNo(plate), PlaqueRightNo: apiPlaque.rightNo ?? (plate ? String(plate.centerDigits) : null), - PlaqueSampleId: apiPlaque.sampleId, + PlaqueSampleId: plaqueSampleId, PlaqueSerial: apiPlaque.serial ?? (plate ? String(plate.ir) : null), PolicyNo: policyNo, PreviousPolicyEndDate: endDate ?? "", @@ -4413,11 +4456,20 @@ export class ClaimRequestManagementService { 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 if (input.claimCase?._id) { await this.claimCaseDbService.findByIdAndUpdate(String(input.claimCase._id), { $set: { - "fanavaranSync.damageCase.lastPayload": payload, + "fanavaranSync.damageCase.lastPayload": persistedPayload, "fanavaranSync.damageCase.lastPayloadBuiltAt": new Date(), ...(driverFanavaranId != null ? { "fanavaranSync.damageCase.driverId": driverFanavaranId } @@ -4425,11 +4477,11 @@ export class ClaimRequestManagementService { ...(vehicleKindId != null ? { "fanavaranSync.damageCase.vehicleKindId": vehicleKindId } : {}), - ...(apiPlaque.kindId != null - ? { "fanavaranSync.damageCase.plaqueKindId": apiPlaque.kindId } + ...(plaqueKindId != null + ? { "fanavaranSync.damageCase.plaqueKindId": plaqueKindId } : {}), - ...(apiPlaque.sampleId != null - ? { "fanavaranSync.damageCase.plaqueSampleId": apiPlaque.sampleId } + ...(plaqueSampleId != null + ? { "fanavaranSync.damageCase.plaqueSampleId": plaqueSampleId } : {}), ...(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 | null, + next: Record, + keys: string[], + ): Record { + 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 { + 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 { + 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 = []; + const seenBirthdays = new Set(); + 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, * else the party that is not the guilty one. diff --git a/src/claim-request-management/fanavaran-driver-inquiry.spec.ts b/src/claim-request-management/fanavaran-driver-inquiry.spec.ts index 99ce826..bad67b7 100644 --- a/src/claim-request-management/fanavaran-driver-inquiry.spec.ts +++ b/src/claim-request-management/fanavaran-driver-inquiry.spec.ts @@ -1,5 +1,6 @@ import { asPartyInquiryRows, + collectPersonNationalCodes, parseJalaliDateParts, pickPersonBirthday, pickPersonNationalCode, @@ -24,8 +25,17 @@ describe("selectFanavaranDriverId", () => { expect(selectFanavaranDriverId([{ Id: 20, RoleId: 166 }], true)).toBe(20); }); - it("unwraps a wrapped Fanavaran payload", () => { - expect(selectFanavaranDriverId({ value: rows }, true)).toBe(10); + it("picks 4773521 from a real Fanavaran unique-identifier response", () => { + 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", () => { - it("parses compact Jalali birthday 13480313", () => { - expect(parseJalaliDateParts("13480313")).toEqual({ - year: 1348, - month: 3, - day: 13, + it("normalizes Persian digits and compact insurer birthday 13640628", () => { + expect( + collectPersonNationalCodes({ + nationalCodeOfInsurer: "۰۰۸۰۰۸۶۵۱۹", + }), + ).toEqual(["0080086519"]); + expect(parseJalaliDateParts(13640628)).toEqual({ + year: 1364, + month: 6, + day: 28, }); }); }); diff --git a/src/claim-request-management/fanavaran-driver-inquiry.ts b/src/claim-request-management/fanavaran-driver-inquiry.ts index 6db9e76..ad97453 100644 --- a/src/claim-request-management/fanavaran-driver-inquiry.ts +++ b/src/claim-request-management/fanavaran-driver-inquiry.ts @@ -1,4 +1,7 @@ +import { toEnglishDigits } from "src/lookups/fanavaran-last-car-policy"; + export const FANAVARAN_INSURER_ROLE_ID = 161; +export const FANAVARAN_PLAQUE_OWNER_ROLE_ID = 163; export const FANAVARAN_DRIVER_ROLE_ID = 166; export type FanavaranPartyInquiryRow = { @@ -42,61 +45,109 @@ export function selectFanavaranDriverId( const parties = asPartyInquiryRows(rows); if (parties.length === 0) return null; - const preferredRole = driverIsInsurer - ? FANAVARAN_INSURER_ROLE_ID - : FANAVARAN_DRIVER_ROLE_ID; - const preferred = parties.find( - (row) => asPositiveId(row.RoleId) === preferredRole, - ); - if (preferred) return asPositiveId(preferred.Id); + const preferredRoles = driverIsInsurer + ? [FANAVARAN_INSURER_ROLE_ID, FANAVARAN_DRIVER_ROLE_ID, FANAVARAN_PLAQUE_OWNER_ROLE_ID] + : [FANAVARAN_DRIVER_ROLE_ID, FANAVARAN_INSURER_ROLE_ID, FANAVARAN_PLAQUE_OWNER_ROLE_ID]; - const fallbackRole = driverIsInsurer - ? FANAVARAN_DRIVER_ROLE_ID - : FANAVARAN_INSURER_ROLE_ID; - const fallback = parties.find( - (row) => asPositiveId(row.RoleId) === fallbackRole, - ); - if (fallback) return asPositiveId(fallback.Id); + for (const roleId of preferredRoles) { + const match = parties.find((row) => asPositiveId(row.RoleId) === roleId); + if (match) return asPositiveId(match.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: { driverIsInsurer?: boolean; nationalCodeOfDriver?: unknown; nationalCodeOfInsurer?: unknown; + nationalCode?: unknown; } | null | undefined): string | null { if (!person) return null; - const driver = String(person.nationalCodeOfDriver ?? "").trim(); - const insurer = String(person.nationalCodeOfInsurer ?? "").trim(); + const driver = normalizeNationalCode(person.nationalCodeOfDriver); + const insurer = normalizeNationalCode(person.nationalCodeOfInsurer); + const generic = normalizeNationalCode(person.nationalCode); if (person.driverIsInsurer) { - return insurer || driver || null; + return insurer || driver || generic; } - return driver || insurer || null; + return driver || insurer || generic; } export function pickPersonBirthday(person: { + driverIsInsurer?: boolean; driverBirthday?: unknown; birthday?: unknown; insurerBirthday?: unknown; } | null | undefined): string | number | null { if (!person) return null; - for (const value of [ - person.driverBirthday, - person.birthday, - person.insurerBirthday, - ]) { + const ordered = person.driverIsInsurer + ? [person.insurerBirthday, person.birthday, person.driverBirthday] + : [person.driverBirthday, person.birthday, person.insurerBirthday]; + for (const value of ordered) { if (value == null || String(value).trim() === "") continue; - return value as string | number; + if (parseJalaliDateParts(value)) return value as string | number; } 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 { + const values = [ + person?.insurerBirthday, + person?.birthday, + person?.driverBirthday, + ].filter((value) => value != null && String(value).trim() !== ""); + const unique: Array = []; + const seen = new Set(); + 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( - birthday: string | number | null | undefined, + birthday: unknown, ): { year: number; month: number; day: number } | 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)) { const year = parseInt(str.slice(0, 4), 10); const month = parseInt(str.slice(4, 6), 10); diff --git a/src/lookups/fanavaran-last-car-policy.spec.ts b/src/lookups/fanavaran-last-car-policy.spec.ts index a9f96b1..67d1239 100644 --- a/src/lookups/fanavaran-last-car-policy.spec.ts +++ b/src/lookups/fanavaran-last-car-policy.spec.ts @@ -1,4 +1,5 @@ import { + collectPartyVinCandidates, filterPoliciesByLine, insuranceLineLabel, parseLastCarPolicyInput, @@ -114,6 +115,39 @@ describe("fanavaran last car policy", () => { ).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", () => { expect( pickFanavaranPlaqueLookupFields({ diff --git a/src/lookups/fanavaran-last-car-policy.ts b/src/lookups/fanavaran-last-car-policy.ts index 17d6602..c2ffe26 100644 --- a/src/lookups/fanavaran-last-car-policy.ts +++ b/src/lookups/fanavaran-last-car-policy.ts @@ -243,6 +243,12 @@ export function pickFanavaranVehicleFromInquiry( 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); @@ -251,15 +257,51 @@ export function pickFanavaranVehicleFromInquiry( export function pickVinFromPartyVehicle(party: { vehicle?: { vin?: unknown; inquiry?: unknown }; } | null | undefined): string | null { - const direct = normalizeVin(party?.vehicle?.vin); - if (direct) return direct; + return collectPartyVinCandidates(party)[0] ?? null; +} - 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; +/** + * 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(); + 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 = {