forked from Yara724/api
update the fanavaran which accepts unknown person too
This commit is contained in:
@@ -185,7 +185,10 @@ import {
|
|||||||
import { FanavaranAuditService } from "src/fanavaran/fanavaran-audit.service";
|
import { FanavaranAuditService } from "src/fanavaran/fanavaran-audit.service";
|
||||||
import { FanavaranAuthService } from "src/fanavaran/fanavaran-auth.service";
|
import { FanavaranAuthService } from "src/fanavaran/fanavaran-auth.service";
|
||||||
import { FanavaranLookupService } from "src/fanavaran/fanavaran-lookup.service";
|
import { FanavaranLookupService } from "src/fanavaran/fanavaran-lookup.service";
|
||||||
import { FANAVARAN_REMOTE_LOOKUPS } from "src/fanavaran/fanavaran-lookup.config";
|
import {
|
||||||
|
FANAVARAN_LOOKUP_BASE_URL,
|
||||||
|
FANAVARAN_REMOTE_LOOKUPS,
|
||||||
|
} from "src/fanavaran/fanavaran-lookup.config";
|
||||||
import type { FanavaranAuditSession } from "src/fanavaran/fanavaran-audit.types";
|
import type { FanavaranAuditSession } from "src/fanavaran/fanavaran-audit.types";
|
||||||
import {
|
import {
|
||||||
FanavaranAuditSource,
|
FanavaranAuditSource,
|
||||||
@@ -207,6 +210,10 @@ import {
|
|||||||
pickPersonNationalCode,
|
pickPersonNationalCode,
|
||||||
selectFanavaranDriverId,
|
selectFanavaranDriverId,
|
||||||
} from "./fanavaran-driver-inquiry";
|
} from "./fanavaran-driver-inquiry";
|
||||||
|
import {
|
||||||
|
buildFanavaranOtherPeoplePayload,
|
||||||
|
pickFanavaranRecordId,
|
||||||
|
} from "./fanavaran-other-people";
|
||||||
import {
|
import {
|
||||||
asObjectRecord,
|
asObjectRecord,
|
||||||
collectPartyVinCandidates,
|
collectPartyVinCandidates,
|
||||||
@@ -4127,6 +4134,8 @@ export class ClaimRequestManagementService {
|
|||||||
DriverIsOwner: number;
|
DriverIsOwner: number;
|
||||||
FaultPercent: number;
|
FaultPercent: number;
|
||||||
};
|
};
|
||||||
|
/** GEN.44 write: register the damaged party in Fanavaran when inquiry misses. */
|
||||||
|
registerMissingPerson?: boolean;
|
||||||
}): Promise<Record<string, unknown>> {
|
}): Promise<Record<string, unknown>> {
|
||||||
const damagedParty = this.pickDamagedPartyForFanavaran(
|
const damagedParty = this.pickDamagedPartyForFanavaran(
|
||||||
input.blameCase,
|
input.blameCase,
|
||||||
@@ -4288,9 +4297,11 @@ export class ClaimRequestManagementService {
|
|||||||
// Prefer claim-level cache, then party.person.fanavaranDriverId, then live inquiry, then last payload
|
// Prefer claim-level cache, then party.person.fanavaranDriverId, then live inquiry, then last payload
|
||||||
let driverFanavaranId = this.firstPositiveFanavaranId(
|
let driverFanavaranId = this.firstPositiveFanavaranId(
|
||||||
cachedDamage.driverId,
|
cachedDamage.driverId,
|
||||||
|
cachedDamage.otherPersonId,
|
||||||
person.fanavaranDriverId,
|
person.fanavaranDriverId,
|
||||||
lastDamagePayload?.DriverId,
|
lastDamagePayload?.DriverId,
|
||||||
);
|
);
|
||||||
|
let otherPersonId = this.firstPositiveFanavaranId(cachedDamage.otherPersonId);
|
||||||
|
|
||||||
if (driverFanavaranId) {
|
if (driverFanavaranId) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
@@ -4304,6 +4315,20 @@ export class ClaimRequestManagementService {
|
|||||||
input.claimCase,
|
input.claimCase,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!driverFanavaranId && input.registerMissingPerson) {
|
||||||
|
const registered = await this.registerFanavaranOtherPerson({
|
||||||
|
clientKey: input.clientKey,
|
||||||
|
person,
|
||||||
|
claimCaseId: input.claimCase?._id
|
||||||
|
? String(input.claimCase._id)
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
if (registered != null) {
|
||||||
|
driverFanavaranId = registered;
|
||||||
|
otherPersonId = registered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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(
|
||||||
@@ -4473,6 +4498,9 @@ export class ClaimRequestManagementService {
|
|||||||
...(driverFanavaranId != null
|
...(driverFanavaranId != null
|
||||||
? { "fanavaranSync.damageCase.driverId": driverFanavaranId }
|
? { "fanavaranSync.damageCase.driverId": driverFanavaranId }
|
||||||
: {}),
|
: {}),
|
||||||
|
...(otherPersonId != null
|
||||||
|
? { "fanavaranSync.damageCase.otherPersonId": otherPersonId }
|
||||||
|
: {}),
|
||||||
...(vehicleKindId != null
|
...(vehicleKindId != null
|
||||||
? { "fanavaranSync.damageCase.vehicleKindId": vehicleKindId }
|
? { "fanavaranSync.damageCase.vehicleKindId": vehicleKindId }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -5075,6 +5103,158 @@ export class ClaimRequestManagementService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async loadFanavaranOtherPeopleLookups(
|
||||||
|
clientKey: FanavaranClientKey,
|
||||||
|
): Promise<{
|
||||||
|
cities: unknown[];
|
||||||
|
gender: unknown[];
|
||||||
|
maritalStatus: unknown[];
|
||||||
|
ans: unknown[];
|
||||||
|
countries: unknown[];
|
||||||
|
personKind: unknown[];
|
||||||
|
}> {
|
||||||
|
const [cities, gender, maritalStatus, ans, countries, personKind] =
|
||||||
|
await Promise.all([
|
||||||
|
this.getFanavaranLookupRows(clientKey, "cities"),
|
||||||
|
this.getFanavaranLookupRows(clientKey, "gender"),
|
||||||
|
this.getFanavaranLookupRows(clientKey, "marital-status"),
|
||||||
|
this.getFanavaranLookupRows(clientKey, "ans"),
|
||||||
|
this.getFanavaranLookupRows(clientKey, "countries"),
|
||||||
|
this.getFanavaranLookupRows(clientKey, "person-kind"),
|
||||||
|
]);
|
||||||
|
return { cities, gender, maritalStatus, ans, countries, personKind };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GEN.44: create the damaged party in Fanavaran when parties inquiry has no row.
|
||||||
|
* Failures stay best-effort — GEN.12 submit continues without DriverId.
|
||||||
|
*/
|
||||||
|
private async registerFanavaranOtherPerson(input: {
|
||||||
|
clientKey: FanavaranClientKey;
|
||||||
|
person: any;
|
||||||
|
claimCaseId?: string;
|
||||||
|
}): Promise<number | null> {
|
||||||
|
const nationalCode = pickPersonNationalCode(input.person);
|
||||||
|
const birthday = pickPersonBirthday(input.person);
|
||||||
|
if (!nationalCode || !parseJalaliDateParts(birthday)) {
|
||||||
|
this.logger.warn(
|
||||||
|
`[registerFanavaranOtherPerson] SKIP: missing nationalCode or birthday`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let user: {
|
||||||
|
fullName?: string;
|
||||||
|
mobile?: string;
|
||||||
|
city?: string;
|
||||||
|
address?: string;
|
||||||
|
gender?: string;
|
||||||
|
} | null = null;
|
||||||
|
const userId = input.person?.userId;
|
||||||
|
if (userId && Types.ObjectId.isValid(String(userId))) {
|
||||||
|
try {
|
||||||
|
user = await this.userDbService.findOne({
|
||||||
|
_id: new Types.ObjectId(String(userId)),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
`[registerFanavaranOtherPerson] user lookup failed: ${
|
||||||
|
error instanceof Error ? error.message : error
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lookups = await this.loadFanavaranOtherPeopleLookups(input.clientKey);
|
||||||
|
const payload = buildFanavaranOtherPeoplePayload(
|
||||||
|
{
|
||||||
|
nationalCode,
|
||||||
|
birthday,
|
||||||
|
fullName: input.person?.fullName ?? user?.fullName,
|
||||||
|
mobile: input.person?.phoneNumber ?? user?.mobile,
|
||||||
|
address: user?.address,
|
||||||
|
cityName: user?.city,
|
||||||
|
gender: user?.gender,
|
||||||
|
},
|
||||||
|
lookups,
|
||||||
|
);
|
||||||
|
if (!payload) {
|
||||||
|
this.logger.warn(
|
||||||
|
`[registerFanavaranOtherPerson] SKIP: could not build GEN.44 payload for nationalCode=${nationalCode}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${FANAVARAN_LOOKUP_BASE_URL}/common/other-people`;
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const auditSession: FanavaranAuditSession = {
|
||||||
|
trackingCode: this.fanavaranAuditService.generateTrackingCode(),
|
||||||
|
clientKey: input.clientKey,
|
||||||
|
source: FanavaranAuditSource.AUTO_SUBMIT,
|
||||||
|
claimCaseId: input.claimCaseId,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.fanavaranAuditService.recordStep({
|
||||||
|
session: auditSession,
|
||||||
|
step: FanavaranAuditStep.CREATE_OTHER_PERSON,
|
||||||
|
status: FanavaranAuditStatus.STARTED,
|
||||||
|
requestUrl: url,
|
||||||
|
requestMethod: "POST",
|
||||||
|
requestBody: payload,
|
||||||
|
requestMeta: { nationalCode },
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await this.fanavaranLookupService.createOtherPerson(
|
||||||
|
input.clientKey,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
const personId = pickFanavaranRecordId(created);
|
||||||
|
await this.fanavaranAuditService.recordStep({
|
||||||
|
session: auditSession,
|
||||||
|
step: FanavaranAuditStep.CREATE_OTHER_PERSON,
|
||||||
|
status: FanavaranAuditStatus.SUCCESS,
|
||||||
|
requestUrl: url,
|
||||||
|
requestMethod: "POST",
|
||||||
|
requestBody: payload,
|
||||||
|
responseBody: created,
|
||||||
|
responseMeta: { otherPersonId: personId, nationalCode },
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
});
|
||||||
|
if (personId != null) {
|
||||||
|
this.logger.log(
|
||||||
|
`[registerFanavaranOtherPerson] CREATED Id=${personId} for nationalCode=${nationalCode}`,
|
||||||
|
);
|
||||||
|
return personId;
|
||||||
|
}
|
||||||
|
this.logger.warn(
|
||||||
|
`[registerFanavaranOtherPerson] create returned no Id for nationalCode=${nationalCode}; retrying inquiry`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
await this.fanavaranAuditService.recordStep({
|
||||||
|
session: auditSession,
|
||||||
|
step: FanavaranAuditStep.CREATE_OTHER_PERSON,
|
||||||
|
status: FanavaranAuditStatus.FAILURE,
|
||||||
|
requestUrl: url,
|
||||||
|
requestMethod: "POST",
|
||||||
|
requestBody: payload,
|
||||||
|
errorMessage: message,
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
});
|
||||||
|
this.logger.warn(
|
||||||
|
`[registerFanavaranOtherPerson] CREATE failed for nationalCode=${nationalCode}: ${message}; retrying inquiry`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.resolveDriverFanavaranId(
|
||||||
|
input.clientKey,
|
||||||
|
nationalCode,
|
||||||
|
birthday,
|
||||||
|
input.person?.driverIsInsurer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* زیاندیده for GEN.12: claim owner when linked to a blame party,
|
* زیاندیده for GEN.12: claim owner when linked to a blame party,
|
||||||
* else the party that is not the guilty one.
|
* else the party that is not the guilty one.
|
||||||
@@ -5521,6 +5701,16 @@ export class ClaimRequestManagementService {
|
|||||||
defaults: profile.defaults,
|
defaults: profile.defaults,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const warnings: string[] = [];
|
||||||
|
if (!claimCase.claimId) {
|
||||||
|
warnings.push("Fanavaran base claimId is required before submit.");
|
||||||
|
}
|
||||||
|
if (payload.DriverId == null) {
|
||||||
|
warnings.push(
|
||||||
|
"Damaged party is not in Fanavaran yet; submit will register them via other-people (GEN.44).",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
clientKey,
|
clientKey,
|
||||||
claimCaseId,
|
claimCaseId,
|
||||||
@@ -5529,9 +5719,7 @@ export class ClaimRequestManagementService {
|
|||||||
submitUrl: claimCase.claimId
|
submitUrl: claimCase.claimId
|
||||||
? `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/dmg-cases`
|
? `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/dmg-cases`
|
||||||
: null,
|
: null,
|
||||||
warning: claimCase.claimId
|
warning: warnings.length ? warnings.join(" ") : undefined,
|
||||||
? undefined
|
|
||||||
: "Fanavaran base claimId is required before submit.",
|
|
||||||
payload,
|
payload,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -7021,6 +7209,7 @@ export class ClaimRequestManagementService {
|
|||||||
selectedParts: selectedParts ?? claimCase.damage?.selectedParts ?? [],
|
selectedParts: selectedParts ?? claimCase.damage?.selectedParts ?? [],
|
||||||
clientKey,
|
clientKey,
|
||||||
defaults: profile.defaults,
|
defaults: profile.defaults,
|
||||||
|
registerMissingPerson: 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.
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ export class FanavaranSyncStage {
|
|||||||
@Prop({ type: Number })
|
@Prop({ type: Number })
|
||||||
driverId?: number;
|
driverId?: number;
|
||||||
|
|
||||||
|
/** Cached GEN.44 other-people Id when the person was created because inquiry missed. */
|
||||||
|
@Prop({ type: Number })
|
||||||
|
otherPersonId?: number;
|
||||||
|
|
||||||
/** Cached Fanavaran VehicleKindId. */
|
/** Cached Fanavaran VehicleKindId. */
|
||||||
@Prop({ type: Number })
|
@Prop({ type: Number })
|
||||||
vehicleKindId?: number;
|
vehicleKindId?: number;
|
||||||
|
|||||||
80
src/claim-request-management/fanavaran-other-people.spec.ts
Normal file
80
src/claim-request-management/fanavaran-other-people.spec.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import {
|
||||||
|
buildFanavaranOtherPeoplePayload,
|
||||||
|
pickFanavaranRecordId,
|
||||||
|
pickLookupIdByCaption,
|
||||||
|
splitPersianFullName,
|
||||||
|
} from "./fanavaran-other-people";
|
||||||
|
|
||||||
|
describe("fanavaran other people (GEN.44)", () => {
|
||||||
|
it("builds a civil-registry create payload from local case + user fields", () => {
|
||||||
|
const payload = buildFanavaranOtherPeoplePayload(
|
||||||
|
{
|
||||||
|
nationalCode: "3392645966",
|
||||||
|
birthday: "1364/09/01",
|
||||||
|
fullName: "الهام مهدوی نیا",
|
||||||
|
mobile: "9366666666",
|
||||||
|
address: "گاندی ک نهم",
|
||||||
|
cityName: "تهران",
|
||||||
|
gender: "female",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cities: [{ Id: 9131, Caption: "تهران", IsActive: 1 }],
|
||||||
|
gender: [
|
||||||
|
{ Id: 26, Caption: "مرد", IsActive: 1 },
|
||||||
|
{ Id: 27, Caption: "زن", IsActive: 1 },
|
||||||
|
],
|
||||||
|
ans: [
|
||||||
|
{ Id: 1, Caption: "بله", IsActive: 1 },
|
||||||
|
{ Id: 0, Caption: "خیر", IsActive: 1 },
|
||||||
|
],
|
||||||
|
personKind: [{ Id: 46, Caption: "حقیقی", IsActive: 1 }],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
NationalCode: "3392645966",
|
||||||
|
Name: "الهام",
|
||||||
|
LastName: "مهدوی نیا",
|
||||||
|
BirthYear: 1364,
|
||||||
|
BirthMonth: 9,
|
||||||
|
BirthDay: 1,
|
||||||
|
Mobile: "09366666666",
|
||||||
|
Address: "گاندی ک نهم",
|
||||||
|
CityId: 9131,
|
||||||
|
GenderId: 27,
|
||||||
|
IsIranian: 1,
|
||||||
|
PersonKindId: 46,
|
||||||
|
ADBirthYear: null,
|
||||||
|
NationalityId: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when national code or birthday is missing", () => {
|
||||||
|
expect(
|
||||||
|
buildFanavaranOtherPeoplePayload({
|
||||||
|
nationalCode: "3392645966",
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("picks the created person Id from a wrapped Fanavaran response", () => {
|
||||||
|
expect(pickFanavaranRecordId({ data: { Id: 4553876 } })).toBe(4553876);
|
||||||
|
expect(pickFanavaranRecordId([{ Id: "4553876" }])).toBe(4553876);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches lookup captions ignoring yeh/keheh and extra spaces", () => {
|
||||||
|
expect(
|
||||||
|
pickLookupIdByCaption(
|
||||||
|
[{ Id: 701, Caption: "ايران", IsActive: 1 }],
|
||||||
|
["ایران"],
|
||||||
|
),
|
||||||
|
).toBe(701);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("splits a Persian full name into first and last name", () => {
|
||||||
|
expect(splitPersianFullName("الهام مهدوی نیا")).toEqual({
|
||||||
|
name: "الهام",
|
||||||
|
lastName: "مهدوی نیا",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
252
src/claim-request-management/fanavaran-other-people.ts
Normal file
252
src/claim-request-management/fanavaran-other-people.ts
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import { toEnglishDigits } from "src/lookups/fanavaran-last-car-policy";
|
||||||
|
import {
|
||||||
|
parseJalaliDateParts,
|
||||||
|
pickPersonBirthday,
|
||||||
|
pickPersonNationalCode,
|
||||||
|
} from "./fanavaran-driver-inquiry";
|
||||||
|
|
||||||
|
export type FanavaranLookupRow = {
|
||||||
|
Id?: unknown;
|
||||||
|
Caption?: unknown;
|
||||||
|
Name?: unknown;
|
||||||
|
IsActive?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FanavaranOtherPeopleLookups = {
|
||||||
|
cities?: unknown;
|
||||||
|
gender?: unknown;
|
||||||
|
maritalStatus?: unknown;
|
||||||
|
ans?: unknown;
|
||||||
|
countries?: unknown;
|
||||||
|
personKind?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FanavaranOtherPeopleSource = {
|
||||||
|
nationalCode?: unknown;
|
||||||
|
birthday?: unknown;
|
||||||
|
fullName?: unknown;
|
||||||
|
fatherName?: unknown;
|
||||||
|
mobile?: unknown;
|
||||||
|
tel?: unknown;
|
||||||
|
email?: unknown;
|
||||||
|
address?: unknown;
|
||||||
|
jobAddress?: unknown;
|
||||||
|
postalCode?: unknown;
|
||||||
|
cityName?: unknown;
|
||||||
|
gender?: unknown;
|
||||||
|
isIranian?: boolean;
|
||||||
|
naturalizedCode?: unknown;
|
||||||
|
identityNo?: unknown;
|
||||||
|
identityNoIssuPlace?: unknown;
|
||||||
|
passportNo?: unknown;
|
||||||
|
companyCode?: unknown;
|
||||||
|
economicCode?: unknown;
|
||||||
|
registerNo?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FanavaranOtherPeoplePayload = {
|
||||||
|
NationalCode: string | null;
|
||||||
|
Name: string | null;
|
||||||
|
LastName: string | null;
|
||||||
|
FatherName: string | null;
|
||||||
|
BirthYear: number | null;
|
||||||
|
BirthMonth: number | null;
|
||||||
|
BirthDay: number | null;
|
||||||
|
ADBirthYear: null;
|
||||||
|
ADBirthMonth: null;
|
||||||
|
ADBirthDay: null;
|
||||||
|
IdentityNoIssuPlace: string | null;
|
||||||
|
PassportNo: string | null;
|
||||||
|
Address: string | null;
|
||||||
|
PostalCode: string | null;
|
||||||
|
Tel: string | null;
|
||||||
|
Mobile: string | null;
|
||||||
|
Email: string | null;
|
||||||
|
JobAddress: string | null;
|
||||||
|
EconomicCode: string | null;
|
||||||
|
NaturalizedCode: string | null;
|
||||||
|
CompanyCode: string | null;
|
||||||
|
IdentityNo: string | null;
|
||||||
|
RegisterNo: string | null;
|
||||||
|
CityId: number | null;
|
||||||
|
GenderId: number | null;
|
||||||
|
MaritalStatus: number | null;
|
||||||
|
IsIranian: number | string | null;
|
||||||
|
NationalityId: number | null;
|
||||||
|
PersonKindId: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GENDER_MALE_TEXTS = ["مرد", "آقا", "male", "m"];
|
||||||
|
const GENDER_FEMALE_TEXTS = ["زن", "خانم", "female", "f"];
|
||||||
|
const ANS_YES_TEXTS = ["بله", "بلی", "yes", "1"];
|
||||||
|
const NATURAL_PERSON_TEXTS = ["حقیقی", "شخص حقیقی", "طبيعي", "natural"];
|
||||||
|
const IRAN_COUNTRY_TEXTS = ["ایران", "ايران", "iran"];
|
||||||
|
|
||||||
|
function asLookupRows(value: unknown): FanavaranLookupRow[] {
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
return value.filter(
|
||||||
|
(row): row is FanavaranLookupRow =>
|
||||||
|
!!row && typeof row === "object" && !Array.isArray(row),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLookupText(value: unknown): string {
|
||||||
|
return toEnglishDigits(value)
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[ي]/g, "ی")
|
||||||
|
.replace(/[ك]/g, "ک")
|
||||||
|
.replace(/[\u200c\s_\-\/]+/g, "")
|
||||||
|
.replace(/[^\p{L}\p{N}]/gu, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function lookupRowId(row: FanavaranLookupRow): number | null {
|
||||||
|
return asPositiveId(row.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lookupRowTexts(row: FanavaranLookupRow): string[] {
|
||||||
|
return [row.Caption, row.Name]
|
||||||
|
.filter((value): value is string => typeof value === "string")
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickLookupIdByCaption(
|
||||||
|
rows: unknown,
|
||||||
|
texts: unknown[],
|
||||||
|
): number | null {
|
||||||
|
const wanted = texts
|
||||||
|
.map((text) => normalizeLookupText(text))
|
||||||
|
.filter(Boolean);
|
||||||
|
if (!wanted.length) return null;
|
||||||
|
|
||||||
|
for (const row of asLookupRows(rows)) {
|
||||||
|
if (row.IsActive === 0) continue;
|
||||||
|
const rowTexts = lookupRowTexts(row).map(normalizeLookupText);
|
||||||
|
const matched = rowTexts.some((rowText) =>
|
||||||
|
wanted.some(
|
||||||
|
(want) =>
|
||||||
|
rowText === want || rowText.includes(want) || want.includes(rowText),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (matched) return lookupRowId(row);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitPersianFullName(
|
||||||
|
fullName: unknown,
|
||||||
|
): { name: string | null; lastName: string | null } {
|
||||||
|
const trimmed = String(fullName ?? "").trim().replace(/\s+/g, " ");
|
||||||
|
if (!trimmed) return { name: null, lastName: null };
|
||||||
|
const parts = trimmed.split(" ");
|
||||||
|
if (parts.length === 1) return { name: parts[0], lastName: null };
|
||||||
|
return { name: parts[0], lastName: parts.slice(1).join(" ") };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeIranMobile(value: unknown): string | null {
|
||||||
|
const digits = toEnglishDigits(value).replace(/\D/g, "");
|
||||||
|
if (!digits) return null;
|
||||||
|
if (digits.length === 10 && digits.startsWith("9")) return `0${digits}`;
|
||||||
|
if (digits.length === 11 && digits.startsWith("09")) return digits;
|
||||||
|
if (digits.length === 12 && digits.startsWith("989")) {
|
||||||
|
return `0${digits.slice(2)}`;
|
||||||
|
}
|
||||||
|
return digits.length >= 8 ? digits : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickFanavaranRecordId(value: unknown): number | null {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (typeof value === "number" || typeof value === "string") {
|
||||||
|
return asPositiveId(value);
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
const id = pickFanavaranRecordId(item);
|
||||||
|
if (id != null) return id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof value !== "object") return null;
|
||||||
|
const record = value as Record<string, unknown>;
|
||||||
|
const direct = asPositiveId(record.Id ?? record.id);
|
||||||
|
if (direct != null) return direct;
|
||||||
|
for (const key of ["value", "Value", "data", "Data", "item", "Item"]) {
|
||||||
|
const nested = pickFanavaranRecordId(record[key]);
|
||||||
|
if (nested != null) return nested;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyToNull(value: unknown): string | null {
|
||||||
|
if (value == null) return null;
|
||||||
|
const text = String(value).trim();
|
||||||
|
return text ? text : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFanavaranOtherPeoplePayload(
|
||||||
|
source: FanavaranOtherPeopleSource,
|
||||||
|
lookups: FanavaranOtherPeopleLookups = {},
|
||||||
|
): FanavaranOtherPeoplePayload | null {
|
||||||
|
const nationalCode = pickPersonNationalCode({
|
||||||
|
nationalCode: source.nationalCode,
|
||||||
|
nationalCodeOfDriver: source.nationalCode,
|
||||||
|
});
|
||||||
|
const birthday = parseJalaliDateParts(
|
||||||
|
source.birthday ?? pickPersonBirthday({ birthday: source.birthday }),
|
||||||
|
);
|
||||||
|
if (!nationalCode || !birthday) return null;
|
||||||
|
|
||||||
|
const isIranian =
|
||||||
|
source.isIranian !== false && !emptyToNull(source.naturalizedCode);
|
||||||
|
const names = splitPersianFullName(source.fullName);
|
||||||
|
const genderTexts =
|
||||||
|
source.gender === "male" || source.gender === "m"
|
||||||
|
? GENDER_MALE_TEXTS
|
||||||
|
: source.gender === "female" || source.gender === "f"
|
||||||
|
? GENDER_FEMALE_TEXTS
|
||||||
|
: [source.gender];
|
||||||
|
|
||||||
|
return {
|
||||||
|
NationalCode: nationalCode,
|
||||||
|
Name: names.name,
|
||||||
|
LastName: names.lastName,
|
||||||
|
FatherName: emptyToNull(source.fatherName),
|
||||||
|
BirthYear: birthday.year,
|
||||||
|
BirthMonth: birthday.month,
|
||||||
|
BirthDay: birthday.day,
|
||||||
|
ADBirthYear: null,
|
||||||
|
ADBirthMonth: null,
|
||||||
|
ADBirthDay: null,
|
||||||
|
IdentityNoIssuPlace: emptyToNull(source.identityNoIssuPlace),
|
||||||
|
PassportNo: emptyToNull(source.passportNo),
|
||||||
|
Address: emptyToNull(source.address),
|
||||||
|
PostalCode: emptyToNull(source.postalCode),
|
||||||
|
Tel: normalizeIranMobile(source.tel) ?? emptyToNull(source.tel),
|
||||||
|
Mobile: normalizeIranMobile(source.mobile),
|
||||||
|
Email: emptyToNull(source.email),
|
||||||
|
JobAddress: emptyToNull(source.jobAddress),
|
||||||
|
EconomicCode: emptyToNull(source.economicCode),
|
||||||
|
NaturalizedCode: emptyToNull(source.naturalizedCode),
|
||||||
|
CompanyCode: emptyToNull(source.companyCode),
|
||||||
|
IdentityNo: emptyToNull(source.identityNo),
|
||||||
|
RegisterNo: emptyToNull(source.registerNo),
|
||||||
|
CityId: pickLookupIdByCaption(lookups.cities, [source.cityName]),
|
||||||
|
GenderId: pickLookupIdByCaption(lookups.gender, genderTexts),
|
||||||
|
MaritalStatus: pickLookupIdByCaption(lookups.maritalStatus, []),
|
||||||
|
IsIranian: isIranian
|
||||||
|
? (pickLookupIdByCaption(lookups.ans, ANS_YES_TEXTS) ?? 1)
|
||||||
|
: (pickLookupIdByCaption(lookups.ans, ["خیر", "no", "0"]) ?? 0),
|
||||||
|
NationalityId: isIranian
|
||||||
|
? null
|
||||||
|
: pickLookupIdByCaption(lookups.countries, IRAN_COUNTRY_TEXTS),
|
||||||
|
PersonKindId: pickLookupIdByCaption(
|
||||||
|
lookups.personKind,
|
||||||
|
NATURAL_PERSON_TEXTS,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -117,6 +117,36 @@ export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [
|
|||||||
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/dmg-business-line`,
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/dmg-business-line`,
|
||||||
cacheFile: "dmg-business-line.json",
|
cacheFile: "dmg-business-line.json",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "gender",
|
||||||
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/gender`,
|
||||||
|
cacheFile: "gender.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "marital-status",
|
||||||
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/marital-status`,
|
||||||
|
cacheFile: "marital-status.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ans",
|
||||||
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/ans`,
|
||||||
|
cacheFile: "ans.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "countries",
|
||||||
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/base-info/countries`,
|
||||||
|
cacheFile: "countries.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "person-kind",
|
||||||
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/person-kind`,
|
||||||
|
cacheFile: "person-kind.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cii-validation-status",
|
||||||
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/cii-validation-status`,
|
||||||
|
cacheFile: "cii-validation-status.json",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const TEJARAT_STATIC_ACCIDENT_FILES = {
|
export const TEJARAT_STATIC_ACCIDENT_FILES = {
|
||||||
|
|||||||
@@ -315,6 +315,63 @@ export class FanavaranLookupService {
|
|||||||
return this.fetchFromFanavaran(clientKey, url);
|
return this.fetchFromFanavaran(clientKey, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOtherPerson(
|
||||||
|
clientKey: FanavaranClientKey,
|
||||||
|
personId: number,
|
||||||
|
): Promise<unknown> {
|
||||||
|
const url = `${FANAVARAN_LOOKUP_BASE_URL}/common/other-people/${personId}`;
|
||||||
|
return this.fetchFromFanavaran(clientKey, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOtherPerson(
|
||||||
|
clientKey: FanavaranClientKey,
|
||||||
|
payload: Record<string, unknown>,
|
||||||
|
): Promise<unknown> {
|
||||||
|
const url = `${FANAVARAN_LOOKUP_BASE_URL}/common/other-people`;
|
||||||
|
return this.postToFanavaran(clientKey, url, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async postToFanavaran(
|
||||||
|
clientKey: FanavaranClientKey,
|
||||||
|
url: string,
|
||||||
|
payload: Record<string, unknown>,
|
||||||
|
): Promise<unknown> {
|
||||||
|
try {
|
||||||
|
const headers = await this.fanavaranAuthService.getRequestHeaders(
|
||||||
|
clientKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(`[${clientKey}] POST Fanavaran: ${url}`);
|
||||||
|
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.post(url, payload, {
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
timeout: 20000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.fanavaranAuthService.clearBackoff(clientKey);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.fanavaranAuthService.registerFailure(clientKey, error);
|
||||||
|
const message = isAxiosError(error)
|
||||||
|
? error.response?.data?.Message ||
|
||||||
|
error.response?.data?.message ||
|
||||||
|
error.message
|
||||||
|
: error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Fanavaran POST request failed";
|
||||||
|
|
||||||
|
this.logger.error(
|
||||||
|
`Fanavaran POST failed for ${clientKey} (${url}): ${message}`,
|
||||||
|
);
|
||||||
|
throw new BadGatewayException(String(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async resolveInsuranceCorpId(
|
async resolveInsuranceCorpId(
|
||||||
clientKey: FanavaranClientKey,
|
clientKey: FanavaranClientKey,
|
||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export enum FanavaranAuditStep {
|
|||||||
POLICY_INQUIRY = "POLICY_INQUIRY",
|
POLICY_INQUIRY = "POLICY_INQUIRY",
|
||||||
BUILD_PAYLOAD = "BUILD_PAYLOAD",
|
BUILD_PAYLOAD = "BUILD_PAYLOAD",
|
||||||
SUBMIT_CLAIM = "SUBMIT_CLAIM",
|
SUBMIT_CLAIM = "SUBMIT_CLAIM",
|
||||||
|
CREATE_OTHER_PERSON = "CREATE_OTHER_PERSON",
|
||||||
SUBMIT_DAMAGE_CASE = "SUBMIT_DAMAGE_CASE",
|
SUBMIT_DAMAGE_CASE = "SUBMIT_DAMAGE_CASE",
|
||||||
SUBMIT_ATTACHMENT = "SUBMIT_ATTACHMENT",
|
SUBMIT_ATTACHMENT = "SUBMIT_ATTACHMENT",
|
||||||
SUBMIT_EXPERTISE = "SUBMIT_EXPERTISE",
|
SUBMIT_EXPERTISE = "SUBMIT_EXPERTISE",
|
||||||
|
|||||||
@@ -327,6 +327,90 @@ export class LookupsController {
|
|||||||
return await this.lookupsService.getDmgBusinessLine();
|
return await this.lookupsService.getDmgBusinessLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("gender")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Fanavaran GEN.44 gender lookup",
|
||||||
|
description:
|
||||||
|
"Returns values for other-people payload field GenderId from common/code-list/gender.",
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Returns Fanavaran gender lookup data",
|
||||||
|
schema: { type: "array", items: { type: "object" } },
|
||||||
|
})
|
||||||
|
async getGender() {
|
||||||
|
return await this.lookupsService.getGender();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("marital-status")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Fanavaran GEN.44 marital status lookup",
|
||||||
|
description:
|
||||||
|
"Returns values for other-people payload field MaritalStatus from common/code-list/marital-status.",
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Returns Fanavaran marital status lookup data",
|
||||||
|
schema: { type: "array", items: { type: "object" } },
|
||||||
|
})
|
||||||
|
async getMaritalStatus() {
|
||||||
|
return await this.lookupsService.getMaritalStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("ans")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Fanavaran GEN.44 yes/no lookup",
|
||||||
|
description:
|
||||||
|
"Returns values for other-people fields IsIranian / IsValid / IsVerified from common/code-list/ans.",
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Returns Fanavaran yes/no (ans) lookup data",
|
||||||
|
schema: { type: "array", items: { type: "object" } },
|
||||||
|
})
|
||||||
|
async getAns() {
|
||||||
|
return await this.lookupsService.getAns();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("countries")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Fanavaran GEN.44 countries lookup",
|
||||||
|
description:
|
||||||
|
"Returns values for other-people payload field NationalityId from common/base-info/countries.",
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Returns Fanavaran countries lookup data",
|
||||||
|
schema: { type: "array", items: { type: "object" } },
|
||||||
|
})
|
||||||
|
async getCountries() {
|
||||||
|
return await this.lookupsService.getCountries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("person-kind")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Fanavaran GEN.44 person kind lookup",
|
||||||
|
description:
|
||||||
|
"Returns values for other-people payload field PersonKindId from common/code-list/person-kind.",
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Returns Fanavaran person kind lookup data",
|
||||||
|
schema: { type: "array", items: { type: "object" } },
|
||||||
|
})
|
||||||
|
async getPersonKind() {
|
||||||
|
return await this.lookupsService.getPersonKind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("cii-validation-status")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Fanavaran GEN.44 CII validation status lookup",
|
||||||
|
description:
|
||||||
|
"Returns values for other-people response fields CIIValidationStatus / CIIMobileStatus from common/code-list/cii-validation-status.",
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Returns Fanavaran CII validation status lookup data",
|
||||||
|
schema: { type: "array", items: { type: "object" } },
|
||||||
|
})
|
||||||
|
async getCiiValidationStatus() {
|
||||||
|
return await this.lookupsService.getCiiValidationStatus();
|
||||||
|
}
|
||||||
|
|
||||||
@Get("inquiry-by-vin")
|
@Get("inquiry-by-vin")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Fanavaran vehicle inquiry by VIN",
|
summary: "Fanavaran vehicle inquiry by VIN",
|
||||||
@@ -382,6 +466,25 @@ export class LookupsController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("other-people/:personId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Fanavaran GEN.44 other person by id",
|
||||||
|
description:
|
||||||
|
"GET common/other-people/{Id}. Use after parties inquiry misses and GEN.44 create, or to inspect a known person Id.",
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: "personId",
|
||||||
|
description: "Fanavaran other-people Id",
|
||||||
|
example: 4553876,
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Returns the Fanavaran other-person record",
|
||||||
|
schema: { type: "object" },
|
||||||
|
})
|
||||||
|
async otherPersonById(@Param("personId", ParseIntPipe) personId: number) {
|
||||||
|
return await this.lookupsService.otherPersonById(personId);
|
||||||
|
}
|
||||||
|
|
||||||
@Get("fanavaran")
|
@Get("fanavaran")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "List configured Fanavaran remote lookups",
|
summary: "List configured Fanavaran remote lookups",
|
||||||
|
|||||||
@@ -182,6 +182,44 @@ export class LookupsService {
|
|||||||
return await this.getClientRemoteLookup("dmg-business-line");
|
return await this.getClientRemoteLookup("dmg-business-line");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getGender(): Promise<any> {
|
||||||
|
return await this.getClientRemoteLookup("gender");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMaritalStatus(): Promise<any> {
|
||||||
|
return await this.getClientRemoteLookup("marital-status");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAns(): Promise<any> {
|
||||||
|
return await this.getClientRemoteLookup("ans");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCountries(): Promise<any> {
|
||||||
|
return await this.getClientRemoteLookup("countries");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPersonKind(): Promise<any> {
|
||||||
|
return await this.getClientRemoteLookup("person-kind");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCiiValidationStatus(): Promise<any> {
|
||||||
|
return await this.getClientRemoteLookup("cii-validation-status");
|
||||||
|
}
|
||||||
|
|
||||||
|
async otherPersonById(personId: number): Promise<unknown> {
|
||||||
|
const clientKey = this.activeClientKey();
|
||||||
|
const person = await this.fanavaranLookupService.getOtherPerson(
|
||||||
|
clientKey,
|
||||||
|
personId,
|
||||||
|
);
|
||||||
|
if (person == null) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Fanavaran other-person ${personId} was not found.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return person;
|
||||||
|
}
|
||||||
|
|
||||||
async inquiryByUniqueIdentifier(query: {
|
async inquiryByUniqueIdentifier(query: {
|
||||||
nationalCode?: string;
|
nationalCode?: string;
|
||||||
birthday?: string;
|
birthday?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user