forked from Yara724/api
Map Fanavaran people and licences by party role; allow unlisted base-info lookups.
Resolve driver, owner, and policyholder from participants when they differ, prefer real licence / driverLicenseDate over dummies, and fall back unknown /lookups/fanavaran/{name} to car/base-info/{name} (including vehicle-groups).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -262,7 +262,16 @@ import {
|
||||
pickPersonBirthday,
|
||||
pickPersonNationalCode,
|
||||
selectFanavaranDriverId,
|
||||
selectFanavaranOwnerId,
|
||||
} from "./fanavaran-driver-inquiry";
|
||||
import {
|
||||
resolveDriverFromParty,
|
||||
resolveDriverIsOwner,
|
||||
resolveFanavaranLicenceIssuDate,
|
||||
resolveFanavaranLicenceNo,
|
||||
resolveOwnerFromParty,
|
||||
resolvePolicyholderFromParty,
|
||||
} from "./fanavaran-party-roles";
|
||||
import {
|
||||
buildFanavaranOtherPeoplePayload,
|
||||
pickFanavaranRecordId,
|
||||
@@ -3676,23 +3685,23 @@ export class ClaimRequestManagementService {
|
||||
/**
|
||||
* Resolve licence number to send to Fanavaran.
|
||||
* Preference order:
|
||||
* - use the first non-empty value from DB (driverLicense/insurerLicense)
|
||||
* - else use the configured 10-digit dummy
|
||||
* - driver participant / person.driverLicense (and insurerLicense as legacy)
|
||||
* - else the configured 10-digit dummy
|
||||
*/
|
||||
private resolveFanavaranLicenceNoFromParty(input: {
|
||||
driverIsInsurer?: boolean;
|
||||
driverLicense?: unknown;
|
||||
insurerLicense?: unknown;
|
||||
/** Optional already-resolved digits from participants. */
|
||||
resolvedLicenseNo?: string | null;
|
||||
}): string {
|
||||
const { driverIsInsurer, driverLicense, insurerLicense } = input;
|
||||
const fromResolved = this.extractNonEmptyLicenceDigits(
|
||||
input.resolvedLicenseNo,
|
||||
);
|
||||
if (fromResolved) return fromResolved;
|
||||
|
||||
// When driverIsInsurer === true, UI usually mirrors insurer/driver values,
|
||||
// but DB can still miss one of the two fields — prefer both.
|
||||
const candidates = driverIsInsurer
|
||||
? [driverLicense, insurerLicense]
|
||||
: [driverLicense, insurerLicense];
|
||||
|
||||
for (const c of candidates) {
|
||||
const { driverLicense, insurerLicense } = input;
|
||||
for (const c of [driverLicense, insurerLicense]) {
|
||||
const digits = this.extractNonEmptyLicenceDigits(c);
|
||||
if (digits) return digits;
|
||||
}
|
||||
@@ -4298,6 +4307,36 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Best-effort OwnerId from parties inquiry (no GEN.44 create). */
|
||||
private async resolveOwnerFanavaranId(
|
||||
clientKey: FanavaranClientKey,
|
||||
nationalCode: string | null | undefined,
|
||||
birthday: string | number | null | undefined,
|
||||
): Promise<number | null> {
|
||||
if (!nationalCode) return null;
|
||||
const parsed = this.parseJalaliBirthday(birthday);
|
||||
if (!parsed) return null;
|
||||
try {
|
||||
const response = await this.fanavaranLookupService.inquiryByUniqueIdentifier(
|
||||
clientKey,
|
||||
{
|
||||
nationalCode,
|
||||
birthYear: parsed.year,
|
||||
birthMonth: parsed.month,
|
||||
birthDay: parsed.day,
|
||||
},
|
||||
);
|
||||
return selectFanavaranOwnerId(response);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`[resolveOwnerFanavaranId] inquiry failed for nationalCode=${nationalCode}: ${
|
||||
error instanceof Error ? error.message : error
|
||||
}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** GEN.03 hull: TotalPremium + parties inquiry (AccidentCulpritId) before hull field whitelist. */
|
||||
private async enrichCarBodyHullBaseClaimPayload(input: {
|
||||
clientKey: FanavaranClientKey;
|
||||
@@ -4357,13 +4396,14 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
| undefined;
|
||||
|
||||
const driverIdentity = resolveDriverFromParty(input.firstParty as any);
|
||||
const nationalCode =
|
||||
driverIdentity.nationalCode ??
|
||||
pickPersonNationalCode(person) ??
|
||||
pickCarBodyPolicyNationalCode(
|
||||
input.blameCase as { parties?: unknown[] },
|
||||
);
|
||||
const birthday = pickPersonBirthday(person);
|
||||
const driverIsInsurer = person?.driverIsInsurer ?? true;
|
||||
const birthday = driverIdentity.birthday ?? pickPersonBirthday(person);
|
||||
|
||||
let accidentCulpritId: number | null = null;
|
||||
if (nationalCode && birthday) {
|
||||
@@ -4371,7 +4411,7 @@ export class ClaimRequestManagementService {
|
||||
input.clientKey,
|
||||
nationalCode,
|
||||
birthday,
|
||||
driverIsInsurer,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (accidentCulpritId == null) {
|
||||
@@ -4386,6 +4426,13 @@ export class ClaimRequestManagementService {
|
||||
input.payload.AccidentCulpritId = accidentCulpritId;
|
||||
}
|
||||
|
||||
input.payload.CulpritLicenceNo = resolveFanavaranLicenceNo(
|
||||
driverIdentity,
|
||||
FANAVARAN_DUMMY_LICENCE_NO,
|
||||
);
|
||||
input.payload.CulpritLicenceIssuDate =
|
||||
resolveFanavaranLicenceIssuDate(driverIdentity);
|
||||
|
||||
input.payload.CustomerFaultPercent =
|
||||
FANAVARAN_DEFAULT_HULL_CUSTOMER_FAULT_PERCENT;
|
||||
|
||||
@@ -4619,6 +4666,9 @@ export class ClaimRequestManagementService {
|
||||
const person = damagedParty?.person ?? {};
|
||||
const vehicle = damagedParty?.vehicle ?? {};
|
||||
const insurance = damagedParty?.insurance ?? {};
|
||||
const driverIdentity = resolveDriverFromParty(damagedParty);
|
||||
const ownerIdentity = resolveOwnerFromParty(damagedParty);
|
||||
const driverIsOwner = resolveDriverIsOwner(damagedParty);
|
||||
const product = fanavaranClaimProductFromBlameType(input.blameCase?.type);
|
||||
const { mapped: inquiryMapped, raw: inquiryRawPayload } =
|
||||
fanavaranPartyInquirySources(product, damagedParty);
|
||||
@@ -4630,12 +4680,14 @@ export class ClaimRequestManagementService {
|
||||
|
||||
this.logger.log(
|
||||
`[buildFanavaranDamageCasePayload] damagedParty resolved: userId=${damagedParty?.person?.userId ?? "NONE"}, ` +
|
||||
`nationalCode=${pickPersonNationalCode(person) ?? "MISSING"}, ` +
|
||||
`driverNationalCode=${driverIdentity.nationalCode ?? "MISSING"}, ` +
|
||||
`ownerNationalCode=${ownerIdentity.nationalCode ?? "MISSING"}, ` +
|
||||
`nationalCodeOfDriver=${person.nationalCodeOfDriver ?? "MISSING"}, ` +
|
||||
`nationalCodeOfInsurer=${person.nationalCodeOfInsurer ?? "MISSING"}, ` +
|
||||
`driverBirthday=${pickPersonBirthday(person) ?? "MISSING"}, ` +
|
||||
`driverIsInsurer=${person.driverIsInsurer ?? "MISSING"}, ` +
|
||||
`driverLicense=${person.driverLicense ?? "MISSING"}`,
|
||||
`driverBirthday=${driverIdentity.birthday ?? "MISSING"}, ` +
|
||||
`driverIsOwner=${driverIsOwner}, ` +
|
||||
`driverLicense=${driverIdentity.licenseNo ?? person.driverLicense ?? "MISSING"}, ` +
|
||||
`driverLicenseDate=${driverIdentity.licenseIssuDate ?? "MISSING"}`,
|
||||
);
|
||||
if (!damagedParty) {
|
||||
this.logger.warn(
|
||||
@@ -4761,12 +4813,23 @@ export class ClaimRequestManagementService {
|
||||
if (vehicleKindId != null) vehicleKindSource = "car-type-lookup";
|
||||
}
|
||||
|
||||
let driverFanavaranId = await this.resolveDriverFanavaranIdFromBlameCase(
|
||||
input.clientKey,
|
||||
input.blameCase?.parties ?? [],
|
||||
person,
|
||||
input.claimCase,
|
||||
);
|
||||
let driverFanavaranId: number | null = null;
|
||||
if (driverIdentity.nationalCode && driverIdentity.birthday) {
|
||||
driverFanavaranId = await this.resolveDriverFanavaranId(
|
||||
input.clientKey,
|
||||
driverIdentity.nationalCode,
|
||||
driverIdentity.birthday,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (driverFanavaranId == null) {
|
||||
driverFanavaranId = await this.resolveDriverFanavaranIdFromBlameCase(
|
||||
input.clientKey,
|
||||
input.blameCase?.parties ?? [],
|
||||
person,
|
||||
input.claimCase,
|
||||
);
|
||||
}
|
||||
let otherPersonId = this.firstPositiveFanavaranId(cachedDamage.otherPersonId);
|
||||
|
||||
if (driverFanavaranId) {
|
||||
@@ -4781,7 +4844,13 @@ export class ClaimRequestManagementService {
|
||||
} else if (input.registerMissingPerson) {
|
||||
const registered = await this.registerFanavaranOtherPerson({
|
||||
clientKey: input.clientKey,
|
||||
person,
|
||||
person: {
|
||||
...person,
|
||||
nationalCodeOfDriver:
|
||||
driverIdentity.nationalCode ?? person.nationalCodeOfDriver,
|
||||
driverBirthday: driverIdentity.birthday ?? person.driverBirthday,
|
||||
driverIsInsurer: false,
|
||||
},
|
||||
claimCaseId: input.claimCase?._id
|
||||
? String(input.claimCase._id)
|
||||
: undefined,
|
||||
@@ -4792,6 +4861,21 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
let ownerFanavaranId: number | null = null;
|
||||
if (
|
||||
ownerIdentity.nationalCode &&
|
||||
ownerIdentity.nationalCode === driverIdentity.nationalCode &&
|
||||
driverFanavaranId != null
|
||||
) {
|
||||
ownerFanavaranId = driverFanavaranId;
|
||||
} else if (ownerIdentity.nationalCode && ownerIdentity.birthday) {
|
||||
ownerFanavaranId = await this.resolveOwnerFanavaranId(
|
||||
input.clientKey,
|
||||
ownerIdentity.nationalCode,
|
||||
ownerIdentity.birthday,
|
||||
);
|
||||
}
|
||||
|
||||
if (driverFanavaranId && input.blameCase?._id && damagedParty) {
|
||||
const partyIndex = (input.blameCase.parties ?? []).findIndex(
|
||||
(p: any) => p === damagedParty ||
|
||||
@@ -4844,7 +4928,7 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[buildFanavaranDamageCasePayload] Final DriverId=${driverFanavaranId ?? "NULL"} for nationalCode=${pickPersonNationalCode(person) ?? "MISSING"}, ` +
|
||||
`[buildFanavaranDamageCasePayload] Final DriverId=${driverFanavaranId ?? "NULL"} OwnerId=${ownerFanavaranId ?? "NULL"} for driverNationalCode=${driverIdentity.nationalCode ?? "MISSING"}, ` +
|
||||
`PolicyNo=${policyNo ?? "NULL"}, PolicyCINumber=${policyCINumber ?? "NULL"}`,
|
||||
);
|
||||
|
||||
@@ -4866,16 +4950,17 @@ export class ClaimRequestManagementService {
|
||||
EstimateAmount: FANAVARAN_PROVISIONAL_ESTIMATE_AMOUNT,
|
||||
FaultPercent: input.defaults.FaultPercent,
|
||||
InsuranceCorpId: insuranceCorpId,
|
||||
DriverIsOwner: person.driverIsInsurer ? 1 : input.defaults.DriverIsOwner,
|
||||
DriverIsOwner: driverIsOwner ? 1 : 0,
|
||||
LicenceCityId: null,
|
||||
LicenceCountryId: null,
|
||||
LicenceForeignCityName: null,
|
||||
LicenceIssuDate: person.driverBirthday ?? "1394/10/13",
|
||||
LicenceIssuDate: resolveFanavaranLicenceIssuDate(driverIdentity),
|
||||
// Must never be empty/null; prefer stored DB value, else use dummy.
|
||||
LicenceNo: this.resolveFanavaranLicenceNoFromParty({
|
||||
driverIsInsurer: person.driverIsInsurer,
|
||||
driverLicense: person.driverLicense,
|
||||
insurerLicense: person.insurerLicense,
|
||||
resolvedLicenseNo: driverIdentity.licenseNo,
|
||||
}),
|
||||
LicenceTypeId: input.defaults.CulpritLicenceTypeId,
|
||||
MotorNo:
|
||||
@@ -4884,7 +4969,7 @@ export class ClaimRequestManagementService {
|
||||
"EngineNumberField",
|
||||
"mtrnum",
|
||||
]) ?? (typeof apiVehicle?.MotorNo === "string" ? apiVehicle.MotorNo : null),
|
||||
OwnerId: null,
|
||||
OwnerId: ownerFanavaranId,
|
||||
PlaqueCityId: apiPlaque.cityId,
|
||||
PlaqueKindId: plaqueKindId,
|
||||
PlaqueLeftNo:
|
||||
@@ -5819,6 +5904,8 @@ export class ClaimRequestManagementService {
|
||||
parties: Array<{
|
||||
role?: PartyRole;
|
||||
person?: { userId?: Types.ObjectId; nationalCodeOfInsurer?: string };
|
||||
participants?: Array<Record<string, any>>;
|
||||
participantRoles?: Record<string, string | undefined>;
|
||||
statement?: { admitsGuilt?: boolean };
|
||||
}>,
|
||||
guiltyPartyId?: Types.ObjectId | string | null,
|
||||
@@ -5830,8 +5917,13 @@ export class ClaimRequestManagementService {
|
||||
const guiltyParty = parties.find(
|
||||
(party) => party.person?.userId?.toString() === guiltyPartyId.toString(),
|
||||
);
|
||||
if (!guiltyParty) return null;
|
||||
|
||||
return guiltyParty?.person?.nationalCodeOfInsurer ?? null;
|
||||
return (
|
||||
resolvePolicyholderFromParty(guiltyParty, "third-party").nationalCode ??
|
||||
guiltyParty.person?.nationalCodeOfInsurer ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private resolveGuiltyPartyIdV2(
|
||||
@@ -6110,6 +6202,31 @@ export class ClaimRequestManagementService {
|
||||
},
|
||||
});
|
||||
|
||||
// GEN.03 CulpritLicence*: guilty-party DRIVER for THIRD_PARTY, FIRST-party DRIVER for CAR_BODY.
|
||||
{
|
||||
const culpritParty =
|
||||
blameCase.type === BlameRequestType.CAR_BODY
|
||||
? firstParty
|
||||
: (() => {
|
||||
const guiltyPartyId = this.resolveGuiltyPartyIdV2(
|
||||
blameCase.parties ?? [],
|
||||
expertDecision?.guiltyPartyId,
|
||||
);
|
||||
return (blameCase.parties ?? []).find(
|
||||
(party: any) =>
|
||||
String(party?.person?.userId ?? "") ===
|
||||
String(guiltyPartyId ?? ""),
|
||||
);
|
||||
})();
|
||||
const culpritDriver = resolveDriverFromParty(culpritParty as any);
|
||||
payload.CulpritLicenceNo = resolveFanavaranLicenceNo(
|
||||
culpritDriver,
|
||||
FANAVARAN_DUMMY_LICENCE_NO,
|
||||
);
|
||||
payload.CulpritLicenceIssuDate =
|
||||
resolveFanavaranLicenceIssuDate(culpritDriver);
|
||||
}
|
||||
|
||||
const carBodyAccidentTime = (
|
||||
firstParty as { statement?: { accidentTime?: string } } | undefined
|
||||
)?.statement?.accidentTime;
|
||||
|
||||
@@ -107,6 +107,29 @@ describe("fanavaran claim product", () => {
|
||||
expect(nationalCode).toBe("0012345678");
|
||||
});
|
||||
|
||||
it("prefers CAR_BODY_POLICYHOLDER participant over person.nationalCodeOfInsurer", () => {
|
||||
const nationalCode = pickCarBodyPolicyNationalCode({
|
||||
parties: [
|
||||
{
|
||||
role: PartyRole.FIRST,
|
||||
person: { nationalCodeOfInsurer: "0012345678" },
|
||||
participants: [
|
||||
{
|
||||
participantId: "CAR_BODY_POLICYHOLDER",
|
||||
nationalCode: "0499370899",
|
||||
birthday: "1368/05/20",
|
||||
},
|
||||
],
|
||||
participantRoles: {
|
||||
carBodyPolicyholder: "CAR_BODY_POLICYHOLDER",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(nationalCode).toBe("0499370899");
|
||||
});
|
||||
|
||||
it("flattens car-body inquiry fields onto the damage-case aliases", () => {
|
||||
const { mapped } = fanavaranPartyInquirySources("car-body", {
|
||||
insurance: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
parseFanavaranId,
|
||||
type FanavaranCarPolicyProduct,
|
||||
} from "src/lookups/fanavaran-last-car-policy";
|
||||
import { resolvePolicyholderFromParty } from "./fanavaran-party-roles";
|
||||
|
||||
export type FanavaranClaimProduct = FanavaranCarPolicyProduct;
|
||||
|
||||
@@ -101,6 +102,8 @@ export function fanavaranInsuranceLineIdForProduct(
|
||||
type BlamePartyForCarBodyPolicy = {
|
||||
role?: string;
|
||||
person?: { nationalCodeOfInsurer?: unknown };
|
||||
participants?: Array<Record<string, any>>;
|
||||
participantRoles?: Record<string, string | undefined>;
|
||||
insurance?: {
|
||||
carBodyInsurance?: {
|
||||
policyId?: unknown;
|
||||
@@ -168,6 +171,7 @@ export function pickCarBodyPolicyNationalCode(
|
||||
): string | null {
|
||||
const party = firstCarBodyParty(blame);
|
||||
return (
|
||||
resolvePolicyholderFromParty(party, "car-body").nationalCode ??
|
||||
nonEmptyText(party?.person?.nationalCodeOfInsurer) ??
|
||||
nonEmptyText(party?.insurance?.carBodyInsurance?.ownerNationalCode) ??
|
||||
nonEmptyText(party?.insurance?.carBodyInsurance?.insurerNationalCode)
|
||||
|
||||
@@ -34,6 +34,21 @@ export function asPartyInquiryRows(value: unknown): FanavaranPartyInquiryRow[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
function selectFanavaranIdByRoles(
|
||||
rows: unknown,
|
||||
preferredRoles: number[],
|
||||
): number | null {
|
||||
const parties = asPartyInquiryRows(rows);
|
||||
if (parties.length === 0) return null;
|
||||
|
||||
for (const roleId of preferredRoles) {
|
||||
const match = parties.find((row) => asPositiveId(row.RoleId) === roleId);
|
||||
if (match) return asPositiveId(match.Id);
|
||||
}
|
||||
|
||||
return asPositiveId(parties[0].Id);
|
||||
}
|
||||
|
||||
/**
|
||||
* DriverId for GEN.12 زیاندیده. Prefer insurer (161) or driver (166)
|
||||
* based on driverIsInsurer, then any returned party id.
|
||||
@@ -42,19 +57,27 @@ export function selectFanavaranDriverId(
|
||||
rows: unknown,
|
||||
driverIsInsurer: boolean | undefined,
|
||||
): number | null {
|
||||
const parties = asPartyInquiryRows(rows);
|
||||
if (parties.length === 0) return null;
|
||||
|
||||
const preferredRoles = driverIsInsurer
|
||||
? [FANAVARAN_INSURER_ROLE_ID, FANAVARAN_DRIVER_ROLE_ID, FANAVARAN_PLAQUE_OWNER_ROLE_ID]
|
||||
: [FANAVARAN_DRIVER_ROLE_ID, FANAVARAN_INSURER_ROLE_ID, FANAVARAN_PLAQUE_OWNER_ROLE_ID];
|
||||
? [
|
||||
FANAVARAN_INSURER_ROLE_ID,
|
||||
FANAVARAN_DRIVER_ROLE_ID,
|
||||
FANAVARAN_PLAQUE_OWNER_ROLE_ID,
|
||||
]
|
||||
: [
|
||||
FANAVARAN_DRIVER_ROLE_ID,
|
||||
FANAVARAN_INSURER_ROLE_ID,
|
||||
FANAVARAN_PLAQUE_OWNER_ROLE_ID,
|
||||
];
|
||||
return selectFanavaranIdByRoles(rows, preferredRoles);
|
||||
}
|
||||
|
||||
for (const roleId of preferredRoles) {
|
||||
const match = parties.find((row) => asPositiveId(row.RoleId) === roleId);
|
||||
if (match) return asPositiveId(match.Id);
|
||||
}
|
||||
|
||||
return asPositiveId(parties[0].Id);
|
||||
/** OwnerId: prefer plaque-owner role (163), then insurer / driver. */
|
||||
export function selectFanavaranOwnerId(rows: unknown): number | null {
|
||||
return selectFanavaranIdByRoles(rows, [
|
||||
FANAVARAN_PLAQUE_OWNER_ROLE_ID,
|
||||
FANAVARAN_INSURER_ROLE_ID,
|
||||
FANAVARAN_DRIVER_ROLE_ID,
|
||||
]);
|
||||
}
|
||||
|
||||
export function normalizeNationalCode(value: unknown): string | null {
|
||||
|
||||
125
src/claim-request-management/fanavaran-party-roles.spec.ts
Normal file
125
src/claim-request-management/fanavaran-party-roles.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { InquiryParticipantRole } from "src/common/dto/inquiry-participants.dto";
|
||||
import {
|
||||
FANAVARAN_DUMMY_LICENCE_ISSU_DATE,
|
||||
formatFanavaranLicenceIssuDate,
|
||||
resolveDriverFromParty,
|
||||
resolveDriverIsOwner,
|
||||
resolveFanavaranLicenceIssuDate,
|
||||
resolveFanavaranLicenceNo,
|
||||
resolveOwnerFromParty,
|
||||
resolvePolicyholderFromParty,
|
||||
} from "./fanavaran-party-roles";
|
||||
|
||||
describe("fanavaran-party-roles", () => {
|
||||
const partyWithDistinctRoles = {
|
||||
participants: [
|
||||
{
|
||||
participantId: "DRIVER",
|
||||
nationalCode: "0012345678",
|
||||
birthday: "1370/01/15",
|
||||
licenseNumber: "1234567890",
|
||||
licenseType: "1",
|
||||
driverLicenseDate: "1398/06/01",
|
||||
},
|
||||
{
|
||||
participantId: "VEHICLE_OWNER",
|
||||
nationalCode: "0499370899",
|
||||
birthday: "1368/05/20",
|
||||
},
|
||||
{
|
||||
participantId: "THIRD_PARTY_POLICYHOLDER",
|
||||
nationalCode: "0080086519",
|
||||
birthday: "1364/06/28",
|
||||
},
|
||||
],
|
||||
participantRoles: {
|
||||
driver: "DRIVER",
|
||||
vehicleOwner: "VEHICLE_OWNER",
|
||||
thirdPartyPolicyholder: "THIRD_PARTY_POLICYHOLDER",
|
||||
},
|
||||
person: {
|
||||
nationalCodeOfDriver: "0012345678",
|
||||
nationalCodeOfInsurer: "0080086519",
|
||||
driverLicense: "1234567890",
|
||||
driverIsInsurer: false,
|
||||
},
|
||||
};
|
||||
|
||||
it("resolves driver licence and driverLicenseDate from participants", () => {
|
||||
const driver = resolveDriverFromParty(partyWithDistinctRoles);
|
||||
expect(driver.nationalCode).toBe("0012345678");
|
||||
expect(driver.licenseNo).toBe("1234567890");
|
||||
expect(driver.licenseIssuDate).toBe("1398/06/01");
|
||||
expect(resolveFanavaranLicenceNo(driver, "9705463515")).toBe("1234567890");
|
||||
expect(resolveFanavaranLicenceIssuDate(driver)).toBe("1398/06/01");
|
||||
});
|
||||
|
||||
it("falls back to dummy licence issue date when driverLicenseDate is missing", () => {
|
||||
const driver = resolveDriverFromParty({
|
||||
participants: [
|
||||
{
|
||||
participantId: InquiryParticipantRole.DRIVER,
|
||||
nationalCode: "0012345678",
|
||||
birthday: "1370/01/15",
|
||||
licenseNumber: "999",
|
||||
},
|
||||
],
|
||||
participantRoles: { driver: InquiryParticipantRole.DRIVER },
|
||||
});
|
||||
expect(driver.licenseIssuDate).toBeNull();
|
||||
expect(resolveFanavaranLicenceIssuDate(driver)).toBe(
|
||||
FANAVARAN_DUMMY_LICENCE_ISSU_DATE,
|
||||
);
|
||||
});
|
||||
|
||||
it("marks DriverIsOwner false when driver and owner differ", () => {
|
||||
expect(resolveDriverIsOwner(partyWithDistinctRoles)).toBe(false);
|
||||
});
|
||||
|
||||
it("marks DriverIsOwner true when driver and owner share a national code", () => {
|
||||
expect(
|
||||
resolveDriverIsOwner({
|
||||
participants: [
|
||||
{
|
||||
participantId: "SAME",
|
||||
nationalCode: "0012345678",
|
||||
birthday: "1370/01/15",
|
||||
},
|
||||
],
|
||||
participantRoles: {
|
||||
driver: "SAME",
|
||||
vehicleOwner: "SAME",
|
||||
thirdPartyPolicyholder: "SAME",
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves owner and policyholder independently of the driver", () => {
|
||||
expect(resolveOwnerFromParty(partyWithDistinctRoles).nationalCode).toBe(
|
||||
"0499370899",
|
||||
);
|
||||
expect(
|
||||
resolvePolicyholderFromParty(partyWithDistinctRoles, "third-party")
|
||||
.nationalCode,
|
||||
).toBe("0080086519");
|
||||
});
|
||||
|
||||
it("formats compact Jalali driverLicenseDate", () => {
|
||||
expect(formatFanavaranLicenceIssuDate(13980601)).toBe("1398/06/01");
|
||||
});
|
||||
|
||||
it("falls back to legacy person.driverLicense when participants are absent", () => {
|
||||
const driver = resolveDriverFromParty({
|
||||
person: {
|
||||
nationalCodeOfDriver: "0011111111",
|
||||
driverBirthday: "13700101",
|
||||
driverLicense: "5555555555",
|
||||
driverLicenseDate: "1390/01/01",
|
||||
},
|
||||
});
|
||||
expect(driver.nationalCode).toBe("0011111111");
|
||||
expect(driver.licenseNo).toBe("5555555555");
|
||||
expect(driver.licenseIssuDate).toBe("1390/01/01");
|
||||
});
|
||||
});
|
||||
217
src/claim-request-management/fanavaran-party-roles.ts
Normal file
217
src/claim-request-management/fanavaran-party-roles.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { InquiryParticipantRole } from "src/common/dto/inquiry-participants.dto";
|
||||
import { participantForStoredPartyRole } from "src/request-management/inquiry-participant-resolver";
|
||||
import {
|
||||
normalizeNationalCode,
|
||||
parseJalaliDateParts,
|
||||
} from "./fanavaran-driver-inquiry";
|
||||
import type { FanavaranClaimProduct } from "./fanavaran-claim-product";
|
||||
|
||||
/** Fanavaran dummy licence issue date when the user did not supply one. */
|
||||
export const FANAVARAN_DUMMY_LICENCE_ISSU_DATE = "1394/10/13";
|
||||
|
||||
export type FanavaranRoleIdentity = {
|
||||
nationalCode: string | null;
|
||||
birthday: string | number | null;
|
||||
/** Digits-only licence number when known. */
|
||||
licenseNo: string | null;
|
||||
licenseType: string | null;
|
||||
/**
|
||||
* Jalali `YYYY/MM/DD` from `driverLicenseDate` when present.
|
||||
* Not a persisted model field — read opportunistically from party/person/participant.
|
||||
*/
|
||||
licenseIssuDate: string | null;
|
||||
};
|
||||
|
||||
type PartyLike = {
|
||||
participants?: Array<Record<string, any>>;
|
||||
participantRoles?: Record<string, string | undefined>;
|
||||
person?: Record<string, any> | null;
|
||||
};
|
||||
|
||||
function nonEmptyText(value: unknown): string | null {
|
||||
if (value == null) return null;
|
||||
const text = String(value).trim();
|
||||
if (!text || text.toLowerCase() === "null" || text.toLowerCase() === "undefined") {
|
||||
return null;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/** Digits-only licence number, or null when empty / non-digit. */
|
||||
export function extractLicenceDigits(value: unknown): string | null {
|
||||
const text = nonEmptyText(value);
|
||||
if (!text) return null;
|
||||
const digits = text.replace(/[^\d]/g, "");
|
||||
return digits || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a stored `driverLicenseDate` (or similar) to Fanavaran Jalali `YYYY/MM/DD`.
|
||||
* Returns null when missing / unparseable so callers can fall back to the dummy.
|
||||
*/
|
||||
export function formatFanavaranLicenceIssuDate(value: unknown): string | null {
|
||||
const parsed = parseJalaliDateParts(value);
|
||||
if (!parsed) return null;
|
||||
const month = String(parsed.month).padStart(2, "0");
|
||||
const day = String(parsed.day).padStart(2, "0");
|
||||
return `${parsed.year}/${month}/${day}`;
|
||||
}
|
||||
|
||||
function readDriverLicenseDate(
|
||||
...sources: Array<Record<string, any> | null | undefined>
|
||||
): string | null {
|
||||
for (const source of sources) {
|
||||
if (!source) continue;
|
||||
const formatted = formatFanavaranLicenceIssuDate(
|
||||
source.driverLicenseDate ?? source.licenseDate ?? source.licenceIssuDate,
|
||||
);
|
||||
if (formatted) return formatted;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function fromParticipant(
|
||||
participant: Record<string, any> | undefined,
|
||||
): FanavaranRoleIdentity {
|
||||
if (!participant) {
|
||||
return {
|
||||
nationalCode: null,
|
||||
birthday: null,
|
||||
licenseNo: null,
|
||||
licenseType: null,
|
||||
licenseIssuDate: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
nationalCode: normalizeNationalCode(participant.nationalCode),
|
||||
birthday: nonEmptyText(participant.birthday) ?? participant.birthday ?? null,
|
||||
licenseNo: extractLicenceDigits(participant.licenseNumber),
|
||||
licenseType: nonEmptyText(participant.licenseType),
|
||||
licenseIssuDate: readDriverLicenseDate(participant),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Damaged/guilty party DRIVER: participants first, then legacy person.* fields.
|
||||
* Licence number prefers real user values; callers fall back to the Fanavaran dummy.
|
||||
*/
|
||||
export function resolveDriverFromParty(party: PartyLike | null | undefined): FanavaranRoleIdentity {
|
||||
const fromRole = fromParticipant(
|
||||
participantForStoredPartyRole(
|
||||
party ?? {},
|
||||
InquiryParticipantRole.DRIVER,
|
||||
) as Record<string, any> | undefined,
|
||||
);
|
||||
const person = party?.person ?? undefined;
|
||||
|
||||
return {
|
||||
nationalCode:
|
||||
fromRole.nationalCode ??
|
||||
normalizeNationalCode(person?.nationalCodeOfDriver) ??
|
||||
normalizeNationalCode(person?.nationalCode),
|
||||
birthday:
|
||||
(fromRole.birthday != null && String(fromRole.birthday).trim() !== ""
|
||||
? fromRole.birthday
|
||||
: null) ??
|
||||
nonEmptyText(person?.driverBirthday) ??
|
||||
nonEmptyText(person?.birthday) ??
|
||||
person?.driverBirthday ??
|
||||
person?.birthday ??
|
||||
null,
|
||||
licenseNo:
|
||||
fromRole.licenseNo ??
|
||||
extractLicenceDigits(person?.driverLicense) ??
|
||||
extractLicenceDigits(person?.insurerLicense),
|
||||
licenseType: fromRole.licenseType ?? nonEmptyText(person?.licenseType),
|
||||
licenseIssuDate:
|
||||
fromRole.licenseIssuDate ?? readDriverLicenseDate(person),
|
||||
};
|
||||
}
|
||||
|
||||
/** Vehicle owner for OwnerId / DriverIsOwner / Sheba national-code routing. */
|
||||
export function resolveOwnerFromParty(party: PartyLike | null | undefined): FanavaranRoleIdentity {
|
||||
const fromRole = fromParticipant(
|
||||
participantForStoredPartyRole(
|
||||
party ?? {},
|
||||
InquiryParticipantRole.VEHICLE_OWNER,
|
||||
) as Record<string, any> | undefined,
|
||||
);
|
||||
if (fromRole.nationalCode || fromRole.birthday) {
|
||||
return fromRole;
|
||||
}
|
||||
// Legacy parties had no dedicated owner fields.
|
||||
return {
|
||||
nationalCode: null,
|
||||
birthday: null,
|
||||
licenseNo: null,
|
||||
licenseType: null,
|
||||
licenseIssuDate: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Policyholder national code for GEN.03 PolicyId inquiry:
|
||||
* THIRD_PARTY → third-party policyholder; CAR_BODY → car-body policyholder.
|
||||
*/
|
||||
export function resolvePolicyholderFromParty(
|
||||
party: PartyLike | null | undefined,
|
||||
product: FanavaranClaimProduct,
|
||||
): FanavaranRoleIdentity {
|
||||
const role =
|
||||
product === "car-body"
|
||||
? InquiryParticipantRole.CAR_BODY_POLICYHOLDER
|
||||
: InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER;
|
||||
const fromRole = fromParticipant(
|
||||
participantForStoredPartyRole(party ?? {}, role) as
|
||||
| Record<string, any>
|
||||
| undefined,
|
||||
);
|
||||
const person = party?.person ?? undefined;
|
||||
|
||||
return {
|
||||
nationalCode:
|
||||
fromRole.nationalCode ??
|
||||
normalizeNationalCode(person?.nationalCodeOfInsurer) ??
|
||||
normalizeNationalCode(person?.nationalCode),
|
||||
birthday:
|
||||
(fromRole.birthday != null && String(fromRole.birthday).trim() !== ""
|
||||
? fromRole.birthday
|
||||
: null) ??
|
||||
nonEmptyText(person?.insurerBirthday) ??
|
||||
person?.insurerBirthday ??
|
||||
null,
|
||||
licenseNo:
|
||||
fromRole.licenseNo ?? extractLicenceDigits(person?.insurerLicense),
|
||||
licenseType: fromRole.licenseType ?? null,
|
||||
licenseIssuDate: fromRole.licenseIssuDate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* GEN.12 `DriverIsOwner`: driver national code equals vehicle-owner national code.
|
||||
* Falls back to legacy `person.driverIsInsurer` when owner role is absent.
|
||||
*/
|
||||
export function resolveDriverIsOwner(party: PartyLike | null | undefined): boolean {
|
||||
const driver = resolveDriverFromParty(party);
|
||||
const owner = resolveOwnerFromParty(party);
|
||||
if (driver.nationalCode && owner.nationalCode) {
|
||||
return driver.nationalCode === owner.nationalCode;
|
||||
}
|
||||
return party?.person?.driverIsInsurer === true;
|
||||
}
|
||||
|
||||
/** Licence number for Fanavaran; never empty — uses `dummy` when user value is missing. */
|
||||
export function resolveFanavaranLicenceNo(
|
||||
identity: Pick<FanavaranRoleIdentity, "licenseNo">,
|
||||
dummy: string,
|
||||
): string {
|
||||
return identity.licenseNo || dummy;
|
||||
}
|
||||
|
||||
/** Licence issue date for Fanavaran; uses dummy when `driverLicenseDate` is absent. */
|
||||
export function resolveFanavaranLicenceIssuDate(
|
||||
identity: Pick<FanavaranRoleIdentity, "licenseIssuDate">,
|
||||
dummy: string = FANAVARAN_DUMMY_LICENCE_ISSU_DATE,
|
||||
): string {
|
||||
return identity.licenseIssuDate || dummy;
|
||||
}
|
||||
@@ -172,8 +172,41 @@ export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [
|
||||
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/cii-validation-status`,
|
||||
cacheFile: "cii-validation-status.json",
|
||||
},
|
||||
{
|
||||
name: "vehicle-groups",
|
||||
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/vehicle-groups`,
|
||||
cacheFile: "vehicle-groups.json",
|
||||
},
|
||||
];
|
||||
|
||||
/** Safe Fanavaran lookup slug: lowercase segments separated by hyphens. */
|
||||
const FANAVARAN_LOOKUP_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||
|
||||
/**
|
||||
* Resolve a remote lookup definition.
|
||||
* Known names use the catalogue; unknown slugs fall back to
|
||||
* `car/base-info/{lookupName}` so `/lookups/fanavaran/{name}` works without
|
||||
* a dedicated route or config entry (e.g. vehicle-groups).
|
||||
*/
|
||||
export function resolveFanavaranRemoteLookup(
|
||||
name: string,
|
||||
): FanavaranRemoteLookupDefinition | null {
|
||||
const configured = FANAVARAN_REMOTE_LOOKUPS.find((item) => item.name === name);
|
||||
if (configured) {
|
||||
return configured;
|
||||
}
|
||||
|
||||
if (!FANAVARAN_LOOKUP_NAME_PATTERN.test(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/${name}`,
|
||||
cacheFile: `${name}.json`,
|
||||
};
|
||||
}
|
||||
|
||||
export const TEJARAT_STATIC_ACCIDENT_FILES = {
|
||||
accidentReason: "ACCIDENT_REASON.json",
|
||||
accidentWay: "ACCIDENT_WAY.json",
|
||||
|
||||
@@ -582,7 +582,8 @@ export class LookupsController {
|
||||
@ApiOperation({
|
||||
summary: "List configured Fanavaran remote lookups",
|
||||
description:
|
||||
"Returns the lookup names available through /lookups/fanavaran/{lookupName}.",
|
||||
"Returns catalogue entries for /lookups/fanavaran/{lookupName}. " +
|
||||
"Unlisted car/base-info slugs still work via the same route (fallback).",
|
||||
})
|
||||
async listFanavaranRemoteLookups() {
|
||||
return this.lookupsService.listFanavaranRemoteLookups().map((lookup) => ({
|
||||
@@ -596,12 +597,14 @@ export class LookupsController {
|
||||
@ApiOperation({
|
||||
summary: "Fetch a Fanavaran remote lookup by name",
|
||||
description:
|
||||
"Generic cached Fanavaran lookup fetcher for configured lookup names, including GEN.08 expertise lookups.",
|
||||
"Generic cached Fanavaran lookup fetcher. Configured names use their catalogue URL; " +
|
||||
"unlisted slugs (e.g. vehicle-groups) fall back to car/base-info/{lookupName}.",
|
||||
})
|
||||
@ApiParam({
|
||||
name: "lookupName",
|
||||
description:
|
||||
"Configured Fanavaran lookup name, for example inspection-place, drop-amount-status, car-components, accident-level, expert-status",
|
||||
"Fanavaran lookup slug, for example vehicle-groups, inspection-place, car-components. " +
|
||||
"Unconfigured names are fetched from car/base-info/{lookupName}.",
|
||||
})
|
||||
@ApiOkResponse({
|
||||
description: "Returns cached or live Fanavaran lookup data",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "src/core/config/fanavaran-client.config";
|
||||
import {
|
||||
FANAVARAN_REMOTE_LOOKUPS,
|
||||
resolveFanavaranRemoteLookup,
|
||||
type FanavaranRemoteLookupDefinition,
|
||||
TEJARAT_STATIC_ACCIDENT_FILES,
|
||||
} from "src/fanavaran/fanavaran-lookup.config";
|
||||
@@ -66,7 +67,7 @@ export class LookupsService {
|
||||
}
|
||||
|
||||
private findRemoteLookup(name: string) {
|
||||
const lookup = FANAVARAN_REMOTE_LOOKUPS.find((item) => item.name === name);
|
||||
const lookup = resolveFanavaranRemoteLookup(name);
|
||||
if (!lookup) {
|
||||
throw new NotFoundException(`Unknown Fanavaran remote lookup: ${name}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user