Merge pull request 'YARA-1241' (#276) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#276
This commit is contained in:
2026-08-24 16:51:16 +03:30
4 changed files with 725 additions and 225 deletions

View 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(
"نیاز به تعویض سپر جلو",
);
});
});

View File

@@ -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 { toJalaliDateAndTime } from "src/helpers/date-jalali";
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum"; import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
import { import {
@@ -15,8 +18,20 @@ const SKIP_FLATTEN_KEYS = new Set([
"workflow", "workflow",
"evidence", "evidence",
"confirmation", "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 { function asString(value: unknown): string | undefined {
if (value === undefined || value === null || value === "") return undefined; if (value === undefined || value === null || value === "") return undefined;
if (value instanceof Date) { if (value instanceof Date) {
@@ -42,6 +57,30 @@ function formatBirthDate(value: unknown): string | undefined {
return raw; 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( function flattenObject(
obj: unknown, obj: unknown,
prefix = "", prefix = "",
@@ -53,13 +92,10 @@ function flattenObject(
} }
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
if (!obj.length) return []; const value = normalizeListValue(obj);
return [ return value
{ ? [{ label: persianFieldPath(prefix || "items"), value }]
label: persianFieldPath(prefix || "items"), : [];
value: obj.map((item) => asString(item) ?? JSON.stringify(item)).join("، "),
},
];
} }
if (typeof obj !== "object") { if (typeof obj !== "object") {
@@ -70,6 +106,7 @@ function flattenObject(
const objRecord = obj as Record<string, unknown>; const objRecord = obj as Record<string, unknown>;
const hasMapped = const hasMapped =
objRecord.mapped != null && typeof objRecord.mapped === "object"; objRecord.mapped != null && typeof objRecord.mapped === "object";
for (const [key, value] of Object.entries(objRecord)) { for (const [key, value] of Object.entries(objRecord)) {
if (SKIP_FLATTEN_KEYS.has(key)) continue; if (SKIP_FLATTEN_KEYS.has(key)) continue;
if (key === "raw" && hasMapped) continue; if (key === "raw" && hasMapped) continue;
@@ -86,9 +123,49 @@ function flattenObject(
rows.push({ label: persianFieldPath(path), value: asString(value) }); rows.push({ label: persianFieldPath(path), value: asString(value) });
} }
} }
return rows; 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?: { function expertNameFromSnapshot(snapshot?: {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
@@ -146,7 +223,6 @@ function inquiryRoleData(
return data; return data;
} }
/** Prefer mapped Tejarat fields; avoid duplicating the entire raw blob in PDF. */
function pickInquiryReportPayload( function pickInquiryReportPayload(
inquiry?: Record<string, unknown>, inquiry?: Record<string, unknown>,
): Record<string, unknown> | undefined { ): Record<string, unknown> | undefined {
@@ -159,184 +235,420 @@ function pickInquiryReportPayload(
return Object.keys(rest).length ? rest : inquiry; return Object.keys(rest).length ? rest : inquiry;
} }
function licenseFieldsFromInquiry( function resolveReportBlameContext(
inquiry?: Record<string, unknown>, blame?: Record<string, unknown> | null,
): { licenseType?: string; licenseDate?: string } { claim?: Record<string, unknown> | null,
if (!inquiry) return {}; ): Record<string, unknown> | null {
const candidates = [ if (blame) return blame;
inquiry.LicenseType, const snapshot = claim?.snapshot as Record<string, unknown> | undefined;
inquiry.licenseType, if (!snapshot) return null;
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 !== "");
return { return {
licenseType: asString(licenseType), type:
licenseDate: asString(licenseDate), (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( function buildOwnerSection(
damagedParty: ReturnType<typeof resolveDamagedPartyRow>, damagedParty: ReportParty | null,
claim?: Record<string, unknown> | null, claim?: Record<string, unknown> | null,
): InsurerFileReportSection { ): InsurerFileReportSection {
const person = damagedParty?.person; const person = damagedParty?.person as ReportRecord | undefined;
const money = claim?.money as const money = claim?.money as
| { sheba?: string; nationalCodeOfInsurer?: string } | { sheba?: string; nationalCodeOfInsurer?: string }
| undefined; | undefined;
const claimOwner = claim?.owner as { fullName?: 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 { return {
title: PR.ownerSection, licenseType: firstDefined(
fields: [ inquiry.LicenseType,
{ label: PR.name, value: person?.fullName ?? claimOwner?.fullName }, inquiry.licenseType,
{ label: PR.phone, value: person?.phoneNumber }, inquiry.Type,
{ inquiry.type,
label: PR.nationalCode, inquiry.LicenseCategory,
value: person?.nationalCodeOfInsurer ?? money?.nationalCodeOfInsurer, inquiry.licenseCategory,
}, ),
{ licenseDate: firstDefined(
label: PR.birthDate, inquiry.IssueDate,
value: formatBirthDate(person?.insurerBirthday ?? person?.birthday), inquiry.issueDate,
}, inquiry.LicenseIssueDate,
{ label: PR.sheba, value: money?.sheba }, inquiry.licenseIssueDate,
], inquiry.ExpireDate,
inquiry.expireDate,
),
}; };
} }
function buildDriverSection( function buildDriverSection(
damagedParty: ReturnType<typeof resolveDamagedPartyRow>, damagedParty: ReportParty | null,
blame?: Record<string, unknown> | null, blame?: Record<string, unknown> | null,
): InsurerFileReportSection | undefined { ): InsurerFileReportSection | undefined {
const person = damagedParty?.person; const person = damagedParty?.person as ReportRecord | undefined;
if (!person || person.driverIsInsurer !== false) return undefined; if (!person || person.driverIsInsurer !== false) return undefined;
const role = damagedParty?.role ?? PartyRole.FIRST; const role = damagedParty?.role ?? PartyRole.FIRST;
const licenseInquiry = inquiryRoleData( const licenseInquiry = inquiryRoleData(
blame?.inquiries as Record<string, unknown> | undefined, blame?.inquiries as Record<string, unknown> | undefined,
"drivingLicence", "drivingLicence",
role, String(role),
); );
const { licenseType, licenseDate } = licenseFieldsFromInquiry(licenseInquiry); const { licenseType, licenseDate } = licenseFieldsFromInquiry(licenseInquiry);
return { return buildSection(PR.driverSection, [
title: PR.driverSection, { label: PR.name, value: asString(person.fullName) },
fields: [ {
{ label: PR.name, value: person.fullName }, label: PR.licenseType,
{ value: licenseType ?? (person.driverLicense ? PR.driverLicense : undefined),
label: PR.licenseType, },
value: licenseType ?? (person.driverLicense ? PR.driverLicense : undefined), {
}, label: PR.licenseDate,
{ value: licenseDate ?? asString(person.driverLicense),
label: PR.licenseDate, },
value: licenseDate ?? person.driverLicense, { label: PR.phone, value: asString(person.phoneNumber) },
}, { label: PR.nationalCode, value: asString(person.nationalCodeOfDriver) },
{ label: PR.phone, value: person.phoneNumber }, { label: PR.birthDate, value: formatBirthDate(person.driverBirthday) },
{ label: PR.nationalCode, value: person.nationalCodeOfDriver }, { label: PR.licenseNumber, value: asString(person.driverLicense) },
{ 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 }],
};
} }
function buildVehicleSection( function buildVehicleSection(
damagedParty: ReturnType<typeof resolveDamagedPartyRow>, damagedParty: ReportParty | null,
claim?: Record<string, unknown> | null, claim?: Record<string, unknown> | null,
): InsurerFileReportSection { ): InsurerFileReportSection {
const partyVehicle = damagedParty?.vehicle; const partyVehicle = damagedParty?.vehicle;
const claimVehicle = claim?.vehicle as Record<string, unknown> | undefined; const claimVehicle = claim?.vehicle as Record<string, unknown> | undefined;
const fields = dedupeFields([ return buildSection(PR.vehicleSection, [
...flattenObject(claimVehicle, "claim.vehicle"), ...flattenObject(claimVehicle, "claim.vehicle"),
...flattenObject(partyVehicle, "party.vehicle"), ...flattenObject(partyVehicle, "party.vehicle"),
]); ]);
}
return { function buildCaseTimelineSection(
title: PR.vehicleSection, overview?: Record<string, unknown> | null,
fields: fields.length ? fields : [{ label: PR.data, value: PR.empty }], 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( function buildAccidentReportSection(
blame?: Record<string, unknown> | null, blame?: Record<string, unknown> | null,
claim?: Record<string, unknown> | null, claim?: Record<string, unknown> | null,
damagedParty?: ReturnType<typeof resolveDamagedPartyRow>, damagedParty?: ReportParty | null,
): InsurerFileReportSection { ): InsurerFileReportSection {
const statement = damagedParty?.statement as const statement = damagedParty?.statement as ReportRecord | undefined;
| Record<string, unknown>
| undefined;
const location = damagedParty?.location; const location = damagedParty?.location;
const snapshotAccident = ( const snapshotAccident = (
claim?.snapshot as { accident?: Record<string, unknown> } | undefined claim?.snapshot as { accident?: Record<string, unknown> } | undefined
@@ -344,22 +656,21 @@ function buildAccidentReportSection(
const blameDecision = ( const blameDecision = (
blame?.expert as Record<string, unknown> | undefined blame?.expert as Record<string, unknown> | undefined
)?.decision as Record<string, unknown> | undefined; )?.decision as Record<string, unknown> | undefined;
const decisionFields = blameDecision?.fields as const decisionFields = blameDecision?.fields as Record<string, unknown> | undefined;
| Record<string, unknown>
| undefined;
const accidentDate = return buildSection(PR.accidentSection, [
asString(statement?.accidentDate) ?? {
asString(snapshotAccident?.date) ?? label: PR.accidentDate,
asString(blame?.createdAtFormatted) ?? value:
asString(blame?.createdAt); asString(statement?.accidentDate) ??
asString(snapshotAccident?.date) ??
const accidentTime = asString(blame?.createdAtFormatted) ??
asString(statement?.accidentTime) ?? asString(snapshotAccident?.time); formatDateTime(blame?.createdAt),
},
const fields: InsurerFileReportField[] = [ {
{ label: PR.date, value: accidentDate }, label: PR.accidentTime,
{ label: PR.time, value: accidentTime }, value: asString(statement?.accidentTime) ?? asString(snapshotAccident?.time),
},
{ {
label: PR.experts, label: PR.experts,
value: collectExpertNames(blame, claim), value: collectExpertNames(blame, claim),
@@ -412,8 +723,7 @@ function buildAccidentReportSection(
{ {
label: PR.accidentReason, label: PR.accidentReason,
value: asString( value: asString(
(decisionFields?.accidentReason as { label?: string } | undefined) (decisionFields?.accidentReason as { label?: string } | undefined)?.label ??
?.label ??
( (
snapshotAccident?.classification as { snapshotAccident?.classification as {
accidentReason?: { label?: string }; accidentReason?: { label?: string };
@@ -432,82 +742,61 @@ function buildAccidentReportSection(
)?.accidentType?.label, )?.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, label: PR.partyDescription,
value: asString(statement?.description), value: asString(statement?.description),
}, },
]; ]);
return {
title: PR.accidentSection,
fields: dedupeFields(fields),
};
} }
function dedupeFields(fields: InsurerFileReportField[]): InsurerFileReportField[] { export function buildInsurerFileReport(file: {
const seen = new Set<string>(); overview?: Record<string, unknown>;
const out: InsurerFileReportField[] = []; blame?: Record<string, unknown>;
for (const field of fields) { claim?: Record<string, unknown>;
const value = asString(field.value); }): InsurerFileReportViewModel {
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 {
const overview = file.overview ?? {}; const overview = file.overview ?? {};
const blame = file.blame ?? null;
const claim = file.claim ?? 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), buildOwnerSection(damagedParty, claim),
]; buildDriverSection(damagedParty, blameContext),
buildSection(
const driverSection = buildDriverSection(damagedParty, blame); PR.damagedThirdPartyInsuranceSection,
if (driverSection) sections.push(driverSection); buildThirdPartyInsuranceFields(damagedParty, blameContext, claim),
),
sections.push( buildSection(
buildInsuranceSection(damagedParty, blame, claim), 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), buildVehicleSection(damagedParty, claim),
buildAccidentReportSection(blame, claim, damagedParty), buildFanavaranCodesSection(claim),
); buildEvaluationSection(claim),
buildAccidentReportSection(blameContext, claim, damagedParty),
]);
return { return {
title: PR.reportTitle, title: PR.reportTitle,

View File

@@ -35,7 +35,7 @@ export class CaseExpertReportInsurerController {
@ApiOperation({ @ApiOperation({
summary: "Get insurer file report data", summary: "Get insurer file report data",
description: 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" }) @ApiParam({ name: "publicId" })
@ApiResponse({ status: 200, description: "Report data" }) @ApiResponse({ status: 200, description: "Report data" })

View File

@@ -5,8 +5,14 @@ export const PR = {
empty: "-", empty: "-",
ownerSection: "مالک خودروی زیان دیده", ownerSection: "مالک خودروی زیان دیده",
driverSection: "راننده خودروی زیان دیده", driverSection: "راننده خودروی زیان دیده",
insuranceSection: "اطلاعات بیمه (بدنه و شخص ثالث)", damagedThirdPartyInsuranceSection: "بیمه شخص ثالث زیان‌دیده",
damagedCarBodyInsuranceSection: "بیمه بدنه زیان‌دیده",
guiltyThirdPartyInsuranceSection: "بیمه شخص ثالث مقصر",
guiltyCarBodyInsuranceSection: "بیمه بدنه مقصر",
vehicleSection: "اطلاعات خودرو", vehicleSection: "اطلاعات خودرو",
timelineSection: "زمان‌بندی پرونده",
fanavaranSection: "کدهای فناوران",
evaluationSection: "نتیجه ارزیابی",
accidentSection: "گزارش حادثه", accidentSection: "گزارش حادثه",
name: "نام", name: "نام",
phone: "شماره تلفن", phone: "شماره تلفن",
@@ -17,9 +23,31 @@ export const PR = {
licenseDate: "تاریخ گواهینامه", licenseDate: "تاریخ گواهینامه",
licenseNumber: "شماره گواهینامه", licenseNumber: "شماره گواهینامه",
driverLicense: "گواهینامه راننده", driverLicense: "گواهینامه راننده",
insuranceCompany: "شرکت بیمه",
policyNumber: "شماره بیمه‌نامه",
policyStartDate: "تاریخ شروع بیمه‌نامه",
policyEndDate: "تاریخ پایان بیمه‌نامه",
financialCeiling: "سقف تعهد مالی",
coverages: "پوشش‌ها",
fileRegisteredAt: "تاریخ و ساعت ثبت پرونده",
evaluationRegisteredAt: "تاریخ و ساعت ثبت نتیجه ارزیابی",
fanavaranClaimNo: "شماره پرونده فناوران",
fanavaranClaimId: "کد پرونده فناوران",
fanavaranDamageCaseId: "کد کیس خسارت فناوران",
fanavaranExpertiseId: "کد کارشناسی فناوران",
fanavaranPolicyId: "کد بیمه‌نامه فناوران",
fanavaranDriverId: "کد راننده فناوران",
fanavaranVehicleKindId: "کد نوع خودرو فناوران",
fanavaranInsuranceCorpId: "کد شرکت بیمه فناوران",
evaluationResult: "نتیجه ارزیابی",
evaluationExpert: "کارشناس ارزیاب",
evaluationSubmittedAt: "تاریخ و ساعت ثبت ارزیابی",
evaluationResponse: "پاسخ / توضیحات کارشناس",
data: "اطلاعات", data: "اطلاعات",
date: "تاریخ", date: "تاریخ",
time: "زمان", time: "زمان",
accidentDate: "تاریخ حادثه",
accidentTime: "ساعت حادثه",
experts: "کارشناس(ان)", experts: "کارشناس(ان)",
location: "موقعیت (عرض و طول جغرافیایی)", location: "موقعیت (عرض و طول جغرافیایی)",
weather: "وضعیت آب و هوا", weather: "وضعیت آب و هوا",