forked from Yara724/api
Merge pull request 'main' (#296) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#296
This commit is contained in:
@@ -191,6 +191,24 @@ import {
|
|||||||
} from "src/fanavaran/schema/fanavaran-audit-log.schema";
|
} from "src/fanavaran/schema/fanavaran-audit-log.schema";
|
||||||
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
||||||
import { selectLatestActiveFanavaranPolicy } from "./fanavaran-policy-selection";
|
import { selectLatestActiveFanavaranPolicy } from "./fanavaran-policy-selection";
|
||||||
|
import {
|
||||||
|
selectAccidentVehicleUsedId,
|
||||||
|
vehicleGroupIdForKind,
|
||||||
|
} from "./fanavaran-accident-vehicle-used";
|
||||||
|
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 {
|
export interface FanavaranAutoSubmitResult {
|
||||||
attempted: boolean;
|
attempted: boolean;
|
||||||
@@ -3826,6 +3844,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(
|
private parseJalaliBirthday(
|
||||||
birthday: string | number | null | undefined,
|
birthday: string | number | null | undefined,
|
||||||
): { year: number; month: number; day: number } | null {
|
): { year: number; month: number; day: number } | null {
|
||||||
@@ -3869,7 +4058,7 @@ export class ClaimRequestManagementService {
|
|||||||
private async resolveDriverFanavaranId(
|
private async resolveDriverFanavaranId(
|
||||||
clientKey: FanavaranClientKey,
|
clientKey: FanavaranClientKey,
|
||||||
nationalCode: string | null | undefined,
|
nationalCode: string | null | undefined,
|
||||||
driverBirthday: string | null | undefined,
|
driverBirthday: string | number | null | undefined,
|
||||||
driverIsInsurer: boolean | undefined,
|
driverIsInsurer: boolean | undefined,
|
||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
if (!nationalCode) {
|
if (!nationalCode) {
|
||||||
@@ -3893,7 +4082,7 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = (await this.fanavaranLookupService.inquiryByUniqueIdentifier(
|
const response = await this.fanavaranLookupService.inquiryByUniqueIdentifier(
|
||||||
clientKey,
|
clientKey,
|
||||||
{
|
{
|
||||||
nationalCode,
|
nationalCode,
|
||||||
@@ -3901,36 +4090,27 @@ export class ClaimRequestManagementService {
|
|||||||
birthMonth: parsed.month,
|
birthMonth: parsed.month,
|
||||||
birthDay: parsed.day,
|
birthDay: parsed.day,
|
||||||
},
|
},
|
||||||
)) as Array<{
|
);
|
||||||
Id: number;
|
|
||||||
RoleId: number;
|
|
||||||
NationalCode: string;
|
|
||||||
Name?: string;
|
|
||||||
LastName?: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[resolveDriverFanavaranId] RESPONSE: ${JSON.stringify(response)}`,
|
`[resolveDriverFanavaranId] RESPONSE: ${JSON.stringify(response)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!Array.isArray(response) || response.length === 0) {
|
const driverId = selectFanavaranDriverId(response, driverIsInsurer);
|
||||||
this.logger.warn(
|
if (driverId != null) {
|
||||||
`[resolveDriverFanavaranId] EMPTY response for nationalCode=${nationalCode}`,
|
const rows = asPartyInquiryRows(response);
|
||||||
);
|
const match = rows.find((row) => Number(row.Id) === driverId);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = response.find((entry) => entry.RoleId === targetRoleId);
|
|
||||||
if (match) {
|
|
||||||
this.logger.log(
|
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(
|
this.logger.warn(
|
||||||
`[resolveDriverFanavaranId] NO MATCH for targetRoleId=${targetRoleId} (driverIsInsurer=${driverIsInsurer}) nationalCode=${nationalCode}. ` +
|
`[resolveDriverFanavaranId] NO MATCH for nationalCode=${nationalCode} driverIsInsurer=${driverIsInsurer}. ` +
|
||||||
`Available entries: ${response.map((e) => `{Id=${e.Id}, RoleId=${e.RoleId}, Name=${e.Name} ${e.LastName}}`).join("; ")}`,
|
`Available entries: ${asPartyInquiryRows(response)
|
||||||
|
.map((e) => `{Id=${e.Id}, RoleId=${e.RoleId}, Name=${e.Name} ${e.LastName}}`)
|
||||||
|
.join("; ")}`,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -3957,13 +4137,10 @@ export class ClaimRequestManagementService {
|
|||||||
FaultPercent: number;
|
FaultPercent: number;
|
||||||
};
|
};
|
||||||
}): Promise<Record<string, unknown>> {
|
}): Promise<Record<string, unknown>> {
|
||||||
const damagedParty = Array.isArray(input.blameCase?.parties)
|
const damagedParty = this.pickDamagedPartyForFanavaran(
|
||||||
? input.blameCase.parties.find(
|
input.blameCase,
|
||||||
(party: any) =>
|
input.claimCase,
|
||||||
String(party?.person?.userId ?? "") ===
|
);
|
||||||
String(input.claimCase?.owner?.userId ?? ""),
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
const person = damagedParty?.person ?? {};
|
const person = damagedParty?.person ?? {};
|
||||||
const vehicle = damagedParty?.vehicle ?? {};
|
const vehicle = damagedParty?.vehicle ?? {};
|
||||||
const insurance = damagedParty?.insurance ?? {};
|
const insurance = damagedParty?.insurance ?? {};
|
||||||
@@ -3983,8 +4160,10 @@ export class ClaimRequestManagementService {
|
|||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[buildFanavaranDamageCasePayload] damagedParty resolved: userId=${damagedParty?.person?.userId ?? "NONE"}, ` +
|
`[buildFanavaranDamageCasePayload] damagedParty resolved: userId=${damagedParty?.person?.userId ?? "NONE"}, ` +
|
||||||
|
`nationalCode=${pickPersonNationalCode(person) ?? "MISSING"}, ` +
|
||||||
`nationalCodeOfDriver=${person.nationalCodeOfDriver ?? "MISSING"}, ` +
|
`nationalCodeOfDriver=${person.nationalCodeOfDriver ?? "MISSING"}, ` +
|
||||||
`driverBirthday=${person.driverBirthday ?? "MISSING"}, ` +
|
`nationalCodeOfInsurer=${person.nationalCodeOfInsurer ?? "MISSING"}, ` +
|
||||||
|
`driverBirthday=${pickPersonBirthday(person) ?? "MISSING"}, ` +
|
||||||
`driverIsInsurer=${person.driverIsInsurer ?? "MISSING"}, ` +
|
`driverIsInsurer=${person.driverIsInsurer ?? "MISSING"}, ` +
|
||||||
`driverLicense=${person.driverLicense ?? "MISSING"}`,
|
`driverLicense=${person.driverLicense ?? "MISSING"}`,
|
||||||
);
|
);
|
||||||
@@ -4032,14 +4211,40 @@ export class ClaimRequestManagementService {
|
|||||||
"ModelField",
|
"ModelField",
|
||||||
"ModelCii",
|
"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 carType = input.claimCase?.vehicle?.carType as string | undefined;
|
||||||
const cachedDamage = (input.claimCase as any)?.fanavaranSync?.damageCase ?? {};
|
const cachedDamage = (input.claimCase as any)?.fanavaranSync?.damageCase ?? {};
|
||||||
let vehicleKindId =
|
let vehicleKindId = parseFanavaranId(apiVehicle?.VehicleKindId);
|
||||||
cachedDamage.vehicleKindId != null
|
if (vehicleKindId == null && cachedDamage.vehicleKindId != null) {
|
||||||
? Number(cachedDamage.vehicleKindId)
|
vehicleKindId = Number(cachedDamage.vehicleKindId);
|
||||||
: null;
|
}
|
||||||
if (vehicleKindId == null) {
|
if (vehicleKindId == null) {
|
||||||
vehicleKindId = await this.resolveVehicleKindId(
|
vehicleKindId = await this.resolveVehicleKindId(
|
||||||
input.clientKey,
|
input.clientKey,
|
||||||
@@ -4058,17 +4263,17 @@ export class ClaimRequestManagementService {
|
|||||||
} else {
|
} else {
|
||||||
driverFanavaranId = await this.resolveDriverFanavaranId(
|
driverFanavaranId = await this.resolveDriverFanavaranId(
|
||||||
input.clientKey,
|
input.clientKey,
|
||||||
person.nationalCodeOfDriver,
|
pickPersonNationalCode(person),
|
||||||
person.driverBirthday,
|
pickPersonBirthday(person),
|
||||||
person.driverIsInsurer,
|
person.driverIsInsurer,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Persist the resolved ID back to the blame case party for future use
|
// Persist the resolved ID back to the blame case party for future use
|
||||||
if (driverFanavaranId && input.blameCase?._id && damagedParty) {
|
if (driverFanavaranId && input.blameCase?._id && damagedParty) {
|
||||||
const partyIndex = input.blameCase.parties.findIndex(
|
const partyIndex = (input.blameCase.parties ?? []).findIndex(
|
||||||
(p: any) =>
|
(p: any) => p === damagedParty ||
|
||||||
String(p?.person?.userId ?? "") ===
|
String(p?.person?.userId ?? "") ===
|
||||||
String(input.claimCase?.owner?.userId ?? ""),
|
String(damagedParty?.person?.userId ?? ""),
|
||||||
);
|
);
|
||||||
if (partyIndex >= 0) {
|
if (partyIndex >= 0) {
|
||||||
await this.blameRequestDbService.findByIdAndUpdate(
|
await this.blameRequestDbService.findByIdAndUpdate(
|
||||||
@@ -4092,19 +4297,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(
|
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"}`,
|
`PolicyNo=${policyNo ?? "NULL"}, PolicyCINumber=${policyCINumber ?? "NULL"}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
BeginDate: beginDate,
|
BeginDate: beginDate,
|
||||||
BuiltYear: builtYear,
|
BuiltYear: builtYear,
|
||||||
ChassisNo: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
|
ChassisNo:
|
||||||
"ShsNum",
|
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
|
||||||
"ChassisNumberField",
|
"ShsNum",
|
||||||
"shsNam",
|
"ChassisNumberField",
|
||||||
]),
|
"shsNam",
|
||||||
|
]) ??
|
||||||
|
(typeof apiVehicle?.ChassisNo === "string" ? apiVehicle.ChassisNo : null),
|
||||||
DmgHistoryStatus: input.defaults.DmgHistoryStatus,
|
DmgHistoryStatus: input.defaults.DmgHistoryStatus,
|
||||||
Desc: this.formatFanavaranSelectedPartsDesc(input.selectedParts),
|
Desc: this.formatFanavaranSelectedPartsDesc(input.selectedParts),
|
||||||
DmgCaseTypeId: input.defaults.DmgCaseTypeId,
|
DmgCaseTypeId: input.defaults.DmgCaseTypeId,
|
||||||
@@ -4125,33 +4378,38 @@ export class ClaimRequestManagementService {
|
|||||||
insurerLicense: person.insurerLicense,
|
insurerLicense: person.insurerLicense,
|
||||||
}),
|
}),
|
||||||
LicenceTypeId: input.defaults.CulpritLicenceTypeId,
|
LicenceTypeId: input.defaults.CulpritLicenceTypeId,
|
||||||
MotorNo: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
|
MotorNo:
|
||||||
"MtrNum",
|
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
|
||||||
"EngineNumberField",
|
"MtrNum",
|
||||||
"mtrnum",
|
"EngineNumberField",
|
||||||
]),
|
"mtrnum",
|
||||||
|
]) ?? (typeof apiVehicle?.MotorNo === "string" ? apiVehicle.MotorNo : null),
|
||||||
OwnerId: null,
|
OwnerId: null,
|
||||||
PlaqueCityId: null,
|
PlaqueCityId: apiPlaque.cityId,
|
||||||
PlaqueKindId: plate ? input.defaults.PlaqueKindId : null,
|
PlaqueKindId: apiPlaque.kindId,
|
||||||
PlaqueLeftNo: plate ? String(plate.leftDigits) : null,
|
PlaqueLeftNo:
|
||||||
PlaqueMiddleCodeId: this.getFanavaranPlateMiddleCode(
|
apiPlaque.leftNo ?? (plate ? String(plate.leftDigits) : null),
|
||||||
plate?.centerAlphabet,
|
PlaqueMiddleCodeId:
|
||||||
),
|
apiPlaque.middleCodeId ??
|
||||||
|
this.getFanavaranPlateMiddleCode(plate?.centerAlphabet),
|
||||||
PlaqueNo:
|
PlaqueNo:
|
||||||
|
apiPlaque.plaqueNo ??
|
||||||
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, ["Plk"]) ??
|
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, ["Plk"]) ??
|
||||||
this.formatFanavaranPlateNo(plate),
|
this.formatFanavaranPlateNo(plate),
|
||||||
PlaqueRightNo: plate ? String(plate.centerDigits) : null,
|
PlaqueRightNo:
|
||||||
PlaqueSampleId: plate ? input.defaults.PlaqueSampleId : null,
|
apiPlaque.rightNo ?? (plate ? String(plate.centerDigits) : null),
|
||||||
PlaqueSerial: plate ? String(plate.ir) : null,
|
PlaqueSampleId: apiPlaque.sampleId,
|
||||||
|
PlaqueSerial: apiPlaque.serial ?? (plate ? String(plate.ir) : null),
|
||||||
PolicyNo: policyNo,
|
PolicyNo: policyNo,
|
||||||
PreviousPolicyEndDate: endDate ?? "",
|
PreviousPolicyEndDate: endDate ?? "",
|
||||||
VehicleKindId: vehicleKindId,
|
VehicleKindId: vehicleKindId,
|
||||||
VIN: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
|
VIN:
|
||||||
"VIN",
|
this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
|
||||||
"vin",
|
"VIN",
|
||||||
"VinNumberField",
|
"vin",
|
||||||
]),
|
"VinNumberField",
|
||||||
AccidentVehicleUsedId: input.defaults.AccidentVehicleUsedId,
|
]) ?? (typeof apiVehicle?.VIN === "string" ? apiVehicle.VIN : null),
|
||||||
|
AccidentVehicleUsedId: accidentVehicleUsedId,
|
||||||
PolicyCINumber: policyCINumber,
|
PolicyCINumber: policyCINumber,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -4167,6 +4425,24 @@ export class ClaimRequestManagementService {
|
|||||||
...(vehicleKindId != null
|
...(vehicleKindId != null
|
||||||
? { "fanavaranSync.damageCase.vehicleKindId": vehicleKindId }
|
? { "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
|
...(insuranceCorpId != null
|
||||||
? { "fanavaranSync.damageCase.insuranceCorpId": insuranceCorpId }
|
? { "fanavaranSync.damageCase.insuranceCorpId": insuranceCorpId }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -4633,6 +4909,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(
|
private getNationalCodeOfInsurerForGuiltyPartyV2(
|
||||||
parties: Array<{
|
parties: Array<{
|
||||||
role?: PartyRole;
|
role?: PartyRole;
|
||||||
@@ -4703,11 +5018,17 @@ export class ClaimRequestManagementService {
|
|||||||
auditSession?: FanavaranAuditSession;
|
auditSession?: FanavaranAuditSession;
|
||||||
/** Persist built payload onto fanavaranSync.baseClaim.lastPayload (default true). */
|
/** Persist built payload onto fanavaranSync.baseClaim.lastPayload (default true). */
|
||||||
persistPayload?: boolean;
|
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> {
|
): Promise<any> {
|
||||||
const profile = getFanavaranClientProfile(clientKey);
|
const profile = getFanavaranClientProfile(clientKey);
|
||||||
const logPrefix = `[Fanavaran ${clientKey} V2] claimCaseId=${claimCaseId}`;
|
const logPrefix = `[Fanavaran ${clientKey} V2] claimCaseId=${claimCaseId}`;
|
||||||
const forceRefreshPolicy = options?.forceRefreshPolicy === true;
|
const forceRefreshPolicy = options?.forceRefreshPolicy === true;
|
||||||
|
const forceRefreshUsedId = options?.forceRefreshUsedId === true;
|
||||||
const requirePolicyId = options?.requirePolicyId === true;
|
const requirePolicyId = options?.requirePolicyId === true;
|
||||||
const persistPayload = options?.persistPayload !== false;
|
const persistPayload = options?.persistPayload !== false;
|
||||||
const auditSession =
|
const auditSession =
|
||||||
@@ -4877,6 +5198,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) {
|
if (persistPayload) {
|
||||||
await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, {
|
await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, {
|
||||||
$set: {
|
$set: {
|
||||||
@@ -4885,6 +5243,18 @@ export class ClaimRequestManagementService {
|
|||||||
...(payload.PolicyId != null
|
...(payload.PolicyId != null
|
||||||
? { "fanavaranSync.baseClaim.policyId": payload.PolicyId }
|
? { "fanavaranSync.baseClaim.policyId": payload.PolicyId }
|
||||||
: {}),
|
: {}),
|
||||||
|
...(typeof payload.AccidentVehicleUsedId === "number"
|
||||||
|
? {
|
||||||
|
"fanavaranSync.baseClaim.accidentVehicleUsedId":
|
||||||
|
payload.AccidentVehicleUsedId,
|
||||||
|
...(usedSource
|
||||||
|
? {
|
||||||
|
"fanavaranSync.baseClaim.accidentVehicleUsedSource":
|
||||||
|
usedSource,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -7280,6 +7650,7 @@ export class ClaimRequestManagementService {
|
|||||||
requirePolicyId: true,
|
requirePolicyId: true,
|
||||||
auditSession,
|
auditSession,
|
||||||
persistPayload: true,
|
persistPayload: true,
|
||||||
|
forceRefreshUsedId: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Guardrail: never send empty/null licence fields even if manual overrides arrive.
|
// Guardrail: never send empty/null licence fields even if manual overrides arrive.
|
||||||
|
|||||||
@@ -93,6 +93,25 @@ export class FanavaranSyncStage {
|
|||||||
@Prop({ type: Number })
|
@Prop({ type: Number })
|
||||||
vehicleKindId?: 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. */
|
/** Cached Fanavaran InsuranceCorpId. */
|
||||||
@Prop({ type: Number })
|
@Prop({ type: Number })
|
||||||
insuranceCorpId?: number;
|
insuranceCorpId?: number;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
114
src/claim-request-management/fanavaran-driver-inquiry.ts
Normal file
114
src/claim-request-management/fanavaran-driver-inquiry.ts
Normal 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 };
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@ import {
|
|||||||
filterPoliciesByLine,
|
filterPoliciesByLine,
|
||||||
insuranceLineLabel,
|
insuranceLineLabel,
|
||||||
parseLastCarPolicyInput,
|
parseLastCarPolicyInput,
|
||||||
|
pickFanavaranPlaqueLookupFields,
|
||||||
|
pickFanavaranVehicleFromInquiry,
|
||||||
pickVehicleId,
|
pickVehicleId,
|
||||||
|
pickVinFromPartyVehicle,
|
||||||
plaqueLetterFromMiddleCode,
|
plaqueLetterFromMiddleCode,
|
||||||
plaqueMatchesVehicle,
|
plaqueMatchesVehicle,
|
||||||
selectLastAmongCarMatches,
|
selectLastAmongCarMatches,
|
||||||
@@ -84,6 +87,57 @@ describe("fanavaran last car policy", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("picks the inquiry row matching policy VehicleId and VIN from the party", () => {
|
||||||
|
const inquired = [
|
||||||
|
{ Id: 1, UsedId: 1, VehicleKindId: 10 },
|
||||||
|
{
|
||||||
|
Id: 3379978,
|
||||||
|
UsedId: 84,
|
||||||
|
VehicleKindId: 5950,
|
||||||
|
VIN: "NAS451100P4934023",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(pickFanavaranVehicleFromInquiry(inquired, 3379978)).toEqual(
|
||||||
|
inquired[1],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
pickVinFromPartyVehicle({
|
||||||
|
vehicle: { vin: "nas451100p4934023" },
|
||||||
|
}),
|
||||||
|
).toBe("NAS451100P4934023");
|
||||||
|
expect(
|
||||||
|
pickVinFromPartyVehicle({
|
||||||
|
vehicle: {
|
||||||
|
inquiry: { mapped: { VIN: "NAS451100P4934023" } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toBe("NAS451100P4934023");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads plaque kind and sample from VIN inquiry for the lookup filter", () => {
|
||||||
|
expect(
|
||||||
|
pickFanavaranPlaqueLookupFields({
|
||||||
|
PlaqueKindId: 15,
|
||||||
|
PlaqueSampleId: 24,
|
||||||
|
PlaqueCityId: null,
|
||||||
|
PlaqueLeftNo: "62",
|
||||||
|
PlaqueMiddleCodeId: 14,
|
||||||
|
PlaqueRightNo: "278",
|
||||||
|
PlaqueSerial: "50",
|
||||||
|
PlaqueNo: "278و62",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
kindId: 15,
|
||||||
|
sampleId: 24,
|
||||||
|
cityId: null,
|
||||||
|
leftNo: "62",
|
||||||
|
middleCodeId: 14,
|
||||||
|
rightNo: "278",
|
||||||
|
serial: "50",
|
||||||
|
plaqueNo: "278و62",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("matches VIN against ChassisNo when VIN is empty", () => {
|
it("matches VIN against ChassisNo when VIN is empty", () => {
|
||||||
expect(
|
expect(
|
||||||
vehicleMatchesCar(
|
vehicleMatchesCar(
|
||||||
|
|||||||
@@ -233,6 +233,71 @@ export function pickVehicleId(source: unknown): number | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Prefer the inquiry row whose Id matches the policy VehicleId. */
|
||||||
|
export function pickFanavaranVehicleFromInquiry(
|
||||||
|
inquired: unknown,
|
||||||
|
vehicleId?: number | null,
|
||||||
|
): Record<string, unknown> | null {
|
||||||
|
if (Array.isArray(inquired)) {
|
||||||
|
if (vehicleId != null) {
|
||||||
|
const match = inquired.find((row) => pickVehicleId(row) === vehicleId);
|
||||||
|
if (match) return asObjectRecord(match);
|
||||||
|
}
|
||||||
|
return asObjectRecord(inquired[0]);
|
||||||
|
}
|
||||||
|
return asObjectRecord(inquired);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickVinFromPartyVehicle(party: {
|
||||||
|
vehicle?: { vin?: unknown; inquiry?: unknown };
|
||||||
|
} | null | undefined): string | null {
|
||||||
|
const direct = normalizeVin(party?.vehicle?.vin);
|
||||||
|
if (direct) return direct;
|
||||||
|
|
||||||
|
const inquiry = party?.vehicle?.inquiry;
|
||||||
|
const nested = asObjectRecord(inquiry);
|
||||||
|
const mapped = pickVehicleVin(asObjectRecord(nested?.mapped));
|
||||||
|
if (mapped) return mapped;
|
||||||
|
const raw = pickVehicleVin(asObjectRecord(nested?.raw ?? inquiry));
|
||||||
|
return raw || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FanavaranPlaqueLookupFields = {
|
||||||
|
kindId: number | null;
|
||||||
|
sampleId: number | null;
|
||||||
|
cityId: number | null;
|
||||||
|
leftNo: string | null;
|
||||||
|
middleCodeId: number | null;
|
||||||
|
rightNo: string | null;
|
||||||
|
serial: string | null;
|
||||||
|
plaqueNo: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function asPlaqueText(value: unknown): string | null {
|
||||||
|
const text = normalizePlaquePart(value);
|
||||||
|
return text || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Plaque lookup-filter fields from VIN inquiry / vehicle GET. */
|
||||||
|
export function pickFanavaranPlaqueLookupFields(
|
||||||
|
vehicle: Record<string, unknown> | null,
|
||||||
|
): FanavaranPlaqueLookupFields {
|
||||||
|
return {
|
||||||
|
kindId: parseFanavaranId(vehicle?.PlaqueKindId ?? vehicle?.plaqueKindId),
|
||||||
|
sampleId: parseFanavaranId(
|
||||||
|
vehicle?.PlaqueSampleId ?? vehicle?.plaqueSampleId,
|
||||||
|
),
|
||||||
|
cityId: parseFanavaranId(vehicle?.PlaqueCityId ?? vehicle?.plaqueCityId),
|
||||||
|
leftNo: asPlaqueText(vehicle?.PlaqueLeftNo ?? vehicle?.plaqueLeftNo),
|
||||||
|
middleCodeId: parseFanavaranId(
|
||||||
|
vehicle?.PlaqueMiddleCodeId ?? vehicle?.plaqueMiddleCodeId,
|
||||||
|
),
|
||||||
|
rightNo: asPlaqueText(vehicle?.PlaqueRightNo ?? vehicle?.plaqueRightNo),
|
||||||
|
serial: asPlaqueText(vehicle?.PlaqueSerial ?? vehicle?.plaqueSerial),
|
||||||
|
plaqueNo: asPlaqueText(vehicle?.PlaqueNo ?? vehicle?.plaqueNo),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function plaqueNumberEquals(left: unknown, right: unknown): boolean {
|
function plaqueNumberEquals(left: unknown, right: unknown): boolean {
|
||||||
const a = normalizePlaquePart(left);
|
const a = normalizePlaquePart(left);
|
||||||
const b = normalizePlaquePart(right);
|
const b = normalizePlaquePart(right);
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ export class LookupsController {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Fanavaran vehicle inquiry by VIN",
|
summary: "Fanavaran vehicle inquiry by VIN",
|
||||||
description:
|
description:
|
||||||
"Returns vehicle details from Fanavaran for the given VIN number via car/vehicles/inquiry-by-vin.",
|
"GET car/vehicles/inquiry-by-vin. Response includes UsedId (AccidentVehicleUsedId), VehicleKindId, plus vehicleKind/used lookup rows. Prefer this over vehicle GET when you already have the VIN.",
|
||||||
})
|
})
|
||||||
@ApiQuery({
|
@ApiQuery({
|
||||||
name: "vin",
|
name: "vin",
|
||||||
@@ -346,6 +346,42 @@ export class LookupsController {
|
|||||||
return await this.lookupsService.inquiryByVin(vin);
|
return await this.lookupsService.inquiryByVin(vin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("inquiry-by-unique-identifier")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Fanavaran party inquiry by national code and birthday",
|
||||||
|
description:
|
||||||
|
"GET common/parties/inquiry-by-unique-identifier. Use this to resolve GEN.12 DriverId. Pass birthday as 13480313 or 1348/03/13, or birthYear+birthMonth+birthDay.",
|
||||||
|
})
|
||||||
|
@ApiQuery({ name: "nationalCode", example: "0012345678" })
|
||||||
|
@ApiQuery({
|
||||||
|
name: "birthday",
|
||||||
|
required: false,
|
||||||
|
example: "13480313",
|
||||||
|
description: "Jalali birthday compact (13480313) or delimited (1348/03/13)",
|
||||||
|
})
|
||||||
|
@ApiQuery({ name: "birthYear", required: false, example: 1348 })
|
||||||
|
@ApiQuery({ name: "birthMonth", required: false, example: 3 })
|
||||||
|
@ApiQuery({ name: "birthDay", required: false, example: 13 })
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Fanavaran parties plus picked driverId / insurerId",
|
||||||
|
schema: { type: "object" },
|
||||||
|
})
|
||||||
|
async inquiryByUniqueIdentifier(
|
||||||
|
@Query("nationalCode") nationalCode?: string,
|
||||||
|
@Query("birthday") birthday?: string,
|
||||||
|
@Query("birthYear") birthYear?: string,
|
||||||
|
@Query("birthMonth") birthMonth?: string,
|
||||||
|
@Query("birthDay") birthDay?: string,
|
||||||
|
) {
|
||||||
|
return await this.lookupsService.inquiryByUniqueIdentifier({
|
||||||
|
nationalCode,
|
||||||
|
birthday,
|
||||||
|
birthYear,
|
||||||
|
birthMonth,
|
||||||
|
birthDay,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Get("fanavaran")
|
@Get("fanavaran")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "List configured Fanavaran remote lookups",
|
summary: "List configured Fanavaran remote lookups",
|
||||||
@@ -627,4 +663,41 @@ export class LookupsController {
|
|||||||
async bodyPolicyById(@Param("policyId", ParseIntPipe) policyId: number) {
|
async bodyPolicyById(@Param("policyId", ParseIntPipe) policyId: number) {
|
||||||
return await this.lookupsService.bodyPolicyById(policyId);
|
return await this.lookupsService.bodyPolicyById(policyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("vehicle/:vehicleId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Fanavaran vehicle by id",
|
||||||
|
description:
|
||||||
|
"GET car/vehicles/{vehicleId} after a third-party/hull policy returns VehicleId. Pass the policy VehicleVersionNo when you need that exact version; omit it for the latest version. Response includes vehicleKind (VehicleGroupId) and used (UsedId) lookup rows.",
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: "vehicleId",
|
||||||
|
description: "Fanavaran vehicle id from the policy VehicleId field",
|
||||||
|
example: 3379978,
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "versionNo",
|
||||||
|
required: false,
|
||||||
|
description:
|
||||||
|
"Policy VehicleVersionNo. Omit to fetch the latest vehicle version.",
|
||||||
|
example: 2,
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description:
|
||||||
|
"Vehicle record plus vehicleKind, used, plaque, and color placeholders",
|
||||||
|
schema: { type: "object" },
|
||||||
|
})
|
||||||
|
async vehicleById(
|
||||||
|
@Param("vehicleId", ParseIntPipe) vehicleId: number,
|
||||||
|
@Query("versionNo") versionNo?: string,
|
||||||
|
) {
|
||||||
|
const parsedVersion =
|
||||||
|
versionNo == null || String(versionNo).trim() === ""
|
||||||
|
? undefined
|
||||||
|
: Number(versionNo);
|
||||||
|
return await this.lookupsService.vehicleById(
|
||||||
|
vehicleId,
|
||||||
|
Number.isFinite(parsedVersion) ? parsedVersion : undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ import {
|
|||||||
TEJARAT_STATIC_ACCIDENT_FILES,
|
TEJARAT_STATIC_ACCIDENT_FILES,
|
||||||
} from "src/fanavaran/fanavaran-lookup.config";
|
} from "src/fanavaran/fanavaran-lookup.config";
|
||||||
import { FanavaranLookupService } from "src/fanavaran/fanavaran-lookup.service";
|
import { FanavaranLookupService } from "src/fanavaran/fanavaran-lookup.service";
|
||||||
|
import {
|
||||||
|
parseJalaliDateParts,
|
||||||
|
selectFanavaranDriverId,
|
||||||
|
} from "src/claim-request-management/fanavaran-driver-inquiry";
|
||||||
import { LookupDbService } from "./entities/db-service/lookup.db.service";
|
import { LookupDbService } from "./entities/db-service/lookup.db.service";
|
||||||
import {
|
import {
|
||||||
asObjectRecord,
|
asObjectRecord,
|
||||||
@@ -178,9 +182,81 @@ export class LookupsService {
|
|||||||
return await this.getClientRemoteLookup("dmg-business-line");
|
return await this.getClientRemoteLookup("dmg-business-line");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async inquiryByUniqueIdentifier(query: {
|
||||||
|
nationalCode?: string;
|
||||||
|
birthday?: string;
|
||||||
|
birthYear?: string | number;
|
||||||
|
birthMonth?: string | number;
|
||||||
|
birthDay?: string | number;
|
||||||
|
}): Promise<unknown> {
|
||||||
|
const nationalCode = String(query.nationalCode ?? "").trim();
|
||||||
|
if (!nationalCode) {
|
||||||
|
throw new BadRequestException("nationalCode is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromParts = {
|
||||||
|
year: Number(query.birthYear),
|
||||||
|
month: Number(query.birthMonth),
|
||||||
|
day: Number(query.birthDay),
|
||||||
|
};
|
||||||
|
const parsed =
|
||||||
|
Number.isFinite(fromParts.year) &&
|
||||||
|
fromParts.year > 0 &&
|
||||||
|
Number.isFinite(fromParts.month) &&
|
||||||
|
fromParts.month > 0 &&
|
||||||
|
Number.isFinite(fromParts.day) &&
|
||||||
|
fromParts.day > 0
|
||||||
|
? fromParts
|
||||||
|
: parseJalaliDateParts(query.birthday);
|
||||||
|
|
||||||
|
if (!parsed) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"birthday (e.g. 13480313) or birthYear+birthMonth+birthDay is required",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientKey = this.activeClientKey();
|
||||||
|
const rows = await this.fanavaranLookupService.inquiryByUniqueIdentifier(
|
||||||
|
clientKey,
|
||||||
|
{
|
||||||
|
nationalCode,
|
||||||
|
birthYear: parsed.year,
|
||||||
|
birthMonth: parsed.month,
|
||||||
|
birthDay: parsed.day,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
nationalCode,
|
||||||
|
birthday: parsed,
|
||||||
|
driverId: selectFanavaranDriverId(rows, undefined),
|
||||||
|
insurerId: selectFanavaranDriverId(rows, true),
|
||||||
|
parties: rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async inquiryByVin(vin: string): Promise<unknown> {
|
async inquiryByVin(vin: string): Promise<unknown> {
|
||||||
const clientKey = this.activeClientKey();
|
const clientKey = this.activeClientKey();
|
||||||
return this.fanavaranLookupService.inquiryByVin(clientKey, vin);
|
const raw = await this.fanavaranLookupService.inquiryByVin(clientKey, vin);
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
if (raw.length === 0) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Fanavaran vehicle for VIN ${vin} was not found.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.all(
|
||||||
|
raw.map(async (item) => {
|
||||||
|
const vehicle = asObjectRecord(item);
|
||||||
|
return vehicle ? this.enrichVehicle(vehicle) : item;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const vehicle = asObjectRecord(raw);
|
||||||
|
if (!vehicle) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Fanavaran vehicle for VIN ${vin} was not found.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.enrichVehicle(vehicle);
|
||||||
}
|
}
|
||||||
|
|
||||||
async myPolicies(
|
async myPolicies(
|
||||||
@@ -514,11 +590,19 @@ export class LookupsService {
|
|||||||
|
|
||||||
async vehicleById(vehicleId: number, versionNo?: number): Promise<unknown> {
|
async vehicleById(vehicleId: number, versionNo?: number): Promise<unknown> {
|
||||||
const clientKey = this.activeClientKey();
|
const clientKey = this.activeClientKey();
|
||||||
return this.fanavaranLookupService.vehicleById(
|
const vehicle = asObjectRecord(
|
||||||
clientKey,
|
await this.fanavaranLookupService.vehicleById(
|
||||||
vehicleId,
|
clientKey,
|
||||||
versionNo,
|
vehicleId,
|
||||||
|
versionNo,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
if (!vehicle) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Fanavaran vehicle ${vehicleId} was not found.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.enrichVehicle(vehicle);
|
||||||
}
|
}
|
||||||
|
|
||||||
async customerById(customerId: number): Promise<unknown> {
|
async customerById(customerId: number): Promise<unknown> {
|
||||||
|
|||||||
145
src/lookups/lookups.service.vehicle.spec.ts
Normal file
145
src/lookups/lookups.service.vehicle.spec.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { NotFoundException } from "@nestjs/common";
|
||||||
|
import { LookupsService } from "./lookups.service";
|
||||||
|
|
||||||
|
describe("LookupsService.vehicleById", () => {
|
||||||
|
const originalClient = process.env.FANAVARAN_CLIENT;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
process.env.FANAVARAN_CLIENT = "parsian";
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
process.env.FANAVARAN_CLIENT = originalClient;
|
||||||
|
});
|
||||||
|
|
||||||
|
function createService(lookup: Record<string, jest.Mock>) {
|
||||||
|
return new LookupsService({} as any, lookup as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("returns the vehicle with kind, used, and plaque lookups", async () => {
|
||||||
|
const lookup = {
|
||||||
|
vehicleById: jest.fn().mockResolvedValue({
|
||||||
|
Id: 3379978,
|
||||||
|
VehicleKindId: 5904,
|
||||||
|
UsedId: 36,
|
||||||
|
VIN: "IRNKAEK4150012345",
|
||||||
|
PlaqueLeftNo: "12",
|
||||||
|
PlaqueMiddleCodeId: 2,
|
||||||
|
PlaqueRightNo: "345",
|
||||||
|
PlaqueSerial: "67",
|
||||||
|
VersionNo: 2,
|
||||||
|
}),
|
||||||
|
getRemoteLookup: jest.fn().mockImplementation((_key, url: string) => {
|
||||||
|
if (url.includes("vehicle-kinds")) {
|
||||||
|
return Promise.resolve([
|
||||||
|
{ Id: 5904, Caption: "بنز", VehicleGroupId: 3, IsActive: 1 },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (url.includes("vehicle-use-types")) {
|
||||||
|
return Promise.resolve([
|
||||||
|
{ Id: 36, Caption: "حمل مواد سريع الاشتعال", VehicleGroupId: 3, IsActive: 1 },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = createService(lookup);
|
||||||
|
const result = (await service.vehicleById(3379978, 2)) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
|
expect(lookup.vehicleById).toHaveBeenCalledWith("parsian", 3379978, 2);
|
||||||
|
expect(result.Id).toBe(3379978);
|
||||||
|
expect(result.vehicleKind).toEqual({
|
||||||
|
Id: 5904,
|
||||||
|
Caption: "بنز",
|
||||||
|
VehicleGroupId: 3,
|
||||||
|
IsActive: 1,
|
||||||
|
});
|
||||||
|
expect(result.used).toEqual({
|
||||||
|
Id: 36,
|
||||||
|
Caption: "حمل مواد سريع الاشتعال",
|
||||||
|
VehicleGroupId: 3,
|
||||||
|
IsActive: 1,
|
||||||
|
});
|
||||||
|
expect(result.plaque).toEqual({
|
||||||
|
leftTwoDigits: "12",
|
||||||
|
serialLetter: "ب",
|
||||||
|
threeDigits: "345",
|
||||||
|
rightTwoDigits: "67",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enriches inquiry-by-vin rows with used and vehicleKind", async () => {
|
||||||
|
const lookup = {
|
||||||
|
inquiryByVin: jest.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
Id: 3379978,
|
||||||
|
UsedId: 84,
|
||||||
|
VehicleKindId: 5950,
|
||||||
|
VIN: "NAS451100P4934023",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
getRemoteLookup: jest.fn().mockImplementation((_key, url: string) => {
|
||||||
|
if (url.includes("vehicle-kinds")) {
|
||||||
|
return Promise.resolve([
|
||||||
|
{ Id: 5950, Caption: "کامیون", VehicleGroupId: 3, IsActive: 1 },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (url.includes("vehicle-use-types")) {
|
||||||
|
return Promise.resolve([
|
||||||
|
{ Id: 84, Caption: "حمل بار", VehicleGroupId: 3, IsActive: 1 },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = createService(lookup);
|
||||||
|
const result = (await service.inquiryByVin("NAS451100P4934023")) as Array<
|
||||||
|
Record<string, unknown>
|
||||||
|
>;
|
||||||
|
|
||||||
|
expect(lookup.inquiryByVin).toHaveBeenCalledWith(
|
||||||
|
"parsian",
|
||||||
|
"NAS451100P4934023",
|
||||||
|
);
|
||||||
|
expect(result[0].UsedId).toBe(84);
|
||||||
|
expect(result[0].used).toEqual({
|
||||||
|
Id: 84,
|
||||||
|
Caption: "حمل بار",
|
||||||
|
VehicleGroupId: 3,
|
||||||
|
IsActive: 1,
|
||||||
|
});
|
||||||
|
expect(result[0].vehicleKind).toEqual({
|
||||||
|
Id: 5950,
|
||||||
|
Caption: "کامیون",
|
||||||
|
VehicleGroupId: 3,
|
||||||
|
IsActive: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("unwraps an array payload and 404s when Fanavaran returns nothing", async () => {
|
||||||
|
const lookup = {
|
||||||
|
vehicleById: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce([{ Id: 3379978, VehicleKindId: 1 }])
|
||||||
|
.mockResolvedValueOnce([]),
|
||||||
|
getRemoteLookup: jest.fn().mockResolvedValue([]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = createService(lookup);
|
||||||
|
const unwrapped = (await service.vehicleById(3379978)) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
expect(unwrapped.Id).toBe(3379978);
|
||||||
|
expect(unwrapped.color).toBeNull();
|
||||||
|
|
||||||
|
await expect(service.vehicleById(1)).rejects.toBeInstanceOf(
|
||||||
|
NotFoundException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user