forked from Yara724/api
YARA-1241
This commit is contained in:
183
src/case-expert-report/case-expert-report.builder.spec.ts
Normal file
183
src/case-expert-report/case-expert-report.builder.spec.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { buildInsurerFileReport } from "./case-expert-report.builder";
|
||||
import { PR } from "./persian-report-labels";
|
||||
import { toJalaliDateAndTime } from "../helpers/date-jalali";
|
||||
|
||||
describe("buildInsurerFileReport", () => {
|
||||
const getFieldValue = (
|
||||
report: ReturnType<typeof buildInsurerFileReport>,
|
||||
sectionTitle: string,
|
||||
fieldLabel: string,
|
||||
) => {
|
||||
const section = report.sections.find((item) => item.title === sectionTitle);
|
||||
expect(section).toBeDefined();
|
||||
const field = section?.fields.find((item) => item.label === fieldLabel);
|
||||
expect(field).toBeDefined();
|
||||
return field?.value;
|
||||
};
|
||||
|
||||
it("separates insurance blocks and exposes Fanavaran/timeline/evaluation data", () => {
|
||||
const fileCreatedAt = new Date("2026-08-10T09:53:23.588Z");
|
||||
const evaluationSubmittedAt = new Date("2026-08-10T12:30:45.000Z");
|
||||
const [fileDate, fileTime] = toJalaliDateAndTime(fileCreatedAt);
|
||||
const [evaluationDate, evaluationTime] =
|
||||
toJalaliDateAndTime(evaluationSubmittedAt);
|
||||
|
||||
const report = buildInsurerFileReport({
|
||||
overview: {
|
||||
publicId: "A00010",
|
||||
requestNo: "REQ-10",
|
||||
createdAt: fileCreatedAt,
|
||||
},
|
||||
blame: {
|
||||
type: "THIRD_PARTY",
|
||||
blameStatus: "AGREED",
|
||||
createdAt: fileCreatedAt,
|
||||
parties: [
|
||||
{
|
||||
role: "FIRST",
|
||||
person: {
|
||||
userId: "guilty-user-id",
|
||||
fullName: "مقصر نمونه",
|
||||
phoneNumber: "09120000000",
|
||||
clientId: "client-guilty",
|
||||
},
|
||||
statement: { admitsGuilt: true },
|
||||
insurance: {
|
||||
policyNumber: "TP-GUILTY-001",
|
||||
company: "بیمه ثالث مقصر",
|
||||
startDate: "1405/01/01",
|
||||
endDate: "1406/01/01",
|
||||
financialCeiling: "900000000",
|
||||
carBodyInsurance: {
|
||||
policyNumber: "CB-GUILTY-001",
|
||||
insurerCompany: "بیمه بدنه مقصر",
|
||||
startDate: "1405/02/01",
|
||||
endDate: "1406/02/01",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
role: "SECOND",
|
||||
person: {
|
||||
userId: "damaged-user-id",
|
||||
fullName: "زیان دیده نمونه",
|
||||
phoneNumber: "09121111111",
|
||||
nationalCodeOfInsurer: "1234567890",
|
||||
clientId: "client-damaged",
|
||||
insurerBirthday: 13700101,
|
||||
},
|
||||
statement: {
|
||||
claimsDamage: true,
|
||||
description: "توضیحات زیاندیده",
|
||||
accidentDate: "2026-08-10",
|
||||
accidentTime: "13:23",
|
||||
},
|
||||
insurance: {
|
||||
policyNumber: "TP-DAMAGED-001",
|
||||
company: "بیمه ثالث زیاندیده",
|
||||
startDate: "1405/03/01",
|
||||
endDate: "1406/03/01",
|
||||
financialCeiling: "700000000",
|
||||
carBodyInsurance: {
|
||||
policyNumber: "CB-DAMAGED-001",
|
||||
insurerCompany: "بیمه بدنه زیاندیده",
|
||||
startDate: "1405/04/01",
|
||||
endDate: "1406/04/01",
|
||||
coverages: ["سرقت", "آتشسوزی"],
|
||||
},
|
||||
},
|
||||
vehicle: {
|
||||
carName: "207",
|
||||
carModel: "1402",
|
||||
plate: {
|
||||
leftDigits: 12,
|
||||
centerAlphabet: "ب",
|
||||
centerDigits: 345,
|
||||
ir: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
expert: {
|
||||
decision: {
|
||||
guiltyPartyId: "guilty-user-id",
|
||||
description: "مقصر شناخته شد",
|
||||
fields: {
|
||||
accidentWay: { label: "از جلو" },
|
||||
accidentReason: { label: "عدم رعایت فاصله" },
|
||||
accidentType: { label: "برخورد" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
claim: {
|
||||
claimStatus: "APPROVED",
|
||||
claimNo: 111,
|
||||
claimId: 222,
|
||||
dmgCaseId: 333,
|
||||
expertiseId: 444,
|
||||
owner: { fullName: "زیان دیده نمونه" },
|
||||
vehicle: {
|
||||
carName: "207",
|
||||
carModel: "1402",
|
||||
carType: "sedan",
|
||||
},
|
||||
fanavaranSync: {
|
||||
baseClaim: {
|
||||
policyId: 555,
|
||||
driverId: 666,
|
||||
vehicleKindId: 777,
|
||||
insuranceCorpId: 888,
|
||||
},
|
||||
},
|
||||
evaluation: {
|
||||
damageExpertReplyFinal: {
|
||||
submittedAt: evaluationSubmittedAt,
|
||||
description: "نیاز به تعویض سپر جلو",
|
||||
actorDetail: { actorName: "کارشناس خسارت نمونه" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getFieldValue(report, PR.damagedThirdPartyInsuranceSection, PR.policyNumber)).toBe(
|
||||
"TP-DAMAGED-001",
|
||||
);
|
||||
expect(getFieldValue(report, PR.guiltyThirdPartyInsuranceSection, PR.policyNumber)).toBe(
|
||||
"TP-GUILTY-001",
|
||||
);
|
||||
expect(getFieldValue(report, PR.damagedCarBodyInsuranceSection, PR.policyNumber)).toBe(
|
||||
"CB-DAMAGED-001",
|
||||
);
|
||||
expect(getFieldValue(report, PR.guiltyCarBodyInsuranceSection, PR.policyNumber)).toBe(
|
||||
"CB-GUILTY-001",
|
||||
);
|
||||
|
||||
expect(getFieldValue(report, PR.fanavaranSection, PR.fanavaranClaimNo)).toBe(
|
||||
"111",
|
||||
);
|
||||
expect(
|
||||
getFieldValue(report, PR.fanavaranSection, PR.fanavaranExpertiseId),
|
||||
).toBe("444");
|
||||
expect(getFieldValue(report, PR.fanavaranSection, PR.fanavaranPolicyId)).toBe(
|
||||
"555",
|
||||
);
|
||||
|
||||
expect(getFieldValue(report, PR.timelineSection, PR.fileRegisteredAt)).toBe(
|
||||
`${fileDate} ${fileTime}`,
|
||||
);
|
||||
expect(
|
||||
getFieldValue(report, PR.timelineSection, PR.evaluationRegisteredAt),
|
||||
).toBe(`${evaluationDate} ${evaluationTime}`);
|
||||
|
||||
expect(getFieldValue(report, PR.evaluationSection, PR.evaluationResult)).toBe(
|
||||
"تأیید شده",
|
||||
);
|
||||
expect(getFieldValue(report, PR.evaluationSection, PR.evaluationExpert)).toBe(
|
||||
"کارشناس خسارت نمونه",
|
||||
);
|
||||
expect(getFieldValue(report, PR.evaluationSection, PR.evaluationResponse)).toBe(
|
||||
"نیاز به تعویض سپر جلو",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,7 @@
|
||||
import { resolveDamagedPartyRow } from "src/helpers/blame-damaged-party";
|
||||
import {
|
||||
resolveClaimOwnerParty,
|
||||
resolveDamagedPartyRow,
|
||||
} from "src/helpers/blame-damaged-party";
|
||||
import { toJalaliDateAndTime } from "src/helpers/date-jalali";
|
||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||
import {
|
||||
@@ -15,8 +18,20 @@ const SKIP_FLATTEN_KEYS = new Set([
|
||||
"workflow",
|
||||
"evidence",
|
||||
"confirmation",
|
||||
"inquiries",
|
||||
"raw",
|
||||
]);
|
||||
|
||||
type ReportRecord = Record<string, unknown>;
|
||||
type ReportParty = ReportRecord & {
|
||||
role?: string;
|
||||
person?: ReportRecord;
|
||||
insurance?: ReportRecord & { carBodyInsurance?: ReportRecord };
|
||||
vehicle?: ReportRecord;
|
||||
statement?: ReportRecord;
|
||||
location?: { lat?: number; lon?: number };
|
||||
};
|
||||
|
||||
function asString(value: unknown): string | undefined {
|
||||
if (value === undefined || value === null || value === "") return undefined;
|
||||
if (value instanceof Date) {
|
||||
@@ -42,6 +57,30 @@ function formatBirthDate(value: unknown): string | undefined {
|
||||
return raw;
|
||||
}
|
||||
|
||||
function formatDateTime(value: unknown): string | undefined {
|
||||
if (value === undefined || value === null || value === "") return undefined;
|
||||
const date = value instanceof Date ? value : new Date(value as string | number);
|
||||
if (Number.isNaN(date.getTime())) return asString(value);
|
||||
const [d, t] = toJalaliDateAndTime(date);
|
||||
return `${d} ${t}`;
|
||||
}
|
||||
|
||||
function firstDefined(...values: unknown[]): string | undefined {
|
||||
for (const value of values) {
|
||||
const str = asString(value);
|
||||
if (str) return str;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeListValue(value: unknown): string | undefined {
|
||||
if (!Array.isArray(value) || !value.length) return undefined;
|
||||
const items = value
|
||||
.map((item) => asString(item) ?? JSON.stringify(item))
|
||||
.filter(Boolean);
|
||||
return items.length ? items.join("، ") : undefined;
|
||||
}
|
||||
|
||||
function flattenObject(
|
||||
obj: unknown,
|
||||
prefix = "",
|
||||
@@ -53,13 +92,10 @@ function flattenObject(
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
if (!obj.length) return [];
|
||||
return [
|
||||
{
|
||||
label: persianFieldPath(prefix || "items"),
|
||||
value: obj.map((item) => asString(item) ?? JSON.stringify(item)).join("، "),
|
||||
},
|
||||
];
|
||||
const value = normalizeListValue(obj);
|
||||
return value
|
||||
? [{ label: persianFieldPath(prefix || "items"), value }]
|
||||
: [];
|
||||
}
|
||||
|
||||
if (typeof obj !== "object") {
|
||||
@@ -70,6 +106,7 @@ function flattenObject(
|
||||
const objRecord = obj as Record<string, unknown>;
|
||||
const hasMapped =
|
||||
objRecord.mapped != null && typeof objRecord.mapped === "object";
|
||||
|
||||
for (const [key, value] of Object.entries(objRecord)) {
|
||||
if (SKIP_FLATTEN_KEYS.has(key)) continue;
|
||||
if (key === "raw" && hasMapped) continue;
|
||||
@@ -86,9 +123,49 @@ function flattenObject(
|
||||
rows.push({ label: persianFieldPath(path), value: asString(value) });
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
function dedupeFields(fields: InsurerFileReportField[]): InsurerFileReportField[] {
|
||||
const seen = new Set<string>();
|
||||
const out: InsurerFileReportField[] = [];
|
||||
|
||||
for (const field of fields) {
|
||||
const value = asString(field.value);
|
||||
if (!value) continue;
|
||||
const key = `${field.label}::${value}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push({ label: field.label, value });
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function filterEmptySections(
|
||||
sections: Array<InsurerFileReportSection | undefined>,
|
||||
): InsurerFileReportSection[] {
|
||||
return sections.filter(
|
||||
(section): section is InsurerFileReportSection => !!section && section.fields.length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function buildSection(
|
||||
title: string,
|
||||
fields: InsurerFileReportField[],
|
||||
withPlaceholder = true,
|
||||
): InsurerFileReportSection {
|
||||
const deduped = dedupeFields(fields);
|
||||
return {
|
||||
title,
|
||||
fields:
|
||||
deduped.length || !withPlaceholder
|
||||
? deduped
|
||||
: [{ label: PR.data, value: PR.empty }],
|
||||
};
|
||||
}
|
||||
|
||||
function expertNameFromSnapshot(snapshot?: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
@@ -146,7 +223,6 @@ function inquiryRoleData(
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Prefer mapped Tejarat fields; avoid duplicating the entire raw blob in PDF. */
|
||||
function pickInquiryReportPayload(
|
||||
inquiry?: Record<string, unknown>,
|
||||
): Record<string, unknown> | undefined {
|
||||
@@ -159,184 +235,420 @@ function pickInquiryReportPayload(
|
||||
return Object.keys(rest).length ? rest : inquiry;
|
||||
}
|
||||
|
||||
function licenseFieldsFromInquiry(
|
||||
inquiry?: Record<string, unknown>,
|
||||
): { licenseType?: string; licenseDate?: string } {
|
||||
if (!inquiry) return {};
|
||||
const candidates = [
|
||||
inquiry.LicenseType,
|
||||
inquiry.licenseType,
|
||||
inquiry.Type,
|
||||
inquiry.type,
|
||||
inquiry.LicenseCategory,
|
||||
inquiry.licenseCategory,
|
||||
];
|
||||
const licenseType = candidates.find((v) => v != null && v !== "");
|
||||
const dateCandidates = [
|
||||
inquiry.IssueDate,
|
||||
inquiry.issueDate,
|
||||
inquiry.LicenseIssueDate,
|
||||
inquiry.licenseIssueDate,
|
||||
inquiry.ExpireDate,
|
||||
inquiry.expireDate,
|
||||
];
|
||||
const licenseDate = dateCandidates.find((v) => v != null && v !== "");
|
||||
function resolveReportBlameContext(
|
||||
blame?: Record<string, unknown> | null,
|
||||
claim?: Record<string, unknown> | null,
|
||||
): Record<string, unknown> | null {
|
||||
if (blame) return blame;
|
||||
const snapshot = claim?.snapshot as Record<string, unknown> | undefined;
|
||||
if (!snapshot) return null;
|
||||
return {
|
||||
licenseType: asString(licenseType),
|
||||
licenseDate: asString(licenseDate),
|
||||
type:
|
||||
(claim?.blameFileContext as Record<string, unknown> | undefined)
|
||||
?.blameRequestType ??
|
||||
(snapshot.accident as Record<string, unknown> | undefined)?.type,
|
||||
blameStatus: (claim?.blameFileContext as Record<string, unknown> | undefined)
|
||||
?.blameStatus,
|
||||
parties: snapshot.parties,
|
||||
};
|
||||
}
|
||||
|
||||
function getPartyRole(party: ReportParty | null | undefined): string | undefined {
|
||||
const role = party?.role;
|
||||
return typeof role === "string" ? role : undefined;
|
||||
}
|
||||
|
||||
function sameParty(
|
||||
first: ReportParty | null | undefined,
|
||||
second: ReportParty | null | undefined,
|
||||
): boolean {
|
||||
if (!first || !second) return false;
|
||||
const firstUserId = first.person?.userId != null ? String(first.person.userId) : "";
|
||||
const secondUserId =
|
||||
second.person?.userId != null ? String(second.person.userId) : "";
|
||||
if (firstUserId && secondUserId) return firstUserId === secondUserId;
|
||||
return getPartyRole(first) === getPartyRole(second);
|
||||
}
|
||||
|
||||
function resolveEvaluationReply(
|
||||
claim?: Record<string, unknown> | null,
|
||||
): Record<string, unknown> | undefined {
|
||||
const evaluation = claim?.evaluation as Record<string, unknown> | undefined;
|
||||
return (
|
||||
(evaluation?.damageExpertReplyFinal as Record<string, unknown> | undefined) ??
|
||||
(evaluation?.damageExpertReply as Record<string, unknown> | undefined)
|
||||
);
|
||||
}
|
||||
|
||||
function insuranceValueFromCandidates(
|
||||
...values: unknown[]
|
||||
): string | undefined {
|
||||
for (const value of values) {
|
||||
if (Array.isArray(value)) {
|
||||
const listValue = normalizeListValue(value);
|
||||
if (listValue) return listValue;
|
||||
continue;
|
||||
}
|
||||
const str = asString(value);
|
||||
if (str) return str;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildThirdPartyInsuranceFields(
|
||||
party: ReportParty | null | undefined,
|
||||
blame?: Record<string, unknown> | null,
|
||||
claim?: Record<string, unknown> | null,
|
||||
): InsurerFileReportField[] {
|
||||
const role = getPartyRole(party);
|
||||
const direct = (party?.insurance ?? {}) as Record<string, unknown>;
|
||||
const blameInquiry = pickInquiryReportPayload(
|
||||
inquiryRoleData(blame?.inquiries as Record<string, unknown> | undefined, "thirdParty", role),
|
||||
);
|
||||
const claimInquiry = pickInquiryReportPayload(
|
||||
inquiryRoleData(claim?.inquiries as Record<string, unknown> | undefined, "thirdParty", role),
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
label: PR.policyNumber,
|
||||
value: insuranceValueFromCandidates(
|
||||
direct.policyNumber,
|
||||
blameInquiry?.policyNumber,
|
||||
blameInquiry?.PolicyNumber,
|
||||
claimInquiry?.policyNumber,
|
||||
claimInquiry?.PolicyNumber,
|
||||
blameInquiry?.ThirdPolicyCode,
|
||||
claimInquiry?.ThirdPolicyCode,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: PR.insuranceCompany,
|
||||
value: insuranceValueFromCandidates(
|
||||
direct.company,
|
||||
direct.insurerCompany,
|
||||
blameInquiry?.company,
|
||||
blameInquiry?.CompanyName,
|
||||
claimInquiry?.company,
|
||||
claimInquiry?.CompanyName,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: PR.policyStartDate,
|
||||
value: insuranceValueFromCandidates(
|
||||
direct.startDate,
|
||||
blameInquiry?.startDate,
|
||||
blameInquiry?.PolicyStartDate,
|
||||
blameInquiry?.SatrtDate,
|
||||
claimInquiry?.startDate,
|
||||
claimInquiry?.PolicyStartDate,
|
||||
claimInquiry?.SatrtDate,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: PR.policyEndDate,
|
||||
value: insuranceValueFromCandidates(
|
||||
direct.endDate,
|
||||
blameInquiry?.endDate,
|
||||
blameInquiry?.PolicyEndDate,
|
||||
blameInquiry?.EndDate,
|
||||
claimInquiry?.endDate,
|
||||
claimInquiry?.PolicyEndDate,
|
||||
claimInquiry?.EndDate,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: PR.financialCeiling,
|
||||
value: insuranceValueFromCandidates(
|
||||
direct.financialCeiling,
|
||||
blameInquiry?.financialCeiling,
|
||||
blameInquiry?.FinancialCvrCptl,
|
||||
blameInquiry?.FnCvrCptl,
|
||||
claimInquiry?.financialCeiling,
|
||||
claimInquiry?.FinancialCvrCptl,
|
||||
claimInquiry?.FnCvrCptl,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: PR.coverages,
|
||||
value: insuranceValueFromCandidates(
|
||||
direct.coverages,
|
||||
blameInquiry?.coverages,
|
||||
claimInquiry?.coverages,
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildCarBodyInsuranceFields(
|
||||
party: ReportParty | null | undefined,
|
||||
blame?: Record<string, unknown> | null,
|
||||
claim?: Record<string, unknown> | null,
|
||||
): InsurerFileReportField[] {
|
||||
const role = getPartyRole(party);
|
||||
const direct = (party?.insurance?.carBodyInsurance ??
|
||||
party?.insurance?.carBody ??
|
||||
{}) as Record<string, unknown>;
|
||||
const blameInquiry = pickInquiryReportPayload(
|
||||
inquiryRoleData(blame?.inquiries as Record<string, unknown> | undefined, "carBody", role),
|
||||
);
|
||||
const claimInquiry = pickInquiryReportPayload(
|
||||
inquiryRoleData(claim?.inquiries as Record<string, unknown> | undefined, "carBody", role),
|
||||
);
|
||||
const legacy = (blame?.carBodyInsuranceDetail ?? {}) as Record<string, unknown>;
|
||||
|
||||
return [
|
||||
{
|
||||
label: PR.policyNumber,
|
||||
value: insuranceValueFromCandidates(
|
||||
direct.policyNumber,
|
||||
legacy.policyNumber,
|
||||
blameInquiry?.policyNumber,
|
||||
blameInquiry?.PolicyNumber,
|
||||
claimInquiry?.policyNumber,
|
||||
claimInquiry?.PolicyNumber,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: PR.insuranceCompany,
|
||||
value: insuranceValueFromCandidates(
|
||||
direct.insurerCompany,
|
||||
direct.company,
|
||||
legacy.insurerCompany,
|
||||
blameInquiry?.company,
|
||||
blameInquiry?.CompanyName,
|
||||
claimInquiry?.company,
|
||||
claimInquiry?.CompanyName,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: PR.policyStartDate,
|
||||
value: insuranceValueFromCandidates(
|
||||
direct.startDate,
|
||||
legacy.startDate,
|
||||
blameInquiry?.startDate,
|
||||
blameInquiry?.PolicyStartDate,
|
||||
claimInquiry?.startDate,
|
||||
claimInquiry?.PolicyStartDate,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: PR.policyEndDate,
|
||||
value: insuranceValueFromCandidates(
|
||||
direct.endDate,
|
||||
legacy.endDate,
|
||||
blameInquiry?.endDate,
|
||||
blameInquiry?.PolicyEndDate,
|
||||
claimInquiry?.endDate,
|
||||
claimInquiry?.PolicyEndDate,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: PR.coverages,
|
||||
value: insuranceValueFromCandidates(
|
||||
direct.coverages,
|
||||
legacy.coverages,
|
||||
blameInquiry?.coverages,
|
||||
claimInquiry?.coverages,
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildOwnerSection(
|
||||
damagedParty: ReturnType<typeof resolveDamagedPartyRow>,
|
||||
damagedParty: ReportParty | null,
|
||||
claim?: Record<string, unknown> | null,
|
||||
): InsurerFileReportSection {
|
||||
const person = damagedParty?.person;
|
||||
const person = damagedParty?.person as ReportRecord | undefined;
|
||||
const money = claim?.money as
|
||||
| { sheba?: string; nationalCodeOfInsurer?: string }
|
||||
| undefined;
|
||||
const claimOwner = claim?.owner as { fullName?: string } | undefined;
|
||||
|
||||
return buildSection(PR.ownerSection, [
|
||||
{ label: PR.name, value: firstDefined(person?.fullName, claimOwner?.fullName) },
|
||||
{ label: PR.phone, value: asString(person?.phoneNumber) },
|
||||
{
|
||||
label: PR.nationalCode,
|
||||
value: firstDefined(person?.nationalCodeOfInsurer, money?.nationalCodeOfInsurer),
|
||||
},
|
||||
{
|
||||
label: PR.birthDate,
|
||||
value: formatBirthDate(person?.insurerBirthday ?? person?.birthday),
|
||||
},
|
||||
{ label: PR.sheba, value: money?.sheba },
|
||||
]);
|
||||
}
|
||||
|
||||
function licenseFieldsFromInquiry(
|
||||
inquiry?: Record<string, unknown>,
|
||||
): { licenseType?: string; licenseDate?: string } {
|
||||
if (!inquiry) return {};
|
||||
return {
|
||||
title: PR.ownerSection,
|
||||
fields: [
|
||||
{ label: PR.name, value: person?.fullName ?? claimOwner?.fullName },
|
||||
{ label: PR.phone, value: person?.phoneNumber },
|
||||
{
|
||||
label: PR.nationalCode,
|
||||
value: person?.nationalCodeOfInsurer ?? money?.nationalCodeOfInsurer,
|
||||
},
|
||||
{
|
||||
label: PR.birthDate,
|
||||
value: formatBirthDate(person?.insurerBirthday ?? person?.birthday),
|
||||
},
|
||||
{ label: PR.sheba, value: money?.sheba },
|
||||
],
|
||||
licenseType: firstDefined(
|
||||
inquiry.LicenseType,
|
||||
inquiry.licenseType,
|
||||
inquiry.Type,
|
||||
inquiry.type,
|
||||
inquiry.LicenseCategory,
|
||||
inquiry.licenseCategory,
|
||||
),
|
||||
licenseDate: firstDefined(
|
||||
inquiry.IssueDate,
|
||||
inquiry.issueDate,
|
||||
inquiry.LicenseIssueDate,
|
||||
inquiry.licenseIssueDate,
|
||||
inquiry.ExpireDate,
|
||||
inquiry.expireDate,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function buildDriverSection(
|
||||
damagedParty: ReturnType<typeof resolveDamagedPartyRow>,
|
||||
damagedParty: ReportParty | null,
|
||||
blame?: Record<string, unknown> | null,
|
||||
): InsurerFileReportSection | undefined {
|
||||
const person = damagedParty?.person;
|
||||
const person = damagedParty?.person as ReportRecord | undefined;
|
||||
if (!person || person.driverIsInsurer !== false) return undefined;
|
||||
|
||||
const role = damagedParty?.role ?? PartyRole.FIRST;
|
||||
const licenseInquiry = inquiryRoleData(
|
||||
blame?.inquiries as Record<string, unknown> | undefined,
|
||||
"drivingLicence",
|
||||
role,
|
||||
String(role),
|
||||
);
|
||||
const { licenseType, licenseDate } = licenseFieldsFromInquiry(licenseInquiry);
|
||||
|
||||
return {
|
||||
title: PR.driverSection,
|
||||
fields: [
|
||||
{ label: PR.name, value: person.fullName },
|
||||
{
|
||||
label: PR.licenseType,
|
||||
value: licenseType ?? (person.driverLicense ? PR.driverLicense : undefined),
|
||||
},
|
||||
{
|
||||
label: PR.licenseDate,
|
||||
value: licenseDate ?? person.driverLicense,
|
||||
},
|
||||
{ label: PR.phone, value: person.phoneNumber },
|
||||
{ label: PR.nationalCode, value: person.nationalCodeOfDriver },
|
||||
{ label: PR.birthDate, value: formatBirthDate(person.driverBirthday) },
|
||||
{ label: PR.licenseNumber, value: person.driverLicense },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function buildInsuranceSection(
|
||||
damagedParty: ReturnType<typeof resolveDamagedPartyRow>,
|
||||
blame?: Record<string, unknown> | null,
|
||||
claim?: Record<string, unknown> | null,
|
||||
): InsurerFileReportSection {
|
||||
const role = damagedParty?.role ?? PartyRole.FIRST;
|
||||
const insurance = damagedParty?.insurance ?? {};
|
||||
const carBodyLegacy = blame?.carBodyInsuranceDetail as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
|
||||
const thirdPartyInquiry = inquiryRoleData(
|
||||
blame?.inquiries as Record<string, unknown> | undefined,
|
||||
"thirdParty",
|
||||
role,
|
||||
);
|
||||
const carBodyInquiry = inquiryRoleData(
|
||||
blame?.inquiries as Record<string, unknown> | undefined,
|
||||
"carBody",
|
||||
role,
|
||||
);
|
||||
const claimThirdParty = inquiryRoleData(
|
||||
claim?.inquiries as Record<string, unknown> | undefined,
|
||||
"thirdParty",
|
||||
role,
|
||||
);
|
||||
const claimCarBody = inquiryRoleData(
|
||||
claim?.inquiries as Record<string, unknown> | undefined,
|
||||
"carBody",
|
||||
role,
|
||||
);
|
||||
|
||||
const fields: InsurerFileReportField[] = [
|
||||
...flattenObject(insurance, "party.insurance"),
|
||||
...flattenObject(
|
||||
(insurance as { carBodyInsurance?: unknown }).carBodyInsurance,
|
||||
"party.insurance.carBodyInsurance",
|
||||
),
|
||||
...flattenObject(
|
||||
pickInquiryReportPayload(thirdPartyInquiry),
|
||||
"inquiry.thirdParty",
|
||||
),
|
||||
...flattenObject(pickInquiryReportPayload(carBodyInquiry), "inquiry.carBody"),
|
||||
...flattenObject(
|
||||
pickInquiryReportPayload(claimThirdParty),
|
||||
"claim.inquiry.thirdParty",
|
||||
),
|
||||
...flattenObject(
|
||||
pickInquiryReportPayload(claimCarBody),
|
||||
"claim.inquiry.carBody",
|
||||
),
|
||||
...flattenObject(carBodyLegacy, "blame.carBodyInsuranceDetail"),
|
||||
];
|
||||
|
||||
const deduped = dedupeFields(fields);
|
||||
return {
|
||||
title: PR.insuranceSection,
|
||||
fields: deduped.length ? deduped : [{ label: PR.data, value: PR.empty }],
|
||||
};
|
||||
return buildSection(PR.driverSection, [
|
||||
{ label: PR.name, value: asString(person.fullName) },
|
||||
{
|
||||
label: PR.licenseType,
|
||||
value: licenseType ?? (person.driverLicense ? PR.driverLicense : undefined),
|
||||
},
|
||||
{
|
||||
label: PR.licenseDate,
|
||||
value: licenseDate ?? asString(person.driverLicense),
|
||||
},
|
||||
{ label: PR.phone, value: asString(person.phoneNumber) },
|
||||
{ label: PR.nationalCode, value: asString(person.nationalCodeOfDriver) },
|
||||
{ label: PR.birthDate, value: formatBirthDate(person.driverBirthday) },
|
||||
{ label: PR.licenseNumber, value: asString(person.driverLicense) },
|
||||
]);
|
||||
}
|
||||
|
||||
function buildVehicleSection(
|
||||
damagedParty: ReturnType<typeof resolveDamagedPartyRow>,
|
||||
damagedParty: ReportParty | null,
|
||||
claim?: Record<string, unknown> | null,
|
||||
): InsurerFileReportSection {
|
||||
const partyVehicle = damagedParty?.vehicle;
|
||||
const claimVehicle = claim?.vehicle as Record<string, unknown> | undefined;
|
||||
|
||||
const fields = dedupeFields([
|
||||
return buildSection(PR.vehicleSection, [
|
||||
...flattenObject(claimVehicle, "claim.vehicle"),
|
||||
...flattenObject(partyVehicle, "party.vehicle"),
|
||||
]);
|
||||
}
|
||||
|
||||
return {
|
||||
title: PR.vehicleSection,
|
||||
fields: fields.length ? fields : [{ label: PR.data, value: PR.empty }],
|
||||
};
|
||||
function buildCaseTimelineSection(
|
||||
overview?: Record<string, unknown> | null,
|
||||
claim?: Record<string, unknown> | null,
|
||||
): InsurerFileReportSection {
|
||||
const evaluationReply = resolveEvaluationReply(claim);
|
||||
return buildSection(PR.timelineSection, [
|
||||
{
|
||||
label: PR.fileRegisteredAt,
|
||||
value: formatDateTime(overview?.createdAt),
|
||||
},
|
||||
{
|
||||
label: PR.evaluationRegisteredAt,
|
||||
value: formatDateTime(evaluationReply?.submittedAt),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function buildFanavaranCodesSection(
|
||||
claim?: Record<string, unknown> | null,
|
||||
): InsurerFileReportSection | undefined {
|
||||
if (!claim) return undefined;
|
||||
const sync = (claim.fanavaranSync as Record<string, unknown> | undefined) ?? {};
|
||||
const baseClaim = (sync.baseClaim as Record<string, unknown> | undefined) ?? {};
|
||||
const damageCase = (sync.damageCase as Record<string, unknown> | undefined) ?? {};
|
||||
const expertise = (sync.expertise as Record<string, unknown> | undefined) ?? {};
|
||||
|
||||
return buildSection(PR.fanavaranSection, [
|
||||
{
|
||||
label: PR.fanavaranClaimNo,
|
||||
value: firstDefined(claim.claimNo, baseClaim.claimNo),
|
||||
},
|
||||
{
|
||||
label: PR.fanavaranClaimId,
|
||||
value: firstDefined(claim.claimId, baseClaim.claimId),
|
||||
},
|
||||
{
|
||||
label: PR.fanavaranDamageCaseId,
|
||||
value: firstDefined(claim.dmgCaseId, damageCase.dmgCaseId, expertise.dmgCaseId),
|
||||
},
|
||||
{
|
||||
label: PR.fanavaranExpertiseId,
|
||||
value: firstDefined(claim.expertiseId, expertise.expertiseId),
|
||||
},
|
||||
{
|
||||
label: PR.fanavaranPolicyId,
|
||||
value: firstDefined(baseClaim.policyId),
|
||||
},
|
||||
{
|
||||
label: PR.fanavaranDriverId,
|
||||
value: firstDefined(baseClaim.driverId),
|
||||
},
|
||||
{
|
||||
label: PR.fanavaranVehicleKindId,
|
||||
value: firstDefined(baseClaim.vehicleKindId),
|
||||
},
|
||||
{
|
||||
label: PR.fanavaranInsuranceCorpId,
|
||||
value: firstDefined(baseClaim.insuranceCorpId),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function buildEvaluationSection(
|
||||
claim?: Record<string, unknown> | null,
|
||||
): InsurerFileReportSection | undefined {
|
||||
if (!claim) return undefined;
|
||||
const reply = resolveEvaluationReply(claim);
|
||||
const actorDetail = reply?.actorDetail as { actorName?: string } | undefined;
|
||||
const snapshotName = expertNameFromSnapshot(
|
||||
reply?.expertProfileSnapshot as
|
||||
| { firstName?: string; lastName?: string }
|
||||
| undefined,
|
||||
);
|
||||
|
||||
return buildSection(PR.evaluationSection, [
|
||||
{
|
||||
label: PR.evaluationResult,
|
||||
value: persianStatus(claim.claimStatus),
|
||||
},
|
||||
{
|
||||
label: PR.evaluationExpert,
|
||||
value: actorDetail?.actorName ?? snapshotName,
|
||||
},
|
||||
{
|
||||
label: PR.evaluationSubmittedAt,
|
||||
value: formatDateTime(reply?.submittedAt),
|
||||
},
|
||||
{
|
||||
label: PR.evaluationResponse,
|
||||
value: asString(reply?.description),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function buildAccidentReportSection(
|
||||
blame?: Record<string, unknown> | null,
|
||||
claim?: Record<string, unknown> | null,
|
||||
damagedParty?: ReturnType<typeof resolveDamagedPartyRow>,
|
||||
damagedParty?: ReportParty | null,
|
||||
): InsurerFileReportSection {
|
||||
const statement = damagedParty?.statement as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const statement = damagedParty?.statement as ReportRecord | undefined;
|
||||
const location = damagedParty?.location;
|
||||
const snapshotAccident = (
|
||||
claim?.snapshot as { accident?: Record<string, unknown> } | undefined
|
||||
@@ -344,22 +656,21 @@ function buildAccidentReportSection(
|
||||
const blameDecision = (
|
||||
blame?.expert as Record<string, unknown> | undefined
|
||||
)?.decision as Record<string, unknown> | undefined;
|
||||
const decisionFields = blameDecision?.fields as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const decisionFields = blameDecision?.fields as Record<string, unknown> | undefined;
|
||||
|
||||
const accidentDate =
|
||||
asString(statement?.accidentDate) ??
|
||||
asString(snapshotAccident?.date) ??
|
||||
asString(blame?.createdAtFormatted) ??
|
||||
asString(blame?.createdAt);
|
||||
|
||||
const accidentTime =
|
||||
asString(statement?.accidentTime) ?? asString(snapshotAccident?.time);
|
||||
|
||||
const fields: InsurerFileReportField[] = [
|
||||
{ label: PR.date, value: accidentDate },
|
||||
{ label: PR.time, value: accidentTime },
|
||||
return buildSection(PR.accidentSection, [
|
||||
{
|
||||
label: PR.accidentDate,
|
||||
value:
|
||||
asString(statement?.accidentDate) ??
|
||||
asString(snapshotAccident?.date) ??
|
||||
asString(blame?.createdAtFormatted) ??
|
||||
formatDateTime(blame?.createdAt),
|
||||
},
|
||||
{
|
||||
label: PR.accidentTime,
|
||||
value: asString(statement?.accidentTime) ?? asString(snapshotAccident?.time),
|
||||
},
|
||||
{
|
||||
label: PR.experts,
|
||||
value: collectExpertNames(blame, claim),
|
||||
@@ -412,8 +723,7 @@ function buildAccidentReportSection(
|
||||
{
|
||||
label: PR.accidentReason,
|
||||
value: asString(
|
||||
(decisionFields?.accidentReason as { label?: string } | undefined)
|
||||
?.label ??
|
||||
(decisionFields?.accidentReason as { label?: string } | undefined)?.label ??
|
||||
(
|
||||
snapshotAccident?.classification as {
|
||||
accidentReason?: { label?: string };
|
||||
@@ -432,82 +742,61 @@ function buildAccidentReportSection(
|
||||
)?.accidentType?.label,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: PR.damageExpertDate,
|
||||
value: asString(
|
||||
(
|
||||
(claim?.evaluation as Record<string, unknown> | undefined)
|
||||
?.damageExpertReplyFinal as Record<string, unknown> | undefined
|
||||
)?.submittedAt ??
|
||||
(
|
||||
(claim?.evaluation as Record<string, unknown> | undefined)
|
||||
?.damageExpertReply as Record<string, unknown> | undefined
|
||||
)?.submittedAt,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: PR.damageExpertNotes,
|
||||
value: asString(
|
||||
(
|
||||
(claim?.evaluation as Record<string, unknown> | undefined)
|
||||
?.damageExpertReplyFinal as Record<string, unknown> | undefined
|
||||
)?.description ??
|
||||
(
|
||||
(claim?.evaluation as Record<string, unknown> | undefined)
|
||||
?.damageExpertReply as Record<string, unknown> | undefined
|
||||
)?.description,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: PR.partyDescription,
|
||||
value: asString(statement?.description),
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
title: PR.accidentSection,
|
||||
fields: dedupeFields(fields),
|
||||
};
|
||||
]);
|
||||
}
|
||||
|
||||
function dedupeFields(fields: InsurerFileReportField[]): InsurerFileReportField[] {
|
||||
const seen = new Set<string>();
|
||||
const out: InsurerFileReportField[] = [];
|
||||
for (const field of fields) {
|
||||
const value = asString(field.value);
|
||||
if (!value) continue;
|
||||
const key = `${field.label}::${value}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push({ label: field.label, value });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function buildInsurerFileReport(
|
||||
file: {
|
||||
overview?: Record<string, unknown>;
|
||||
blame?: Record<string, unknown>;
|
||||
claim?: Record<string, unknown>;
|
||||
},
|
||||
): InsurerFileReportViewModel {
|
||||
export function buildInsurerFileReport(file: {
|
||||
overview?: Record<string, unknown>;
|
||||
blame?: Record<string, unknown>;
|
||||
claim?: Record<string, unknown>;
|
||||
}): InsurerFileReportViewModel {
|
||||
const overview = file.overview ?? {};
|
||||
const blame = file.blame ?? null;
|
||||
const claim = file.claim ?? null;
|
||||
const damagedParty = blame ? resolveDamagedPartyRow(blame) : null;
|
||||
const blame = file.blame ?? null;
|
||||
const blameContext = resolveReportBlameContext(blame, claim);
|
||||
const damagedParty = blameContext
|
||||
? (resolveDamagedPartyRow(blameContext as any) as ReportParty | null)
|
||||
: null;
|
||||
const guiltyParty = blameContext
|
||||
? (resolveClaimOwnerParty(blameContext as any) as ReportParty | null)
|
||||
: null;
|
||||
const isCarBody =
|
||||
String((blameContext?.type as string | undefined) ?? "") === "CAR_BODY";
|
||||
const includeGuiltySections = !!guiltyParty && !(isCarBody && sameParty(damagedParty, guiltyParty));
|
||||
|
||||
const sections: InsurerFileReportSection[] = [
|
||||
const sections = filterEmptySections([
|
||||
buildCaseTimelineSection(overview, claim),
|
||||
buildOwnerSection(damagedParty, claim),
|
||||
];
|
||||
|
||||
const driverSection = buildDriverSection(damagedParty, blame);
|
||||
if (driverSection) sections.push(driverSection);
|
||||
|
||||
sections.push(
|
||||
buildInsuranceSection(damagedParty, blame, claim),
|
||||
buildDriverSection(damagedParty, blameContext),
|
||||
buildSection(
|
||||
PR.damagedThirdPartyInsuranceSection,
|
||||
buildThirdPartyInsuranceFields(damagedParty, blameContext, claim),
|
||||
),
|
||||
buildSection(
|
||||
PR.damagedCarBodyInsuranceSection,
|
||||
buildCarBodyInsuranceFields(damagedParty, blameContext, claim),
|
||||
),
|
||||
includeGuiltySections
|
||||
? buildSection(
|
||||
PR.guiltyThirdPartyInsuranceSection,
|
||||
buildThirdPartyInsuranceFields(guiltyParty, blameContext, claim),
|
||||
)
|
||||
: undefined,
|
||||
includeGuiltySections
|
||||
? buildSection(
|
||||
PR.guiltyCarBodyInsuranceSection,
|
||||
buildCarBodyInsuranceFields(guiltyParty, blameContext, claim),
|
||||
)
|
||||
: undefined,
|
||||
buildVehicleSection(damagedParty, claim),
|
||||
buildAccidentReportSection(blame, claim, damagedParty),
|
||||
);
|
||||
buildFanavaranCodesSection(claim),
|
||||
buildEvaluationSection(claim),
|
||||
buildAccidentReportSection(blameContext, claim, damagedParty),
|
||||
]);
|
||||
|
||||
return {
|
||||
title: PR.reportTitle,
|
||||
|
||||
@@ -35,7 +35,7 @@ export class CaseExpertReportInsurerController {
|
||||
@ApiOperation({
|
||||
summary: "Get insurer file report data",
|
||||
description:
|
||||
"Returns the structured report data for the given publicId (blame + claim combined): damaged owner, driver when different, insurance, vehicle, and accident report sections. The front-end uses this data to render and generate the PDF.",
|
||||
"Returns structured insurer PDF data for the given publicId (blame + claim combined), including separated guilty/damaged insurance blocks, third-party/body policy details, Fanavaran codes, case/evaluation timestamps, and the evaluation result with expert response.",
|
||||
})
|
||||
@ApiParam({ name: "publicId" })
|
||||
@ApiResponse({ status: 200, description: "Report data" })
|
||||
|
||||
@@ -5,8 +5,14 @@ export const PR = {
|
||||
empty: "-",
|
||||
ownerSection: "مالک خودروی زیان دیده",
|
||||
driverSection: "راننده خودروی زیان دیده",
|
||||
insuranceSection: "اطلاعات بیمه (بدنه و شخص ثالث)",
|
||||
damagedThirdPartyInsuranceSection: "بیمه شخص ثالث زیاندیده",
|
||||
damagedCarBodyInsuranceSection: "بیمه بدنه زیاندیده",
|
||||
guiltyThirdPartyInsuranceSection: "بیمه شخص ثالث مقصر",
|
||||
guiltyCarBodyInsuranceSection: "بیمه بدنه مقصر",
|
||||
vehicleSection: "اطلاعات خودرو",
|
||||
timelineSection: "زمانبندی پرونده",
|
||||
fanavaranSection: "کدهای فناوران",
|
||||
evaluationSection: "نتیجه ارزیابی",
|
||||
accidentSection: "گزارش حادثه",
|
||||
name: "نام",
|
||||
phone: "شماره تلفن",
|
||||
@@ -17,9 +23,31 @@ export const PR = {
|
||||
licenseDate: "تاریخ گواهینامه",
|
||||
licenseNumber: "شماره گواهینامه",
|
||||
driverLicense: "گواهینامه راننده",
|
||||
insuranceCompany: "شرکت بیمه",
|
||||
policyNumber: "شماره بیمهنامه",
|
||||
policyStartDate: "تاریخ شروع بیمهنامه",
|
||||
policyEndDate: "تاریخ پایان بیمهنامه",
|
||||
financialCeiling: "سقف تعهد مالی",
|
||||
coverages: "پوششها",
|
||||
fileRegisteredAt: "تاریخ و ساعت ثبت پرونده",
|
||||
evaluationRegisteredAt: "تاریخ و ساعت ثبت نتیجه ارزیابی",
|
||||
fanavaranClaimNo: "شماره پرونده فناوران",
|
||||
fanavaranClaimId: "کد پرونده فناوران",
|
||||
fanavaranDamageCaseId: "کد کیس خسارت فناوران",
|
||||
fanavaranExpertiseId: "کد کارشناسی فناوران",
|
||||
fanavaranPolicyId: "کد بیمهنامه فناوران",
|
||||
fanavaranDriverId: "کد راننده فناوران",
|
||||
fanavaranVehicleKindId: "کد نوع خودرو فناوران",
|
||||
fanavaranInsuranceCorpId: "کد شرکت بیمه فناوران",
|
||||
evaluationResult: "نتیجه ارزیابی",
|
||||
evaluationExpert: "کارشناس ارزیاب",
|
||||
evaluationSubmittedAt: "تاریخ و ساعت ثبت ارزیابی",
|
||||
evaluationResponse: "پاسخ / توضیحات کارشناس",
|
||||
data: "اطلاعات",
|
||||
date: "تاریخ",
|
||||
time: "زمان",
|
||||
accidentDate: "تاریخ حادثه",
|
||||
accidentTime: "ساعت حادثه",
|
||||
experts: "کارشناس(ان)",
|
||||
location: "موقعیت (عرض و طول جغرافیایی)",
|
||||
weather: "وضعیت آب و هوا",
|
||||
|
||||
Reference in New Issue
Block a user