Merge pull request 'fanavaran stages matching data is double checked and more controlled +' (#318) from s.hajizadeh/yara724api:main into main

Reviewed-on: Yara724/api#318
This commit is contained in:
2026-09-15 15:39:38 +03:30
6 changed files with 1016 additions and 117 deletions

View File

@@ -36,6 +36,16 @@ import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.se
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service"; import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service"; import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
import { fanavaranClaimReferences } from "./fanavaran-claim-references"; import { fanavaranClaimReferences } from "./fanavaran-claim-references";
import {
fanavaranClaimProductFromBlameType,
fanavaranClaimsBaseUrl,
fanavaranInsuranceLineIdForProduct,
fanavaranPartyInquirySources,
isFanavaranSubmitSupportedBlameType,
pickCarBodyPolicyNationalCode,
pickStoredCarBodyPolicyId,
type FanavaranClaimProduct,
} from "./fanavaran-claim-product";
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service"; import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto"; import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto";
import { import {
@@ -181,6 +191,7 @@ import {
fanavaranSubmitPath, fanavaranSubmitPath,
getFanavaranClientProfile, getFanavaranClientProfile,
resolveFanavaranClientKey, resolveFanavaranClientKey,
resolveFanavaranProductContractId,
} from "src/core/config/fanavaran-client.config"; } from "src/core/config/fanavaran-client.config";
import { FanavaranAuditService } from "src/fanavaran/fanavaran-audit.service"; import { FanavaranAuditService } from "src/fanavaran/fanavaran-audit.service";
import { FanavaranAuthService } from "src/fanavaran/fanavaran-auth.service"; import { FanavaranAuthService } from "src/fanavaran/fanavaran-auth.service";
@@ -216,14 +227,21 @@ import {
} from "./fanavaran-other-people"; } from "./fanavaran-other-people";
import { import {
asObjectRecord, asObjectRecord,
asVehicleInquiryRows,
canReuseCachedFanavaranVehicleIds,
collectPartyVinCandidates, collectPartyVinCandidates,
completePlaqueParts,
fanavaranIdOrConfigDefault, fanavaranIdOrConfigDefault,
fanavaranPayloadMatchesDamagedIdentity,
fanavaranVehicleMatchesDamagedIdentity,
normalizeVin, normalizeVin,
parseFanavaranId, parseFanavaranId,
pickFanavaranPlaqueLookupFields, pickFanavaranPlaqueLookupFields,
pickFanavaranVehicleFromInquiry, pickFanavaranVehicleFromInquiry,
pickVehicleId, pickVehicleId,
pickVinFromPartyVehicle, pickVinFromPartyVehicle,
selectFanavaranVehicleForDamageCase,
type FanavaranDamagedVehicleIdentity,
} from "src/lookups/fanavaran-last-car-policy"; } from "src/lookups/fanavaran-last-car-policy";
export interface FanavaranAutoSubmitResult { export interface FanavaranAutoSubmitResult {
@@ -358,9 +376,8 @@ const FANAVARAN_PLATE_LETTER_CODE: Record<string, number> = {
export class ClaimRequestManagementService { export class ClaimRequestManagementService {
private readonly logger = new Logger(ClaimRequestManagementService.name); private readonly logger = new Logger(ClaimRequestManagementService.name);
// Business submit URL (auth lives in FanavaranAuthService) // Legacy Tejaratno third-party URL. V2 stages use fanavaranClaimsBaseUrl(product).
private readonly FANAVARAN_SUBMIT_URL = private readonly FANAVARAN_SUBMIT_URL = fanavaranClaimsBaseUrl("third-party");
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/third-party-car-financial-claims";
// Authentication credentials // Authentication credentials
private readonly APP_NAME = "fanhab"; private readonly APP_NAME = "fanhab";
@@ -3932,29 +3949,47 @@ export class ClaimRequestManagementService {
} }
/** /**
* Damaged cars often store chassis on vehicle.vin. Try every VIN-like id * Damaged-car VIN inquiry: keep only rows that match VIN/plate/chassis.
* until Fanavaran returns PlaqueKindId (required on GEN.12). * Then GET that VehicleId so PlaqueKindId is the current version, not a
* leftover row that merely had some PlaqueKindId.
*/ */
private async fetchFanavaranVehicleByVinCandidates( private async fetchFanavaranVehicleByVinCandidates(
clientKey: FanavaranClientKey, clientKey: FanavaranClientKey,
vins: string[], vins: string[],
identity: FanavaranDamagedVehicleIdentity,
product: FanavaranClaimProduct = "third-party",
): Promise<Record<string, unknown> | null> { ): Promise<Record<string, unknown> | null> {
let fallback: Record<string, unknown> | null = null; const requestOptions = {
contractIdOverride: resolveFanavaranProductContractId(clientKey, product),
};
let matched: Record<string, unknown> | null = null;
for (const vin of vins) { for (const vin of vins) {
try { try {
const vehicle = await this.fetchFanavaranVehicleByVin(clientKey, vin); const inquired = await this.fanavaranLookupService.inquiryByVin(
if (!vehicle) { clientKey,
vin,
requestOptions,
);
const rows = asVehicleInquiryRows(inquired);
if (rows.length === 0) {
this.logger.warn( this.logger.warn(
`[buildFanavaranDamageCasePayload] VIN inquiry for ${vin} returned no vehicle`, `[buildFanavaranDamageCasePayload] VIN inquiry for ${vin} returned no vehicle`,
); );
continue; continue;
} }
const plaqueKindId = pickFanavaranPlaqueLookupFields(vehicle).kindId; const selected = selectFanavaranVehicleForDamageCase(rows, identity);
this.logger.log( if (!selected) {
`[buildFanavaranDamageCasePayload] VIN inquiry ${vin} VehicleId=${pickVehicleId(vehicle) ?? "none"} VehicleKindId=${parseFanavaranId(vehicle.VehicleKindId) ?? "none"} UsedId=${parseFanavaranId(vehicle.UsedId) ?? "none"} PlaqueKindId=${plaqueKindId ?? "none"}`, this.logger.warn(
`[buildFanavaranDamageCasePayload] VIN inquiry ${vin} returned ${rows.length} row(s) but none matched damaged VIN/plate/chassis`,
); );
if (plaqueKindId != null) return vehicle; continue;
if (!fallback) fallback = vehicle; }
const plaqueKindId = pickFanavaranPlaqueLookupFields(selected).kindId;
this.logger.log(
`[buildFanavaranDamageCasePayload] VIN inquiry ${vin} matched VehicleId=${pickVehicleId(selected) ?? "none"} VehicleKindId=${parseFanavaranId(selected.VehicleKindId) ?? "none"} UsedId=${parseFanavaranId(selected.UsedId) ?? "none"} PlaqueKindId=${plaqueKindId ?? "none"} VersionNo=${parseFanavaranId(selected.VersionNo) ?? "none"}`,
);
matched = selected;
break;
} catch (error) { } catch (error) {
this.logger.warn( this.logger.warn(
`[buildFanavaranDamageCasePayload] VIN inquiry failed for ${vin}: ${ `[buildFanavaranDamageCasePayload] VIN inquiry failed for ${vin}: ${
@@ -3963,7 +3998,43 @@ export class ClaimRequestManagementService {
); );
} }
} }
return fallback; if (!matched) return null;
const vehicleId = pickVehicleId(matched);
const versionNo = parseFanavaranId(matched.VersionNo);
if (vehicleId == null) return matched;
try {
const fetched = asObjectRecord(
await this.fanavaranLookupService.vehicleById(
clientKey,
vehicleId,
versionNo ?? undefined,
requestOptions,
),
);
if (
fetched &&
fanavaranVehicleMatchesDamagedIdentity(fetched, identity)
) {
this.logger.log(
`[buildFanavaranDamageCasePayload] vehicle GET ${vehicleId} confirmed identity PlaqueKindId=${pickFanavaranPlaqueLookupFields(fetched).kindId ?? "none"} VehicleKindId=${parseFanavaranId(fetched.VehicleKindId) ?? "none"}`,
);
return fetched;
}
if (fetched) {
this.logger.warn(
`[buildFanavaranDamageCasePayload] vehicle GET ${vehicleId} did not match damaged identity; keeping VIN-inquiry row`,
);
}
} catch (error) {
this.logger.warn(
`[buildFanavaranDamageCasePayload] vehicle GET failed for VehicleId=${vehicleId}: ${
error instanceof Error ? error.message : error
}`,
);
}
return matched;
} }
/** /**
@@ -3972,12 +4043,26 @@ export class ClaimRequestManagementService {
private async resolveAccidentVehicleUsedFromPolicy( private async resolveAccidentVehicleUsedFromPolicy(
clientKey: FanavaranClientKey, clientKey: FanavaranClientKey,
policyId: number, policyId: number,
product: FanavaranClaimProduct = "third-party",
): Promise<{ usedId: number | null; vehicleKindId: number | null }> { ): Promise<{ usedId: number | null; vehicleKindId: number | null }> {
try { try {
const requestOptions = {
contractIdOverride: resolveFanavaranProductContractId(
clientKey,
product,
),
};
const policy = asObjectRecord( const policy = asObjectRecord(
await this.fanavaranLookupService.thirdPartyPolicyById( product === "car-body"
? await this.fanavaranLookupService.bodyPolicyById(
clientKey, clientKey,
policyId, policyId,
requestOptions,
)
: await this.fanavaranLookupService.thirdPartyPolicyById(
clientKey,
policyId,
requestOptions,
), ),
); );
const vehicleId = pickVehicleId(policy?.VehicleId ?? policy?.vehicleId); const vehicleId = pickVehicleId(policy?.VehicleId ?? policy?.vehicleId);
@@ -3992,6 +4077,7 @@ export class ClaimRequestManagementService {
clientKey, clientKey,
vehicleId, vehicleId,
versionNo ?? undefined, versionNo ?? undefined,
requestOptions,
), ),
); );
this.logger.log( this.logger.log(
@@ -4013,6 +4099,7 @@ export class ClaimRequestManagementService {
parties: unknown[]; parties: unknown[];
guiltyPartyId?: unknown; guiltyPartyId?: unknown;
policyId?: number | null; policyId?: number | null;
product?: FanavaranClaimProduct;
}): Promise<{ }): Promise<{
usedId: number | null; usedId: number | null;
vehicleKindId: number | null; vehicleKindId: number | null;
@@ -4038,6 +4125,7 @@ export class ClaimRequestManagementService {
const fromPolicy = await this.resolveAccidentVehicleUsedFromPolicy( const fromPolicy = await this.resolveAccidentVehicleUsedFromPolicy(
input.clientKey, input.clientKey,
input.policyId, input.policyId,
input.product ?? "third-party",
); );
if (fromPolicy.usedId != null) { if (fromPolicy.usedId != null) {
return { ...fromPolicy, source: "policy-vehicle" }; return { ...fromPolicy, source: "policy-vehicle" };
@@ -4144,14 +4232,9 @@ export class ClaimRequestManagementService {
const person = damagedParty?.person ?? {}; const person = damagedParty?.person ?? {};
const vehicle = damagedParty?.vehicle ?? {}; const vehicle = damagedParty?.vehicle ?? {};
const insurance = damagedParty?.insurance ?? {}; const insurance = damagedParty?.insurance ?? {};
// Prefer nested inquiry (ESG/Tejarat) on the damaged party — not flattened insurance.* const product = fanavaranClaimProductFromBlameType(input.blameCase?.type);
const inquiryRawPayload = const { mapped: inquiryMapped, raw: inquiryRawPayload } =
vehicle?.inquiry?.raw?.data != null && fanavaranPartyInquirySources(product, damagedParty);
typeof vehicle.inquiry.raw.data === "object" &&
!Array.isArray(vehicle.inquiry.raw.data)
? vehicle.inquiry.raw.data
: (vehicle?.inquiry?.raw ?? {});
const inquiryMapped = vehicle?.inquiry?.mapped ?? inquiryRawPayload ?? {};
const inquiryRaw = inquiryRawPayload ?? {}; const inquiryRaw = inquiryRawPayload ?? {};
const plate = const plate =
@@ -4213,20 +4296,38 @@ export class ClaimRequestManagementService {
]); ]);
const builtYearFromInquiry = builtYearRaw ? Number(builtYearRaw) || null : null; const builtYearFromInquiry = builtYearRaw ? Number(builtYearRaw) || null : null;
const damagedVinCandidates: string[] = []; const damagedVin =
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",
]), ]) ?? collectPartyVinCandidates(damagedParty)[0] ??
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [ null;
const damagedChassis = this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"ShsNum", "ShsNum",
"ChassisNumberField", "ChassisNumberField",
"shsNam", "shsNam",
]), ]);
const damagedIdentity: FanavaranDamagedVehicleIdentity = {
vin: damagedVin,
chassis: damagedChassis,
plaque: plate
? completePlaqueParts({
plaqueLeft: String(plate.leftDigits),
plaqueLetter: plate.centerAlphabet,
plaqueRight: String(plate.centerDigits),
plaqueSerial: String(plate.ir),
})
: null,
policyCINumber,
};
const damagedVinCandidates: string[] = [];
const seenVins = new Set<string>();
for (const value of [
...collectPartyVinCandidates(damagedParty),
damagedVin,
damagedChassis,
]) { ]) {
const vin = normalizeVin(value); const vin = normalizeVin(value);
if (!vin || seenVins.has(vin)) continue; if (!vin || seenVins.has(vin)) continue;
@@ -4236,29 +4337,46 @@ export class ClaimRequestManagementService {
const cachedDamage = const cachedDamage =
(input.claimCase as any)?.fanavaranSync?.damageCase ?? {}; (input.claimCase as any)?.fanavaranSync?.damageCase ?? {};
const lastDamagePayload = asObjectRecord(cachedDamage.lastPayload); const lastDamagePayload = asObjectRecord(cachedDamage.lastPayload);
const cacheMatchesIdentity = fanavaranPayloadMatchesDamagedIdentity(
lastDamagePayload,
damagedIdentity,
);
const canReuseCachedPlaque =
cacheMatchesIdentity &&
canReuseCachedFanavaranVehicleIds(cachedDamage.plaqueKindSource);
const canReuseCachedVehicleKind =
cacheMatchesIdentity &&
canReuseCachedFanavaranVehicleIds(cachedDamage.vehicleKindSource);
this.logger.log( this.logger.log(
`[buildFanavaranDamageCasePayload] VIN candidates=${damagedVinCandidates.join(",") || "none"}`, `[buildFanavaranDamageCasePayload] identity VIN=${damagedIdentity.vin ?? "none"} chassis=${damagedIdentity.chassis ?? "none"} plate=${damagedIdentity.plaque ? `${damagedIdentity.plaque.right}${damagedIdentity.plaque.letter}${damagedIdentity.plaque.left}/${damagedIdentity.plaque.serial}` : "none"} CI=${damagedIdentity.policyCINumber ?? "none"} VIN candidates=${damagedVinCandidates.join(",") || "none"}`,
); );
const apiVehicle = await this.fetchFanavaranVehicleByVinCandidates( const apiVehicle = await this.fetchFanavaranVehicleByVinCandidates(
input.clientKey, input.clientKey,
damagedVinCandidates, damagedVinCandidates,
damagedIdentity,
product,
); );
const apiPlaque = pickFanavaranPlaqueLookupFields(apiVehicle); const apiPlaque = pickFanavaranPlaqueLookupFields(apiVehicle);
const inquiryPlaque = pickFanavaranPlaqueLookupFields( const inquiryVehicle =
asObjectRecord(inquiryMapped) ?? asObjectRecord(inquiryRaw), asObjectRecord(inquiryMapped) ?? asObjectRecord(inquiryRaw);
); const inquiryPlaque = fanavaranVehicleMatchesDamagedIdentity(
inquiryVehicle,
damagedIdentity,
)
? pickFanavaranPlaqueLookupFields(inquiryVehicle)
: pickFanavaranPlaqueLookupFields(null);
const plaqueKindFromApi = this.firstPositiveFanavaranId( const plaqueKindFromApi = this.firstPositiveFanavaranId(
apiPlaque.kindId, apiPlaque.kindId,
inquiryPlaque.kindId, inquiryPlaque.kindId,
cachedDamage.plaqueKindId, canReuseCachedPlaque ? cachedDamage.plaqueKindId : null,
lastDamagePayload?.PlaqueKindId, canReuseCachedPlaque ? lastDamagePayload?.PlaqueKindId : null,
); );
const plaqueSampleFromApi = this.firstPositiveFanavaranId( const plaqueSampleFromApi = this.firstPositiveFanavaranId(
apiPlaque.sampleId, apiPlaque.sampleId,
inquiryPlaque.sampleId, inquiryPlaque.sampleId,
cachedDamage.plaqueSampleId, canReuseCachedPlaque ? cachedDamage.plaqueSampleId : null,
lastDamagePayload?.PlaqueSampleId, canReuseCachedPlaque ? lastDamagePayload?.PlaqueSampleId : null,
); );
const plaqueKindId = fanavaranIdOrConfigDefault( const plaqueKindId = fanavaranIdOrConfigDefault(
plaqueKindFromApi, plaqueKindFromApi,
@@ -4268,6 +4386,10 @@ export class ClaimRequestManagementService {
plaqueSampleFromApi, plaqueSampleFromApi,
input.defaults.PlaqueSampleId, input.defaults.PlaqueSampleId,
); );
const plaqueKindSource =
plaqueKindFromApi != null ? "vehicle-inquiry" : "config-default";
const plaqueSampleSource =
plaqueSampleFromApi != null ? "vehicle-inquiry" : "config-default";
if (plaqueKindFromApi == null) { if (plaqueKindFromApi == null) {
this.logger.log( this.logger.log(
`[buildFanavaranDamageCasePayload] PlaqueKindId API miss; using config default ${plaqueKindId}`, `[buildFanavaranDamageCasePayload] PlaqueKindId API miss; using config default ${plaqueKindId}`,
@@ -4284,14 +4406,21 @@ export class ClaimRequestManagementService {
const carType = input.claimCase?.vehicle?.carType as string | undefined; const carType = input.claimCase?.vehicle?.carType as string | undefined;
let vehicleKindId = this.firstPositiveFanavaranId( let vehicleKindId = this.firstPositiveFanavaranId(
apiVehicle?.VehicleKindId, apiVehicle?.VehicleKindId,
cachedDamage.vehicleKindId, canReuseCachedVehicleKind ? cachedDamage.vehicleKindId : null,
lastDamagePayload?.VehicleKindId, canReuseCachedVehicleKind ? lastDamagePayload?.VehicleKindId : null,
); );
let vehicleKindSource: "vehicle-inquiry" | "car-type-lookup" | null =
parseFanavaranId(apiVehicle?.VehicleKindId) != null
? "vehicle-inquiry"
: canReuseCachedVehicleKind
? "vehicle-inquiry"
: null;
if (vehicleKindId == null) { if (vehicleKindId == null) {
vehicleKindId = await this.resolveVehicleKindId( vehicleKindId = await this.resolveVehicleKindId(
input.clientKey, input.clientKey,
carType, carType,
); );
if (vehicleKindId != null) vehicleKindSource = "car-type-lookup";
} }
// Prefer claim-level cache, then party.person.fanavaranDriverId, then live inquiry, then last payload // Prefer claim-level cache, then party.person.fanavaranDriverId, then live inquiry, then last payload
@@ -4359,49 +4488,37 @@ export class ClaimRequestManagementService {
} }
const cachedUsedId = const cachedUsedId =
cachedDamage.accidentVehicleUsedId != null cacheMatchesIdentity && cachedDamage.accidentVehicleUsedId != null
? Number(cachedDamage.accidentVehicleUsedId) ? Number(cachedDamage.accidentVehicleUsedId)
: (input.claimCase as any)?.fanavaranSync?.baseClaim
?.accidentVehicleUsedId != null
? Number(
(input.claimCase as any).fanavaranSync.baseClaim
.accidentVehicleUsedId,
)
: null; : null;
const cachedUsedSource = const cachedUsedSource = cachedDamage.accidentVehicleUsedSource;
cachedDamage.accidentVehicleUsedSource ??
(input.claimCase as any)?.fanavaranSync?.baseClaim
?.accidentVehicleUsedSource;
const canReuseApiUsedId = const canReuseApiUsedId =
cachedUsedId != null && cachedUsedId != null &&
cachedUsedId > 0 && cachedUsedId > 0 &&
(cachedUsedSource === "vin-inquiry" || cachedUsedSource === "vehicle-inquiry";
cachedUsedSource === "policy-vehicle");
let accidentVehicleUsedId: number | null = canReuseApiUsedId let accidentVehicleUsedId: number | null = canReuseApiUsedId
? cachedUsedId ? cachedUsedId
: null; : null;
let usedSource: "vin-inquiry" | "policy-vehicle" | null = canReuseApiUsedId let usedSource: "vin-inquiry" | "policy-vehicle" | "vehicle-inquiry" | null =
? cachedUsedSource canReuseApiUsedId ? "vehicle-inquiry" : null;
: null; if (accidentVehicleUsedId == null && apiVehicle) {
if (accidentVehicleUsedId == null) { const fromDamagedVehicle =
const resolved = await this.resolveAccidentVehicleUsedFromApis({ await this.resolveAccidentVehicleUsedFromVehicleRecord(
clientKey: input.clientKey, input.clientKey,
parties: input.blameCase?.parties ?? [], apiVehicle,
guiltyPartyId: this.resolveGuiltyPartyIdV2( );
input.blameCase?.parties ?? [], accidentVehicleUsedId = fromDamagedVehicle.usedId;
input.blameCase?.expert?.decision?.guiltyPartyId, if (accidentVehicleUsedId != null) usedSource = "vehicle-inquiry";
),
policyId: (input.claimCase as any)?.fanavaranSync?.baseClaim?.policyId,
});
accidentVehicleUsedId = resolved.usedId;
usedSource = resolved.source;
} }
if (accidentVehicleUsedId == null) { if (accidentVehicleUsedId == null) {
accidentVehicleUsedId = await this.resolveAccidentVehicleUsedFromVehicleKind( accidentVehicleUsedId = await this.resolveAccidentVehicleUsedFromVehicleKind(
input.clientKey, input.clientKey,
vehicleKindId, vehicleKindId,
null, parseFanavaranId(apiVehicle?.UsedId),
); );
if (accidentVehicleUsedId != null && usedSource == null) {
usedSource = apiVehicle ? "vehicle-inquiry" : null;
}
} }
if (accidentVehicleUsedId == null) { if (accidentVehicleUsedId == null) {
accidentVehicleUsedId = input.defaults.AccidentVehicleUsedId; accidentVehicleUsedId = input.defaults.AccidentVehicleUsedId;
@@ -4482,10 +4599,6 @@ export class ClaimRequestManagementService {
const persistedPayload = this.keepPreviousPositiveIds(lastDamagePayload, payload, [ const persistedPayload = this.keepPreviousPositiveIds(lastDamagePayload, payload, [
"DriverId", "DriverId",
"PlaqueKindId",
"PlaqueSampleId",
"VehicleKindId",
"AccidentVehicleUsedId",
"InsuranceCorpId", "InsuranceCorpId",
]); ]);
@@ -4502,14 +4615,16 @@ export class ClaimRequestManagementService {
? { "fanavaranSync.damageCase.otherPersonId": otherPersonId } ? { "fanavaranSync.damageCase.otherPersonId": otherPersonId }
: {}), : {}),
...(vehicleKindId != null ...(vehicleKindId != null
? { "fanavaranSync.damageCase.vehicleKindId": vehicleKindId } ? {
: {}), "fanavaranSync.damageCase.vehicleKindId": vehicleKindId,
...(plaqueKindId != null "fanavaranSync.damageCase.vehicleKindSource":
? { "fanavaranSync.damageCase.plaqueKindId": plaqueKindId } vehicleKindSource ?? "car-type-lookup",
: {}), }
...(plaqueSampleId != null
? { "fanavaranSync.damageCase.plaqueSampleId": plaqueSampleId }
: {}), : {}),
"fanavaranSync.damageCase.plaqueKindId": plaqueKindId,
"fanavaranSync.damageCase.plaqueKindSource": plaqueKindSource,
"fanavaranSync.damageCase.plaqueSampleId": plaqueSampleId,
"fanavaranSync.damageCase.plaqueSampleSource": plaqueSampleSource,
...(accidentVehicleUsedId != null ...(accidentVehicleUsedId != null
? { ? {
"fanavaranSync.damageCase.accidentVehicleUsedId": "fanavaranSync.damageCase.accidentVehicleUsedId":
@@ -4720,6 +4835,31 @@ export class ClaimRequestManagementService {
return resolveFanavaranClientKey(); return resolveFanavaranClientKey();
} }
private async resolveFanavaranClaimProduct(
claimCaseId?: string,
blameCase?: { type?: string } | null,
): Promise<FanavaranClaimProduct> {
if (blameCase?.type) {
return fanavaranClaimProductFromBlameType(blameCase.type);
}
if (!claimCaseId) return "third-party";
const claimCase = await this.claimCaseDbService.findById(claimCaseId);
if (!claimCase?.blameRequestId) return "third-party";
const blame = await this.blameRequestDbService.findById(
String(claimCase.blameRequestId),
);
return fanavaranClaimProductFromBlameType(blame?.type);
}
private async fanavaranClaimsUrlForClaim(
claimCaseId?: string,
blameCase?: { type?: string } | null,
): Promise<string> {
return fanavaranClaimsBaseUrl(
await this.resolveFanavaranClaimProduct(claimCaseId, blameCase),
);
}
private async getFanavaranAuthHeaders( private async getFanavaranAuthHeaders(
clientKey: FanavaranClientKey, clientKey: FanavaranClientKey,
auditSession?: FanavaranAuditSession, auditSession?: FanavaranAuditSession,
@@ -4729,9 +4869,11 @@ export class ClaimRequestManagementService {
const locationOverride = caseId const locationOverride = caseId
? await this.resolveFanavaranLocationForClaim(caseId, clientKey) ? await this.resolveFanavaranLocationForClaim(caseId, clientKey)
: undefined; : undefined;
const product = await this.resolveFanavaranClaimProduct(caseId);
return this.fanavaranAuthService.getRequestHeaders(clientKey, { return this.fanavaranAuthService.getRequestHeaders(clientKey, {
auditSession, auditSession,
locationOverride, locationOverride,
contractIdOverride: resolveFanavaranProductContractId(clientKey, product),
}); });
} }
@@ -4793,21 +4935,31 @@ export class ClaimRequestManagementService {
}, },
logPrefix: string, logPrefix: string,
auditSession?: FanavaranAuditSession, auditSession?: FanavaranAuditSession,
options?: { clientKey?: FanavaranClientKey; claimCaseId?: string }, options?: {
clientKey?: FanavaranClientKey;
claimCaseId?: string;
product?: FanavaranClaimProduct;
insuranceLineId?: number;
},
): Promise<number | null> { ): Promise<number | null> {
const clientKey = const clientKey =
options?.clientKey ?? this.resolveClientKeyFromAuthConfig(config); options?.clientKey ?? this.resolveClientKeyFromAuthConfig(config);
const policyInquiryUrl = `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/common/Policies/inquiry-my-policies?InsuranceLineId=5&NationalCode=${nationalCodeOfInsurer}`; const product =
options?.product ??
(await this.resolveFanavaranClaimProduct(options?.claimCaseId));
const insuranceLineId =
options?.insuranceLineId ?? fanavaranInsuranceLineIdForProduct(product);
const policyInquiryUrl = `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/common/Policies/inquiry-my-policies?InsuranceLineId=${insuranceLineId}&NationalCode=${nationalCodeOfInsurer}`;
const startedAt = Date.now(); const startedAt = Date.now();
const requestMeta = { const requestMeta = {
corpId: config.corpId, corpId: config.corpId,
contractId: config.contractId, contractId: resolveFanavaranProductContractId(clientKey, product),
location: config.location, location: config.location,
nationalCode: this.fanavaranAuditService.maskNationalCode( nationalCode: this.fanavaranAuditService.maskNationalCode(
nationalCodeOfInsurer, nationalCodeOfInsurer,
), ),
InsuranceLineId: 5, InsuranceLineId: insuranceLineId,
}; };
if (auditSession) { if (auditSession) {
@@ -5268,6 +5420,13 @@ export class ClaimRequestManagementService {
: []; : [];
if (parties.length === 0) return null; if (parties.length === 0) return null;
if (
blameCase?.type === BlameRequestType.CAR_BODY ||
blameCase?.type === "CAR_BODY"
) {
return resolveDamagedPartyRow(blameCase) ?? parties[0] ?? null;
}
const ownerId = claimCase?.owner?.userId; const ownerId = claimCase?.owner?.userId;
if (ownerId) { if (ownerId) {
const byOwner = parties.find( const byOwner = parties.find(
@@ -5474,15 +5633,69 @@ export class ClaimRequestManagementService {
(claimCase as any)?.fanavaranSync?.baseClaim?.lastPayload?.PolicyId ?? (claimCase as any)?.fanavaranSync?.baseClaim?.lastPayload?.PolicyId ??
null; null;
const accidentAt =
blameCase.type === BlameRequestType.CAR_BODY
? ((firstParty as { statement?: { accidentDate?: Date } })?.statement
?.accidentDate ??
(blameCase as { createdAt?: Date }).createdAt)
: (blameCase as { createdAt?: Date }).createdAt;
const payload = await this.buildFanavaranSubmitPayload({ const payload = await this.buildFanavaranSubmitPayload({
accidentReason: selectedAccidentReason, accidentReason: selectedAccidentReason,
createdAt: (blameCase as { createdAt?: Date }).createdAt, createdAt: accidentAt,
location: firstParty?.location, location: firstParty?.location,
damageParts, damageParts,
defaults: profile.defaults, defaults: profile.defaults,
logPrefix, logPrefix,
requirePolicyId, requirePolicyId,
resolvePolicyId: async () => { resolvePolicyId: async () => {
const product = fanavaranClaimProductFromBlameType(blameCase.type);
if (cachedPolicyId != null && !forceRefreshPolicy) {
debug.steps.policyIdFromCache = true;
debug.values.policyId = Number(cachedPolicyId);
return Number(cachedPolicyId);
}
if (product === "car-body") {
const storedPolicyId = pickStoredCarBodyPolicyId(blameCase);
if (storedPolicyId != null) {
debug.values.policyId = storedPolicyId;
debug.steps.policyInquirySucceeded = true;
return storedPolicyId;
}
const nationalCodeOfInsurer =
pickCarBodyPolicyNationalCode(blameCase);
debug.steps.nationalCodeOfInsurerFound = !!nationalCodeOfInsurer;
debug.values.nationalCodeOfInsurer = nationalCodeOfInsurer;
if (!nationalCodeOfInsurer) {
this.logger.warn(
`${logPrefix} car-body policyholder national code is missing`,
);
debug.failureReason =
"car-body policyholder national code is missing";
return null;
}
debug.steps.policyInquiryAttempted = true;
const policyId = await this.getPolicyIdFromNationalCode(
nationalCodeOfInsurer,
profile.auth,
logPrefix,
auditSession,
{ clientKey, claimCaseId, product: "car-body" },
);
debug.values.policyId = policyId;
debug.steps.policyInquirySucceeded = policyId !== null;
if (policyId === null) {
debug.failureReason =
debug.failureReason ??
"car-body policy inquiry returned no PolicyId (timeout, network error, empty response, or backoff)";
}
return policyId;
}
const guiltyPartyId = this.resolveGuiltyPartyIdV2( const guiltyPartyId = this.resolveGuiltyPartyIdV2(
blameCase.parties ?? [], blameCase.parties ?? [],
expertDecision?.guiltyPartyId, expertDecision?.guiltyPartyId,
@@ -5519,20 +5732,13 @@ export class ClaimRequestManagementService {
return null; return null;
} }
if (cachedPolicyId != null && !forceRefreshPolicy) {
debug.steps.policyIdFromCache = true;
debug.values.policyId = Number(cachedPolicyId);
// Silent reuse — no Fanavaran HTTP and no audit noise on warm preview.
return Number(cachedPolicyId);
}
debug.steps.policyInquiryAttempted = true; debug.steps.policyInquiryAttempted = true;
const policyId = await this.getPolicyIdFromNationalCode( const policyId = await this.getPolicyIdFromNationalCode(
nationalCodeOfInsurer, nationalCodeOfInsurer,
profile.auth, profile.auth,
logPrefix, logPrefix,
auditSession, auditSession,
{ clientKey, claimCaseId }, { clientKey, claimCaseId, product: "third-party" },
); );
debug.values.policyId = policyId; debug.values.policyId = policyId;
debug.steps.policyInquirySucceeded = policyId !== null; debug.steps.policyInquirySucceeded = policyId !== null;
@@ -5545,6 +5751,17 @@ export class ClaimRequestManagementService {
}, },
}); });
const carBodyAccidentTime = (
firstParty as { statement?: { accidentTime?: string } } | undefined
)?.statement?.accidentTime;
if (
blameCase.type === BlameRequestType.CAR_BODY &&
typeof carBodyAccidentTime === "string" &&
carBodyAccidentTime.trim()
) {
payload.AccidentTime = carBodyAccidentTime.trim();
}
const cachedUsedId = const cachedUsedId =
forceRefreshUsedId || forceRefreshPolicy forceRefreshUsedId || forceRefreshPolicy
? null ? null
@@ -5575,6 +5792,7 @@ export class ClaimRequestManagementService {
guiltyPartyId: guiltyPartyIdForVin, guiltyPartyId: guiltyPartyIdForVin,
policyId: policyId:
typeof payload.PolicyId === "number" ? payload.PolicyId : null, typeof payload.PolicyId === "number" ? payload.PolicyId : null,
product: fanavaranClaimProductFromBlameType(blameCase.type),
}); });
if (resolved.usedId != null) { if (resolved.usedId != null) {
payload.AccidentVehicleUsedId = resolved.usedId; payload.AccidentVehicleUsedId = resolved.usedId;
@@ -5687,9 +5905,9 @@ export class ClaimRequestManagementService {
if (!blameCase) { if (!blameCase) {
throw new NotFoundException("Blame case not found"); throw new NotFoundException("Blame case not found");
} }
if (blameCase.type !== BlameRequestType.THIRD_PARTY) { if (!isFanavaranSubmitSupportedBlameType(blameCase.type)) {
throw new BadRequestException( throw new BadRequestException(
"Fanavaran damage-case submit only applies to THIRD_PARTY claims", "Fanavaran damage-case submit only applies to THIRD_PARTY and CAR_BODY claims",
); );
} }
@@ -5717,7 +5935,7 @@ export class ClaimRequestManagementService {
claimId: claimCase.claimId ?? null, claimId: claimCase.claimId ?? null,
dmgCaseId: claimCase.dmgCaseId ?? null, dmgCaseId: claimCase.dmgCaseId ?? null,
submitUrl: claimCase.claimId submitUrl: claimCase.claimId
? `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/dmg-cases` ? `${fanavaranClaimsBaseUrl(fanavaranClaimProductFromBlameType(blameCase.type))}/${claimCase.claimId}/dmg-cases`
: null, : null,
warning: warnings.length ? warnings.join(" ") : undefined, warning: warnings.length ? warnings.join(" ") : undefined,
payload, payload,
@@ -5881,6 +6099,7 @@ export class ClaimRequestManagementService {
): Promise<any> { ): Promise<any> {
const { claimCase, candidates } = const { claimCase, candidates } =
await this.collectFanavaranAttachmentCandidates(claimCaseId, clientKey); await this.collectFanavaranAttachmentCandidates(claimCaseId, clientKey);
const claimsUrl = await this.fanavaranClaimsUrlForClaim(claimCaseId);
return { return {
clientKey, clientKey,
@@ -5889,7 +6108,7 @@ export class ClaimRequestManagementService {
claimNo: claimCase.claimNo ?? null, claimNo: claimCase.claimNo ?? null,
dmgCaseId: claimCase.dmgCaseId ?? null, dmgCaseId: claimCase.dmgCaseId ?? null,
submitUrl: claimCase.claimId submitUrl: claimCase.claimId
? `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/files` ? `${claimsUrl}/${claimCase.claimId}/files`
: null, : null,
warning: claimCase.claimId warning: claimCase.claimId
? undefined ? undefined
@@ -5984,7 +6203,7 @@ export class ClaimRequestManagementService {
}; };
} }
const url = `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/files`; const url = `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${claimCase.claimId}/files`;
const fileTypeId = profile.defaults.ClaimFileTypeId; const fileTypeId = profile.defaults.ClaimFileTypeId;
const logPrefix = `[Fanavaran ${clientKey} V2 Attachment Batch] claimCaseId=${claimCaseId}`; const logPrefix = `[Fanavaran ${clientKey} V2 Attachment Batch] claimCaseId=${claimCaseId}`;
@@ -6143,7 +6362,7 @@ export class ClaimRequestManagementService {
}; };
} }
url = `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/files`; url = `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${claimCase.claimId}/files`;
content = { content = {
FileName: fileName, FileName: fileName,
FileTypeId: profile.defaults.ClaimFileTypeId, FileTypeId: profile.defaults.ClaimFileTypeId,
@@ -6636,7 +6855,7 @@ export class ClaimRequestManagementService {
expertiseId: claimCase.expertiseId ?? null, expertiseId: claimCase.expertiseId ?? null,
replyKey, replyKey,
submitUrl: claimCase.claimId submitUrl: claimCase.claimId
? `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/expertise` ? `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${claimCase.claimId}/expertise`
: null, : null,
ready: warnings.length === 0, ready: warnings.length === 0,
warnings: Array.from(new Set(warnings)), warnings: Array.from(new Set(warnings)),
@@ -6721,7 +6940,7 @@ export class ClaimRequestManagementService {
} }
); );
} }
const url = `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/expertise`; const url = `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${claimCase.claimId}/expertise`;
const startedAt = Date.now(); const startedAt = Date.now();
const auditSession: FanavaranAuditSession = { const auditSession: FanavaranAuditSession = {
trackingCode: this.fanavaranAuditService.generateTrackingCode(), trackingCode: this.fanavaranAuditService.generateTrackingCode(),
@@ -6938,7 +7157,7 @@ export class ClaimRequestManagementService {
fanavaran: FanavaranAutoSubmitResult, fanavaran: FanavaranAutoSubmitResult,
): string { ): string {
if (fanavaran.submitted) { if (fanavaran.submitted) {
return `${baseMessage} The initial third-party case was sent to Fanavaran successfully.`; return `${baseMessage} The initial Fanavaran case was sent successfully.`;
} }
if (fanavaran.warning) { if (fanavaran.warning) {
return `${baseMessage} ${fanavaran.warning}`; return `${baseMessage} ${fanavaran.warning}`;
@@ -6964,8 +7183,8 @@ export class ClaimRequestManagementService {
return "Blame case not found"; return "Blame case not found";
} }
if (blameCase.type !== BlameRequestType.THIRD_PARTY) { if (!isFanavaranSubmitSupportedBlameType(blameCase.type)) {
return "Fanavaran third-party financial submit only applies to THIRD_PARTY claims"; return "Fanavaran submit only applies to THIRD_PARTY and CAR_BODY claims";
} }
return null; return null;
@@ -7197,9 +7416,9 @@ export class ClaimRequestManagementService {
if (!blameCase) { if (!blameCase) {
throw new NotFoundException("Blame case not found"); throw new NotFoundException("Blame case not found");
} }
if (blameCase.type !== BlameRequestType.THIRD_PARTY) { if (!isFanavaranSubmitSupportedBlameType(blameCase.type)) {
throw new BadRequestException( throw new BadRequestException(
"Fanavaran damage-case submit only applies to THIRD_PARTY claims", "Fanavaran damage-case submit only applies to THIRD_PARTY and CAR_BODY claims",
); );
} }
@@ -7215,7 +7434,7 @@ export class ClaimRequestManagementService {
// Guardrail: never send empty/null licence fields even if manual overrides arrive. // Guardrail: never send empty/null licence fields even if manual overrides arrive.
payload.LicenceNo = this.ensureFanavaranNonEmptyLicenceNo(payload.LicenceNo); payload.LicenceNo = this.ensureFanavaranNonEmptyLicenceNo(payload.LicenceNo);
const url = `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/dmg-cases`; const url = `${fanavaranClaimsBaseUrl(fanavaranClaimProductFromBlameType(blameCase.type))}/${claimCase.claimId}/dmg-cases`;
this.logger.log( this.logger.log(
`[executeFanavaranDamageCaseSubmit] claimCaseId=${claimCaseId} claimId=${claimCase.claimId} payload.DriverId=${payload.DriverId ?? "NULL"} payload keys=${Object.keys(payload).join(", ")}`, `[executeFanavaranDamageCaseSubmit] claimCaseId=${claimCaseId} claimId=${claimCase.claimId} payload.DriverId=${payload.DriverId ?? "NULL"} payload keys=${Object.keys(payload).join(", ")}`,
); );
@@ -7439,7 +7658,7 @@ export class ClaimRequestManagementService {
} }
/** /**
* Auto-submit the minimum third-party Fanavaran claim as soon as the local * Auto-submit the minimum Fanavaran claim as soon as the local
* claim case exists. This preserves progress in Fanavaran even if parties stop * claim case exists. This preserves progress in Fanavaran even if parties stop
* later in the damage-assessment flow. * later in the damage-assessment flow.
*/ */
@@ -8046,6 +8265,7 @@ export class ClaimRequestManagementService {
auditSession, auditSession,
claimCaseId, claimCaseId,
); );
const submitUrl = await this.fanavaranClaimsUrlForClaim(claimCaseId);
const requestHeaders = { const requestHeaders = {
...headers, ...headers,
@@ -8055,7 +8275,7 @@ export class ClaimRequestManagementService {
session: auditSession, session: auditSession,
step: FanavaranAuditStep.SUBMIT_CLAIM, step: FanavaranAuditStep.SUBMIT_CLAIM,
status: FanavaranAuditStatus.STARTED, status: FanavaranAuditStatus.STARTED,
requestUrl: this.FANAVARAN_SUBMIT_URL, requestUrl: submitUrl,
requestMethod: "POST", requestMethod: "POST",
requestHeaders, requestHeaders,
requestBody: fanavaranData, requestBody: fanavaranData,
@@ -8065,7 +8285,7 @@ export class ClaimRequestManagementService {
try { try {
const response = await firstValueFrom( const response = await firstValueFrom(
this.httpService.post(this.FANAVARAN_SUBMIT_URL, fanavaranData, { this.httpService.post(submitUrl, fanavaranData, {
headers: requestHeaders, headers: requestHeaders,
}), }),
); );
@@ -8086,7 +8306,7 @@ export class ClaimRequestManagementService {
session: auditSession, session: auditSession,
step: FanavaranAuditStep.SUBMIT_CLAIM, step: FanavaranAuditStep.SUBMIT_CLAIM,
status: FanavaranAuditStatus.SUCCESS, status: FanavaranAuditStatus.SUCCESS,
requestUrl: this.FANAVARAN_SUBMIT_URL, requestUrl: submitUrl,
requestMethod: "POST", requestMethod: "POST",
httpStatus: response.status, httpStatus: response.status,
requestHeaders: exchange.requestHeaders, requestHeaders: exchange.requestHeaders,
@@ -8138,7 +8358,7 @@ export class ClaimRequestManagementService {
session: auditSession, session: auditSession,
step: FanavaranAuditStep.SUBMIT_CLAIM, step: FanavaranAuditStep.SUBMIT_CLAIM,
status: FanavaranAuditStatus.FAILURE, status: FanavaranAuditStatus.FAILURE,
requestUrl: this.FANAVARAN_SUBMIT_URL, requestUrl: submitUrl,
requestMethod: "POST", requestMethod: "POST",
httpStatus: exchange.httpStatus, httpStatus: exchange.httpStatus,
requestHeaders: exchange.requestHeaders, requestHeaders: exchange.requestHeaders,

View File

@@ -97,14 +97,26 @@ export class FanavaranSyncStage {
@Prop({ type: Number }) @Prop({ type: Number })
vehicleKindId?: 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. */ /** Cached Fanavaran PlaqueKindId from VIN inquiry / vehicle GET. */
@Prop({ type: Number }) @Prop({ type: Number })
plaqueKindId?: number; plaqueKindId?: number;
/** How PlaqueKindId was resolved: vehicle-inquiry | config-default */
@Prop({ type: String })
plaqueKindSource?: string;
/** Cached Fanavaran PlaqueSampleId from VIN inquiry / vehicle GET. */ /** Cached Fanavaran PlaqueSampleId from VIN inquiry / vehicle GET. */
@Prop({ type: Number }) @Prop({ type: Number })
plaqueSampleId?: 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 * Cached Fanavaran AccidentVehicleUsedId, resolved from VIN inquiry / vehicle GET
* UsedId (lookup-filter safe). Not the Mongo tenant default. * UsedId (lookup-filter safe). Not the Mongo tenant default.

View File

@@ -0,0 +1,133 @@
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
import {
fanavaranClaimProductFromBlameType,
fanavaranClaimsBaseUrl,
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("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");
});
});

View File

@@ -0,0 +1,227 @@
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 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,
},
};
}

View File

@@ -1,6 +1,9 @@
import { import {
canReuseCachedFanavaranVehicleIds,
collectPartyVinCandidates, collectPartyVinCandidates,
fanavaranIdOrConfigDefault, fanavaranIdOrConfigDefault,
fanavaranPayloadMatchesDamagedIdentity,
fanavaranVehicleMatchesDamagedIdentity,
filterPoliciesByLine, filterPoliciesByLine,
insuranceLineLabel, insuranceLineLabel,
parseLastCarPolicyInput, parseLastCarPolicyInput,
@@ -10,6 +13,7 @@ import {
pickVinFromPartyVehicle, pickVinFromPartyVehicle,
plaqueLetterFromMiddleCode, plaqueLetterFromMiddleCode,
plaqueMatchesVehicle, plaqueMatchesVehicle,
selectFanavaranVehicleForDamageCase,
selectLastAmongCarMatches, selectLastAmongCarMatches,
toAppPlaque, toAppPlaque,
vehicleMatchesCar, vehicleMatchesCar,
@@ -311,4 +315,154 @@ describe("fanavaran last car policy", () => {
it("returns null when there are no matches", () => { it("returns null when there are no matches", () => {
expect(selectLastAmongCarMatches([], today)).toBeNull(); 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); 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: { export function pickVinFromPartyVehicle(party: {
vehicle?: { vin?: unknown; inquiry?: unknown }; vehicle?: { vin?: unknown; inquiry?: unknown };
} | null | undefined): string | null { } | null | undefined): string | null {