From 165afe56aeb9d1d65f9f1477d224dd21740861bd Mon Sep 17 00:00:00 2001 From: "s.hajizadeh" Date: Mon, 28 Sep 2026 16:13:32 +0330 Subject: [PATCH] fanavaran driver and owner id (GEN..44 defauts) - culprit type id dynamic --- .../claim-request-management.service.ts | 134 ++++++++++++++++-- .../fanavaran-driver-inquiry.spec.ts | 38 ++++- .../fanavaran-driver-inquiry.ts | 57 +++++--- .../fanavaran-other-people.spec.ts | 23 +++ .../fanavaran-other-people.ts | 34 +++-- .../fanavaran-party-roles.spec.ts | 28 ++++ .../fanavaran-party-roles.ts | 25 ++++ 7 files changed, 303 insertions(+), 36 deletions(-) diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 6f8dfb2..d8291a8 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -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 { 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).", ); } diff --git a/src/claim-request-management/fanavaran-driver-inquiry.spec.ts b/src/claim-request-management/fanavaran-driver-inquiry.spec.ts index bad67b7..3ad2d52 100644 --- a/src/claim-request-management/fanavaran-driver-inquiry.spec.ts +++ b/src/claim-request-management/fanavaran-driver-inquiry.spec.ts @@ -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( diff --git a/src/claim-request-management/fanavaran-driver-inquiry.ts b/src/claim-request-management/fanavaran-driver-inquiry.ts index 1edcb14..8d62cd7 100644 --- a/src/claim-request-management/fanavaran-driver-inquiry.ts +++ b/src/claim-request-management/fanavaran-driver-inquiry.ts @@ -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 { diff --git a/src/claim-request-management/fanavaran-other-people.spec.ts b/src/claim-request-management/fanavaran-other-people.spec.ts index 7eae3b0..de176e1 100644 --- a/src/claim-request-management/fanavaran-other-people.spec.ts +++ b/src/claim-request-management/fanavaran-other-people.spec.ts @@ -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({ diff --git a/src/claim-request-management/fanavaran-other-people.ts b/src/claim-request-management/fanavaran-other-people.ts index f6953c4..34bc8f7 100644 --- a/src/claim-request-management/fanavaran-other-people.ts +++ b/src/claim-request-management/fanavaran-other-people.ts @@ -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, }; } diff --git a/src/claim-request-management/fanavaran-party-roles.spec.ts b/src/claim-request-management/fanavaran-party-roles.spec.ts index 2924564..16205b5 100644 --- a/src/claim-request-management/fanavaran-party-roles.spec.ts +++ b/src/claim-request-management/fanavaran-party-roles.spec.ts @@ -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"); }); diff --git a/src/claim-request-management/fanavaran-party-roles.ts b/src/claim-request-management/fanavaran-party-roles.ts index 0367d1f..b8b6f10 100644 --- a/src/claim-request-management/fanavaran-party-roles.ts +++ b/src/claim-request-management/fanavaran-party-roles.ts @@ -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,