fanavaran stages matching data is double checked and more controlled +

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

View File

@@ -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 { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
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 { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto";
import {
@@ -181,6 +191,7 @@ import {
fanavaranSubmitPath,
getFanavaranClientProfile,
resolveFanavaranClientKey,
resolveFanavaranProductContractId,
} from "src/core/config/fanavaran-client.config";
import { FanavaranAuditService } from "src/fanavaran/fanavaran-audit.service";
import { FanavaranAuthService } from "src/fanavaran/fanavaran-auth.service";
@@ -216,14 +227,21 @@ import {
} from "./fanavaran-other-people";
import {
asObjectRecord,
asVehicleInquiryRows,
canReuseCachedFanavaranVehicleIds,
collectPartyVinCandidates,
completePlaqueParts,
fanavaranIdOrConfigDefault,
fanavaranPayloadMatchesDamagedIdentity,
fanavaranVehicleMatchesDamagedIdentity,
normalizeVin,
parseFanavaranId,
pickFanavaranPlaqueLookupFields,
pickFanavaranVehicleFromInquiry,
pickVehicleId,
pickVinFromPartyVehicle,
selectFanavaranVehicleForDamageCase,
type FanavaranDamagedVehicleIdentity,
} from "src/lookups/fanavaran-last-car-policy";
export interface FanavaranAutoSubmitResult {
@@ -358,9 +376,8 @@ const FANAVARAN_PLATE_LETTER_CODE: Record<string, number> = {
export class ClaimRequestManagementService {
private readonly logger = new Logger(ClaimRequestManagementService.name);
// Business submit URL (auth lives in FanavaranAuthService)
private readonly FANAVARAN_SUBMIT_URL =
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/third-party-car-financial-claims";
// Legacy Tejaratno third-party URL. V2 stages use fanavaranClaimsBaseUrl(product).
private readonly FANAVARAN_SUBMIT_URL = fanavaranClaimsBaseUrl("third-party");
// Authentication credentials
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
* until Fanavaran returns PlaqueKindId (required on GEN.12).
* Damaged-car VIN inquiry: keep only rows that match VIN/plate/chassis.
* Then GET that VehicleId so PlaqueKindId is the current version, not a
* leftover row that merely had some PlaqueKindId.
*/
private async fetchFanavaranVehicleByVinCandidates(
clientKey: FanavaranClientKey,
vins: string[],
identity: FanavaranDamagedVehicleIdentity,
product: FanavaranClaimProduct = "third-party",
): 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) {
try {
const vehicle = await this.fetchFanavaranVehicleByVin(clientKey, vin);
if (!vehicle) {
const inquired = await this.fanavaranLookupService.inquiryByVin(
clientKey,
vin,
requestOptions,
);
const rows = asVehicleInquiryRows(inquired);
if (rows.length === 0) {
this.logger.warn(
`[buildFanavaranDamageCasePayload] VIN inquiry for ${vin} returned no vehicle`,
);
continue;
}
const plaqueKindId = pickFanavaranPlaqueLookupFields(vehicle).kindId;
const selected = selectFanavaranVehicleForDamageCase(rows, identity);
if (!selected) {
this.logger.warn(
`[buildFanavaranDamageCasePayload] VIN inquiry ${vin} returned ${rows.length} row(s) but none matched damaged VIN/plate/chassis`,
);
continue;
}
const plaqueKindId = pickFanavaranPlaqueLookupFields(selected).kindId;
this.logger.log(
`[buildFanavaranDamageCasePayload] VIN inquiry ${vin} VehicleId=${pickVehicleId(vehicle) ?? "none"} VehicleKindId=${parseFanavaranId(vehicle.VehicleKindId) ?? "none"} UsedId=${parseFanavaranId(vehicle.UsedId) ?? "none"} PlaqueKindId=${plaqueKindId ?? "none"}`,
`[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"}`,
);
if (plaqueKindId != null) return vehicle;
if (!fallback) fallback = vehicle;
matched = selected;
break;
} catch (error) {
this.logger.warn(
`[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,13 +4043,27 @@ export class ClaimRequestManagementService {
private async resolveAccidentVehicleUsedFromPolicy(
clientKey: FanavaranClientKey,
policyId: number,
product: FanavaranClaimProduct = "third-party",
): Promise<{ usedId: number | null; vehicleKindId: number | null }> {
try {
const policy = asObjectRecord(
await this.fanavaranLookupService.thirdPartyPolicyById(
const requestOptions = {
contractIdOverride: resolveFanavaranProductContractId(
clientKey,
policyId,
product,
),
};
const policy = asObjectRecord(
product === "car-body"
? await this.fanavaranLookupService.bodyPolicyById(
clientKey,
policyId,
requestOptions,
)
: await this.fanavaranLookupService.thirdPartyPolicyById(
clientKey,
policyId,
requestOptions,
),
);
const vehicleId = pickVehicleId(policy?.VehicleId ?? policy?.vehicleId);
if (vehicleId === null) {
@@ -3992,6 +4077,7 @@ export class ClaimRequestManagementService {
clientKey,
vehicleId,
versionNo ?? undefined,
requestOptions,
),
);
this.logger.log(
@@ -4013,6 +4099,7 @@ export class ClaimRequestManagementService {
parties: unknown[];
guiltyPartyId?: unknown;
policyId?: number | null;
product?: FanavaranClaimProduct;
}): Promise<{
usedId: number | null;
vehicleKindId: number | null;
@@ -4038,6 +4125,7 @@ export class ClaimRequestManagementService {
const fromPolicy = await this.resolveAccidentVehicleUsedFromPolicy(
input.clientKey,
input.policyId,
input.product ?? "third-party",
);
if (fromPolicy.usedId != null) {
return { ...fromPolicy, source: "policy-vehicle" };
@@ -4144,14 +4232,9 @@ export class ClaimRequestManagementService {
const person = damagedParty?.person ?? {};
const vehicle = damagedParty?.vehicle ?? {};
const insurance = damagedParty?.insurance ?? {};
// Prefer nested inquiry (ESG/Tejarat) on the damaged party — not flattened insurance.*
const inquiryRawPayload =
vehicle?.inquiry?.raw?.data != null &&
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 product = fanavaranClaimProductFromBlameType(input.blameCase?.type);
const { mapped: inquiryMapped, raw: inquiryRawPayload } =
fanavaranPartyInquirySources(product, damagedParty);
const inquiryRaw = inquiryRawPayload ?? {};
const plate =
@@ -4213,20 +4296,38 @@ export class ClaimRequestManagementService {
]);
const builtYearFromInquiry = builtYearRaw ? Number(builtYearRaw) || null : null;
const damagedVinCandidates: string[] = [];
const seenVins = new Set<string>();
for (const value of [
...collectPartyVinCandidates(damagedParty),
const damagedVin =
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"VIN",
"vin",
"VinNumberField",
]),
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"ShsNum",
"ChassisNumberField",
"shsNam",
]),
]) ?? collectPartyVinCandidates(damagedParty)[0] ??
null;
const damagedChassis = this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"ShsNum",
"ChassisNumberField",
"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);
if (!vin || seenVins.has(vin)) continue;
@@ -4236,29 +4337,46 @@ export class ClaimRequestManagementService {
const cachedDamage =
(input.claimCase as any)?.fanavaranSync?.damageCase ?? {};
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(
`[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(
input.clientKey,
damagedVinCandidates,
damagedIdentity,
product,
);
const apiPlaque = pickFanavaranPlaqueLookupFields(apiVehicle);
const inquiryPlaque = pickFanavaranPlaqueLookupFields(
asObjectRecord(inquiryMapped) ?? asObjectRecord(inquiryRaw),
);
const inquiryVehicle =
asObjectRecord(inquiryMapped) ?? asObjectRecord(inquiryRaw);
const inquiryPlaque = fanavaranVehicleMatchesDamagedIdentity(
inquiryVehicle,
damagedIdentity,
)
? pickFanavaranPlaqueLookupFields(inquiryVehicle)
: pickFanavaranPlaqueLookupFields(null);
const plaqueKindFromApi = this.firstPositiveFanavaranId(
apiPlaque.kindId,
inquiryPlaque.kindId,
cachedDamage.plaqueKindId,
lastDamagePayload?.PlaqueKindId,
canReuseCachedPlaque ? cachedDamage.plaqueKindId : null,
canReuseCachedPlaque ? lastDamagePayload?.PlaqueKindId : null,
);
const plaqueSampleFromApi = this.firstPositiveFanavaranId(
apiPlaque.sampleId,
inquiryPlaque.sampleId,
cachedDamage.plaqueSampleId,
lastDamagePayload?.PlaqueSampleId,
canReuseCachedPlaque ? cachedDamage.plaqueSampleId : null,
canReuseCachedPlaque ? lastDamagePayload?.PlaqueSampleId : null,
);
const plaqueKindId = fanavaranIdOrConfigDefault(
plaqueKindFromApi,
@@ -4268,6 +4386,10 @@ export class ClaimRequestManagementService {
plaqueSampleFromApi,
input.defaults.PlaqueSampleId,
);
const plaqueKindSource =
plaqueKindFromApi != null ? "vehicle-inquiry" : "config-default";
const plaqueSampleSource =
plaqueSampleFromApi != null ? "vehicle-inquiry" : "config-default";
if (plaqueKindFromApi == null) {
this.logger.log(
`[buildFanavaranDamageCasePayload] PlaqueKindId API miss; using config default ${plaqueKindId}`,
@@ -4284,14 +4406,21 @@ export class ClaimRequestManagementService {
const carType = input.claimCase?.vehicle?.carType as string | undefined;
let vehicleKindId = this.firstPositiveFanavaranId(
apiVehicle?.VehicleKindId,
cachedDamage.vehicleKindId,
lastDamagePayload?.VehicleKindId,
canReuseCachedVehicleKind ? cachedDamage.vehicleKindId : null,
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) {
vehicleKindId = await this.resolveVehicleKindId(
input.clientKey,
carType,
);
if (vehicleKindId != null) vehicleKindSource = "car-type-lookup";
}
// Prefer claim-level cache, then party.person.fanavaranDriverId, then live inquiry, then last payload
@@ -4359,49 +4488,37 @@ export class ClaimRequestManagementService {
}
const cachedUsedId =
cachedDamage.accidentVehicleUsedId != null
cacheMatchesIdentity && cachedDamage.accidentVehicleUsedId != null
? Number(cachedDamage.accidentVehicleUsedId)
: (input.claimCase as any)?.fanavaranSync?.baseClaim
?.accidentVehicleUsedId != null
? Number(
(input.claimCase as any).fanavaranSync.baseClaim
.accidentVehicleUsedId,
)
: null;
const cachedUsedSource =
cachedDamage.accidentVehicleUsedSource ??
(input.claimCase as any)?.fanavaranSync?.baseClaim
?.accidentVehicleUsedSource;
: null;
const cachedUsedSource = cachedDamage.accidentVehicleUsedSource;
const canReuseApiUsedId =
cachedUsedId != null &&
cachedUsedId > 0 &&
(cachedUsedSource === "vin-inquiry" ||
cachedUsedSource === "policy-vehicle");
cachedUsedSource === "vehicle-inquiry";
let accidentVehicleUsedId: number | null = canReuseApiUsedId
? cachedUsedId
: null;
let usedSource: "vin-inquiry" | "policy-vehicle" | null = canReuseApiUsedId
? cachedUsedSource
: null;
if (accidentVehicleUsedId == null) {
const resolved = await this.resolveAccidentVehicleUsedFromApis({
clientKey: input.clientKey,
parties: input.blameCase?.parties ?? [],
guiltyPartyId: this.resolveGuiltyPartyIdV2(
input.blameCase?.parties ?? [],
input.blameCase?.expert?.decision?.guiltyPartyId,
),
policyId: (input.claimCase as any)?.fanavaranSync?.baseClaim?.policyId,
});
accidentVehicleUsedId = resolved.usedId;
usedSource = resolved.source;
let usedSource: "vin-inquiry" | "policy-vehicle" | "vehicle-inquiry" | null =
canReuseApiUsedId ? "vehicle-inquiry" : null;
if (accidentVehicleUsedId == null && apiVehicle) {
const fromDamagedVehicle =
await this.resolveAccidentVehicleUsedFromVehicleRecord(
input.clientKey,
apiVehicle,
);
accidentVehicleUsedId = fromDamagedVehicle.usedId;
if (accidentVehicleUsedId != null) usedSource = "vehicle-inquiry";
}
if (accidentVehicleUsedId == null) {
accidentVehicleUsedId = await this.resolveAccidentVehicleUsedFromVehicleKind(
input.clientKey,
vehicleKindId,
null,
parseFanavaranId(apiVehicle?.UsedId),
);
if (accidentVehicleUsedId != null && usedSource == null) {
usedSource = apiVehicle ? "vehicle-inquiry" : null;
}
}
if (accidentVehicleUsedId == null) {
accidentVehicleUsedId = input.defaults.AccidentVehicleUsedId;
@@ -4482,10 +4599,6 @@ export class ClaimRequestManagementService {
const persistedPayload = this.keepPreviousPositiveIds(lastDamagePayload, payload, [
"DriverId",
"PlaqueKindId",
"PlaqueSampleId",
"VehicleKindId",
"AccidentVehicleUsedId",
"InsuranceCorpId",
]);
@@ -4502,14 +4615,16 @@ export class ClaimRequestManagementService {
? { "fanavaranSync.damageCase.otherPersonId": otherPersonId }
: {}),
...(vehicleKindId != null
? { "fanavaranSync.damageCase.vehicleKindId": vehicleKindId }
: {}),
...(plaqueKindId != null
? { "fanavaranSync.damageCase.plaqueKindId": plaqueKindId }
: {}),
...(plaqueSampleId != null
? { "fanavaranSync.damageCase.plaqueSampleId": plaqueSampleId }
? {
"fanavaranSync.damageCase.vehicleKindId": vehicleKindId,
"fanavaranSync.damageCase.vehicleKindSource":
vehicleKindSource ?? "car-type-lookup",
}
: {}),
"fanavaranSync.damageCase.plaqueKindId": plaqueKindId,
"fanavaranSync.damageCase.plaqueKindSource": plaqueKindSource,
"fanavaranSync.damageCase.plaqueSampleId": plaqueSampleId,
"fanavaranSync.damageCase.plaqueSampleSource": plaqueSampleSource,
...(accidentVehicleUsedId != null
? {
"fanavaranSync.damageCase.accidentVehicleUsedId":
@@ -4720,6 +4835,31 @@ export class ClaimRequestManagementService {
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(
clientKey: FanavaranClientKey,
auditSession?: FanavaranAuditSession,
@@ -4729,9 +4869,11 @@ export class ClaimRequestManagementService {
const locationOverride = caseId
? await this.resolveFanavaranLocationForClaim(caseId, clientKey)
: undefined;
const product = await this.resolveFanavaranClaimProduct(caseId);
return this.fanavaranAuthService.getRequestHeaders(clientKey, {
auditSession,
locationOverride,
contractIdOverride: resolveFanavaranProductContractId(clientKey, product),
});
}
@@ -4793,21 +4935,31 @@ export class ClaimRequestManagementService {
},
logPrefix: string,
auditSession?: FanavaranAuditSession,
options?: { clientKey?: FanavaranClientKey; claimCaseId?: string },
options?: {
clientKey?: FanavaranClientKey;
claimCaseId?: string;
product?: FanavaranClaimProduct;
insuranceLineId?: number;
},
): Promise<number | null> {
const clientKey =
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 requestMeta = {
corpId: config.corpId,
contractId: config.contractId,
contractId: resolveFanavaranProductContractId(clientKey, product),
location: config.location,
nationalCode: this.fanavaranAuditService.maskNationalCode(
nationalCodeOfInsurer,
),
InsuranceLineId: 5,
InsuranceLineId: insuranceLineId,
};
if (auditSession) {
@@ -5268,6 +5420,13 @@ export class ClaimRequestManagementService {
: [];
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;
if (ownerId) {
const byOwner = parties.find(
@@ -5474,15 +5633,69 @@ export class ClaimRequestManagementService {
(claimCase as any)?.fanavaranSync?.baseClaim?.lastPayload?.PolicyId ??
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({
accidentReason: selectedAccidentReason,
createdAt: (blameCase as { createdAt?: Date }).createdAt,
createdAt: accidentAt,
location: firstParty?.location,
damageParts,
defaults: profile.defaults,
logPrefix,
requirePolicyId,
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(
blameCase.parties ?? [],
expertDecision?.guiltyPartyId,
@@ -5519,20 +5732,13 @@ export class ClaimRequestManagementService {
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;
const policyId = await this.getPolicyIdFromNationalCode(
nationalCodeOfInsurer,
profile.auth,
logPrefix,
auditSession,
{ clientKey, claimCaseId },
{ clientKey, claimCaseId, product: "third-party" },
);
debug.values.policyId = policyId;
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 =
forceRefreshUsedId || forceRefreshPolicy
? null
@@ -5575,6 +5792,7 @@ export class ClaimRequestManagementService {
guiltyPartyId: guiltyPartyIdForVin,
policyId:
typeof payload.PolicyId === "number" ? payload.PolicyId : null,
product: fanavaranClaimProductFromBlameType(blameCase.type),
});
if (resolved.usedId != null) {
payload.AccidentVehicleUsedId = resolved.usedId;
@@ -5687,9 +5905,9 @@ export class ClaimRequestManagementService {
if (!blameCase) {
throw new NotFoundException("Blame case not found");
}
if (blameCase.type !== BlameRequestType.THIRD_PARTY) {
if (!isFanavaranSubmitSupportedBlameType(blameCase.type)) {
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,
dmgCaseId: claimCase.dmgCaseId ?? null,
submitUrl: claimCase.claimId
? `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/dmg-cases`
? `${fanavaranClaimsBaseUrl(fanavaranClaimProductFromBlameType(blameCase.type))}/${claimCase.claimId}/dmg-cases`
: null,
warning: warnings.length ? warnings.join(" ") : undefined,
payload,
@@ -5881,6 +6099,7 @@ export class ClaimRequestManagementService {
): Promise<any> {
const { claimCase, candidates } =
await this.collectFanavaranAttachmentCandidates(claimCaseId, clientKey);
const claimsUrl = await this.fanavaranClaimsUrlForClaim(claimCaseId);
return {
clientKey,
@@ -5889,7 +6108,7 @@ export class ClaimRequestManagementService {
claimNo: claimCase.claimNo ?? null,
dmgCaseId: claimCase.dmgCaseId ?? null,
submitUrl: claimCase.claimId
? `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/files`
? `${claimsUrl}/${claimCase.claimId}/files`
: null,
warning: claimCase.claimId
? 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 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 = {
FileName: fileName,
FileTypeId: profile.defaults.ClaimFileTypeId,
@@ -6636,7 +6855,7 @@ export class ClaimRequestManagementService {
expertiseId: claimCase.expertiseId ?? null,
replyKey,
submitUrl: claimCase.claimId
? `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/expertise`
? `${await this.fanavaranClaimsUrlForClaim(claimCaseId)}/${claimCase.claimId}/expertise`
: null,
ready: warnings.length === 0,
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 auditSession: FanavaranAuditSession = {
trackingCode: this.fanavaranAuditService.generateTrackingCode(),
@@ -6938,7 +7157,7 @@ export class ClaimRequestManagementService {
fanavaran: FanavaranAutoSubmitResult,
): string {
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) {
return `${baseMessage} ${fanavaran.warning}`;
@@ -6964,8 +7183,8 @@ export class ClaimRequestManagementService {
return "Blame case not found";
}
if (blameCase.type !== BlameRequestType.THIRD_PARTY) {
return "Fanavaran third-party financial submit only applies to THIRD_PARTY claims";
if (!isFanavaranSubmitSupportedBlameType(blameCase.type)) {
return "Fanavaran submit only applies to THIRD_PARTY and CAR_BODY claims";
}
return null;
@@ -7197,9 +7416,9 @@ export class ClaimRequestManagementService {
if (!blameCase) {
throw new NotFoundException("Blame case not found");
}
if (blameCase.type !== BlameRequestType.THIRD_PARTY) {
if (!isFanavaranSubmitSupportedBlameType(blameCase.type)) {
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.
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(
`[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
* later in the damage-assessment flow.
*/
@@ -8046,6 +8265,7 @@ export class ClaimRequestManagementService {
auditSession,
claimCaseId,
);
const submitUrl = await this.fanavaranClaimsUrlForClaim(claimCaseId);
const requestHeaders = {
...headers,
@@ -8055,7 +8275,7 @@ export class ClaimRequestManagementService {
session: auditSession,
step: FanavaranAuditStep.SUBMIT_CLAIM,
status: FanavaranAuditStatus.STARTED,
requestUrl: this.FANAVARAN_SUBMIT_URL,
requestUrl: submitUrl,
requestMethod: "POST",
requestHeaders,
requestBody: fanavaranData,
@@ -8065,7 +8285,7 @@ export class ClaimRequestManagementService {
try {
const response = await firstValueFrom(
this.httpService.post(this.FANAVARAN_SUBMIT_URL, fanavaranData, {
this.httpService.post(submitUrl, fanavaranData, {
headers: requestHeaders,
}),
);
@@ -8086,7 +8306,7 @@ export class ClaimRequestManagementService {
session: auditSession,
step: FanavaranAuditStep.SUBMIT_CLAIM,
status: FanavaranAuditStatus.SUCCESS,
requestUrl: this.FANAVARAN_SUBMIT_URL,
requestUrl: submitUrl,
requestMethod: "POST",
httpStatus: response.status,
requestHeaders: exchange.requestHeaders,
@@ -8138,7 +8358,7 @@ export class ClaimRequestManagementService {
session: auditSession,
step: FanavaranAuditStep.SUBMIT_CLAIM,
status: FanavaranAuditStatus.FAILURE,
requestUrl: this.FANAVARAN_SUBMIT_URL,
requestUrl: submitUrl,
requestMethod: "POST",
httpStatus: exchange.httpStatus,
requestHeaders: exchange.requestHeaders,