forked from Yara724/api
Compare commits
12 Commits
cb4f02918f
...
8173764913
| Author | SHA1 | Date | |
|---|---|---|---|
| 8173764913 | |||
| c72cb265f1 | |||
| 271dc4df2a | |||
| b2ff3f574b | |||
| 769a581a51 | |||
| 466773fb2b | |||
| 37041ba19f | |||
| 0b0a1dfa13 | |||
| 073b4dec38 | |||
| c1f0d041ce | |||
| f9c60a2854 | |||
| 4310ce6398 |
File diff suppressed because it is too large
Load Diff
@@ -97,14 +97,26 @@ export class FanavaranSyncStage {
|
||||
@Prop({ type: Number })
|
||||
vehicleKindId?: number;
|
||||
|
||||
/** How VehicleKindId was resolved: vehicle-inquiry | car-type-lookup */
|
||||
@Prop({ type: String })
|
||||
vehicleKindSource?: string;
|
||||
|
||||
/** Cached Fanavaran PlaqueKindId from VIN inquiry / vehicle GET. */
|
||||
@Prop({ type: Number })
|
||||
plaqueKindId?: number;
|
||||
|
||||
/** How PlaqueKindId was resolved: vehicle-inquiry | config-default */
|
||||
@Prop({ type: String })
|
||||
plaqueKindSource?: string;
|
||||
|
||||
/** Cached Fanavaran PlaqueSampleId from VIN inquiry / vehicle GET. */
|
||||
@Prop({ type: Number })
|
||||
plaqueSampleId?: number;
|
||||
|
||||
/** How PlaqueSampleId was resolved: vehicle-inquiry | config-default */
|
||||
@Prop({ type: String })
|
||||
plaqueSampleSource?: string;
|
||||
|
||||
/**
|
||||
* Cached Fanavaran AccidentVehicleUsedId, resolved from VIN inquiry / vehicle GET
|
||||
* UsedId (lookup-filter safe). Not the Mongo tenant default.
|
||||
|
||||
157
src/claim-request-management/fanavaran-claim-product.spec.ts
Normal file
157
src/claim-request-management/fanavaran-claim-product.spec.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||
import {
|
||||
fanavaranClaimProductFromBlameType,
|
||||
fanavaranClaimsBaseUrl,
|
||||
fanavaranClaimStageUrl,
|
||||
fanavaranHasDamageCaseStage,
|
||||
fanavaranHullImportClaimsUrl,
|
||||
fanavaranPartyInquirySources,
|
||||
isFanavaranSubmitSupportedBlameType,
|
||||
pickCarBodyPolicyNationalCode,
|
||||
pickStoredCarBodyPolicyId,
|
||||
} from "./fanavaran-claim-product";
|
||||
|
||||
describe("fanavaran claim product", () => {
|
||||
it("maps CAR_BODY blame files onto the hull claims resource", () => {
|
||||
expect(fanavaranClaimProductFromBlameType(BlameRequestType.CAR_BODY)).toBe(
|
||||
"car-body",
|
||||
);
|
||||
expect(fanavaranClaimsBaseUrl("car-body")).toBe(
|
||||
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps THIRD_PARTY on the financial third-party claims resource", () => {
|
||||
expect(
|
||||
fanavaranClaimProductFromBlameType(BlameRequestType.THIRD_PARTY),
|
||||
).toBe("third-party");
|
||||
expect(fanavaranClaimsBaseUrl("third-party")).toBe(
|
||||
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/third-party-car-financial-claims",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not POST hull damage to the missing dmg-cases resource", () => {
|
||||
expect(fanavaranHasDamageCaseStage("car-body")).toBe(false);
|
||||
expect(fanavaranClaimStageUrl("car-body", "damage-case", 5023617)).toBeNull();
|
||||
expect(fanavaranClaimStageUrl("car-body", "attachments", 5023617)).toBe(
|
||||
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/5023617/files",
|
||||
);
|
||||
expect(fanavaranClaimStageUrl("car-body", "expertise", 5023617)).toBe(
|
||||
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/5023617/expertise",
|
||||
);
|
||||
expect(fanavaranClaimStageUrl("car-body", "culprits", 5023617)).toBe(
|
||||
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/5023617/culprits",
|
||||
);
|
||||
expect(fanavaranClaimStageUrl("third-party", "culprits", 1)).toBeNull();
|
||||
expect(fanavaranHullImportClaimsUrl()).toBe(
|
||||
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-import-claims",
|
||||
);
|
||||
expect(fanavaranClaimsBaseUrl("car-body")).not.toContain(
|
||||
"vehicle-hull-import-claims",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows Fanavaran submit for both car products", () => {
|
||||
expect(isFanavaranSubmitSupportedBlameType("THIRD_PARTY")).toBe(true);
|
||||
expect(isFanavaranSubmitSupportedBlameType("CAR_BODY")).toBe(true);
|
||||
expect(isFanavaranSubmitSupportedBlameType("OTHER")).toBe(false);
|
||||
});
|
||||
|
||||
it("reuses the car-body inquiry PolicyId without another policy list call", () => {
|
||||
const policyId = pickStoredCarBodyPolicyId({
|
||||
parties: [
|
||||
{
|
||||
role: PartyRole.FIRST,
|
||||
insurance: { carBodyInsurance: { policyId: 15292336 } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(policyId).toBe(15292336);
|
||||
});
|
||||
|
||||
it("reads PolicyId from nested car-body inquiry raw when the flat field is missing", () => {
|
||||
const policyId = pickStoredCarBodyPolicyId({
|
||||
parties: [
|
||||
{
|
||||
role: PartyRole.FIRST,
|
||||
vehicle: {
|
||||
inquiry: {
|
||||
carBody: {
|
||||
mapped: {},
|
||||
raw: { policyId: "15292336", policy: { PolicyId: 99 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(policyId).toBe(15292336);
|
||||
});
|
||||
|
||||
it("uses the first-party car-body policyholder national code", () => {
|
||||
const nationalCode = pickCarBodyPolicyNationalCode({
|
||||
parties: [
|
||||
{
|
||||
role: PartyRole.FIRST,
|
||||
person: { nationalCodeOfInsurer: "0012345678" },
|
||||
insurance: {
|
||||
carBodyInsurance: { ownerNationalCode: "0098765432" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(nationalCode).toBe("0012345678");
|
||||
});
|
||||
|
||||
it("flattens car-body inquiry fields onto the damage-case aliases", () => {
|
||||
const { mapped } = fanavaranPartyInquirySources("car-body", {
|
||||
insurance: {
|
||||
carBodyInsurance: {
|
||||
policyNumber: "70019846985",
|
||||
chassisNumber: "IRNKAEK4150012345",
|
||||
},
|
||||
},
|
||||
vehicle: {
|
||||
inquiry: {
|
||||
carBody: {
|
||||
mapped: {
|
||||
policyId: 15292336,
|
||||
policyNumber: "70019846985",
|
||||
chassisNumber: "IRNKAEK4150012345",
|
||||
motorNumber: "M123",
|
||||
vin: "IRNKAEK4150012345",
|
||||
StartDate: "1405/05/18",
|
||||
EndDate: "1406/05/18",
|
||||
builtYear: 1402,
|
||||
platePartOne: "29",
|
||||
plateLetterTitle: "د",
|
||||
platePartThree: "782",
|
||||
plateSerialNumber: "44",
|
||||
},
|
||||
raw: {
|
||||
policy: { CINumber: "70019846985" },
|
||||
vehicle: { ChassisNo: "IRNKAEK4150012345" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mapped.policyId).toBe(15292336);
|
||||
expect(mapped.PrntCmpDocNo).toBe("70019846985");
|
||||
expect(mapped.ShsNum).toBe("IRNKAEK4150012345");
|
||||
expect(mapped.MtrNum).toBe("M123");
|
||||
expect(mapped.VIN).toBe("IRNKAEK4150012345");
|
||||
expect(mapped.HBgnDte).toBe("1405/05/18");
|
||||
expect(mapped.HEndDte).toBe("1406/05/18");
|
||||
expect(mapped.PrdDte).toBe(1402);
|
||||
expect(mapped.plk1).toBe("29");
|
||||
expect(mapped.plk2).toBe("د");
|
||||
expect(mapped.plk3).toBe("782");
|
||||
expect(mapped.plksrl).toBe("44");
|
||||
});
|
||||
});
|
||||
271
src/claim-request-management/fanavaran-claim-product.ts
Normal file
271
src/claim-request-management/fanavaran-claim-product.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||
import {
|
||||
FANAVARAN_CAR_BODY_LINE_ID,
|
||||
FANAVARAN_THIRD_PARTY_LINE_ID,
|
||||
asObjectRecord,
|
||||
parseFanavaranId,
|
||||
type FanavaranCarPolicyProduct,
|
||||
} from "src/lookups/fanavaran-last-car-policy";
|
||||
|
||||
export type FanavaranClaimProduct = FanavaranCarPolicyProduct;
|
||||
|
||||
const FANAVARAN_CLAIMS_HOST =
|
||||
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car";
|
||||
|
||||
export function fanavaranClaimProductFromBlameType(
|
||||
type?: string | null,
|
||||
): FanavaranClaimProduct {
|
||||
return type === BlameRequestType.CAR_BODY || type === "CAR_BODY"
|
||||
? "car-body"
|
||||
: "third-party";
|
||||
}
|
||||
|
||||
export function isFanavaranSubmitSupportedBlameType(
|
||||
type?: string | null,
|
||||
): boolean {
|
||||
return (
|
||||
type === BlameRequestType.THIRD_PARTY ||
|
||||
type === "THIRD_PARTY" ||
|
||||
type === BlameRequestType.CAR_BODY ||
|
||||
type === "CAR_BODY"
|
||||
);
|
||||
}
|
||||
|
||||
export function fanavaranClaimsResourcePath(
|
||||
product: FanavaranClaimProduct,
|
||||
): string {
|
||||
return product === "car-body"
|
||||
? "vehicle-hull-claims"
|
||||
: "third-party-car-financial-claims";
|
||||
}
|
||||
|
||||
export function fanavaranClaimsBaseUrl(
|
||||
product: FanavaranClaimProduct,
|
||||
): string {
|
||||
return `${FANAVARAN_CLAIMS_HOST}/${fanavaranClaimsResourcePath(product)}`;
|
||||
}
|
||||
|
||||
export type FanavaranClaimStage =
|
||||
| "damage-case"
|
||||
| "attachments"
|
||||
| "expertise"
|
||||
| "culprits";
|
||||
|
||||
/** Hull (بدنه) has no GEN.12 dmg-cases resource. Proven 2026-09-15: Feature:url(.../vehicle-hull-claims/{id}/dmg-cases) not found. Files and expertise do exist. */
|
||||
export const FANAVARAN_HULL_NO_DAMAGE_CASE_REASON =
|
||||
"Fanavaran vehicle-hull-claims has no dmg-cases resource. Hull is first-party (one damaged vehicle on the base claim). Continue with files and expertise.";
|
||||
|
||||
export const FANAVARAN_HULL_IMPORT_CLAIMS_PATH = "vehicle-hull-import-claims";
|
||||
|
||||
export function fanavaranHasDamageCaseStage(
|
||||
product: FanavaranClaimProduct,
|
||||
): boolean {
|
||||
return product === "third-party";
|
||||
}
|
||||
|
||||
export function fanavaranHullImportClaimsUrl(): string {
|
||||
return `${FANAVARAN_CLAIMS_HOST}/${FANAVARAN_HULL_IMPORT_CLAIMS_PATH}`;
|
||||
}
|
||||
|
||||
export function fanavaranClaimStageUrl(
|
||||
product: FanavaranClaimProduct,
|
||||
stage: FanavaranClaimStage,
|
||||
claimId: number | string,
|
||||
): string | null {
|
||||
if (stage === "damage-case" && !fanavaranHasDamageCaseStage(product)) {
|
||||
return null;
|
||||
}
|
||||
if (stage === "culprits" && product !== "car-body") {
|
||||
return null;
|
||||
}
|
||||
const suffix =
|
||||
stage === "damage-case"
|
||||
? "dmg-cases"
|
||||
: stage === "attachments"
|
||||
? "files"
|
||||
: stage === "culprits"
|
||||
? "culprits"
|
||||
: "expertise";
|
||||
return `${fanavaranClaimsBaseUrl(product)}/${claimId}/${suffix}`;
|
||||
}
|
||||
|
||||
export function fanavaranInsuranceLineIdForProduct(
|
||||
product: FanavaranClaimProduct,
|
||||
): number {
|
||||
return product === "car-body"
|
||||
? FANAVARAN_CAR_BODY_LINE_ID
|
||||
: FANAVARAN_THIRD_PARTY_LINE_ID;
|
||||
}
|
||||
|
||||
type BlamePartyForCarBodyPolicy = {
|
||||
role?: string;
|
||||
person?: { nationalCodeOfInsurer?: unknown };
|
||||
insurance?: {
|
||||
carBodyInsurance?: {
|
||||
policyId?: unknown;
|
||||
ownerNationalCode?: unknown;
|
||||
insurerNationalCode?: unknown;
|
||||
policyNumber?: unknown;
|
||||
chassisNumber?: unknown;
|
||||
motorNumber?: unknown;
|
||||
vin?: unknown;
|
||||
startDate?: unknown;
|
||||
endDate?: unknown;
|
||||
};
|
||||
};
|
||||
vehicle?: {
|
||||
inquiry?: {
|
||||
mapped?: Record<string, unknown>;
|
||||
raw?: Record<string, unknown> | { data?: Record<string, unknown> };
|
||||
carBody?: {
|
||||
mapped?: Record<string, unknown>;
|
||||
raw?: Record<string, unknown> & {
|
||||
policyId?: unknown;
|
||||
policy?: Record<string, unknown>;
|
||||
vehicle?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
function firstCarBodyParty(
|
||||
blame?: { parties?: BlamePartyForCarBodyPolicy[] } | null,
|
||||
): BlamePartyForCarBodyPolicy | null {
|
||||
const parties = blame?.parties ?? [];
|
||||
return (
|
||||
parties.find((party) => party?.role === PartyRole.FIRST) ??
|
||||
parties[0] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function nonEmptyText(value: unknown): string | null {
|
||||
if (value == null) return null;
|
||||
const text = String(value).trim();
|
||||
return text ? text : null;
|
||||
}
|
||||
|
||||
export function pickStoredCarBodyPolicyId(
|
||||
blame?: { parties?: BlamePartyForCarBodyPolicy[] } | null,
|
||||
): number | null {
|
||||
const party = firstCarBodyParty(blame);
|
||||
const carBody = party?.vehicle?.inquiry?.carBody;
|
||||
const raw = asObjectRecord(carBody?.raw) ?? {};
|
||||
const mapped = asObjectRecord(carBody?.mapped) ?? {};
|
||||
const rawPolicy = asObjectRecord(raw.policy);
|
||||
return parseFanavaranId(
|
||||
party?.insurance?.carBodyInsurance?.policyId ??
|
||||
mapped.policyId ??
|
||||
raw.policyId ??
|
||||
rawPolicy?.PolicyId,
|
||||
);
|
||||
}
|
||||
|
||||
export function pickCarBodyPolicyNationalCode(
|
||||
blame?: { parties?: BlamePartyForCarBodyPolicy[] } | null,
|
||||
): string | null {
|
||||
const party = firstCarBodyParty(blame);
|
||||
return (
|
||||
nonEmptyText(party?.person?.nationalCodeOfInsurer) ??
|
||||
nonEmptyText(party?.insurance?.carBodyInsurance?.ownerNationalCode) ??
|
||||
nonEmptyText(party?.insurance?.carBodyInsurance?.insurerNationalCode)
|
||||
);
|
||||
}
|
||||
|
||||
export function fanavaranPartyInquirySources(
|
||||
product: FanavaranClaimProduct,
|
||||
party?: BlamePartyForCarBodyPolicy | null,
|
||||
): {
|
||||
mapped: Record<string, any>;
|
||||
raw: Record<string, any>;
|
||||
} {
|
||||
const inquiry = party?.vehicle?.inquiry ?? {};
|
||||
if (product !== "car-body") {
|
||||
const mapped = asObjectRecord(inquiry.mapped) ?? {};
|
||||
const rawValue = inquiry.raw as
|
||||
| Record<string, unknown>
|
||||
| { data?: Record<string, unknown> }
|
||||
| undefined;
|
||||
const raw =
|
||||
asObjectRecord(
|
||||
rawValue &&
|
||||
typeof rawValue === "object" &&
|
||||
"data" in rawValue &&
|
||||
rawValue.data != null &&
|
||||
typeof rawValue.data === "object" &&
|
||||
!Array.isArray(rawValue.data)
|
||||
? rawValue.data
|
||||
: rawValue,
|
||||
) ?? {};
|
||||
return { mapped, raw };
|
||||
}
|
||||
|
||||
const carBody = inquiry.carBody ?? {};
|
||||
const mapped = asObjectRecord(carBody.mapped) ?? {};
|
||||
const raw = asObjectRecord(carBody.raw) ?? {};
|
||||
const policy = asObjectRecord(raw.policy) ?? {};
|
||||
const vehicle = asObjectRecord(raw.vehicle) ?? {};
|
||||
const insurance = party?.insurance?.carBodyInsurance ?? {};
|
||||
|
||||
const policyNumber =
|
||||
mapped.policyNumber ??
|
||||
insurance.policyNumber ??
|
||||
policy.CINumber ??
|
||||
policy.PolicyNo;
|
||||
const chassisNumber =
|
||||
mapped.chassisNumber ?? insurance.chassisNumber ?? vehicle.ChassisNo;
|
||||
const motorNumber =
|
||||
mapped.motorNumber ?? insurance.motorNumber ?? vehicle.MotorNo;
|
||||
const vin = mapped.vin ?? insurance.vin ?? vehicle.VIN ?? vehicle.ChassisNo;
|
||||
const beginDate =
|
||||
mapped.StartDate ?? mapped.startDate ?? insurance.startDate ?? policy.BeginDate;
|
||||
const endDate =
|
||||
mapped.EndDate ?? mapped.endDate ?? insurance.endDate ?? policy.EndDate;
|
||||
const builtYear = mapped.builtYear ?? vehicle.BuiltYear;
|
||||
const plaque = asObjectRecord(vehicle.plaque) ?? {};
|
||||
const plk1 =
|
||||
mapped.platePartOne ?? plaque.leftTwoDigits ?? vehicle.PlaqueLeftNo;
|
||||
const plk2 =
|
||||
mapped.plateLetterTitle ?? plaque.serialLetter;
|
||||
const plk3 =
|
||||
mapped.platePartThree ?? plaque.threeDigits ?? vehicle.PlaqueRightNo;
|
||||
const plksrl =
|
||||
mapped.plateSerialNumber ??
|
||||
plaque.rightTwoDigits ??
|
||||
vehicle.PlaqueSerial;
|
||||
|
||||
return {
|
||||
mapped: {
|
||||
...mapped,
|
||||
...insurance,
|
||||
policyId:
|
||||
mapped.policyId ??
|
||||
insurance.policyId ??
|
||||
raw.policyId ??
|
||||
policy.PolicyId,
|
||||
PrntCmpDocNo: policyNumber,
|
||||
PlcyUnqCod: policy.PolicyUnqCod ?? mapped.policyNumber,
|
||||
ShsNum: chassisNumber,
|
||||
MtrNum: motorNumber,
|
||||
VIN: vin,
|
||||
HBgnDte: beginDate,
|
||||
HEndDte: endDate,
|
||||
PrdDte: builtYear,
|
||||
plk1,
|
||||
Plk1: plk1,
|
||||
plk2,
|
||||
Plk2: plk2,
|
||||
plk3,
|
||||
Plk3: plk3,
|
||||
plksrl,
|
||||
PlkSrl: plksrl,
|
||||
},
|
||||
raw: {
|
||||
...policy,
|
||||
...vehicle,
|
||||
...raw,
|
||||
},
|
||||
};
|
||||
}
|
||||
156
src/claim-request-management/fanavaran-hull-expertise.spec.ts
Normal file
156
src/claim-request-management/fanavaran-hull-expertise.spec.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||
import {
|
||||
hullExpertiseAssertFields,
|
||||
collectFanavaranExpertiseReadinessWarnings,
|
||||
pickHullVehicleIdentity,
|
||||
toFanavaranHullExpertiseDmgSection,
|
||||
toFanavaranHullExpertisePayload,
|
||||
} from "./fanavaran-hull-expertise";
|
||||
|
||||
describe("fanavaran hull expertise", () => {
|
||||
it("maps car-body inquiry identity onto GEN.06 vehicle fields", () => {
|
||||
const vehicle = pickHullVehicleIdentity({
|
||||
parties: [
|
||||
{
|
||||
role: PartyRole.FIRST,
|
||||
insurance: {
|
||||
carBodyInsurance: {
|
||||
chassisNumber: "NAS431100K1050805",
|
||||
motorNumber: "M136251767",
|
||||
vin: "VIN123",
|
||||
},
|
||||
},
|
||||
vehicle: {
|
||||
inquiry: {
|
||||
carBody: {
|
||||
mapped: {
|
||||
platePartOne: "29",
|
||||
plateLetterTitle: "د",
|
||||
platePartThree: "782",
|
||||
plateSerialNumber: "60",
|
||||
builtYear: 1398,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(vehicle).toEqual({
|
||||
motorNo: "M136251767",
|
||||
chassisNo: "NAS431100K1050805",
|
||||
vin: "VIN123",
|
||||
plaqueNo: "29د782",
|
||||
plaqueSerial: "60",
|
||||
builtYear: 1398,
|
||||
});
|
||||
});
|
||||
|
||||
it("builds a GEN.06 payload without ثالث expertise fields", () => {
|
||||
const payload = toFanavaranHullExpertisePayload({
|
||||
claimExpertId: 29,
|
||||
dmgAssessmentDate: "1405/06/24",
|
||||
inspectionTime: "10:33",
|
||||
wage: 1000,
|
||||
componentReplacementCost: 2000,
|
||||
wasteValue: 0,
|
||||
dropAmount: 12,
|
||||
vehicleCurrentValue: 1900000000,
|
||||
vehicle: {
|
||||
motorNo: "M1",
|
||||
chassisNo: "C1",
|
||||
vin: null,
|
||||
plaqueNo: "695ط61",
|
||||
plaqueSerial: "87",
|
||||
builtYear: 1398,
|
||||
},
|
||||
dmgSections: [
|
||||
toFanavaranHullExpertiseDmgSection({
|
||||
partId: 9,
|
||||
desc: "bumper",
|
||||
wasteValue: 0,
|
||||
amount: 3000,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(payload.Wage).toBe(1000);
|
||||
expect(payload.RepairWage).toBeUndefined();
|
||||
expect(payload.DmgCaseId).toBeUndefined();
|
||||
expect(payload.InspectionPlaceId).toBeUndefined();
|
||||
expect(payload.DropAmountStatus).toBeUndefined();
|
||||
expect(payload.DamagedVehicleCurrentPrice).toBeUndefined();
|
||||
expect(payload.ClaimExpertId).toBe(29);
|
||||
expect(payload.PlaqueNo).toBe("695ط61");
|
||||
expect(payload.DmgSections).toEqual([
|
||||
{
|
||||
Count: 1,
|
||||
Desc: "bumper",
|
||||
WasteValue: 0,
|
||||
AccessoryKindId: 9,
|
||||
DmgKindId: null,
|
||||
VehicleHullAccessoryId: null,
|
||||
DmgSectionCosts: [
|
||||
{ Caption: "bumper", Amount: 3000, DmgCostKindId: null },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not require ثالث expertise fields on a GEN.06 hull payload", () => {
|
||||
const payload = toFanavaranHullExpertisePayload({
|
||||
claimExpertId: 29,
|
||||
dmgAssessmentDate: "1405/06/24",
|
||||
inspectionTime: "10:41",
|
||||
wage: 6000000,
|
||||
componentReplacementCost: 10000000,
|
||||
wasteValue: 0,
|
||||
dropAmount: 10000,
|
||||
vehicleCurrentValue: null,
|
||||
vehicle: {
|
||||
motorNo: "13389030167",
|
||||
chassisNo: "NAAP41FD5BJ301065",
|
||||
vin: "IRFC891V7D2301065",
|
||||
plaqueNo: "56د394",
|
||||
plaqueSerial: "66",
|
||||
builtYear: 1389,
|
||||
},
|
||||
dmgSections: [
|
||||
toFanavaranHullExpertiseDmgSection({
|
||||
partId: 9,
|
||||
desc: "سپر جلو",
|
||||
wasteValue: 0,
|
||||
amount: 11000000,
|
||||
}),
|
||||
toFanavaranHullExpertiseDmgSection({
|
||||
partId: 11,
|
||||
desc: "آينه سمت راننده",
|
||||
wasteValue: 0,
|
||||
amount: 5000000,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
collectFanavaranExpertiseReadinessWarnings(
|
||||
payload,
|
||||
hullExpertiseAssertFields("car-body"),
|
||||
),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
collectFanavaranExpertiseReadinessWarnings(
|
||||
payload,
|
||||
hullExpertiseAssertFields("third-party"),
|
||||
),
|
||||
).toEqual([
|
||||
"DmgCaseId is required.",
|
||||
"InspectionPlaceId is required.",
|
||||
"DropAmountStatus is required.",
|
||||
"DmgSections[0].DmgSectionId is required.",
|
||||
"DmgSections[0].AccidentLevel is required.",
|
||||
"DmgSections[1].DmgSectionId is required.",
|
||||
"DmgSections[1].AccidentLevel is required.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
160
src/claim-request-management/fanavaran-hull-expertise.ts
Normal file
160
src/claim-request-management/fanavaran-hull-expertise.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { parseFanavaranId } from "src/lookups/fanavaran-last-car-policy";
|
||||
import {
|
||||
fanavaranPartyInquirySources,
|
||||
type FanavaranClaimProduct,
|
||||
} from "./fanavaran-claim-product";
|
||||
|
||||
export type FanavaranHullVehicleIdentity = {
|
||||
motorNo: string | null;
|
||||
chassisNo: string | null;
|
||||
vin: string | null;
|
||||
plaqueNo: string | null;
|
||||
plaqueSerial: string | null;
|
||||
builtYear: number | null;
|
||||
};
|
||||
|
||||
export function pickHullVehicleIdentity(
|
||||
blame?: { parties?: unknown[] } | null,
|
||||
): FanavaranHullVehicleIdentity {
|
||||
const party = (blame?.parties ?? [])[0] as
|
||||
| Parameters<typeof fanavaranPartyInquirySources>[1]
|
||||
| undefined;
|
||||
const first =
|
||||
(blame?.parties ?? []).find(
|
||||
(row) => (row as { role?: string })?.role === "FIRST",
|
||||
) ?? party;
|
||||
const { mapped } = fanavaranPartyInquirySources(
|
||||
"car-body",
|
||||
first as Parameters<typeof fanavaranPartyInquirySources>[1],
|
||||
);
|
||||
const left = text(mapped.plk1 ?? mapped.Plk1);
|
||||
const letter = text(mapped.plk2 ?? mapped.Plk2);
|
||||
const right = text(mapped.plk3 ?? mapped.Plk3);
|
||||
const serial = text(mapped.plksrl ?? mapped.PlkSrl);
|
||||
const plaqueNo = [left, letter, right].filter(Boolean).join("") || null;
|
||||
return {
|
||||
motorNo: text(mapped.MtrNum),
|
||||
chassisNo: text(mapped.ShsNum),
|
||||
vin: text(mapped.VIN),
|
||||
plaqueNo,
|
||||
plaqueSerial: serial,
|
||||
builtYear: parseFanavaranId(mapped.PrdDte),
|
||||
};
|
||||
}
|
||||
|
||||
function text(value: unknown): string | null {
|
||||
if (value == null) return null;
|
||||
const next = String(value).trim();
|
||||
return next ? next : null;
|
||||
}
|
||||
|
||||
export function toFanavaranHullExpertisePayload(input: {
|
||||
claimExpertId: number;
|
||||
dmgAssessmentDate: string;
|
||||
inspectionTime: string;
|
||||
wage: number;
|
||||
componentReplacementCost: number;
|
||||
wasteValue: number;
|
||||
dropAmount: number;
|
||||
vehicleCurrentValue: number | null;
|
||||
vehicle: FanavaranHullVehicleIdentity;
|
||||
dmgSections: Record<string, unknown>[];
|
||||
}): Record<string, unknown> {
|
||||
return {
|
||||
ClaimExpertId: input.claimExpertId,
|
||||
DmgAssessmentDate: input.dmgAssessmentDate,
|
||||
InspectionTime: input.inspectionTime,
|
||||
MotorNo: input.vehicle.motorNo,
|
||||
ChassisNo: input.vehicle.chassisNo,
|
||||
BuiltYear: input.vehicle.builtYear,
|
||||
VIN: input.vehicle.vin,
|
||||
PlaqueNo: input.vehicle.plaqueNo,
|
||||
PlaqueSerial: input.vehicle.plaqueSerial,
|
||||
Wage: input.wage,
|
||||
ComponentReplacementCost: input.componentReplacementCost,
|
||||
WasteValue: input.wasteValue,
|
||||
CarryAndRescueCost: null,
|
||||
RepairDuration: null,
|
||||
VehicleCurrentValue: input.vehicleCurrentValue,
|
||||
WreckHighestValue: null,
|
||||
InspectionDeduction: null,
|
||||
DmgAndWasteDesc: null,
|
||||
WentDistanceByExpert: null,
|
||||
IsDestruction: null,
|
||||
DropAmount: input.dropAmount,
|
||||
ColorId: null,
|
||||
PlaqueDesignId: null,
|
||||
PlaqueCityId: null,
|
||||
AccidentPercent: null,
|
||||
TheftCases: [],
|
||||
DmgSections: input.dmgSections,
|
||||
};
|
||||
}
|
||||
|
||||
export function toFanavaranHullExpertiseDmgSection(input: {
|
||||
partId?: unknown;
|
||||
desc: string;
|
||||
wasteValue: number;
|
||||
amount: number;
|
||||
}): Record<string, unknown> {
|
||||
return {
|
||||
Count: 1,
|
||||
Desc: input.desc,
|
||||
WasteValue: input.wasteValue,
|
||||
AccessoryKindId: parseFanavaranId(input.partId),
|
||||
DmgKindId: null,
|
||||
VehicleHullAccessoryId: null,
|
||||
DmgSectionCosts: [
|
||||
{
|
||||
Caption: input.desc,
|
||||
Amount: input.amount,
|
||||
DmgCostKindId: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function hullExpertiseAssertFields(
|
||||
product: FanavaranClaimProduct,
|
||||
): { requireDmgCaseId: boolean; requireThirdPartyLookups: boolean } {
|
||||
if (product === "car-body") {
|
||||
return { requireDmgCaseId: false, requireThirdPartyLookups: false };
|
||||
}
|
||||
return { requireDmgCaseId: true, requireThirdPartyLookups: true };
|
||||
}
|
||||
|
||||
export function collectFanavaranExpertiseReadinessWarnings(
|
||||
payload: Record<string, unknown>,
|
||||
options?: { requireDmgCaseId?: boolean; requireThirdPartyLookups?: boolean },
|
||||
): string[] {
|
||||
const warnings: string[] = [];
|
||||
const sections = Array.isArray(payload.DmgSections)
|
||||
? payload.DmgSections
|
||||
: [];
|
||||
const thirdPartyLookups = options?.requireThirdPartyLookups !== false;
|
||||
if (options?.requireDmgCaseId !== false && !payload.DmgCaseId) {
|
||||
warnings.push("DmgCaseId is required.");
|
||||
}
|
||||
if (thirdPartyLookups) {
|
||||
if (!payload.InspectionPlaceId) {
|
||||
warnings.push("InspectionPlaceId is required.");
|
||||
}
|
||||
if (!payload.DropAmountStatus) {
|
||||
warnings.push("DropAmountStatus is required.");
|
||||
}
|
||||
}
|
||||
if (!sections.length) {
|
||||
warnings.push("At least one DmgSections row is required.");
|
||||
}
|
||||
for (const [index, section] of sections.entries()) {
|
||||
const row = section as Record<string, unknown>;
|
||||
if (!thirdPartyLookups) continue;
|
||||
if (!row.DmgSectionId) {
|
||||
warnings.push(`DmgSections[${index}].DmgSectionId is required.`);
|
||||
}
|
||||
if (!row.AccidentLevel) {
|
||||
warnings.push(`DmgSections[${index}].AccidentLevel is required.`);
|
||||
}
|
||||
}
|
||||
return Array.from(new Set(warnings));
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
@@ -57,7 +58,7 @@ export class FanavaranController {
|
||||
@ApiOperation({
|
||||
summary: "Preview Fanavaran base claim create payload",
|
||||
description:
|
||||
"Builds the GEN.03 payload from local claim/blame data. Fanavaran-sourced PolicyId is resolve-once: first call may inquire and caches on the claim; later calls reuse the cache. Auth token is shared until Asia/Tehran midnight. Pass forceRefreshPolicy=true to re-inquire.",
|
||||
"GET has no request body. Always rebuilds GEN.03 from live policy + VIN/vehicle inquiries. Does not reuse fanavaranSync lastPayload / PolicyId / UsedId cache. Auth token is still shared until Asia/Tehran midnight.",
|
||||
})
|
||||
@ApiParam({
|
||||
name: "client",
|
||||
@@ -76,34 +77,30 @@ export class FanavaranController {
|
||||
@ApiQuery({
|
||||
name: "forceRefreshPolicy",
|
||||
required: false,
|
||||
description:
|
||||
"When true, ignores cached PolicyId and performs a live Fanavaran policy inquiry again. Do not pass this from normal UI loads.",
|
||||
deprecated: true,
|
||||
description: "Ignored. Preview always re-inquires PolicyId.",
|
||||
})
|
||||
@ApiQuery({
|
||||
name: "resolvePolicy",
|
||||
required: false,
|
||||
deprecated: true,
|
||||
description:
|
||||
"Deprecated. Ignored for cache-busting. PolicyId is resolve-once from fanavaranSync.baseClaim.policyId; use forceRefreshPolicy=true only to re-inquire.",
|
||||
description: "Ignored. Preview always re-inquires PolicyId.",
|
||||
})
|
||||
async preview(
|
||||
@Param("client") client: string,
|
||||
@Param("claimCaseId") claimCaseId: string,
|
||||
@Query("debug") debug?: string,
|
||||
@Query("forceRefreshPolicy") forceRefreshPolicy?: string,
|
||||
@Query("forceRefreshPolicy") _forceRefreshPolicy?: string,
|
||||
@Query("resolvePolicy") _resolvePolicy?: string,
|
||||
) {
|
||||
const clientKey = this.parseClientParam(client);
|
||||
// IMPORTANT: resolvePolicy must NOT force a live inquiry. Older UI clients
|
||||
// send resolvePolicy=true on every preview load; that used to defeat the
|
||||
// PolicyId cache and re-Login Fanavaran on every click.
|
||||
return await this.claimRequestManagementService.previewFanavaranSubmitV2(
|
||||
claimCaseId,
|
||||
clientKey,
|
||||
{
|
||||
debug: debug === "1" || debug === "true",
|
||||
forceRefreshPolicy:
|
||||
forceRefreshPolicy === "1" || forceRefreshPolicy === "true",
|
||||
forceRefreshPolicy: true,
|
||||
forceRefreshUsedId: true,
|
||||
requirePolicyId: false,
|
||||
},
|
||||
);
|
||||
@@ -113,7 +110,7 @@ export class FanavaranController {
|
||||
@ApiOperation({
|
||||
summary: "Submit Fanavaran base claim create request",
|
||||
description:
|
||||
"Authenticates with the selected tenant credentials and submits the GEN.03 base claim create request. Stores returned Id as claimId and ClaimNo when present.",
|
||||
"Swagger JSON body is optional. Always rebuilds GEN.03 from live inquiries, then POSTs that rebuilt payload. Pasting a previous preview JSON does not skip inquiry. Only user-entry keys are applied from the body (EstimateAmount, licence, dates, address, police report). PolicyId and AccidentVehicleUsedId in pasted JSON are ignored.",
|
||||
})
|
||||
@ApiParam({
|
||||
name: "client",
|
||||
@@ -124,6 +121,12 @@ export class FanavaranController {
|
||||
name: "claimCaseId",
|
||||
description: "Claim case MongoDB ObjectId",
|
||||
})
|
||||
@ApiBody({
|
||||
required: false,
|
||||
description:
|
||||
"Optional. Paste previous preview JSON if you want to tweak user-entry fields. Lookup ids in that JSON are ignored.",
|
||||
schema: { type: "object", additionalProperties: true, example: {} },
|
||||
})
|
||||
async submit(
|
||||
@Param("client") client: string,
|
||||
@Param("claimCaseId") claimCaseId: string,
|
||||
@@ -141,7 +144,7 @@ export class FanavaranController {
|
||||
@ApiOperation({
|
||||
summary: "Preview Fanavaran damage-case payload",
|
||||
description:
|
||||
"Builds the GEN.12 damaged vehicle/person case payload without calling Fanavaran. Requires selected damaged parts; submit requires a Fanavaran claimId.",
|
||||
"GET has no request body. Always rebuilds GEN.12 from live VIN/vehicle/driver inquiries. Does not reuse lastPayload / plaqueKindId / DriverId cache.",
|
||||
})
|
||||
@ApiParam({
|
||||
name: "client",
|
||||
@@ -167,7 +170,7 @@ export class FanavaranController {
|
||||
@ApiOperation({
|
||||
summary: "Submit Fanavaran damage-case request",
|
||||
description:
|
||||
"Submits the GEN.12 dmg-cases request. If base claim (claimId) is missing, soft-ensures GEN.03 base claim first, then submits damage. Skips when dmgCaseId already exists.",
|
||||
"Swagger JSON body is optional. Always rebuilds GEN.12 from live VIN/vehicle/driver inquiries, then POSTs that rebuilt payload. Pasting a previous preview JSON does not skip inquiry. Only Desc, EstimateAmount, LicenceNo, LicenceIssuDate, DriverIsOwner are taken from the body. PlaqueKindId, PlaqueSampleId, VehicleKindId, DriverId, InsuranceCorpId in pasted JSON are ignored.",
|
||||
})
|
||||
@ApiParam({
|
||||
name: "client",
|
||||
@@ -178,6 +181,12 @@ export class FanavaranController {
|
||||
name: "claimCaseId",
|
||||
description: "Claim case MongoDB ObjectId",
|
||||
})
|
||||
@ApiBody({
|
||||
required: false,
|
||||
description:
|
||||
"Optional. Paste previous preview JSON if you want to tweak Desc / licence / estimate. Lookup-filter ids in that JSON are ignored.",
|
||||
schema: { type: "object", additionalProperties: true, example: {} },
|
||||
})
|
||||
async submitDamageCase(
|
||||
@Param("client") client: string,
|
||||
@Param("claimCaseId") claimCaseId: string,
|
||||
@@ -247,7 +256,7 @@ export class FanavaranController {
|
||||
@ApiOperation({
|
||||
summary: "Preview Fanavaran expertise payload",
|
||||
description:
|
||||
"Builds the GEN.08 expertise payload from the active damage expert reply, price-drop data, and Fanavaran lookup mappings without calling Fanavaran.",
|
||||
"GET has no request body. Rebuilds GEN.08 from the active expert reply and lookups on every call. Does not reuse lastPayload cache.",
|
||||
})
|
||||
@ApiParam({
|
||||
name: "client",
|
||||
@@ -273,7 +282,7 @@ export class FanavaranController {
|
||||
@ApiOperation({
|
||||
summary: "Submit Fanavaran expertise request",
|
||||
description:
|
||||
"Submits the GEN.08 expertise payload for a claim with existing Fanavaran claimId and dmgCaseId. Stores returned Id as local expertiseId.",
|
||||
"Swagger JSON body is optional. Always rebuilds GEN.08 from the local expert reply and lookups, then POSTs that rebuilt payload. Pasting a previous preview JSON does not skip rebuild. Only money/date fields are taken from the body. DmgCaseId, ClaimExpertId, and DmgSections in pasted JSON are ignored.",
|
||||
})
|
||||
@ApiParam({
|
||||
name: "client",
|
||||
@@ -284,6 +293,12 @@ export class FanavaranController {
|
||||
name: "claimCaseId",
|
||||
description: "Claim case MongoDB ObjectId",
|
||||
})
|
||||
@ApiBody({
|
||||
required: false,
|
||||
description:
|
||||
"Optional. Paste previous preview JSON if you want to tweak money/date fields. DmgCaseId / ClaimExpertId / DmgSections in that JSON are ignored.",
|
||||
schema: { type: "object", additionalProperties: true, example: {} },
|
||||
})
|
||||
async submitExpertise(
|
||||
@Param("client") client: string,
|
||||
@Param("claimCaseId") claimCaseId: string,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import {
|
||||
canReuseCachedFanavaranVehicleIds,
|
||||
collectPartyVinCandidates,
|
||||
fanavaranIdOrConfigDefault,
|
||||
fanavaranPayloadMatchesDamagedIdentity,
|
||||
fanavaranVehicleMatchesDamagedIdentity,
|
||||
filterPoliciesByLine,
|
||||
applyFanavaranManualPayloadOverrides,
|
||||
resolveDamageCasePlaqueLookupIds,
|
||||
insuranceLineLabel,
|
||||
parseLastCarPolicyInput,
|
||||
pickFanavaranPlaqueLookupFields,
|
||||
@@ -10,6 +15,7 @@ import {
|
||||
pickVinFromPartyVehicle,
|
||||
plaqueLetterFromMiddleCode,
|
||||
plaqueMatchesVehicle,
|
||||
selectFanavaranVehicleForDamageCase,
|
||||
selectLastAmongCarMatches,
|
||||
toAppPlaque,
|
||||
vehicleMatchesCar,
|
||||
@@ -311,4 +317,188 @@ 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);
|
||||
});
|
||||
|
||||
it("uses national plaque 8/10 when the damaged VIN is unknown to Fanavaran", () => {
|
||||
expect(resolveDamageCasePlaqueLookupIds(null)).toEqual({
|
||||
kindId: 8,
|
||||
sampleId: 10,
|
||||
source: "national-plate-default",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps live PlaqueKindId 8 when Swagger body still has stale 15", () => {
|
||||
expect(
|
||||
applyFanavaranManualPayloadOverrides(
|
||||
{
|
||||
PlaqueKindId: 8,
|
||||
PlaqueSampleId: 10,
|
||||
VehicleKindId: 6704,
|
||||
AccidentVehicleUsedId: 1,
|
||||
Desc: "سپر جلو",
|
||||
},
|
||||
{
|
||||
PlaqueKindId: 15,
|
||||
PlaqueSampleId: 24,
|
||||
Desc: "سپر جلو (edited)",
|
||||
},
|
||||
["Desc", "EstimateAmount", "LicenceNo"],
|
||||
),
|
||||
).toEqual({
|
||||
PlaqueKindId: 8,
|
||||
PlaqueSampleId: 10,
|
||||
VehicleKindId: 6704,
|
||||
AccidentVehicleUsedId: 1,
|
||||
Desc: "سپر جلو (edited)",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -262,6 +262,242 @@ 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";
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
|
||||
Reference in New Issue
Block a user