fanavaran stages matching data is double checked and more controlled +

This commit is contained in:
2026-09-15 15:36:28 +03:30
parent 4310ce6398
commit f9c60a2854
6 changed files with 1016 additions and 117 deletions

View File

@@ -1,6 +1,9 @@
import {
canReuseCachedFanavaranVehicleIds,
collectPartyVinCandidates,
fanavaranIdOrConfigDefault,
fanavaranPayloadMatchesDamagedIdentity,
fanavaranVehicleMatchesDamagedIdentity,
filterPoliciesByLine,
insuranceLineLabel,
parseLastCarPolicyInput,
@@ -10,6 +13,7 @@ import {
pickVinFromPartyVehicle,
plaqueLetterFromMiddleCode,
plaqueMatchesVehicle,
selectFanavaranVehicleForDamageCase,
selectLastAmongCarMatches,
toAppPlaque,
vehicleMatchesCar,
@@ -311,4 +315,154 @@ describe("fanavaran last car policy", () => {
it("returns null when there are no matches", () => {
expect(selectLastAmongCarMatches([], today)).toBeNull();
});
const damagedIdentity = {
vin: "IRPC941V2BD798656",
chassis: "NAS431100E5798656",
plaque: { left: "59", letter: "ی", right: "419", serial: "78" },
policyCINumber: "16040446424",
};
const culpritVehicle = {
Id: 475449,
VIN: "IRFC891V7D2301065",
ChassisNo: "NAAP41FD5BJ301065",
VehicleKindId: 6953,
VersionNo: 3,
PlaqueKindId: 8,
PlaqueSampleId: 10,
PlaqueLeftNo: "56",
PlaqueMiddleCodeId: 5,
PlaqueRightNo: "394",
PlaqueSerial: "66",
PlaqueNo: "394د56",
};
it("does not treat the culprit vehicle as the damaged car", () => {
expect(
fanavaranVehicleMatchesDamagedIdentity(culpritVehicle, damagedIdentity),
).toBe(false);
expect(
selectFanavaranVehicleForDamageCase(
[
culpritVehicle,
{ Id: 9, VIN: "OTHER", PlaqueKindId: 15, PlaqueSampleId: 24 },
],
damagedIdentity,
),
).toBeNull();
});
it("ignores a PlaqueKindId 15 row that does not share VIN or plate", () => {
expect(
selectFanavaranVehicleForDamageCase(
[
{ Id: 1, VIN: "IRPC941V2BD798656" },
{
Id: 2,
VIN: "IRFC901V119493683",
PlaqueKindId: 15,
PlaqueSampleId: 24,
PlaqueLeftNo: "62",
PlaqueMiddleCodeId: 14,
PlaqueRightNo: "278",
PlaqueSerial: "50",
},
],
damagedIdentity,
),
).toEqual({ Id: 1, VIN: "IRPC941V2BD798656" });
});
it("requires plate agreement when the inquiry row has plaque parts", () => {
expect(
selectFanavaranVehicleForDamageCase(
[
{
Id: 1,
VIN: "IRPC941V2BD798656",
PlaqueKindId: 15,
PlaqueLeftNo: "56",
PlaqueMiddleCodeId: 5,
PlaqueRightNo: "394",
PlaqueSerial: "66",
},
],
damagedIdentity,
),
).toBeNull();
});
it("picks the newest matching version of the damaged VIN and plate", () => {
expect(
selectFanavaranVehicleForDamageCase(
[
{
Id: 10,
VIN: "IRPC941V2BD798656",
ChassisNo: "NAS431100E5798656",
VersionNo: 1,
PlaqueKindId: 15,
PlaqueSampleId: 24,
PlaqueLeftNo: "59",
PlaqueMiddleCodeId: 16,
PlaqueRightNo: "419",
PlaqueSerial: "78",
},
{
Id: 10,
VIN: "IRPC941V2BD798656",
ChassisNo: "NAS431100E5798656",
VersionNo: 3,
PlaqueKindId: 8,
PlaqueSampleId: 10,
PlaqueLeftNo: "59",
PlaqueMiddleCodeId: 16,
PlaqueRightNo: "419",
PlaqueSerial: "78",
},
],
damagedIdentity,
)?.PlaqueKindId,
).toBe(8);
});
it("rejects cached payload ids from a different VIN, plate, or CI number", () => {
expect(
fanavaranPayloadMatchesDamagedIdentity(
{
VIN: "IRFC891V7D2301065",
ChassisNo: "NAAP41FD5BJ301065",
PolicyCINumber: "16018466143",
PlaqueKindId: 8,
PlaqueLeftNo: "56",
PlaqueMiddleCodeId: 5,
PlaqueRightNo: "394",
PlaqueSerial: "66",
},
damagedIdentity,
),
).toBe(false);
expect(canReuseCachedFanavaranVehicleIds("config-default")).toBe(false);
expect(canReuseCachedFanavaranVehicleIds("vehicle-inquiry")).toBe(true);
expect(canReuseCachedFanavaranVehicleIds(undefined)).toBe(false);
});
it("accepts cache only when VIN, plate, and CI number all agree", () => {
expect(
fanavaranPayloadMatchesDamagedIdentity(
{
VIN: "IRPC941V2BD798656",
ChassisNo: "NAS431100E5798656",
PolicyCINumber: "16040446424",
PlaqueLeftNo: "59",
PlaqueMiddleCodeId: 16,
PlaqueRightNo: "419",
PlaqueSerial: "78",
PlaqueKindId: 8,
},
damagedIdentity,
),
).toBe(true);
});
});

View File

@@ -262,6 +262,159 @@ export function pickFanavaranVehicleFromInquiry(
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";
}
/**
* 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 {