diff --git a/src/expert-insurer/expert-insurer.service.ts b/src/expert-insurer/expert-insurer.service.ts index b586240..ba50fa4 100644 --- a/src/expert-insurer/expert-insurer.service.ts +++ b/src/expert-insurer/expert-insurer.service.ts @@ -81,6 +81,7 @@ import { import { getEventFaDescription, getEventFaLabel, + localizeTimelineMetadata, } from "./helper/timeline-fa-labels"; import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher"; @@ -2831,7 +2832,7 @@ export class ExpertInsurerService { ExpertInsurerService.resolveTimelinePerformedByCategory(performedBy), performedByFaLabel: ExpertInsurerService.resolveTimelinePerformedByFaLabel(performedBy), - metadata: this.localizeTimelineMetadata(ev), + metadata: localizeTimelineMetadata(ev), }); } } @@ -2869,7 +2870,7 @@ export class ExpertInsurerService { ExpertInsurerService.resolveTimelinePerformedByCategory(performedBy), performedByFaLabel: ExpertInsurerService.resolveTimelinePerformedByFaLabel(performedBy), - metadata: this.localizeTimelineMetadata(ev), + metadata: localizeTimelineMetadata(ev), }); } } @@ -2883,22 +2884,4 @@ export class ExpertInsurerService { // Resolve insurer's client name for context if needed — just return raw events return { publicId, events }; } - - /** - * Keep the timeline metadata intact, but replace persisted system-generated - * descriptions with their Persian presentation value. - */ - private localizeTimelineMetadata(event: { - type: string; - metadata?: Record; - }): Record | null { - if (!event.metadata) return null; - - return { - ...event.metadata, - ...(Object.prototype.hasOwnProperty.call(event.metadata, "description") - ? { description: getEventFaDescription(event) } - : {}), - }; - } } diff --git a/src/expert-insurer/helper/timeline-fa-labels.spec.ts b/src/expert-insurer/helper/timeline-fa-labels.spec.ts index 8e04ef4..557d2fa 100644 --- a/src/expert-insurer/helper/timeline-fa-labels.spec.ts +++ b/src/expert-insurer/helper/timeline-fa-labels.spec.ts @@ -1,4 +1,8 @@ -import { getEventFaDescription, getEventFaLabel } from "./timeline-fa-labels"; +import { + getEventFaDescription, + getEventFaLabel, + localizeTimelineMetadata, +} from "./timeline-fa-labels"; describe("insurer timeline Persian labels", () => { it("localizes the user-submission detail shown to insurers", () => { @@ -22,4 +26,73 @@ describe("insurer timeline Persian labels", () => { "رویدادی در روند پرونده ثبت شد.", ); }); + + it.each([ + ["FIRST_INITIAL_FORM_SUBMITTED", "فرم اولیه طرف اول ثبت شد"], + ["FIRST_LOCATION_SUBMITTED", "موقعیت مکانی طرف اول ثبت شد"], + ["FIRST_VOICE_UPLOADED", "صدای طرف اول بارگذاری شد"], + ["FIRST_DESCRIPTION_SUBMITTED", "توضیحات طرف اول ثبت شد"], + ["EXPERT_REPLY_SUBMITTED", "پاسخ کارشناس ثبت شد"], + ])("localizes sample timeline event %s", (type, expectedLabel) => { + expect(getEventFaLabel({ type })).toBe(expectedLabel); + }); + + it("uses a Persian faLabel fallback for an unknown event type", () => { + expect(getEventFaLabel({ type: "UNRECOGNIZED_EVENT" })).toBe( + "رویداد پرونده", + ); + }); + + it("localizes human-readable assignment metadata", () => { + expect( + localizeTimelineMetadata({ + type: "CLAIM_ASSIGNED", + metadata: { note: "Expert assigned via review assign endpoint" }, + }), + ).toEqual({ + note: "پرونده از طریق فرایند بررسی به کارشناس تخصیص داده شد.", + }); + }); + + it("localizes Fanavaran errors while retaining machine-facing metadata", () => { + expect( + localizeTimelineMetadata({ + type: "FANAVARAN_EXPERTISE_AUTO_SUBMIT_FAILED", + metadata: { + clientKey: "parsian", + error: "Fanavaran expertise payload is not ready.", + }, + }), + ).toEqual({ + clientKey: "parsian", + clientFaLabel: "پارسیان", + error: "اطلاعات کارشناسی برای ارسال به فناوران آماده نیست.", + }); + }); + + it("adds Persian labels for displayable metadata enums", () => { + expect( + localizeTimelineMetadata({ + type: "STEP_COMPLETED", + metadata: { + role: "FIRST", + stepKey: "USER_SUBMISSION_COMPLETE", + carType: "sedan", + captureType: "angle", + captureKey: "front", + }, + }), + ).toEqual({ + role: "FIRST", + roleFaLabel: "طرف اول", + stepKey: "USER_SUBMISSION_COMPLETE", + stepFaLabel: "ارسال اطلاعات توسط کاربر", + carType: "sedan", + carTypeFaLabel: "سواری", + captureType: "angle", + captureTypeFaLabel: "زاویه خودرو", + captureKey: "front", + captureKeyFaLabel: "نمای جلو", + }); + }); }); diff --git a/src/expert-insurer/helper/timeline-fa-labels.ts b/src/expert-insurer/helper/timeline-fa-labels.ts index c41e110..600da67 100644 --- a/src/expert-insurer/helper/timeline-fa-labels.ts +++ b/src/expert-insurer/helper/timeline-fa-labels.ts @@ -9,7 +9,7 @@ * Usage: * const label = EVENT_TYPE_FA_LABELS[event.type] * ?? resolveStepCompletedFaLabel(event) - * ?? event.type; + * ?? "رویداد پرونده"; */ // --------------------------------------------------------------------------- @@ -40,6 +40,16 @@ export const EVENT_TYPE_FA_LABELS: Record = { // First video FIRST_VIDEO_UPLOADED: "ویدیوی اول بارگذاری شد", + // Legacy first/second-party self-service flow + FIRST_INITIAL_FORM_SUBMITTED: "فرم اولیه طرف اول ثبت شد", + SECOND_INITIAL_FORM_SUBMITTED: "فرم اولیه طرف دوم ثبت شد", + FIRST_LOCATION_SUBMITTED: "موقعیت مکانی طرف اول ثبت شد", + SECOND_LOCATION_SUBMITTED: "موقعیت مکانی طرف دوم ثبت شد", + FIRST_VOICE_UPLOADED: "صدای طرف اول بارگذاری شد", + SECOND_VOICE_UPLOADED: "صدای طرف دوم بارگذاری شد", + FIRST_DESCRIPTION_SUBMITTED: "توضیحات طرف اول ثبت شد", + SECOND_DESCRIPTION_SUBMITTED: "توضیحات طرف دوم ثبت شد", + // Second party invitation SECOND_PARTY_INVITED: "طرف دوم دعوت شد", @@ -101,6 +111,12 @@ export const EVENT_TYPE_FA_LABELS: Record = { EXPERT_RESEND_FULFILLED: "مدارک درخواستی ارسال شد", IN_PERSON_VISIT_REQUESTED: "کارشناس بازدید حضوری درخواست کرد", EXPERT_DAMAGED_PARTS_UPDATED: "کارشناس قطعات آسیب‌دیده را به‌روزرسانی کرد", + EXPERT_REPLY_SUBMITTED: "پاسخ کارشناس ثبت شد", + EXPERT_FINAL_REPLY_SUBMITTED: "پاسخ نهایی کارشناس ثبت شد", + FACTORS_REJECTED_REPRICED_AUTO_COMPLETED: + "بررسی فاکتورها با قیمت‌گذاری مجدد تکمیل شد", + FACTORS_VALIDATED_ALL_APPROVED_AUTO_COMPLETED: + "همه فاکتورها تأیید و پرونده تکمیل شد", // Workflow steps (generic) STEP_COMPLETED: "مرحله تکمیل شد", @@ -171,6 +187,82 @@ export const STEP_KEY_FA_LABELS: Record = { CLAIM_COMPLETED: "پرونده خسارت تکمیل شد", }; +const TIMELINE_STEP_FA_LABELS: Record = { + ...STEP_KEY_FA_LABELS, + FIRST_VIDEO: "ویدیوی طرف اول", + SECOND_VIDEO: "ویدیوی طرف دوم", +}; + +const ROLE_FA_LABELS: Record = { + FIRST: "طرف اول", + SECOND: "طرف دوم", + FIRST_PARTY: "طرف اول", + SECOND_PARTY: "طرف دوم", +}; + +const VEHICLE_TYPE_FA_LABELS: Record = { + sedan: "سواری", + suv: "شاسی‌بلند", + hatchback: "هاچ‌بک", + pickup: "وانت", + van: "ون", +}; + +const CAPTURE_TYPE_FA_LABELS: Record = { + angle: "زاویه خودرو", + part: "قطعه آسیب‌دیده", +}; + +const CAPTURE_KEY_FA_LABELS: Record = { + front: "نمای جلو", + back: "نمای عقب", + left: "نمای چپ", + right: "نمای راست", + top: "نمای بالا", +}; + +const DOCUMENT_KEY_FA_LABELS: Record = { + car_green_card: "کارت سبز خودرو", + car_certificate: "سند خودرو", + national_card: "کارت ملی", + damaged_driving_license_front: "روی گواهینامه طرف آسیب‌دیده", + damaged_driving_license_back: "پشت گواهینامه طرف آسیب‌دیده", + damaged_chassis_number: "شماره شاسی خودروی آسیب‌دیده", + damaged_engine_photo: "تصویر موتور خودروی آسیب‌دیده", + damaged_car_card_front: "روی کارت خودروی آسیب‌دیده", + damaged_car_card_back: "پشت کارت خودروی آسیب‌دیده", + damaged_metal_plate: "پلاک خودروی آسیب‌دیده", + guilty_driving_license_front: "روی گواهینامه طرف مقصر", + guilty_driving_license_back: "پشت گواهینامه طرف مقصر", + guilty_car_card_front: "روی کارت خودروی مقصر", + guilty_car_card_back: "پشت کارت خودروی مقصر", + guilty_metal_plate: "پلاک خودروی مقصر", + guilty_damage_area: "ناحیه آسیب‌دیده خودروی مقصر", +}; + +const CLIENT_FA_LABELS: Record = { + parsian: "پارسیان", +}; + +const REPLY_FIELD_FA_LABELS: Record = { + damageExpertReply: "پاسخ کارشناس خسارت", + damageExpertReplyFinal: "پاسخ نهایی کارشناس خسارت", +}; + +const TIMELINE_SYSTEM_TEXT_FA: Record = { + "Expert assigned via review assign endpoint": + "پرونده از طریق فرایند بررسی به کارشناس تخصیص داده شد.", + "Expert assigned for factor validation review": + "پرونده برای بررسی و تأیید فاکتورها به کارشناس تخصیص داده شد.", + "Field expert portfolio lock (no status change)": + "پرونده برای بررسی در اختیار کارشناس میدانی قرار گرفت.", + "Fanavaran expertise payload is not ready.": + "اطلاعات کارشناسی برای ارسال به فناوران آماده نیست.", +}; + +const GENERIC_EVENT_FA_LABEL = "رویداد پرونده"; +const GENERIC_EVENT_FA_DESCRIPTION = "رویدادی در روند پرونده ثبت شد."; + /** * Persian descriptions for workflow-step events. These are deliberately * complete sentences because the insurer timeline exposes them as the event @@ -222,7 +314,7 @@ export function getEventFaLabel(event: { return ( resolveStepCompletedFaLabel(event) ?? EVENT_TYPE_FA_LABELS[event.type] ?? - event.type + GENERIC_EVENT_FA_LABEL ); } @@ -242,6 +334,124 @@ export function getEventFaDescription(event: { return STEP_KEY_FA_DESCRIPTIONS[stepKey]; } - const label = getEventFaLabel(event); - return label === event.type ? "رویدادی در روند پرونده ثبت شد." : `${label}.`; + const label = + resolveStepCompletedFaLabel(event) ?? EVENT_TYPE_FA_LABELS[event.type]; + return label ? `${label}.` : GENERIC_EVENT_FA_DESCRIPTION; +} + +function containsLatinText(value: string): boolean { + return /[A-Za-z]/.test(value); +} + +function localizeHumanReadableMetadataText( + value: unknown, + event: { type: string; metadata?: Record }, + field: "note" | "error" | "message" | "warning", +): unknown { + if (typeof value !== "string" || !containsLatinText(value)) return value; + + const knownTranslation = TIMELINE_SYSTEM_TEXT_FA[value.trim()]; + if (knownTranslation) return knownTranslation; + + if (field === "error" || field === "warning") { + return event.type.endsWith("_FAILED") + ? getEventFaDescription(event) + : "در اجرای این عملیات خطایی رخ داد."; + } + + return field === "note" + ? "یادداشت سیستمی این رویداد ثبت شد." + : getEventFaDescription(event); +} + +/** + * Builds presentation-safe timeline metadata. Raw identifiers and enum values + * remain available for integrations; Persian companion labels are added for + * UI rendering, while persisted human-readable English text is replaced. + */ +export function localizeTimelineMetadata(event: { + type: string; + metadata?: Record | null; +}): Record | null { + if (!event.metadata) return null; + + const metadata: Record = { ...event.metadata }; + + if (Object.prototype.hasOwnProperty.call(metadata, "description")) { + metadata.description = getEventFaDescription(event); + } + + for (const field of ["note", "error", "message", "warning"] as const) { + if (Object.prototype.hasOwnProperty.call(metadata, field)) { + metadata[field] = localizeHumanReadableMetadataText( + metadata[field], + event, + field, + ); + } + } + + if (Array.isArray(metadata.warnings)) { + metadata.warnings = metadata.warnings.map((warning) => + localizeHumanReadableMetadataText(warning, event, "warning"), + ); + } + + const role = typeof metadata.role === "string" ? metadata.role : undefined; + if (role && ROLE_FA_LABELS[role]) { + metadata.roleFaLabel = ROLE_FA_LABELS[role]; + } + + const stepKey = + typeof metadata.stepKey === "string" ? metadata.stepKey : undefined; + if (stepKey && TIMELINE_STEP_FA_LABELS[stepKey]) { + metadata.stepFaLabel = TIMELINE_STEP_FA_LABELS[stepKey]; + } + + const carType = + typeof metadata.carType === "string" ? metadata.carType : undefined; + if (carType && VEHICLE_TYPE_FA_LABELS[carType]) { + metadata.carTypeFaLabel = VEHICLE_TYPE_FA_LABELS[carType]; + } + + const captureType = + typeof metadata.captureType === "string" ? metadata.captureType : undefined; + if (captureType && CAPTURE_TYPE_FA_LABELS[captureType]) { + metadata.captureTypeFaLabel = CAPTURE_TYPE_FA_LABELS[captureType]; + } + + const captureKey = + typeof metadata.captureKey === "string" ? metadata.captureKey : undefined; + if (captureKey && CAPTURE_KEY_FA_LABELS[captureKey]) { + metadata.captureKeyFaLabel = CAPTURE_KEY_FA_LABELS[captureKey]; + } + + const resolvedCarAngleKey = + typeof metadata.resolvedCarAngleKey === "string" + ? metadata.resolvedCarAngleKey + : undefined; + if (resolvedCarAngleKey && CAPTURE_KEY_FA_LABELS[resolvedCarAngleKey]) { + metadata.resolvedCarAngleFaLabel = + CAPTURE_KEY_FA_LABELS[resolvedCarAngleKey]; + } + + const documentKey = + typeof metadata.documentKey === "string" ? metadata.documentKey : undefined; + if (documentKey && DOCUMENT_KEY_FA_LABELS[documentKey]) { + metadata.documentFaLabel = DOCUMENT_KEY_FA_LABELS[documentKey]; + } + + const clientKey = + typeof metadata.clientKey === "string" ? metadata.clientKey : undefined; + if (clientKey && CLIENT_FA_LABELS[clientKey]) { + metadata.clientFaLabel = CLIENT_FA_LABELS[clientKey]; + } + + const replyField = + typeof metadata.replyField === "string" ? metadata.replyField : undefined; + if (replyField && REPLY_FIELD_FA_LABELS[replyField]) { + metadata.replyFieldFaLabel = REPLY_FIELD_FA_LABELS[replyField]; + } + + return metadata; }