forked from Yara724/api
Merge pull request 'Fixed FaLabels in Timeline' (#305) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#305
This commit is contained in:
@@ -22,6 +22,8 @@
|
|||||||
* SEED_REPORTS_TAG=reports-load-v1
|
* SEED_REPORTS_TAG=reports-load-v1
|
||||||
* SEED_REPORTS_PUBLIC_PREFIX=RPT1
|
* SEED_REPORTS_PUBLIC_PREFIX=RPT1
|
||||||
* SEED_REPORTS_DEFAULT_PASSWORD=Reports@724
|
* SEED_REPORTS_DEFAULT_PASSWORD=Reports@724
|
||||||
|
* SEED_REPORTS_PURGE_ONLY=true # remove seeded data; do not insert it again
|
||||||
|
* SEED_REPORTS_PURGE_ALL=true # with PURGE_ONLY, remove every reports seed tag
|
||||||
*/
|
*/
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
@@ -652,24 +654,31 @@ function randNationalBase(prefix: string): string {
|
|||||||
return n.padEnd(6, "7");
|
return n.padEnd(6, "7");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function purgeSeedPortfolio(db: mongoose.mongo.Db, seedTag: string) {
|
async function purgeSeedPortfolio(
|
||||||
|
db: mongoose.mongo.Db,
|
||||||
|
seedTag?: string,
|
||||||
|
) {
|
||||||
const blameCases = db.collection("blameCases");
|
const blameCases = db.collection("blameCases");
|
||||||
const claimCases = db.collection("claimCases");
|
const claimCases = db.collection("claimCases");
|
||||||
const expertFileActivities = db.collection("expertFileActivities");
|
const expertFileActivities = db.collection("expertFileActivities");
|
||||||
|
const fixtureFilter = {
|
||||||
|
"seedMeta.script": SCRIPT_NAME,
|
||||||
|
...(seedTag ? { "seedMeta.tag": seedTag } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
const [existingBlames, existingClaims] = await Promise.all([
|
const [existingBlames, existingClaims] = await Promise.all([
|
||||||
blameCases.find({ "seedMeta.script": SCRIPT_NAME, "seedMeta.tag": seedTag }, { projection: { _id: 1 } }).toArray(),
|
blameCases.find(fixtureFilter, { projection: { _id: 1 } }).toArray(),
|
||||||
claimCases.find({ "seedMeta.script": SCRIPT_NAME, "seedMeta.tag": seedTag }, { projection: { _id: 1 } }).toArray(),
|
claimCases.find(fixtureFilter, { projection: { _id: 1 } }).toArray(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fileIds = [...existingBlames, ...existingClaims].map((d) => asObjectId(d._id));
|
const fileIds = [...existingBlames, ...existingClaims].map((d) => asObjectId(d._id));
|
||||||
|
|
||||||
const [deletedBlames, deletedClaims, deletedActivities] = await Promise.all([
|
const [deletedBlames, deletedClaims, deletedActivities] = await Promise.all([
|
||||||
blameCases.deleteMany({ "seedMeta.script": SCRIPT_NAME, "seedMeta.tag": seedTag }),
|
blameCases.deleteMany(fixtureFilter),
|
||||||
claimCases.deleteMany({ "seedMeta.script": SCRIPT_NAME, "seedMeta.tag": seedTag }),
|
claimCases.deleteMany(fixtureFilter),
|
||||||
expertFileActivities.deleteMany({
|
expertFileActivities.deleteMany({
|
||||||
$or: [
|
$or: [
|
||||||
{ "seedMeta.script": SCRIPT_NAME, "seedMeta.tag": seedTag },
|
fixtureFilter,
|
||||||
...(fileIds.length ? [{ fileId: { $in: fileIds } }] : []),
|
...(fileIds.length ? [{ fileId: { $in: fileIds } }] : []),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -682,6 +691,32 @@ async function purgeSeedPortfolio(db: mongoose.mongo.Db, seedTag: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The portfolio seed creates a shared synthetic roster and three report-only
|
||||||
|
* branches. Remove those too once no fixture portfolio remains, so a purge
|
||||||
|
* actually returns the insurer tenant to its pre-fixture state.
|
||||||
|
*/
|
||||||
|
async function purgeSeedSupportData(db: mongoose.mongo.Db) {
|
||||||
|
const collectionNames = [
|
||||||
|
"branches",
|
||||||
|
"expert",
|
||||||
|
"damage-expert",
|
||||||
|
"field-expert",
|
||||||
|
"file-maker",
|
||||||
|
"file-reviewer",
|
||||||
|
"call-center-agents",
|
||||||
|
];
|
||||||
|
const deleted = await Promise.all(
|
||||||
|
collectionNames.map(async (collectionName) => {
|
||||||
|
const result = await db
|
||||||
|
.collection(collectionName)
|
||||||
|
.deleteMany({ "seedMeta.script": SCRIPT_NAME });
|
||||||
|
return [collectionName, result.deletedCount ?? 0] as const;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return Object.fromEntries(deleted);
|
||||||
|
}
|
||||||
|
|
||||||
function scaleBucketCounts(total: number): Record<UnifiedBucket, number> {
|
function scaleBucketCounts(total: number): Record<UnifiedBucket, number> {
|
||||||
const keys = Object.keys(BUCKET_WEIGHTS) as UnifiedBucket[];
|
const keys = Object.keys(BUCKET_WEIGHTS) as UnifiedBucket[];
|
||||||
const raw = keys.map((key) => ({ key, exact: BUCKET_WEIGHTS[key] * total }));
|
const raw = keys.map((key) => ({ key, exact: BUCKET_WEIGHTS[key] * total }));
|
||||||
@@ -2307,6 +2342,8 @@ async function main() {
|
|||||||
const mongoUri = resolveMongoUri();
|
const mongoUri = resolveMongoUri();
|
||||||
const count = Math.max(1, Number(process.env.SEED_REPORTS_COUNT?.trim() || "1000"));
|
const count = Math.max(1, Number(process.env.SEED_REPORTS_COUNT?.trim() || "1000"));
|
||||||
const seedTag = process.env.SEED_REPORTS_TAG?.trim() || "reports-load-v1";
|
const seedTag = process.env.SEED_REPORTS_TAG?.trim() || "reports-load-v1";
|
||||||
|
const purgeOnly = process.env.SEED_REPORTS_PURGE_ONLY?.trim() === "true";
|
||||||
|
const purgeAll = process.env.SEED_REPORTS_PURGE_ALL?.trim() === "true";
|
||||||
const publicPrefix =
|
const publicPrefix =
|
||||||
process.env.SEED_REPORTS_PUBLIC_PREFIX?.trim() || `RPT${shortHash(seedTag, 3)}`;
|
process.env.SEED_REPORTS_PUBLIC_PREFIX?.trim() || `RPT${shortHash(seedTag, 3)}`;
|
||||||
const defaultPassword =
|
const defaultPassword =
|
||||||
@@ -2325,6 +2362,29 @@ async function main() {
|
|||||||
const clientLabel =
|
const clientLabel =
|
||||||
client?.clientName?.persian || client?.clientName?.english || String(client._id);
|
client?.clientName?.persian || client?.clientName?.english || String(client._id);
|
||||||
|
|
||||||
|
if (purgeOnly) {
|
||||||
|
const purge = await purgeSeedPortfolio(db, purgeAll ? undefined : seedTag);
|
||||||
|
const remainingPortfolio = await Promise.all([
|
||||||
|
db.collection("blameCases").countDocuments({ "seedMeta.script": SCRIPT_NAME }),
|
||||||
|
db.collection("claimCases").countDocuments({ "seedMeta.script": SCRIPT_NAME }),
|
||||||
|
]);
|
||||||
|
const supportData =
|
||||||
|
remainingPortfolio[0] + remainingPortfolio[1] === 0
|
||||||
|
? await purgeSeedSupportData(db)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
console.log("\n=== Insurer reports seed purge complete ===");
|
||||||
|
console.log(`Client : ${clientLabel} (${clientId.toHexString()})`);
|
||||||
|
console.log(`Seed scope : ${purgeAll ? "all tags" : seedTag}`);
|
||||||
|
console.log(`Removed : ${purge.deletedBlames} blame, ${purge.deletedClaims} claim, ${purge.deletedActivities} activities`);
|
||||||
|
if (supportData) {
|
||||||
|
console.log("Support data :", supportData);
|
||||||
|
} else {
|
||||||
|
console.log("Support data : retained because other report-fixture portfolios remain");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rng = mulberry32(seedToInt(`${seedTag}:${clientId.toHexString()}:${count}`));
|
const rng = mulberry32(seedToInt(`${seedTag}:${clientId.toHexString()}:${count}`));
|
||||||
const passwordHash = await hashPassword(defaultPassword);
|
const passwordHash = await hashPassword(defaultPassword);
|
||||||
const branches = await ensureBranches(db, clientId, String(clientLabel));
|
const branches = await ensureBranches(db, clientId, String(clientLabel));
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
getEventFaDescription,
|
getEventFaDescription,
|
||||||
getEventFaLabel,
|
getEventFaLabel,
|
||||||
|
localizeTimelineMetadata,
|
||||||
} from "./helper/timeline-fa-labels";
|
} from "./helper/timeline-fa-labels";
|
||||||
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
|
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
|
||||||
|
|
||||||
@@ -2831,7 +2832,7 @@ export class ExpertInsurerService {
|
|||||||
ExpertInsurerService.resolveTimelinePerformedByCategory(performedBy),
|
ExpertInsurerService.resolveTimelinePerformedByCategory(performedBy),
|
||||||
performedByFaLabel:
|
performedByFaLabel:
|
||||||
ExpertInsurerService.resolveTimelinePerformedByFaLabel(performedBy),
|
ExpertInsurerService.resolveTimelinePerformedByFaLabel(performedBy),
|
||||||
metadata: this.localizeTimelineMetadata(ev),
|
metadata: localizeTimelineMetadata(ev),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2869,7 +2870,7 @@ export class ExpertInsurerService {
|
|||||||
ExpertInsurerService.resolveTimelinePerformedByCategory(performedBy),
|
ExpertInsurerService.resolveTimelinePerformedByCategory(performedBy),
|
||||||
performedByFaLabel:
|
performedByFaLabel:
|
||||||
ExpertInsurerService.resolveTimelinePerformedByFaLabel(performedBy),
|
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
|
// Resolve insurer's client name for context if needed — just return raw events
|
||||||
return { publicId, 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<string, unknown>;
|
|
||||||
}): Record<string, unknown> | null {
|
|
||||||
if (!event.metadata) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...event.metadata,
|
|
||||||
...(Object.prototype.hasOwnProperty.call(event.metadata, "description")
|
|
||||||
? { description: getEventFaDescription(event) }
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { getEventFaDescription, getEventFaLabel } from "./timeline-fa-labels";
|
import {
|
||||||
|
getEventFaDescription,
|
||||||
|
getEventFaLabel,
|
||||||
|
localizeTimelineMetadata,
|
||||||
|
} from "./timeline-fa-labels";
|
||||||
|
|
||||||
describe("insurer timeline Persian labels", () => {
|
describe("insurer timeline Persian labels", () => {
|
||||||
it("localizes the user-submission detail shown to insurers", () => {
|
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: "نمای جلو",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* Usage:
|
* Usage:
|
||||||
* const label = EVENT_TYPE_FA_LABELS[event.type]
|
* const label = EVENT_TYPE_FA_LABELS[event.type]
|
||||||
* ?? resolveStepCompletedFaLabel(event)
|
* ?? resolveStepCompletedFaLabel(event)
|
||||||
* ?? event.type;
|
* ?? "رویداد پرونده";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -40,6 +40,16 @@ export const EVENT_TYPE_FA_LABELS: Record<string, string> = {
|
|||||||
// First video
|
// First video
|
||||||
FIRST_VIDEO_UPLOADED: "ویدیوی اول بارگذاری شد",
|
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 invitation
|
||||||
SECOND_PARTY_INVITED: "طرف دوم دعوت شد",
|
SECOND_PARTY_INVITED: "طرف دوم دعوت شد",
|
||||||
|
|
||||||
@@ -101,6 +111,12 @@ export const EVENT_TYPE_FA_LABELS: Record<string, string> = {
|
|||||||
EXPERT_RESEND_FULFILLED: "مدارک درخواستی ارسال شد",
|
EXPERT_RESEND_FULFILLED: "مدارک درخواستی ارسال شد",
|
||||||
IN_PERSON_VISIT_REQUESTED: "کارشناس بازدید حضوری درخواست کرد",
|
IN_PERSON_VISIT_REQUESTED: "کارشناس بازدید حضوری درخواست کرد",
|
||||||
EXPERT_DAMAGED_PARTS_UPDATED: "کارشناس قطعات آسیبدیده را بهروزرسانی کرد",
|
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)
|
// Workflow steps (generic)
|
||||||
STEP_COMPLETED: "مرحله تکمیل شد",
|
STEP_COMPLETED: "مرحله تکمیل شد",
|
||||||
@@ -171,6 +187,82 @@ export const STEP_KEY_FA_LABELS: Record<string, string> = {
|
|||||||
CLAIM_COMPLETED: "پرونده خسارت تکمیل شد",
|
CLAIM_COMPLETED: "پرونده خسارت تکمیل شد",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TIMELINE_STEP_FA_LABELS: Record<string, string> = {
|
||||||
|
...STEP_KEY_FA_LABELS,
|
||||||
|
FIRST_VIDEO: "ویدیوی طرف اول",
|
||||||
|
SECOND_VIDEO: "ویدیوی طرف دوم",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROLE_FA_LABELS: Record<string, string> = {
|
||||||
|
FIRST: "طرف اول",
|
||||||
|
SECOND: "طرف دوم",
|
||||||
|
FIRST_PARTY: "طرف اول",
|
||||||
|
SECOND_PARTY: "طرف دوم",
|
||||||
|
};
|
||||||
|
|
||||||
|
const VEHICLE_TYPE_FA_LABELS: Record<string, string> = {
|
||||||
|
sedan: "سواری",
|
||||||
|
suv: "شاسیبلند",
|
||||||
|
hatchback: "هاچبک",
|
||||||
|
pickup: "وانت",
|
||||||
|
van: "ون",
|
||||||
|
};
|
||||||
|
|
||||||
|
const CAPTURE_TYPE_FA_LABELS: Record<string, string> = {
|
||||||
|
angle: "زاویه خودرو",
|
||||||
|
part: "قطعه آسیبدیده",
|
||||||
|
};
|
||||||
|
|
||||||
|
const CAPTURE_KEY_FA_LABELS: Record<string, string> = {
|
||||||
|
front: "نمای جلو",
|
||||||
|
back: "نمای عقب",
|
||||||
|
left: "نمای چپ",
|
||||||
|
right: "نمای راست",
|
||||||
|
top: "نمای بالا",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DOCUMENT_KEY_FA_LABELS: Record<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
parsian: "پارسیان",
|
||||||
|
};
|
||||||
|
|
||||||
|
const REPLY_FIELD_FA_LABELS: Record<string, string> = {
|
||||||
|
damageExpertReply: "پاسخ کارشناس خسارت",
|
||||||
|
damageExpertReplyFinal: "پاسخ نهایی کارشناس خسارت",
|
||||||
|
};
|
||||||
|
|
||||||
|
const TIMELINE_SYSTEM_TEXT_FA: Record<string, string> = {
|
||||||
|
"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
|
* Persian descriptions for workflow-step events. These are deliberately
|
||||||
* complete sentences because the insurer timeline exposes them as the event
|
* complete sentences because the insurer timeline exposes them as the event
|
||||||
@@ -222,7 +314,7 @@ export function getEventFaLabel(event: {
|
|||||||
return (
|
return (
|
||||||
resolveStepCompletedFaLabel(event) ??
|
resolveStepCompletedFaLabel(event) ??
|
||||||
EVENT_TYPE_FA_LABELS[event.type] ??
|
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];
|
return STEP_KEY_FA_DESCRIPTIONS[stepKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = getEventFaLabel(event);
|
const label =
|
||||||
return label === event.type ? "رویدادی در روند پرونده ثبت شد." : `${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<string, unknown> },
|
||||||
|
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<string, unknown> | null;
|
||||||
|
}): Record<string, unknown> | null {
|
||||||
|
if (!event.metadata) return null;
|
||||||
|
|
||||||
|
const metadata: Record<string, unknown> = { ...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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user