diff --git a/src/case-expert-report/case-expert-report.builder.spec.ts b/src/case-expert-report/case-expert-report.builder.spec.ts new file mode 100644 index 0000000..28d94b9 --- /dev/null +++ b/src/case-expert-report/case-expert-report.builder.spec.ts @@ -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, + 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( + "نیاز به تعویض سپر جلو", + ); + }); +}); diff --git a/src/case-expert-report/case-expert-report.builder.ts b/src/case-expert-report/case-expert-report.builder.ts index 6d585f4..0abf1af 100644 --- a/src/case-expert-report/case-expert-report.builder.ts +++ b/src/case-expert-report/case-expert-report.builder.ts @@ -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; +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; 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(); + 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[] { + 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, ): Record | undefined { @@ -159,184 +235,420 @@ function pickInquiryReportPayload( return Object.keys(rest).length ? rest : inquiry; } -function licenseFieldsFromInquiry( - inquiry?: Record, -): { 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 | null, + claim?: Record | null, +): Record | null { + if (blame) return blame; + const snapshot = claim?.snapshot as Record | undefined; + if (!snapshot) return null; return { - licenseType: asString(licenseType), - licenseDate: asString(licenseDate), + type: + (claim?.blameFileContext as Record | undefined) + ?.blameRequestType ?? + (snapshot.accident as Record | undefined)?.type, + blameStatus: (claim?.blameFileContext as Record | 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 | null, +): Record | undefined { + const evaluation = claim?.evaluation as Record | undefined; + return ( + (evaluation?.damageExpertReplyFinal as Record | undefined) ?? + (evaluation?.damageExpertReply as Record | 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 | null, + claim?: Record | null, +): InsurerFileReportField[] { + const role = getPartyRole(party); + const direct = (party?.insurance ?? {}) as Record; + const blameInquiry = pickInquiryReportPayload( + inquiryRoleData(blame?.inquiries as Record | undefined, "thirdParty", role), + ); + const claimInquiry = pickInquiryReportPayload( + inquiryRoleData(claim?.inquiries as Record | 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 | null, + claim?: Record | null, +): InsurerFileReportField[] { + const role = getPartyRole(party); + const direct = (party?.insurance?.carBodyInsurance ?? + party?.insurance?.carBody ?? + {}) as Record; + const blameInquiry = pickInquiryReportPayload( + inquiryRoleData(blame?.inquiries as Record | undefined, "carBody", role), + ); + const claimInquiry = pickInquiryReportPayload( + inquiryRoleData(claim?.inquiries as Record | undefined, "carBody", role), + ); + const legacy = (blame?.carBodyInsuranceDetail ?? {}) as Record; + + 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, + damagedParty: ReportParty | null, claim?: Record | 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, +): { 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, + damagedParty: ReportParty | null, blame?: Record | 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 | 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, - blame?: Record | null, - claim?: Record | null, -): InsurerFileReportSection { - const role = damagedParty?.role ?? PartyRole.FIRST; - const insurance = damagedParty?.insurance ?? {}; - const carBodyLegacy = blame?.carBodyInsuranceDetail as - | Record - | undefined; - - const thirdPartyInquiry = inquiryRoleData( - blame?.inquiries as Record | undefined, - "thirdParty", - role, - ); - const carBodyInquiry = inquiryRoleData( - blame?.inquiries as Record | undefined, - "carBody", - role, - ); - const claimThirdParty = inquiryRoleData( - claim?.inquiries as Record | undefined, - "thirdParty", - role, - ); - const claimCarBody = inquiryRoleData( - claim?.inquiries as Record | 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, + damagedParty: ReportParty | null, claim?: Record | null, ): InsurerFileReportSection { const partyVehicle = damagedParty?.vehicle; const claimVehicle = claim?.vehicle as Record | 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 | null, + claim?: Record | 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 | null, +): InsurerFileReportSection | undefined { + if (!claim) return undefined; + const sync = (claim.fanavaranSync as Record | undefined) ?? {}; + const baseClaim = (sync.baseClaim as Record | undefined) ?? {}; + const damageCase = (sync.damageCase as Record | undefined) ?? {}; + const expertise = (sync.expertise as Record | 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 | 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 | null, claim?: Record | null, - damagedParty?: ReturnType, + damagedParty?: ReportParty | null, ): InsurerFileReportSection { - const statement = damagedParty?.statement as - | Record - | undefined; + const statement = damagedParty?.statement as ReportRecord | undefined; const location = damagedParty?.location; const snapshotAccident = ( claim?.snapshot as { accident?: Record } | undefined @@ -344,22 +656,21 @@ function buildAccidentReportSection( const blameDecision = ( blame?.expert as Record | undefined )?.decision as Record | undefined; - const decisionFields = blameDecision?.fields as - | Record - | undefined; + const decisionFields = blameDecision?.fields as Record | 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 | undefined) - ?.damageExpertReplyFinal as Record | undefined - )?.submittedAt ?? - ( - (claim?.evaluation as Record | undefined) - ?.damageExpertReply as Record | undefined - )?.submittedAt, - ), - }, - { - label: PR.damageExpertNotes, - value: asString( - ( - (claim?.evaluation as Record | undefined) - ?.damageExpertReplyFinal as Record | undefined - )?.description ?? - ( - (claim?.evaluation as Record | undefined) - ?.damageExpertReply as Record | 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(); - 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; - blame?: Record; - claim?: Record; - }, -): InsurerFileReportViewModel { +export function buildInsurerFileReport(file: { + overview?: Record; + blame?: Record; + claim?: Record; +}): 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, diff --git a/src/case-expert-report/case-expert-report.controller.ts b/src/case-expert-report/case-expert-report.controller.ts index 41f17cd..226b028 100644 --- a/src/case-expert-report/case-expert-report.controller.ts +++ b/src/case-expert-report/case-expert-report.controller.ts @@ -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" }) diff --git a/src/case-expert-report/persian-report-labels.ts b/src/case-expert-report/persian-report-labels.ts index ba238bd..86b081e 100644 --- a/src/case-expert-report/persian-report-labels.ts +++ b/src/case-expert-report/persian-report-labels.ts @@ -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: "وضعیت آب و هوا",