fanavaran driver and owner id (GEN..44 defauts) - culprit type id dynamic

This commit is contained in:
2026-09-28 16:13:32 +03:30
parent a74e20b25f
commit 165afe56ae
7 changed files with 303 additions and 36 deletions

View File

@@ -272,6 +272,7 @@ import {
resolveFanavaranLicenceNo,
resolveOwnerFromParty,
resolvePolicyholderFromParty,
resolveThirdPartyCulpritTypeId,
} from "./fanavaran-party-roles";
import {
buildFanavaranOtherPeoplePayload,
@@ -4294,10 +4295,10 @@ export class ClaimRequestManagementService {
}
this.logger.warn(
`[resolveDriverFanavaranId] NO MATCH for nationalCode=${nationalCode} driverIsInsurer=${driverIsInsurer}. ` +
`[resolveDriverFanavaranId] NO acceptable RoleId 161/166 for nationalCode=${nationalCode} driverIsInsurer=${driverIsInsurer}. ` +
`Available entries: ${asPartyInquiryRows(response)
.map((e) => `{Id=${e.Id}, RoleId=${e.RoleId}, Name=${e.Name} ${e.LastName}}`)
.join("; ")}`,
.join("; ")} — submit will register GEN.44 other-people`,
);
return null;
} catch (error) {
@@ -4308,7 +4309,7 @@ export class ClaimRequestManagementService {
}
}
/** Best-effort OwnerId from parties inquiry (no GEN.44 create). */
/** Best-effort OwnerId from parties inquiry (RoleId 161/166 only; GEN.44 on submit). */
private async resolveOwnerFanavaranId(
clientKey: FanavaranClientKey,
nationalCode: string | null | undefined,
@@ -4327,7 +4328,20 @@ export class ClaimRequestManagementService {
birthDay: parsed.day,
},
);
return selectFanavaranOwnerId(response);
const ownerId = selectFanavaranOwnerId(response);
if (ownerId != null) {
this.logger.log(
`[resolveOwnerFanavaranId] MATCH: Id=${ownerId} for nationalCode=${nationalCode}`,
);
return ownerId;
}
this.logger.warn(
`[resolveOwnerFanavaranId] NO acceptable RoleId 161/166 for nationalCode=${nationalCode}. ` +
`Available: ${asPartyInquiryRows(response)
.map((e) => `{Id=${e.Id}, RoleId=${e.RoleId}}`)
.join("; ")} — submit will register GEN.44 other-people`,
);
return null;
} catch (error) {
this.logger.warn(
`[resolveOwnerFanavaranId] inquiry failed for nationalCode=${nationalCode}: ${
@@ -4877,6 +4891,37 @@ export class ClaimRequestManagementService {
);
}
if (
ownerFanavaranId == null &&
input.registerMissingPerson &&
ownerIdentity.nationalCode &&
ownerIdentity.birthday
) {
const registeredOwner = await this.registerFanavaranOtherPerson({
clientKey: input.clientKey,
person: {
nationalCode: ownerIdentity.nationalCode,
nationalCodeOfDriver: ownerIdentity.nationalCode,
birthday: ownerIdentity.birthday,
driverBirthday: ownerIdentity.birthday,
driverIsInsurer: false,
fullName: (damagedParty as any)?.person?.fullName,
phoneNumber: (damagedParty as any)?.person?.phoneNumber,
userId: (damagedParty as any)?.person?.userId,
},
claimCaseId: input.claimCase?._id
? String(input.claimCase._id)
: undefined,
});
if (registeredOwner != null) {
ownerFanavaranId = registeredOwner;
if (otherPersonId == null) otherPersonId = registeredOwner;
this.logger.log(
`[buildFanavaranDamageCasePayload] OwnerId=${ownerFanavaranId} from GEN.44 other-people`,
);
}
}
if (driverFanavaranId && input.blameCase?._id && damagedParty) {
const partyIndex = (input.blameCase.parties ?? []).findIndex(
(p: any) => p === damagedParty ||
@@ -5726,7 +5771,8 @@ export class ClaimRequestManagementService {
}
/**
* GEN.44: create the damaged party in Fanavaran when parties inquiry has no row.
* GEN.44: create the damaged party in Fanavaran when parties inquiry has no
* RoleId 161/166 row (missing entirely, or only e.g. 162 بیمه‌شده).
* Failures stay best-effort — GEN.12 submit continues without DriverId.
*/
private async registerFanavaranOtherPerson(input: {
@@ -5736,7 +5782,8 @@ export class ClaimRequestManagementService {
}): Promise<number | null> {
const nationalCode = pickPersonNationalCode(input.person);
const birthday = pickPersonBirthday(input.person);
if (!nationalCode || !parseJalaliDateParts(birthday)) {
const parsedBirthday = parseJalaliDateParts(birthday);
if (!nationalCode || !parsedBirthday) {
this.logger.warn(
`[registerFanavaranOtherPerson] SKIP: missing nationalCode or birthday`,
);
@@ -5765,12 +5812,77 @@ export class ClaimRequestManagementService {
}
}
/** Seed GEN.44 from parties inquiry when the person exists under a non-driver role. */
let inquirySeed: {
name?: string;
lastName?: string;
fatherName?: string;
genderId?: number;
identityNo?: string;
} = {};
try {
const inquiryRows =
await this.fanavaranLookupService.inquiryByUniqueIdentifier(
input.clientKey,
{
nationalCode,
birthYear: parsedBirthday.year,
birthMonth: parsedBirthday.month,
birthDay: parsedBirthday.day,
},
);
const parties = asPartyInquiryRows(inquiryRows);
const seedRow =
parties.find(
(row) =>
(typeof row.Name === "string" && row.Name.trim()) ||
(typeof row.LastName === "string" && row.LastName.trim()),
) ?? parties[0];
if (seedRow) {
const roleId = Number(seedRow.RoleId);
this.logger.log(
`[registerFanavaranOtherPerson] inquiry seed Id=${seedRow.Id} RoleId=${roleId} — creating GEN.44 سایر (adds RoleId 166 entry; existing roles stay)`,
);
inquirySeed = {
name:
typeof seedRow.Name === "string" ? seedRow.Name : undefined,
lastName:
typeof seedRow.LastName === "string"
? seedRow.LastName
: undefined,
fatherName:
typeof seedRow.FatherName === "string"
? seedRow.FatherName
: undefined,
genderId: (() => {
const g = Number(seedRow.GenderId);
return Number.isFinite(g) && g > 0 ? g : undefined;
})(),
identityNo:
typeof seedRow.IdentityNo === "string"
? seedRow.IdentityNo
: undefined,
};
}
} catch (error) {
this.logger.warn(
`[registerFanavaranOtherPerson] pre-create inquiry failed (continuing with local data): ${
error instanceof Error ? error.message : error
}`,
);
}
const lookups = await this.loadFanavaranOtherPeopleLookups(input.clientKey);
const payload = buildFanavaranOtherPeoplePayload(
{
nationalCode,
birthday,
fullName: input.person?.fullName ?? user?.fullName,
name: inquirySeed.name,
lastName: inquirySeed.lastName,
fatherName: inquirySeed.fatherName,
identityNo: inquirySeed.identityNo,
genderId: inquirySeed.genderId,
mobile: input.person?.phoneNumber ?? user?.mobile,
address: user?.address,
cityName: user?.city,
@@ -6203,7 +6315,9 @@ export class ClaimRequestManagementService {
},
});
// GEN.03 CulpritLicence*: guilty-party DRIVER for THIRD_PARTY, FIRST-party DRIVER for CAR_BODY.
// GEN.03 CulpritLicence* + CulpritTypeId: guilty-party DRIVER for THIRD_PARTY,
// FIRST-party DRIVER for CAR_BODY. CulpritTypeId uses accident-culprit-type
// (337 بیمه‌گذار / 338 راننده غير از بيمه گذار) — hull remaps separately.
{
const culpritParty =
blameCase.type === BlameRequestType.CAR_BODY
@@ -6226,6 +6340,10 @@ export class ClaimRequestManagementService {
);
payload.CulpritLicenceIssuDate =
resolveFanavaranLicenceIssuDate(culpritDriver);
if (blameCase.type !== BlameRequestType.CAR_BODY) {
payload.CulpritTypeId =
resolveThirdPartyCulpritTypeId(culpritParty as any);
}
}
const carBodyAccidentTime = (
@@ -6445,7 +6563,7 @@ export class ClaimRequestManagementService {
}
if (payload.DriverId == null) {
warnings.push(
"Damaged party is not in Fanavaran yet; submit will register them via other-people (GEN.44).",
"Damaged party has no Fanavaran RoleId 161/166 yet; submit will register them via other-people (GEN.44).",
);
}

View File

@@ -5,6 +5,7 @@ import {
pickPersonBirthday,
pickPersonNationalCode,
selectFanavaranDriverId,
selectFanavaranOwnerId,
} from "./fanavaran-driver-inquiry";
describe("selectFanavaranDriverId", () => {
@@ -21,10 +22,32 @@ describe("selectFanavaranDriverId", () => {
expect(selectFanavaranDriverId(rows, false)).toBe(20);
});
it("uses the other role when the preferred role is missing", () => {
it("falls back to 166 when preferred 161 is missing", () => {
expect(selectFanavaranDriverId([{ Id: 20, RoleId: 166 }], true)).toBe(20);
});
it("returns null for RoleId 162 (بیمه‌شده) — GEN.44 required", () => {
expect(
selectFanavaranDriverId(
[
{
Id: 2390312,
RoleId: 162,
Name: "محمدعلي",
LastName: "موسوي",
},
],
false,
),
).toBeNull();
});
it("does not fall back to RoleId 163 for DriverId", () => {
expect(
selectFanavaranDriverId([{ Id: 99, RoleId: 163 }], false),
).toBeNull();
});
it("picks 4773521 from a real Fanavaran unique-identifier response", () => {
expect(
selectFanavaranDriverId(
@@ -39,6 +62,19 @@ describe("selectFanavaranDriverId", () => {
});
});
describe("selectFanavaranOwnerId", () => {
it("accepts only RoleId 161 or 166", () => {
expect(
selectFanavaranOwnerId([
{ Id: 1, RoleId: 162 },
{ Id: 2, RoleId: 163 },
]),
).toBeNull();
expect(selectFanavaranOwnerId([{ Id: 10, RoleId: 161 }])).toBe(10);
expect(selectFanavaranOwnerId([{ Id: 20, RoleId: 166 }])).toBe(20);
});
});
describe("pickPersonNationalCode / birthday", () => {
it("uses insurer national code when driverIsInsurer", () => {
expect(

View File

@@ -10,6 +10,9 @@ export type FanavaranPartyInquiryRow = {
NationalCode?: unknown;
Name?: unknown;
LastName?: unknown;
FatherName?: unknown;
GenderId?: unknown;
IdentityNo?: unknown;
};
function asPositiveId(value: unknown): number | null {
@@ -34,9 +37,25 @@ export function asPartyInquiryRows(value: unknown): FanavaranPartyInquiryRow[] {
return [];
}
/** GEN.12 DriverId is only accepted for بیمه‌گذار (161) or سایر (166). */
export const FANAVARAN_DRIVER_ACCEPTED_ROLE_IDS = [
FANAVARAN_INSURER_ROLE_ID,
FANAVARAN_DRIVER_ROLE_ID,
] as const;
export function isAcceptableFanavaranDriverRole(
roleId: unknown,
): boolean {
const id = asPositiveId(roleId);
return (
id === FANAVARAN_INSURER_ROLE_ID || id === FANAVARAN_DRIVER_ROLE_ID
);
}
function selectFanavaranIdByRoles(
rows: unknown,
preferredRoles: number[],
options?: { allowFallbackToFirst?: boolean },
): number | null {
const parties = asPartyInquiryRows(rows);
if (parties.length === 0) return null;
@@ -46,38 +65,38 @@ function selectFanavaranIdByRoles(
if (match) return asPositiveId(match.Id);
}
if (options?.allowFallbackToFirst === false) return null;
return asPositiveId(parties[0].Id);
}
/**
* DriverId for GEN.12 زیان‌دیده. Prefer insurer (161) or driver (166)
* based on driverIsInsurer, then any returned party id.
* DriverId for GEN.12 زیان‌دیده.
* Only RoleId 161 (بیمه‌گذار) or 166 (سایر) are accepted by Fanavaran.
* Other roles (e.g. 162 بیمه‌شده) must go through GEN.44 other-people first
* (GEN.44 adds a RoleId-166 entry; it does not replace existing role rows).
*/
export function selectFanavaranDriverId(
rows: unknown,
driverIsInsurer: boolean | undefined,
): number | 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,
];
return selectFanavaranIdByRoles(rows, preferredRoles);
? [FANAVARAN_INSURER_ROLE_ID, FANAVARAN_DRIVER_ROLE_ID]
: [FANAVARAN_DRIVER_ROLE_ID, FANAVARAN_INSURER_ROLE_ID];
return selectFanavaranIdByRoles(rows, preferredRoles, {
allowFallbackToFirst: false,
});
}
/** OwnerId: prefer plaque-owner role (163), then insurer / driver. */
/**
* OwnerId for GEN.12: same accepted roles as DriverId (161 / 166 only).
* Prefer بیمه‌گذار then سایر. RoleId 162/163/… → null → GEN.44 on submit.
*/
export function selectFanavaranOwnerId(rows: unknown): number | null {
return selectFanavaranIdByRoles(rows, [
FANAVARAN_PLAQUE_OWNER_ROLE_ID,
FANAVARAN_INSURER_ROLE_ID,
FANAVARAN_DRIVER_ROLE_ID,
]);
return selectFanavaranIdByRoles(
rows,
[FANAVARAN_INSURER_ROLE_ID, FANAVARAN_DRIVER_ROLE_ID],
{ allowFallbackToFirst: false },
);
}
export function normalizeNationalCode(value: unknown): string | null {

View File

@@ -49,6 +49,29 @@ describe("fanavaran other people (GEN.44)", () => {
});
});
it("defaults CityId=9131 and PersonKindId=46 when lookups miss", () => {
const payload = buildFanavaranOtherPeoplePayload({
nationalCode: "2002166323",
birthday: { year: 1353, month: 2, day: 15 },
name: "محمدعلي",
lastName: "موسوي",
fatherName: "حسن",
identityNo: "1205",
genderId: 26,
});
expect(payload).toMatchObject({
NationalCode: "2002166323",
CityId: 9131,
PersonKindId: 46,
GenderId: 26,
Name: "محمدعلي",
LastName: "موسوي",
FatherName: "حسن",
IdentityNo: "1205",
});
});
it("returns null when national code or birthday is missing", () => {
expect(
buildFanavaranOtherPeoplePayload({

View File

@@ -25,6 +25,9 @@ export type FanavaranOtherPeopleSource = {
nationalCode?: unknown;
birthday?: unknown;
fullName?: unknown;
/** Prefer these over splitting fullName when inquiry already returned them. */
name?: unknown;
lastName?: unknown;
fatherName?: unknown;
mobile?: unknown;
tel?: unknown;
@@ -33,7 +36,13 @@ export type FanavaranOtherPeopleSource = {
jobAddress?: unknown;
postalCode?: unknown;
cityName?: unknown;
/** Direct Fanavaran CityId when known; else lookup / default 9131. */
cityId?: unknown;
gender?: unknown;
/** Direct Fanavaran GenderId from parties inquiry when available. */
genderId?: unknown;
/** Direct Fanavaran PersonKindId; else lookup / default 46 (حقیقی). */
personKindId?: unknown;
isIranian?: boolean;
naturalizedCode?: unknown;
identityNo?: unknown;
@@ -82,6 +91,10 @@ const ANS_YES_TEXTS = ["بله", "بلی", "yes", "1"];
const NATURAL_PERSON_TEXTS = ["حقیقی", "شخص حقیقی", "طبيعي", "natural"];
const IRAN_COUNTRY_TEXTS = ["ایران", "ايران", "iran"];
/** Proven Parsian GEN.44 defaults when lookup / city name is missing. */
export const FANAVARAN_OTHER_PEOPLE_DEFAULT_CITY_ID = 9131;
export const FANAVARAN_OTHER_PEOPLE_DEFAULT_PERSON_KIND_ID = 46;
function asLookupRows(value: unknown): FanavaranLookupRow[] {
if (!Array.isArray(value)) return [];
return value.filter(
@@ -213,8 +226,8 @@ export function buildFanavaranOtherPeoplePayload(
return {
NationalCode: nationalCode,
Name: names.name,
LastName: names.lastName,
Name: emptyToNull(source.name) ?? names.name,
LastName: emptyToNull(source.lastName) ?? names.lastName,
FatherName: emptyToNull(source.fatherName),
BirthYear: birthday.year,
BirthMonth: birthday.month,
@@ -235,8 +248,13 @@ export function buildFanavaranOtherPeoplePayload(
CompanyCode: emptyToNull(source.companyCode),
IdentityNo: emptyToNull(source.identityNo),
RegisterNo: emptyToNull(source.registerNo),
CityId: pickLookupIdByCaption(lookups.cities, [source.cityName]),
GenderId: pickLookupIdByCaption(lookups.gender, genderTexts),
CityId:
asPositiveId(source.cityId) ??
pickLookupIdByCaption(lookups.cities, [source.cityName]) ??
FANAVARAN_OTHER_PEOPLE_DEFAULT_CITY_ID,
GenderId:
asPositiveId(source.genderId) ??
pickLookupIdByCaption(lookups.gender, genderTexts),
MaritalStatus: pickLookupIdByCaption(lookups.maritalStatus, []),
IsIranian: isIranian
? (pickLookupIdByCaption(lookups.ans, ANS_YES_TEXTS) ?? 1)
@@ -244,9 +262,9 @@ export function buildFanavaranOtherPeoplePayload(
NationalityId: isIranian
? null
: pickLookupIdByCaption(lookups.countries, IRAN_COUNTRY_TEXTS),
PersonKindId: pickLookupIdByCaption(
lookups.personKind,
NATURAL_PERSON_TEXTS,
),
PersonKindId:
asPositiveId(source.personKindId) ??
pickLookupIdByCaption(lookups.personKind, NATURAL_PERSON_TEXTS) ??
FANAVARAN_OTHER_PEOPLE_DEFAULT_PERSON_KIND_ID,
};
}

View File

@@ -1,5 +1,7 @@
import { InquiryParticipantRole } from "src/common/dto/inquiry-participants.dto";
import {
FANAVARAN_CULPRIT_TYPE_DRIVER_NOT_INSURER,
FANAVARAN_CULPRIT_TYPE_INSURER,
FANAVARAN_DUMMY_LICENCE_ISSU_DATE,
formatFanavaranLicenceIssuDate,
resolveDriverFromParty,
@@ -8,6 +10,7 @@ import {
resolveFanavaranLicenceNo,
resolveOwnerFromParty,
resolvePolicyholderFromParty,
resolveThirdPartyCulpritTypeId,
} from "./fanavaran-party-roles";
describe("fanavaran-party-roles", () => {
@@ -105,6 +108,31 @@ describe("fanavaran-party-roles", () => {
).toBe("0080086519");
});
it("maps CulpritTypeId 338 when guilty driver ≠ insurer", () => {
expect(resolveThirdPartyCulpritTypeId(partyWithDistinctRoles)).toBe(
FANAVARAN_CULPRIT_TYPE_DRIVER_NOT_INSURER,
);
});
it("maps CulpritTypeId 337 when guilty driver is the insurer", () => {
expect(
resolveThirdPartyCulpritTypeId({
participants: [
{
participantId: "SAME",
nationalCode: "0080086519",
birthday: "1364/06/28",
},
],
participantRoles: {
driver: "SAME",
thirdPartyPolicyholder: "SAME",
},
person: { driverIsInsurer: true },
}),
).toBe(FANAVARAN_CULPRIT_TYPE_INSURER);
});
it("formats compact Jalali driverLicenseDate", () => {
expect(formatFanavaranLicenceIssuDate(13980601)).toBe("1398/06/01");
});

View File

@@ -201,6 +201,31 @@ export function resolveDriverIsOwner(party: PartyLike | null | undefined): boole
return party?.person?.driverIsInsurer === true;
}
/** Lookup: `accident-culprit-type` — بیمه‌گذار */
export const FANAVARAN_CULPRIT_TYPE_INSURER = 337;
/** Lookup: `accident-culprit-type` — راننده غير از بيمه گذار */
export const FANAVARAN_CULPRIT_TYPE_DRIVER_NOT_INSURER = 338;
/**
* GEN.03 ثالث `CulpritTypeId` from whether the guilty party's driver is the insurer.
* Never uses 728 (نا معلوم).
*/
export function resolveThirdPartyCulpritTypeId(
party: PartyLike | null | undefined,
): number {
const driver = resolveDriverFromParty(party);
const insurer = resolvePolicyholderFromParty(party, "third-party");
if (driver.nationalCode && insurer.nationalCode) {
return driver.nationalCode === insurer.nationalCode
? FANAVARAN_CULPRIT_TYPE_INSURER
: FANAVARAN_CULPRIT_TYPE_DRIVER_NOT_INSURER;
}
if (party?.person?.driverIsInsurer === false) {
return FANAVARAN_CULPRIT_TYPE_DRIVER_NOT_INSURER;
}
return FANAVARAN_CULPRIT_TYPE_INSURER;
}
/** Licence number for Fanavaran; never empty — uses `dummy` when user value is missing. */
export function resolveFanavaranLicenceNo(
identity: Pick<FanavaranRoleIdentity, "licenseNo">,