forked from Yara724/api
1152 lines
33 KiB
TypeScript
1152 lines
33 KiB
TypeScript
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 { sanitizeStoredInquiryParticipants } from "src/request-management/inquiry-participant-resolver";
|
||
import {
|
||
InsurerFileReportField,
|
||
InsurerFileReportSection,
|
||
InsurerFileReportViewModel,
|
||
} from "./case-expert-report.types";
|
||
import {
|
||
PR,
|
||
persianAccidentCondition,
|
||
persianFieldPath,
|
||
persianStatus,
|
||
} from "./persian-report-labels";
|
||
|
||
const SKIP_FLATTEN_KEYS = new Set([
|
||
"_id",
|
||
"__v",
|
||
"history",
|
||
"workflow",
|
||
"evidence",
|
||
"confirmation",
|
||
"inquiries",
|
||
"raw",
|
||
"source",
|
||
]);
|
||
|
||
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 };
|
||
participants?: ReportRecord[];
|
||
participantRoles?: ReportRecord;
|
||
};
|
||
|
||
function asString(value: unknown): string | undefined {
|
||
if (value === undefined || value === null || value === "") return undefined;
|
||
if (typeof value === "boolean") return value ? "بله" : "خیر";
|
||
if (value instanceof Date) {
|
||
const [d, t] = toJalaliDateAndTime(value);
|
||
return `${d} ${t}`;
|
||
}
|
||
if (typeof value === "object") {
|
||
if (typeof (value as { toString?: () => string }).toString === "function") {
|
||
const s = String(value);
|
||
if (s !== "[object Object]") return s;
|
||
}
|
||
return undefined;
|
||
}
|
||
return String(value);
|
||
}
|
||
|
||
function formatBirthDate(value: unknown): string | undefined {
|
||
if (value === undefined || value === null || value === "") return undefined;
|
||
const raw = String(value);
|
||
if (/^\d{8}$/.test(raw)) {
|
||
return `${raw.slice(0, 4)}/${raw.slice(4, 6)}/${raw.slice(6, 8)}`;
|
||
}
|
||
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}`;
|
||
}
|
||
|
||
const PERSIAN_NUMBER_FORMATTER = new Intl.NumberFormat("fa-IR", {
|
||
maximumFractionDigits: 0,
|
||
});
|
||
|
||
function normalizeNumber(value: unknown): number | undefined {
|
||
if (typeof value === "number") {
|
||
return Number.isFinite(value) ? value : undefined;
|
||
}
|
||
if (typeof value !== "string" || !value.trim()) return undefined;
|
||
|
||
const normalized = value
|
||
.trim()
|
||
.replace(/[۰-۹]/g, (digit) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(digit)))
|
||
.replace(/[٠-٩]/g, (digit) => String("٠١٢٣٤٥٦٧٨٩".indexOf(digit)))
|
||
.replace(/[٬,\s]/g, "");
|
||
const parsed = Number(normalized);
|
||
return Number.isFinite(parsed) ? parsed : undefined;
|
||
}
|
||
|
||
function formatToman(value: unknown): string | undefined {
|
||
if (value === undefined || value === null || value === "") return undefined;
|
||
const amount = normalizeNumber(value);
|
||
if (amount === undefined) return asString(value);
|
||
return `${PERSIAN_NUMBER_FORMATTER.format(amount)} تومان`;
|
||
}
|
||
|
||
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 = "",
|
||
depth = 0,
|
||
): InsurerFileReportField[] {
|
||
if (obj == null) return [];
|
||
if (depth > 4) {
|
||
return [{ label: persianFieldPath(prefix), value: asString(obj) }];
|
||
}
|
||
|
||
if (Array.isArray(obj)) {
|
||
const value = normalizeListValue(obj);
|
||
return value ? [{ label: persianFieldPath(prefix || "items"), value }] : [];
|
||
}
|
||
|
||
if (typeof obj !== "object") {
|
||
return [
|
||
{ label: persianFieldPath(prefix || "value"), value: asString(obj) },
|
||
];
|
||
}
|
||
|
||
const rows: InsurerFileReportField[] = [];
|
||
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;
|
||
if (value === undefined || value === null || value === "") continue;
|
||
|
||
const path = prefix ? `${prefix}.${key}` : key;
|
||
if (
|
||
typeof value === "object" &&
|
||
!Array.isArray(value) &&
|
||
!(value instanceof Date)
|
||
) {
|
||
rows.push(...flattenObject(value, path, depth + 1));
|
||
} else {
|
||
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;
|
||
}): string | undefined {
|
||
if (!snapshot) return undefined;
|
||
const name = [snapshot.firstName, snapshot.lastName]
|
||
.filter(Boolean)
|
||
.join(" ");
|
||
return name || undefined;
|
||
}
|
||
|
||
function collectExpertNames(
|
||
blame?: Record<string, unknown> | null,
|
||
claim?: Record<string, unknown> | null,
|
||
): string | undefined {
|
||
const names = new Set<string>();
|
||
|
||
const blameDecision = (blame?.expert as Record<string, unknown> | undefined)
|
||
?.decision as Record<string, unknown> | undefined;
|
||
const blameExpert = expertNameFromSnapshot(
|
||
blameDecision?.expertProfileSnapshot as
|
||
| { firstName?: string; lastName?: string }
|
||
| undefined,
|
||
);
|
||
if (blameExpert) names.add(blameExpert);
|
||
|
||
const evaluation = claim?.evaluation as Record<string, unknown> | undefined;
|
||
for (const key of ["damageExpertReplyFinal", "damageExpertReply"] as const) {
|
||
const reply = evaluation?.[key] as Record<string, unknown> | undefined;
|
||
if (!reply) continue;
|
||
const actor = (reply.actorDetail as { actorName?: string } | undefined)
|
||
?.actorName;
|
||
if (actor) names.add(actor);
|
||
const snap = expertNameFromSnapshot(
|
||
reply.expertProfileSnapshot as
|
||
| { firstName?: string; lastName?: string }
|
||
| undefined,
|
||
);
|
||
if (snap) names.add(snap);
|
||
}
|
||
|
||
return names.size ? [...names].join(", ") : undefined;
|
||
}
|
||
|
||
function inquiryRoleData(
|
||
inquiries: Record<string, unknown> | undefined,
|
||
key: string,
|
||
role?: string,
|
||
): Record<string, unknown> | undefined {
|
||
const block = inquiries?.[key] as Record<string, unknown> | undefined;
|
||
const data = block?.data as Record<string, unknown> | undefined;
|
||
if (!data) return undefined;
|
||
if (role && data[role] && typeof data[role] === "object") {
|
||
return data[role] as Record<string, unknown>;
|
||
}
|
||
return data;
|
||
}
|
||
|
||
function pickInquiryReportPayload(
|
||
inquiry?: Record<string, unknown>,
|
||
): Record<string, unknown> | undefined {
|
||
if (!inquiry) return undefined;
|
||
const mapped = inquiry.mapped;
|
||
if (mapped && typeof mapped === "object" && !Array.isArray(mapped)) {
|
||
return mapped as Record<string, unknown>;
|
||
}
|
||
const { raw: _raw, ...rest } = inquiry;
|
||
return Object.keys(rest).length ? rest : inquiry;
|
||
}
|
||
|
||
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 {
|
||
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 partyKindLabel(
|
||
party: ReportParty | null | undefined,
|
||
damagedParty: ReportParty | null,
|
||
guiltyParty: ReportParty | null,
|
||
): string | undefined {
|
||
if (sameParty(party, damagedParty)) return "damaged";
|
||
if (sameParty(party, guiltyParty)) return "guilty";
|
||
return undefined;
|
||
}
|
||
|
||
function partyRoleLabel(role: string | undefined): string | undefined {
|
||
if (!role) return undefined;
|
||
if (role === PartyRole.FIRST) return "طرف اول";
|
||
if (role === PartyRole.SECOND) return "طرف دوم";
|
||
return role;
|
||
}
|
||
|
||
function statementBoolean(value: unknown): string | undefined {
|
||
if (typeof value !== "boolean") return undefined;
|
||
return persianStatus(value);
|
||
}
|
||
|
||
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 resolveEvaluationSubmittedAt(
|
||
claim: Record<string, unknown> | null | undefined,
|
||
reply: Record<string, unknown> | undefined,
|
||
): unknown {
|
||
if (reply?.submittedAt != null) return reply.submittedAt;
|
||
|
||
const history = Array.isArray(claim?.history) ? claim.history : [];
|
||
const submittedEvent = [...history]
|
||
.reverse()
|
||
.find((event) =>
|
||
["EXPERT_REPLY_SUBMITTED", "EXPERT_FINAL_REPLY_SUBMITTED"].includes(
|
||
String((event as Record<string, unknown>)?.type ?? ""),
|
||
),
|
||
) as Record<string, unknown> | undefined;
|
||
return submittedEvent?.timestamp;
|
||
}
|
||
|
||
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 buildPartyOwnerSection(
|
||
title: string,
|
||
party: ReportParty | null | undefined,
|
||
options?: {
|
||
claim?: Record<string, unknown> | null;
|
||
useClaimOwnerFallback?: boolean;
|
||
includeSheba?: boolean;
|
||
},
|
||
): InsurerFileReportSection | undefined {
|
||
const person = party?.person as ReportRecord | undefined;
|
||
const claim = options?.claim;
|
||
const money = claim?.money as
|
||
| { sheba?: string; nationalCodeOfInsurer?: string }
|
||
| undefined;
|
||
const claimOwner = claim?.owner as { fullName?: string } | undefined;
|
||
|
||
if (!person && !options?.useClaimOwnerFallback) return undefined;
|
||
|
||
return buildSection(title, [
|
||
{
|
||
label: PR.name,
|
||
value: options?.useClaimOwnerFallback
|
||
? firstDefined(person?.fullName, claimOwner?.fullName)
|
||
: firstDefined(person?.fullName),
|
||
},
|
||
{ label: PR.phone, value: asString(person?.phoneNumber) },
|
||
{
|
||
label: PR.nationalCode,
|
||
value: options?.useClaimOwnerFallback
|
||
? firstDefined(
|
||
person?.nationalCodeOfInsurer,
|
||
money?.nationalCodeOfInsurer,
|
||
)
|
||
: firstDefined(person?.nationalCodeOfInsurer, person?.nationalCode),
|
||
},
|
||
{
|
||
label: PR.birthDate,
|
||
value: formatBirthDate(
|
||
person?.insurerBirthday ?? person?.birthday ?? person?.driverBirthday,
|
||
),
|
||
},
|
||
{
|
||
label: PR.sheba,
|
||
value: options?.includeSheba ? money?.sheba : undefined,
|
||
},
|
||
]);
|
||
}
|
||
|
||
function buildParticipantRolesSection(
|
||
title: string,
|
||
party: ReportParty | null | undefined,
|
||
): InsurerFileReportSection | undefined {
|
||
const participants =
|
||
sanitizeStoredInquiryParticipants(party?.participants) ?? [];
|
||
const assignments = party?.participantRoles;
|
||
if (!assignments || participants.length === 0) return undefined;
|
||
|
||
const roleLabels: Record<string, string> = {
|
||
driver: PR.driverRole,
|
||
vehicleOwner: PR.vehicleOwnerRole,
|
||
thirdPartyPolicyholder: PR.thirdPartyPolicyholderRole,
|
||
carBodyPolicyholder: PR.carBodyPolicyholderRole,
|
||
};
|
||
const fields = Object.entries(assignments).map(([role, participantId]) => {
|
||
const participant = participants.find(
|
||
(candidate) => String(candidate.participantId) === String(participantId),
|
||
);
|
||
const value = [
|
||
asString(participant?.fullName),
|
||
participant?.nationalCode
|
||
? `${PR.nationalCode}: ${asString(participant.nationalCode)}`
|
||
: undefined,
|
||
participant?.birthday
|
||
? `${PR.birthDate}: ${formatBirthDate(participant.birthday)}`
|
||
: undefined,
|
||
participant?.licenseNumber
|
||
? `${PR.licenseNumber}: ${asString(participant.licenseNumber)}`
|
||
: undefined,
|
||
]
|
||
.filter(Boolean)
|
||
.join("، ");
|
||
return {
|
||
label: roleLabels[role] ?? persianFieldPath(role),
|
||
value: value || PR.empty,
|
||
};
|
||
});
|
||
|
||
return buildSection(title, fields, false);
|
||
}
|
||
|
||
function licenseFieldsFromInquiry(inquiry?: Record<string, unknown>): {
|
||
licenseType?: string;
|
||
licenseDate?: string;
|
||
} {
|
||
if (!inquiry) return {};
|
||
return {
|
||
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: ReportParty | null,
|
||
blame?: Record<string, unknown> | null,
|
||
): InsurerFileReportSection | undefined {
|
||
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",
|
||
String(role),
|
||
);
|
||
const { licenseType, licenseDate } = licenseFieldsFromInquiry(licenseInquiry);
|
||
|
||
return buildSection(PR.driverSection, [
|
||
{ label: PR.name, value: asString(person.fullName) },
|
||
{
|
||
label: PR.licenseType,
|
||
value:
|
||
licenseType ??
|
||
asString(person.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 buildPartyVehicleSection(
|
||
title: string,
|
||
party: ReportParty | null | undefined,
|
||
claimVehicle?: Record<string, unknown>,
|
||
): InsurerFileReportSection | undefined {
|
||
if (!party && !claimVehicle) return undefined;
|
||
|
||
return buildSection(title, [
|
||
...flattenObject(claimVehicle, "claim.vehicle"),
|
||
...flattenObject(party?.vehicle, "party.vehicle"),
|
||
]);
|
||
}
|
||
|
||
function buildPartyStatementSection(
|
||
title: string,
|
||
party: ReportParty | null | undefined,
|
||
damagedParty: ReportParty | null,
|
||
guiltyParty: ReportParty | null,
|
||
): InsurerFileReportSection | undefined {
|
||
if (!party) return undefined;
|
||
const statement = (party.statement ?? {}) as ReportRecord;
|
||
const kind = partyKindLabel(party, damagedParty, guiltyParty);
|
||
|
||
return buildSection(title, [
|
||
{
|
||
label: PR.partyRole,
|
||
value: partyRoleLabel(getPartyRole(party)),
|
||
},
|
||
{
|
||
label: PR.name,
|
||
value: firstDefined(party.person?.fullName),
|
||
},
|
||
{
|
||
label: PR.admitsGuilt,
|
||
value:
|
||
kind === "damaged"
|
||
? undefined
|
||
: statementBoolean(statement.admitsGuilt),
|
||
},
|
||
{
|
||
label: PR.claimsDamage,
|
||
value:
|
||
kind === "guilty"
|
||
? undefined
|
||
: statementBoolean(statement.claimsDamage),
|
||
},
|
||
{
|
||
label: PR.acceptsExpertOpinion,
|
||
value: statementBoolean(statement.acceptsExpertOpinion),
|
||
},
|
||
{
|
||
label: PR.partyDescription,
|
||
value: asString(statement.description),
|
||
},
|
||
]);
|
||
}
|
||
|
||
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(
|
||
resolveEvaluationSubmittedAt(claim, evaluationReply),
|
||
),
|
||
},
|
||
]);
|
||
}
|
||
|
||
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(resolveEvaluationSubmittedAt(claim, reply)),
|
||
},
|
||
{
|
||
label: PR.evaluationResponse,
|
||
value: asString(reply?.description),
|
||
},
|
||
]);
|
||
}
|
||
|
||
function evaluationPartName(part: ReportRecord): string | undefined {
|
||
const damage = part.carPartDamage;
|
||
if (damage && typeof damage === "object" && !Array.isArray(damage)) {
|
||
const record = damage as ReportRecord;
|
||
return firstDefined(
|
||
record.label_fa,
|
||
record.name,
|
||
record.part,
|
||
record.label,
|
||
part.partId != null ? `${PR.part} ${part.partId}` : undefined,
|
||
);
|
||
}
|
||
return firstDefined(
|
||
damage,
|
||
part.partName,
|
||
part.partId != null ? `${PR.part} ${part.partId}` : undefined,
|
||
);
|
||
}
|
||
|
||
function evaluationDaghiValue(value: unknown): string | undefined {
|
||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||
return asString(value);
|
||
}
|
||
const daghi = value as ReportRecord;
|
||
const option = asString(daghi.option);
|
||
const price = formatToman(daghi.price);
|
||
return [option, price].filter(Boolean).join(" - ") || undefined;
|
||
}
|
||
|
||
function buildEvaluationPartsSection(
|
||
claim?: Record<string, unknown> | null,
|
||
): InsurerFileReportSection | undefined {
|
||
const reply = resolveEvaluationReply(claim);
|
||
const parts = Array.isArray(reply?.parts)
|
||
? (reply.parts as ReportRecord[])
|
||
: [];
|
||
if (!parts.length) return undefined;
|
||
|
||
const fields = parts.flatMap((part, index) => {
|
||
const prefix = `${PR.part} ${PERSIAN_NUMBER_FORMATTER.format(index + 1)}`;
|
||
return [
|
||
{ label: `${prefix} / ${PR.partName}`, value: evaluationPartName(part) },
|
||
{
|
||
label: `${prefix} / ${PR.damageType}`,
|
||
value: asString(part.typeOfDamage),
|
||
},
|
||
{ label: `${prefix} / ${PR.partPrice}`, value: formatToman(part.price) },
|
||
{
|
||
label: `${prefix} / ${PR.repairSalary}`,
|
||
value: formatToman(part.salary),
|
||
},
|
||
{
|
||
label: `${prefix} / ${PR.totalPayment}`,
|
||
value: formatToman(part.totalPayment),
|
||
},
|
||
{
|
||
label: `${prefix} / ${PR.factorNeeded}`,
|
||
value:
|
||
typeof part.factorNeeded === "boolean"
|
||
? persianStatus(part.factorNeeded)
|
||
: undefined,
|
||
},
|
||
{
|
||
label: `${prefix} / ${PR.daghi}`,
|
||
value: evaluationDaghiValue(part.daghi),
|
||
},
|
||
];
|
||
});
|
||
|
||
return buildSection(PR.evaluationPartsSection, fields, false);
|
||
}
|
||
|
||
function buildAccidentReportSection(
|
||
blame?: Record<string, unknown> | null,
|
||
claim?: Record<string, unknown> | null,
|
||
damagedParty?: ReportParty | null,
|
||
): InsurerFileReportSection {
|
||
const statement = damagedParty?.statement as ReportRecord | undefined;
|
||
const location = damagedParty?.location;
|
||
const snapshotAccident = (
|
||
claim?.snapshot as { accident?: Record<string, unknown> } | undefined
|
||
)?.accident;
|
||
const blameDecision = (blame?.expert as Record<string, unknown> | undefined)
|
||
?.decision as Record<string, unknown> | undefined;
|
||
const decisionFields = blameDecision?.fields as
|
||
| Record<string, unknown>
|
||
| undefined;
|
||
|
||
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),
|
||
},
|
||
{
|
||
label: PR.location,
|
||
value:
|
||
location?.lat != null && location?.lon != null
|
||
? `${location.lat}، ${location.lon}`
|
||
: undefined,
|
||
},
|
||
{
|
||
label: PR.weather,
|
||
value: persianAccidentCondition(
|
||
"weather",
|
||
statement?.weatherCondition ?? snapshotAccident?.weatherCondition,
|
||
),
|
||
},
|
||
{
|
||
label: PR.road,
|
||
value: persianAccidentCondition(
|
||
"road",
|
||
statement?.roadCondition ?? snapshotAccident?.roadCondition,
|
||
),
|
||
},
|
||
{
|
||
label: PR.light,
|
||
value: persianAccidentCondition(
|
||
"light",
|
||
statement?.lightCondition ?? snapshotAccident?.lightCondition,
|
||
),
|
||
},
|
||
{
|
||
label: PR.blameStatus,
|
||
value: persianStatus(blame?.blameStatus),
|
||
},
|
||
{
|
||
label: PR.claimStatus,
|
||
value: persianStatus(claim?.claimStatus),
|
||
},
|
||
{ label: PR.expertDecision, value: asString(blameDecision?.description) },
|
||
{
|
||
label: PR.accidentWay,
|
||
value: asString(
|
||
(decisionFields?.accidentWay as { label?: string } | undefined)
|
||
?.label ??
|
||
(
|
||
snapshotAccident?.classification as {
|
||
accidentWay?: { label?: string };
|
||
}
|
||
)?.accidentWay?.label,
|
||
),
|
||
},
|
||
{
|
||
label: PR.accidentReason,
|
||
value: asString(
|
||
(decisionFields?.accidentReason as { label?: string } | undefined)
|
||
?.label ??
|
||
(
|
||
snapshotAccident?.classification as {
|
||
accidentReason?: { label?: string };
|
||
}
|
||
)?.accidentReason?.label,
|
||
),
|
||
},
|
||
{
|
||
label: PR.accidentType,
|
||
value: asString(
|
||
(decisionFields?.accidentType as { label?: string } | undefined)
|
||
?.label ??
|
||
(
|
||
snapshotAccident?.classification as {
|
||
accidentType?: { label?: string };
|
||
}
|
||
)?.accidentType?.label,
|
||
),
|
||
},
|
||
{
|
||
label: PR.partyDescription,
|
||
value: asString(statement?.description),
|
||
},
|
||
]);
|
||
}
|
||
|
||
export function buildInsurerFileReport(file: {
|
||
overview?: Record<string, unknown>;
|
||
blame?: Record<string, unknown>;
|
||
claim?: Record<string, unknown>;
|
||
}): InsurerFileReportViewModel {
|
||
const overview = file.overview ?? {};
|
||
const claim = file.claim ?? 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 claimVehicle = claim?.vehicle as Record<string, unknown> | undefined;
|
||
|
||
const sections = filterEmptySections([
|
||
buildCaseTimelineSection(overview, claim),
|
||
buildParticipantRolesSection(PR.damagedParticipantsSection, damagedParty),
|
||
includeGuiltySections
|
||
? buildParticipantRolesSection(PR.guiltyParticipantsSection, guiltyParty)
|
||
: undefined,
|
||
buildPartyOwnerSection(PR.ownerSection, damagedParty, {
|
||
claim,
|
||
useClaimOwnerFallback: true,
|
||
includeSheba: true,
|
||
}),
|
||
includeGuiltySections
|
||
? buildPartyOwnerSection(PR.guiltyOwnerSection, guiltyParty)
|
||
: undefined,
|
||
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,
|
||
buildPartyVehicleSection(
|
||
PR.damagedVehicleSection,
|
||
damagedParty,
|
||
claimVehicle,
|
||
),
|
||
includeGuiltySections
|
||
? buildPartyVehicleSection(PR.guiltyVehicleSection, guiltyParty)
|
||
: undefined,
|
||
buildPartyStatementSection(
|
||
PR.damagedStatementSection,
|
||
damagedParty,
|
||
damagedParty,
|
||
guiltyParty,
|
||
),
|
||
includeGuiltySections
|
||
? buildPartyStatementSection(
|
||
PR.guiltyStatementSection,
|
||
guiltyParty,
|
||
damagedParty,
|
||
guiltyParty,
|
||
)
|
||
: undefined,
|
||
buildFanavaranCodesSection(claim),
|
||
buildEvaluationSection(claim),
|
||
buildEvaluationPartsSection(claim),
|
||
buildAccidentReportSection(blameContext, claim, damagedParty),
|
||
]);
|
||
|
||
return {
|
||
title: PR.reportTitle,
|
||
publicId: asString(overview.publicId) ?? PR.empty,
|
||
requestNo: asString(overview.requestNo),
|
||
sections,
|
||
};
|
||
}
|