update the lookups list

This commit is contained in:
2026-09-07 15:08:08 +03:30
parent 80e112f09b
commit c7cd9e8ce5
11 changed files with 1161 additions and 72 deletions

View File

@@ -191,6 +191,20 @@ import {
} from "src/fanavaran/schema/fanavaran-audit-log.schema";
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
import { selectLatestActiveFanavaranPolicy } from "./fanavaran-policy-selection";
import {
asPartyInquiryRows,
pickPersonBirthday,
pickPersonNationalCode,
selectFanavaranDriverId,
} from "./fanavaran-driver-inquiry";
import {
asObjectRecord,
parseFanavaranId,
pickFanavaranPlaqueLookupFields,
pickFanavaranVehicleFromInquiry,
pickVehicleId,
pickVinFromPartyVehicle,
} from "src/lookups/fanavaran-last-car-policy";
export interface FanavaranAutoSubmitResult {
attempted: boolean;
@@ -3826,6 +3840,177 @@ export class ClaimRequestManagementService {
}
}
private async resolveAccidentVehicleUsedFromVehicleKind(
clientKey: FanavaranClientKey,
vehicleKindId: number | null,
preferredId: number | null,
): Promise<number | null> {
if (vehicleKindId == null) return preferredId;
try {
const [kinds, useTypes] = await Promise.all([
this.getFanavaranLookupRows(clientKey, "vehicle-kinds"),
this.getFanavaranLookupRows(clientKey, "vehicle-use-types"),
]);
const groupId = vehicleGroupIdForKind(kinds, vehicleKindId);
const usedId = selectAccidentVehicleUsedId(
useTypes,
groupId,
preferredId,
);
this.logger.log(
`Resolved AccidentVehicleUsedId=${usedId} from VehicleKindId=${vehicleKindId} VehicleGroupId=${groupId ?? "none"} (UsedId=${preferredId ?? "none"})`,
);
return usedId;
} catch (error) {
this.logger.warn(
`Failed to resolve AccidentVehicleUsedId from VehicleKindId=${vehicleKindId}: ${
error instanceof Error ? error.message : error
}`,
);
return preferredId;
}
}
/**
* GEN.03 lookup-filter: AccidentVehicleUsedId comes from Fanavaran vehicle
* UsedId (VIN inquiry or vehicle GET), not Mongo tenant defaults.
*/
private async resolveAccidentVehicleUsedFromVehicleRecord(
clientKey: FanavaranClientKey,
vehicle: Record<string, unknown> | null,
): Promise<{ usedId: number | null; vehicleKindId: number | null }> {
const vehicleKindId = parseFanavaranId(vehicle?.VehicleKindId);
const vehicleUsedId = parseFanavaranId(vehicle?.UsedId);
const usedId = await this.resolveAccidentVehicleUsedFromVehicleKind(
clientKey,
vehicleKindId,
vehicleUsedId,
);
return { usedId, vehicleKindId };
}
private async resolveAccidentVehicleUsedFromVin(
clientKey: FanavaranClientKey,
vin: string,
vehicleId?: number | null,
): Promise<{ usedId: number | null; vehicleKindId: number | null }> {
try {
const vehicle = await this.fetchFanavaranVehicleByVin(
clientKey,
vin,
vehicleId,
);
if (!vehicle) {
this.logger.warn(`VIN inquiry for ${vin} returned no vehicle`);
return { usedId: null, vehicleKindId: null };
}
this.logger.log(
`VIN inquiry ${vin} VehicleId=${pickVehicleId(vehicle) ?? "none"} VehicleKindId=${parseFanavaranId(vehicle.VehicleKindId) ?? "none"} UsedId=${parseFanavaranId(vehicle.UsedId) ?? "none"} PlaqueKindId=${parseFanavaranId(vehicle.PlaqueKindId) ?? "none"}`,
);
return this.resolveAccidentVehicleUsedFromVehicleRecord(clientKey, vehicle);
} catch (error) {
this.logger.warn(
`Failed to resolve AccidentVehicleUsedId from VIN=${vin}: ${
error instanceof Error ? error.message : error
}`,
);
return { usedId: null, vehicleKindId: null };
}
}
private async fetchFanavaranVehicleByVin(
clientKey: FanavaranClientKey,
vin: string,
vehicleId?: number | null,
): Promise<Record<string, unknown> | null> {
const inquired = await this.fanavaranLookupService.inquiryByVin(
clientKey,
vin,
);
return pickFanavaranVehicleFromInquiry(inquired, vehicleId);
}
/**
* Fallback when VIN is missing: policy GET → vehicle GET → UsedId/VehicleKindId.
*/
private async resolveAccidentVehicleUsedFromPolicy(
clientKey: FanavaranClientKey,
policyId: number,
): Promise<{ usedId: number | null; vehicleKindId: number | null }> {
try {
const policy = asObjectRecord(
await this.fanavaranLookupService.thirdPartyPolicyById(
clientKey,
policyId,
),
);
const vehicleId = pickVehicleId(policy?.VehicleId ?? policy?.vehicleId);
if (vehicleId === null) {
this.logger.warn(`Policy ${policyId} has no VehicleId`);
return { usedId: null, vehicleKindId: null };
}
const versionNo = parseFanavaranId(policy?.VehicleVersionNo);
const vehicle = asObjectRecord(
await this.fanavaranLookupService.vehicleById(
clientKey,
vehicleId,
versionNo ?? undefined,
),
);
this.logger.log(
`Policy ${policyId} VehicleId=${vehicleId} version=${versionNo ?? "latest"} VehicleKindId=${parseFanavaranId(vehicle?.VehicleKindId) ?? "none"} UsedId=${parseFanavaranId(vehicle?.UsedId) ?? "none"}`,
);
return this.resolveAccidentVehicleUsedFromVehicleRecord(clientKey, vehicle);
} catch (error) {
this.logger.warn(
`Failed to resolve AccidentVehicleUsedId from PolicyId=${policyId}: ${
error instanceof Error ? error.message : error
}`,
);
return { usedId: null, vehicleKindId: null };
}
}
private async resolveAccidentVehicleUsedFromApis(input: {
clientKey: FanavaranClientKey;
parties: unknown[];
guiltyPartyId?: unknown;
policyId?: number | null;
}): Promise<{
usedId: number | null;
vehicleKindId: number | null;
source: "vin-inquiry" | "policy-vehicle" | null;
}> {
const guiltyParty = (input.parties ?? []).find(
(party: any) =>
String(party?.person?.userId ?? "") === String(input.guiltyPartyId ?? ""),
);
const vin = pickVinFromPartyVehicle(guiltyParty);
if (vin) {
const fromVin = await this.resolveAccidentVehicleUsedFromVin(
input.clientKey,
vin,
);
if (fromVin.usedId != null) {
return { ...fromVin, source: "vin-inquiry" };
}
}
if (typeof input.policyId === "number") {
const fromPolicy = await this.resolveAccidentVehicleUsedFromPolicy(
input.clientKey,
input.policyId,
);
if (fromPolicy.usedId != null) {
return { ...fromPolicy, source: "policy-vehicle" };
}
}
return { usedId: null, vehicleKindId: null, source: null };
}
private parseJalaliBirthday(
birthday: string | number | null | undefined,
): { year: number; month: number; day: number } | null {
@@ -3869,7 +4054,7 @@ export class ClaimRequestManagementService {
private async resolveDriverFanavaranId(
clientKey: FanavaranClientKey,
nationalCode: string | null | undefined,
driverBirthday: string | null | undefined,
driverBirthday: string | number | null | undefined,
driverIsInsurer: boolean | undefined,
): Promise<number | null> {
if (!nationalCode) {
@@ -3893,7 +4078,7 @@ export class ClaimRequestManagementService {
);
try {
const response = (await this.fanavaranLookupService.inquiryByUniqueIdentifier(
const response = await this.fanavaranLookupService.inquiryByUniqueIdentifier(
clientKey,
{
nationalCode,
@@ -3901,36 +4086,27 @@ export class ClaimRequestManagementService {
birthMonth: parsed.month,
birthDay: parsed.day,
},
)) as Array<{
Id: number;
RoleId: number;
NationalCode: string;
Name?: string;
LastName?: string;
}>;
);
this.logger.log(
`[resolveDriverFanavaranId] RESPONSE: ${JSON.stringify(response)}`,
);
if (!Array.isArray(response) || response.length === 0) {
this.logger.warn(
`[resolveDriverFanavaranId] EMPTY response for nationalCode=${nationalCode}`,
);
return null;
}
const match = response.find((entry) => entry.RoleId === targetRoleId);
if (match) {
const driverId = selectFanavaranDriverId(response, driverIsInsurer);
if (driverId != null) {
const rows = asPartyInquiryRows(response);
const match = rows.find((row) => Number(row.Id) === driverId);
this.logger.log(
`[resolveDriverFanavaranId] MATCH: Id=${match.Id} RoleId=${match.RoleId} name=${match.Name} ${match.LastName} for nationalCode=${nationalCode}`,
`[resolveDriverFanavaranId] MATCH: Id=${driverId} RoleId=${match?.RoleId ?? "none"} name=${match?.Name ?? ""} ${match?.LastName ?? ""} for nationalCode=${nationalCode}`,
);
return match.Id;
return driverId;
}
this.logger.warn(
`[resolveDriverFanavaranId] NO MATCH for targetRoleId=${targetRoleId} (driverIsInsurer=${driverIsInsurer}) nationalCode=${nationalCode}. ` +
`Available entries: ${response.map((e) => `{Id=${e.Id}, RoleId=${e.RoleId}, Name=${e.Name} ${e.LastName}}`).join("; ")}`,
`[resolveDriverFanavaranId] NO MATCH for nationalCode=${nationalCode} driverIsInsurer=${driverIsInsurer}. ` +
`Available entries: ${asPartyInquiryRows(response)
.map((e) => `{Id=${e.Id}, RoleId=${e.RoleId}, Name=${e.Name} ${e.LastName}}`)
.join("; ")}`,
);
return null;
} catch (error) {
@@ -3957,13 +4133,10 @@ export class ClaimRequestManagementService {
FaultPercent: number;
};
}): Promise<Record<string, unknown>> {
const damagedParty = Array.isArray(input.blameCase?.parties)
? input.blameCase.parties.find(
(party: any) =>
String(party?.person?.userId ?? "") ===
String(input.claimCase?.owner?.userId ?? ""),
)
: null;
const damagedParty = this.pickDamagedPartyForFanavaran(
input.blameCase,
input.claimCase,
);
const person = damagedParty?.person ?? {};
const vehicle = damagedParty?.vehicle ?? {};
const insurance = damagedParty?.insurance ?? {};
@@ -3983,8 +4156,10 @@ export class ClaimRequestManagementService {
this.logger.log(
`[buildFanavaranDamageCasePayload] damagedParty resolved: userId=${damagedParty?.person?.userId ?? "NONE"}, ` +
`nationalCode=${pickPersonNationalCode(person) ?? "MISSING"}, ` +
`nationalCodeOfDriver=${person.nationalCodeOfDriver ?? "MISSING"}, ` +
`driverBirthday=${person.driverBirthday ?? "MISSING"}, ` +
`nationalCodeOfInsurer=${person.nationalCodeOfInsurer ?? "MISSING"}, ` +
`driverBirthday=${pickPersonBirthday(person) ?? "MISSING"}, ` +
`driverIsInsurer=${person.driverIsInsurer ?? "MISSING"}, ` +
`driverLicense=${person.driverLicense ?? "MISSING"}`,
);
@@ -4032,14 +4207,40 @@ export class ClaimRequestManagementService {
"ModelField",
"ModelCii",
]);
const builtYear = builtYearRaw ? Number(builtYearRaw) || null : null;
const builtYearFromInquiry = builtYearRaw ? Number(builtYearRaw) || null : null;
const damagedVin =
pickVinFromPartyVehicle(damagedParty) ??
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"VIN",
"vin",
"VinNumberField",
]);
let apiVehicle: Record<string, unknown> | null = null;
if (damagedVin) {
try {
apiVehicle = await this.fetchFanavaranVehicleByVin(
input.clientKey,
damagedVin,
);
} catch (error) {
this.logger.warn(
`[buildFanavaranDamageCasePayload] VIN inquiry failed for ${damagedVin}: ${
error instanceof Error ? error.message : error
}`,
);
}
}
const apiPlaque = pickFanavaranPlaqueLookupFields(apiVehicle);
const builtYear =
parseFanavaranId(apiVehicle?.BuiltYear) ?? builtYearFromInquiry;
const carType = input.claimCase?.vehicle?.carType as string | undefined;
const cachedDamage = (input.claimCase as any)?.fanavaranSync?.damageCase ?? {};
let vehicleKindId =
cachedDamage.vehicleKindId != null
? Number(cachedDamage.vehicleKindId)
: null;
let vehicleKindId = parseFanavaranId(apiVehicle?.VehicleKindId);
if (vehicleKindId == null && cachedDamage.vehicleKindId != null) {
vehicleKindId = Number(cachedDamage.vehicleKindId);
}
if (vehicleKindId == null) {
vehicleKindId = await this.resolveVehicleKindId(
input.clientKey,
@@ -4058,17 +4259,17 @@ export class ClaimRequestManagementService {
} else {
driverFanavaranId = await this.resolveDriverFanavaranId(
input.clientKey,
person.nationalCodeOfDriver,
person.driverBirthday,
pickPersonNationalCode(person),
pickPersonBirthday(person),
person.driverIsInsurer,
);
// Persist the resolved ID back to the blame case party for future use
if (driverFanavaranId && input.blameCase?._id && damagedParty) {
const partyIndex = input.blameCase.parties.findIndex(
(p: any) =>
const partyIndex = (input.blameCase.parties ?? []).findIndex(
(p: any) => p === damagedParty ||
String(p?.person?.userId ?? "") ===
String(input.claimCase?.owner?.userId ?? ""),
String(damagedParty?.person?.userId ?? ""),
);
if (partyIndex >= 0) {
await this.blameRequestDbService.findByIdAndUpdate(
@@ -4092,19 +4293,67 @@ export class ClaimRequestManagementService {
);
}
const cachedUsedId =
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;
const canReuseApiUsedId =
cachedUsedId != null &&
cachedUsedId > 0 &&
(cachedUsedSource === "vin-inquiry" ||
cachedUsedSource === "policy-vehicle");
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;
}
if (accidentVehicleUsedId == null) {
accidentVehicleUsedId = await this.resolveAccidentVehicleUsedFromVehicleKind(
input.clientKey,
vehicleKindId,
null,
);
}
this.logger.log(
`[buildFanavaranDamageCasePayload] Final DriverId=${driverFanavaranId ?? "NULL"} for nationalCodeOfDriver=${person.nationalCodeOfDriver ?? "MISSING"}, ` +
`[buildFanavaranDamageCasePayload] Final DriverId=${driverFanavaranId ?? "NULL"} for nationalCode=${pickPersonNationalCode(person) ?? "MISSING"}, ` +
`PolicyNo=${policyNo ?? "NULL"}, PolicyCINumber=${policyCINumber ?? "NULL"}`,
);
const payload = {
BeginDate: beginDate,
BuiltYear: builtYear,
ChassisNo: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"ShsNum",
"ChassisNumberField",
"shsNam",
]),
ChassisNo:
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"ShsNum",
"ChassisNumberField",
"shsNam",
]) ??
(typeof apiVehicle?.ChassisNo === "string" ? apiVehicle.ChassisNo : null),
DmgHistoryStatus: input.defaults.DmgHistoryStatus,
Desc: this.formatFanavaranSelectedPartsDesc(input.selectedParts),
DmgCaseTypeId: input.defaults.DmgCaseTypeId,
@@ -4125,33 +4374,38 @@ export class ClaimRequestManagementService {
insurerLicense: person.insurerLicense,
}),
LicenceTypeId: input.defaults.CulpritLicenceTypeId,
MotorNo: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"MtrNum",
"EngineNumberField",
"mtrnum",
]),
MotorNo:
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"MtrNum",
"EngineNumberField",
"mtrnum",
]) ?? (typeof apiVehicle?.MotorNo === "string" ? apiVehicle.MotorNo : null),
OwnerId: null,
PlaqueCityId: null,
PlaqueKindId: plate ? input.defaults.PlaqueKindId : null,
PlaqueLeftNo: plate ? String(plate.leftDigits) : null,
PlaqueMiddleCodeId: this.getFanavaranPlateMiddleCode(
plate?.centerAlphabet,
),
PlaqueCityId: apiPlaque.cityId,
PlaqueKindId: apiPlaque.kindId,
PlaqueLeftNo:
apiPlaque.leftNo ?? (plate ? String(plate.leftDigits) : null),
PlaqueMiddleCodeId:
apiPlaque.middleCodeId ??
this.getFanavaranPlateMiddleCode(plate?.centerAlphabet),
PlaqueNo:
apiPlaque.plaqueNo ??
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, ["Plk"]) ??
this.formatFanavaranPlateNo(plate),
PlaqueRightNo: plate ? String(plate.centerDigits) : null,
PlaqueSampleId: plate ? input.defaults.PlaqueSampleId : null,
PlaqueSerial: plate ? String(plate.ir) : null,
PlaqueRightNo:
apiPlaque.rightNo ?? (plate ? String(plate.centerDigits) : null),
PlaqueSampleId: apiPlaque.sampleId,
PlaqueSerial: apiPlaque.serial ?? (plate ? String(plate.ir) : null),
PolicyNo: policyNo,
PreviousPolicyEndDate: endDate ?? "",
VehicleKindId: vehicleKindId,
VIN: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"VIN",
"vin",
"VinNumberField",
]),
AccidentVehicleUsedId: input.defaults.AccidentVehicleUsedId,
VIN:
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"VIN",
"vin",
"VinNumberField",
]) ?? (typeof apiVehicle?.VIN === "string" ? apiVehicle.VIN : null),
AccidentVehicleUsedId: accidentVehicleUsedId,
PolicyCINumber: policyCINumber,
};
@@ -4167,6 +4421,24 @@ export class ClaimRequestManagementService {
...(vehicleKindId != null
? { "fanavaranSync.damageCase.vehicleKindId": vehicleKindId }
: {}),
...(apiPlaque.kindId != null
? { "fanavaranSync.damageCase.plaqueKindId": apiPlaque.kindId }
: {}),
...(apiPlaque.sampleId != null
? { "fanavaranSync.damageCase.plaqueSampleId": apiPlaque.sampleId }
: {}),
...(accidentVehicleUsedId != null
? {
"fanavaranSync.damageCase.accidentVehicleUsedId":
accidentVehicleUsedId,
...(usedSource
? {
"fanavaranSync.damageCase.accidentVehicleUsedSource":
usedSource,
}
: {}),
}
: {}),
...(insuranceCorpId != null
? { "fanavaranSync.damageCase.insuranceCorpId": insuranceCorpId }
: {}),
@@ -4633,6 +4905,45 @@ export class ClaimRequestManagementService {
}
}
/**
* زیان‌دیده for GEN.12: claim owner when linked to a blame party,
* else the party that is not the guilty one.
*/
private pickDamagedPartyForFanavaran(
blameCase: any,
claimCase: any,
): any | null {
const parties: any[] = Array.isArray(blameCase?.parties)
? blameCase.parties
: [];
if (parties.length === 0) return null;
const ownerId = claimCase?.owner?.userId;
if (ownerId) {
const byOwner = parties.find(
(party) =>
String(party?.person?.userId ?? "") === String(ownerId),
);
if (byOwner) return byOwner;
}
const guiltyPartyId = this.resolveGuiltyPartyIdV2(
parties,
blameCase?.expert?.decision?.guiltyPartyId,
);
if (guiltyPartyId) {
const injured = parties.find(
(party) =>
String(party?.person?.userId ?? "") !== String(guiltyPartyId),
);
if (injured) return injured;
}
return (
parties.find((party) => party?.role === PartyRole.FIRST) ?? parties[0]
);
}
private getNationalCodeOfInsurerForGuiltyPartyV2(
parties: Array<{
role?: PartyRole;
@@ -4703,11 +5014,17 @@ export class ClaimRequestManagementService {
auditSession?: FanavaranAuditSession;
/** Persist built payload onto fanavaranSync.baseClaim.lastPayload (default true). */
persistPayload?: boolean;
/**
* Re-run VIN inquiry / vehicle GET for AccidentVehicleUsedId.
* Submit always sets this so Mongo tenant default 1 cannot be reused.
*/
forceRefreshUsedId?: boolean;
},
): Promise<any> {
const profile = getFanavaranClientProfile(clientKey);
const logPrefix = `[Fanavaran ${clientKey} V2] claimCaseId=${claimCaseId}`;
const forceRefreshPolicy = options?.forceRefreshPolicy === true;
const forceRefreshUsedId = options?.forceRefreshUsedId === true;
const requirePolicyId = options?.requirePolicyId === true;
const persistPayload = options?.persistPayload !== false;
const auditSession =
@@ -4877,6 +5194,43 @@ export class ClaimRequestManagementService {
},
});
const cachedUsedId =
forceRefreshUsedId || forceRefreshPolicy
? null
: (claimCase as any)?.fanavaranSync?.baseClaim?.accidentVehicleUsedId;
const cachedUsedSource = (claimCase as any)?.fanavaranSync?.baseClaim
?.accidentVehicleUsedSource;
const canReuseApiUsedId =
!forceRefreshUsedId &&
!forceRefreshPolicy &&
cachedUsedId != null &&
Number(cachedUsedId) > 0 &&
(cachedUsedSource === "vin-inquiry" ||
cachedUsedSource === "policy-vehicle");
delete payload.AccidentVehicleUsedId;
let usedSource: "vin-inquiry" | "policy-vehicle" | null = null;
if (canReuseApiUsedId) {
payload.AccidentVehicleUsedId = Number(cachedUsedId);
usedSource = cachedUsedSource;
} else {
const guiltyPartyIdForVin = this.resolveGuiltyPartyIdV2(
blameCase.parties ?? [],
expertDecision?.guiltyPartyId,
);
const resolved = await this.resolveAccidentVehicleUsedFromApis({
clientKey,
parties: blameCase.parties ?? [],
guiltyPartyId: guiltyPartyIdForVin,
policyId:
typeof payload.PolicyId === "number" ? payload.PolicyId : null,
});
if (resolved.usedId != null) {
payload.AccidentVehicleUsedId = resolved.usedId;
usedSource = resolved.source;
}
}
if (persistPayload) {
await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, {
$set: {
@@ -4885,6 +5239,18 @@ export class ClaimRequestManagementService {
...(payload.PolicyId != null
? { "fanavaranSync.baseClaim.policyId": payload.PolicyId }
: {}),
...(typeof payload.AccidentVehicleUsedId === "number"
? {
"fanavaranSync.baseClaim.accidentVehicleUsedId":
payload.AccidentVehicleUsedId,
...(usedSource
? {
"fanavaranSync.baseClaim.accidentVehicleUsedSource":
usedSource,
}
: {}),
}
: {}),
},
});
}
@@ -7280,6 +7646,7 @@ export class ClaimRequestManagementService {
requirePolicyId: true,
auditSession,
persistPayload: true,
forceRefreshUsedId: true,
}));
// Guardrail: never send empty/null licence fields even if manual overrides arrive.

View File

@@ -93,6 +93,25 @@ export class FanavaranSyncStage {
@Prop({ type: Number })
vehicleKindId?: number;
/** Cached Fanavaran PlaqueKindId from VIN inquiry / vehicle GET. */
@Prop({ type: Number })
plaqueKindId?: number;
/** Cached Fanavaran PlaqueSampleId from VIN inquiry / vehicle GET. */
@Prop({ type: Number })
plaqueSampleId?: number;
/**
* Cached Fanavaran AccidentVehicleUsedId, resolved from VIN inquiry / vehicle GET
* UsedId (lookup-filter safe). Not the Mongo tenant default.
*/
@Prop({ type: Number })
accidentVehicleUsedId?: number;
/** How AccidentVehicleUsedId was resolved: vin-inquiry | policy-vehicle */
@Prop({ type: String })
accidentVehicleUsedSource?: string;
/** Cached Fanavaran InsuranceCorpId. */
@Prop({ type: Number })
insuranceCorpId?: number;

View File

@@ -0,0 +1,36 @@
import {
selectAccidentVehicleUsedId,
vehicleGroupIdForKind,
} from "./fanavaran-accident-vehicle-used";
describe("selectAccidentVehicleUsedId", () => {
const useTypes = [
{ Id: 1, Caption: "شخصي", IsActive: 1, VehicleGroupId: 1 },
{ Id: 3, Caption: "تاکسي درون شهري", IsActive: 1, VehicleGroupId: 1 },
{ Id: 35, Caption: "آمبولانس", IsActive: 1, VehicleGroupId: 2 },
{ Id: 36, Caption: "حمل مواد سريع الاشتعال", IsActive: 1, VehicleGroupId: 3 },
];
const kinds = [
{ Id: 8671, VehicleGroupId: 1, IsActive: 1 },
{ Id: 5904, VehicleGroupId: 3, IsActive: 1 },
];
it("keeps preferred id 1 when the car is passenger group 1", () => {
expect(vehicleGroupIdForKind(kinds, 8671)).toBe(1);
expect(selectAccidentVehicleUsedId(useTypes, 1, 1)).toBe(1);
});
it("keeps the vehicle UsedId when it is valid for that group", () => {
expect(selectAccidentVehicleUsedId(useTypes, 3, 36)).toBe(36);
});
it("picks an active use type for a non-passenger group instead of 1", () => {
expect(vehicleGroupIdForKind(kinds, 5904)).toBe(3);
expect(selectAccidentVehicleUsedId(useTypes, 3, 1)).toBe(36);
});
it("falls back to the vehicle UsedId when group is unknown", () => {
expect(selectAccidentVehicleUsedId(useTypes, null, 84)).toBe(84);
});
});

View File

@@ -0,0 +1,71 @@
export type FanavaranVehicleKindRow = {
Id?: unknown;
VehicleGroupId?: unknown;
IsActive?: unknown;
Caption?: unknown;
};
export type FanavaranVehicleUseTypeRow = {
Id?: unknown;
VehicleGroupId?: unknown;
IsActive?: unknown;
Caption?: unknown;
};
function asPositiveId(value: unknown): number | null {
if (value === null || value === undefined) return null;
const id = Number(value);
return Number.isFinite(id) && id > 0 ? id : null;
}
export function vehicleGroupIdForKind(
kinds: unknown,
vehicleKindId: number,
): number | null {
if (!Array.isArray(kinds)) return null;
const match = kinds.find(
(row) => asPositiveId((row as FanavaranVehicleKindRow)?.Id) === vehicleKindId,
) as FanavaranVehicleKindRow | undefined;
return asPositiveId(match?.VehicleGroupId);
}
/**
* Fanavaran filters AccidentVehicleUsedId by the policy car's VehicleGroupId.
* `preferredId` is the vehicle UsedId from VIN inquiry / vehicle GET — never a Mongo tenant default.
*/
export function selectAccidentVehicleUsedId(
useTypes: unknown,
vehicleGroupId: number | null,
preferredId: number | null,
): number | null {
if (!Array.isArray(useTypes) || vehicleGroupId == null) {
return preferredId;
}
const active = useTypes.filter((row) => {
const item = row as FanavaranVehicleUseTypeRow;
return (
item?.IsActive === 1 &&
asPositiveId(item.VehicleGroupId) === vehicleGroupId &&
asPositiveId(item.Id) != null
);
}) as FanavaranVehicleUseTypeRow[];
if (active.length === 0) {
return preferredId;
}
const preferred = active.find((row) => asPositiveId(row.Id) === preferredId);
if (preferred && preferredId != null) {
return preferredId;
}
const personal = active.find((row) =>
String(row.Caption ?? "").includes("شخص"),
);
if (personal) {
return asPositiveId(personal.Id) ?? preferredId;
}
return asPositiveId(active[0].Id) ?? preferredId;
}

View File

@@ -0,0 +1,61 @@
import {
asPartyInquiryRows,
parseJalaliDateParts,
pickPersonBirthday,
pickPersonNationalCode,
selectFanavaranDriverId,
} from "./fanavaran-driver-inquiry";
describe("selectFanavaranDriverId", () => {
const rows = [
{ Id: 10, RoleId: 161, Name: "insurer" },
{ Id: 20, RoleId: 166, Name: "driver" },
];
it("prefers insurer role 161 when the driver is the owner", () => {
expect(selectFanavaranDriverId(rows, true)).toBe(10);
});
it("prefers driver role 166 when the driver is not the owner", () => {
expect(selectFanavaranDriverId(rows, false)).toBe(20);
});
it("uses the other role when the preferred role is missing", () => {
expect(selectFanavaranDriverId([{ Id: 20, RoleId: 166 }], true)).toBe(20);
});
it("unwraps a wrapped Fanavaran payload", () => {
expect(selectFanavaranDriverId({ value: rows }, true)).toBe(10);
});
});
describe("pickPersonNationalCode / birthday", () => {
it("uses insurer national code when driverIsInsurer", () => {
expect(
pickPersonNationalCode({
driverIsInsurer: true,
nationalCodeOfInsurer: "0012345678",
nationalCodeOfDriver: "",
}),
).toBe("0012345678");
});
it("falls back across birthday fields", () => {
expect(
pickPersonBirthday({
driverBirthday: null,
birthday: "13480313",
}),
).toBe("13480313");
});
});
describe("asPartyInquiryRows", () => {
it("parses compact Jalali birthday 13480313", () => {
expect(parseJalaliDateParts("13480313")).toEqual({
year: 1348,
month: 3,
day: 13,
});
});
});

View File

@@ -0,0 +1,114 @@
export const FANAVARAN_INSURER_ROLE_ID = 161;
export const FANAVARAN_DRIVER_ROLE_ID = 166;
export type FanavaranPartyInquiryRow = {
Id?: unknown;
RoleId?: unknown;
NationalCode?: unknown;
Name?: unknown;
LastName?: unknown;
};
function asPositiveId(value: unknown): number | null {
if (value === null || value === undefined) return null;
const id = Number(value);
return Number.isFinite(id) && id > 0 ? id : null;
}
export function asPartyInquiryRows(value: unknown): FanavaranPartyInquiryRow[] {
if (Array.isArray(value)) {
return value.filter(
(row): row is FanavaranPartyInquiryRow =>
!!row && typeof row === "object" && !Array.isArray(row),
);
}
if (!value || typeof value !== "object") return [];
const record = value as Record<string, unknown>;
for (const key of ["value", "Value", "items", "Items", "data", "Data"]) {
const nested = record[key];
if (Array.isArray(nested)) return asPartyInquiryRows(nested);
}
return [];
}
/**
* DriverId for GEN.12 زیان‌دیده. Prefer insurer (161) or driver (166)
* based on driverIsInsurer, then any returned party id.
*/
export function selectFanavaranDriverId(
rows: unknown,
driverIsInsurer: boolean | undefined,
): number | null {
const parties = asPartyInquiryRows(rows);
if (parties.length === 0) return null;
const preferredRole = driverIsInsurer
? FANAVARAN_INSURER_ROLE_ID
: FANAVARAN_DRIVER_ROLE_ID;
const preferred = parties.find(
(row) => asPositiveId(row.RoleId) === preferredRole,
);
if (preferred) return asPositiveId(preferred.Id);
const fallbackRole = driverIsInsurer
? FANAVARAN_DRIVER_ROLE_ID
: FANAVARAN_INSURER_ROLE_ID;
const fallback = parties.find(
(row) => asPositiveId(row.RoleId) === fallbackRole,
);
if (fallback) return asPositiveId(fallback.Id);
return asPositiveId(parties[0].Id);
}
export function pickPersonNationalCode(person: {
driverIsInsurer?: boolean;
nationalCodeOfDriver?: unknown;
nationalCodeOfInsurer?: unknown;
} | null | undefined): string | null {
if (!person) return null;
const driver = String(person.nationalCodeOfDriver ?? "").trim();
const insurer = String(person.nationalCodeOfInsurer ?? "").trim();
if (person.driverIsInsurer) {
return insurer || driver || null;
}
return driver || insurer || null;
}
export function pickPersonBirthday(person: {
driverBirthday?: unknown;
birthday?: unknown;
insurerBirthday?: unknown;
} | null | undefined): string | number | null {
if (!person) return null;
for (const value of [
person.driverBirthday,
person.birthday,
person.insurerBirthday,
]) {
if (value == null || String(value).trim() === "") continue;
return value as string | number;
}
return null;
}
export function parseJalaliDateParts(
birthday: string | number | null | undefined,
): { year: number; month: number; day: number } | null {
if (birthday == null) return null;
const str = String(birthday).trim();
if (/^\d{8}$/.test(str)) {
const year = parseInt(str.slice(0, 4), 10);
const month = parseInt(str.slice(4, 6), 10);
const day = parseInt(str.slice(6, 8), 10);
if (!year || !month || !day) return null;
return { year, month, day };
}
const parts = str.split(/[/\-.]/);
if (parts.length < 3) return null;
const year = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10);
const day = parseInt(parts[2], 10);
if (!year || !month || !day) return null;
return { year, month, day };
}