Merge pull request 'Added case type for all GET APIs' (#274) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#274
This commit is contained in:
2026-08-19 12:31:47 +03:30
16 changed files with 5923 additions and 14 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,596 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<title>مرجع یکپارچه‌سازی‌های خارجی</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "Vazirmatn", "Tahoma", "Segoe UI", system-ui, sans-serif;
font-size: 14px; line-height: 1.8;
background: #ffffff; color: #1f2328; padding: 24px;
}
h1 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.subtitle { font-size: 13px; color: #57606a; margin-bottom: 28px; }
h2 {
font-size: 15px; font-weight: 700;
margin-bottom: 10px; margin-top: 32px;
border-bottom: 1px solid #e5e7eb; padding-bottom: 6px;
}
h3 {
font-size: 12px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.03em;
color: #57606a; margin-bottom: 8px; margin-top: 14px;
}
.section-intro {
font-size: 13px; color: #57606a;
margin-bottom: 14px; line-height: 1.7;
}
.card {
border: 1px solid #e5e7eb; border-radius: 6px;
padding: 16px; background: #f7f8fa; margin-bottom: 16px;
}
.card.card-blue { border-right: 4px solid #3b82f6; }
.card.card-green { border-right: 4px solid #22c55e; }
.card.card-purple { border-right: 4px solid #8b5cf6; }
.card.card-orange { border-right: 4px solid #f97316; }
.card.card-teal { border-right: 4px solid #14b8a6; }
.card.card-indigo { border-right: 4px solid #6366f1; }
.card.card-gray { border-right: 4px solid #94a3b8; }
.card.card-red { border-right: 4px solid #ef4444; }
.card.card-yellow { border-right: 4px solid #eab308; }
table {
border-collapse: collapse; width: 100%;
font-size: 12px; margin-top: 4px; direction: rtl;
}
th {
background: #f1f5f9; font-weight: 600;
text-align: right; padding: 5px 8px; border: 1px solid #e5e7eb;
}
td { padding: 4px 8px; border: 1px solid #e5e7eb; vertical-align: top; }
tr:nth-child(even) td { background: #ffffff; }
code {
font-family: monospace; font-size: 11px; color: #3b82d4;
direction: ltr; unicode-bidi: embed;
}
.method {
font-family: monospace; font-size: 11px;
font-weight: 700; white-space: nowrap;
direction: ltr; unicode-bidi: embed;
}
.method.get { color: #059669; }
.method.post { color: #2563eb; }
.method.put { color: #d97706; }
.method.patch { color: #7c3aed; }
.note { font-size: 11px; color: #57606a; font-style: normal; margin-top: 6px; }
.warn { font-size: 11px; color: #9a3412; font-style: normal; margin-top: 6px; }
.status-badge {
display: inline-block; font-size: 11px; font-weight: 600;
padding: 1px 7px; border-radius: 10px;
}
.status-live { background: #dcfce7; color: #166534; }
.status-partial { background: #ffedd5; color: #9a3412; }
.status-disabled { background: #fee2e2; color: #991b1b; }
.status-internal { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }
.toc {
background: #f7f8fa; border: 1px solid #e5e7eb;
border-radius: 6px; padding: 14px 18px; margin-bottom: 28px;
}
.toc-title { font-size: 13px; font-weight: 700; margin-bottom: 8px; }
.toc ol { padding-right: 18px; padding-left: 0; }
.toc li { font-size: 13px; margin-bottom: 3px; }
.toc a { color: #3b82d4; text-decoration: none; }
.toc a:hover { text-decoration: underline; }
footer {
text-align: center; font-size: 12px; color: #57606a;
border-top: 1px solid #e5e7eb; margin-top: 40px; padding-top: 12px;
}
.max-wrap { max-width: 760px; margin: 0 auto; }
.flow-box {
background: #ffffff; border: 1px solid #e5e7eb; border-radius: 4px;
padding: 10px 14px; font-size: 12px; margin-top: 8px;
}
.flow-step {
display: flex; gap: 10px; align-items: flex-start; margin-bottom: 6px;
direction: rtl;
}
.flow-num {
flex-shrink: 0; width: 20px; height: 20px; border-radius: 50%;
background: #3b82d4; color: #fff; font-size: 11px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.flow-text { flex: 1; padding-top: 2px; }
.decision-tree {
font-size: 12px; background: #ffffff;
border: 1px solid #e5e7eb; border-radius: 4px; padding: 12px 16px;
margin-top: 8px; line-height: 1.9;
}
.decision-tree ul { padding-right: 20px; padding-left: 0; }
.decision-tree li { margin-bottom: 2px; }
.env-table th:last-child { width: 220px; }
pre {
font-family: monospace; font-size: 11px;
background: #f1f5f9; border: 1px solid #e5e7eb;
border-radius: 4px; padding: 10px 12px;
white-space: pre-wrap; word-break: break-all;
margin-top: 6px; color: #1f2328;
direction: ltr; unicode-bidi: embed;
}
</style>
</head>
<body>
<div class="max-wrap">
<h1>مرجع یکپارچه‌سازی‌های خارجی</h1>
<p class="subtitle">
تمام یکپارچه‌سازی‌های خروجی: کاربرد، زمان فعال‌شدن، نحوه احراز هویت،
رفتار retry، fallbackها و تمام متغیرهای محیطی. سرویس‌های داخلی
(کپچا، داده‌های پرس‌وجوی آفلاین) برای کامل‌بودن گنجانده شده‌اند.
</p>
<!-- TOC -->
<div class="toc">
<div class="toc-title">فهرست</div>
<ol>
<li><a href="#inquiry-routing">درخت تصمیم مسیریابی پرس‌وجو</a></li>
<li><a href="#fanavaran">فناوران — پلتفرم خسارت بیمه</a></li>
<li><a href="#sanhub">SandHub — درگاه پرس‌وجوی قدیمی</a></li>
<li><a href="#tejarat">پرس‌وجوی تجارت — درگاه block-inquiry (V2+)</a></li>
<li><a href="#esg">ESG — ارائه‌دهنده پرس‌وجوی تنانت پارسیان</a></li>
<li><a href="#sms">پیامک — درگاه‌های کاوه‌نگار و پارسیان</a></li>
<li><a href="#ai">سرویس هوش مصنوعی — تشخیص خسارت خودرو</a></li>
<li><a href="#car-pricing">سرویس قیمت خودرو — جستجوی ارزش بازار</a></li>
<li><a href="#offline-inquiry">پرس‌وجوی آفلاین — داده‌های fallback</a></li>
<li><a href="#env-ref">مرجع متغیرهای محیطی</a></li>
</ol>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="inquiry-routing">۱ — درخت تصمیم مسیریابی پرس‌وجو</h2>
<p class="section-intro">
هر فایل تقصیر با فراخوانی "run-inquiries" آغاز می‌شود که بیمه‌نامه طرف مقصر
را از یک ارائه‌دهنده خارجی دریافت می‌کند. اینکه کدام ارائه‌دهنده واقعاً فراخوانی
می‌شود به سه عامل بستگی دارد: تنانت (<code>CLIENT_ID</code>)، نوع فایل
(THIRD_PARTY در مقابل CAR_BODY) و اینکه آیا حالت API زنده در تنظیمات سیستم
فعال است یا خیر. لایه داده‌های پرس‌وجوی آفلاین در جلوی هر سه ارائه‌دهنده قرار دارد.
</p>
<div class="card card-indigo">
<h3>انتخاب ارائه‌دهنده</h3>
<div class="decision-tree">
<strong>برای هر پرس‌وجوی مبتنی بر پلاک:</strong>
<ul>
<li>۱. بررسی داده‌های آفلاین (MongoDB) — اگر داده مطابق یافت شد، آن را برگردانده و تمام HTTP را رد کن.</li>
<li>۲. اگر <code>CLIENT_ID=8</code> (تنانت پارسیان/ESG) → مسیریابی به <strong>ESG</strong> <code>/inquiry/policyByPlate</code> یا <code>/inquiry/policyByChassis</code>.</li>
<li>۳. در غیر این صورت → مسیریابی به <strong>پرس‌وجوی تجارت</strong> <code>/block-inquiry-tejarat</code> (THIRD_PARTY) یا <code>/block-inquiry-tejarat/badane</code> (CAR_BODY).</li>
<li>۴. اگر <code>system_settings.externalApis.sandHubUseLiveApi = false</code> (پیش‌فرض) → پاسخ mock برگردانده شود به جای انجام فراخوانی‌های HTTP.</li>
</ul>
<br>
<strong>برای بررسی‌های هویت شخصی، گواهینامه، مالکیت و شبا:</strong>
<ul>
<li>اگر <code>CLIENT_ID=8</code> → ESG <code>/inquiry/person</code> و <code>/inquiry/sheba</code>.</li>
<li>در غیر این صورت → تجارت/SandHub <code>/personal-inquiry/tejarat-no</code>، <code>/driver-license-check</code>، <code>/ownership</code>، <code>/sheba/sheba-tejaratno</code>.</li>
</ul>
<br>
<strong>تفاوت کلیدی — فرمت تاریخ تولد:</strong>
SandHub/تجارت تاریخ تولد <em>میلادی</em> انتظار دارند (داخلی از جلالی تبدیل می‌شود).
ESG مستقیماً تاریخ <em>جلالی</em> انتظار دارد.
</div>
<p class="note" style="margin-top:8px;">
اندپوینت‌های SandHub فقط در مسیرهای قدیمی کد استفاده می‌شوند. تمام جریان‌های فعال تقصیر V2+ از طریق ارائه‌دهندگان تجارت یا ESG می‌روند.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="fanavaran">۲ — فناوران <span class="status-badge status-live">فعال</span></h2>
<p class="section-intro">
فناوران (<code>apimanager.iraneit.com</code>) پلتفرم ملی پرونده خسارت بیمه است.
پس از اینکه کارشناس خسارت ارزیابی خود را ارسال می‌کند، سیستم به‌صورت خودکار
یک خسارت ساختاریافته را از طریق یک پروتکل چهار مرحله‌ای به فناوران ارسال می‌کند.
فناوران همچنین به‌عنوان منبع جستجوی code-listها (انواع تصادف، اجزای خودرو،
کدهای شهر و غیره) در سراسر پلتفرم عمل می‌کند.
</p>
<div class="card card-blue">
<h3>چرخه حیات احراز هویت</h3>
<div class="flow-box">
<div class="flow-step"><div class="flow-num">۱</div><div class="flow-text"><strong>GET AppToken</strong> — <code>POST /EITAuthentication/GetAppToken</code> با هدرهای <code>appname</code> + <code>secret</code>. هدر <code>apptoken</code> را برمی‌گرداند.</div></div>
<div class="flow-step"><div class="flow-num">۲</div><div class="flow-text"><strong>Login</strong> — <code>POST /EITAuthentication/Login</code> با هدرهای <code>appToken</code> + <code>userName</code> + <code>password</code>. هدر <code>authenticationToken</code> را برمی‌گرداند.</div></div>
<div class="flow-step"><div class="flow-num">۳</div><div class="flow-text"><strong>Cache</strong> — توکن در حافظه <em>و</em> پایدار در MongoDB (<code>fanavaran_auth_tokens</code>) ذخیره می‌شود. تا نیمه‌شب <strong>Asia/Tehran</strong> معتبر است — اولین فراخوانی پس از ۰۰:۰۰ یک توکن تازه دریافت می‌کند.</div></div>
<div class="flow-step"><div class="flow-num">۴</div><div class="flow-text"><strong>تمام فراخوانی‌های بعدی</strong> چهار هدر شامل می‌شوند: <code>authenticationToken</code>، <code>CorpId</code>، <code>ContractId</code>، <code>Location</code> — مختص تنانت، hardcoded به ازای هر کلید <code>FANAVARAN_CLIENT</code>.</div></div>
</div>
<p class="note" style="margin-top:8px;">
یک اثر انگشت پیکربندی (هش appName + secret + username + password + corpId + contractId + location)
یک ورود تازه را زمانی که هر مدرکی تغییر کند، حتی قبل از نیمه‌شب، مجبور می‌کند.
</p>
</div>
<div class="card card-blue">
<h3>پروتکل ارسال خسارت (۴ مرحله)</h3>
<div class="flow-box">
<div class="flow-step"><div class="flow-num">۱</div><div class="flow-text"><strong>خسارت پایه (GEN.03)</strong> — <code>POST /car/third-party-car-financial-claims</code>. داده‌های مالک، راننده، بیمه، وسیله نقلیه و تصادف را ارسال می‌کند. یک <code>claimId</code> و <code>claimNo</code> فناوران برمی‌گرداند. پیامک با هر دو شناسه برای مالک ارسال می‌شود.</div></div>
<div class="flow-step"><div class="flow-num">۲</div><div class="flow-text"><strong>موارد خسارت (GEN.05)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/dmg-cases</code>. یک ورودی به ازای هر قطعه آسیب‌دیده با شناسه کامپوننت، شدت و قیمت. سقف: کل ≤ ۵۳،۰۰۰،۰۰۰ تومان.</div></div>
<div class="flow-step"><div class="flow-num">۳</div><div class="flow-text"><strong>پیوست‌ها (GEN.07)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/files</code>. اسناد، تصاویر car-capture و ویدیوها که با شناسه فایل ارجاع داده شده‌اند.</div></div>
<div class="flow-step"><div class="flow-num">۴</div><div class="flow-text"><strong>کارشناسی (GEN.08)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/expertise</code>. متادیتای ارزیابی کارشناس (نقش کارشناس، تاریخ، نتیجه). ارسال را نهایی می‌کند.</div></div>
</div>
<p class="note" style="margin-top:8px;">
هر چهار مرحله در مجموعه <code>fanavaran_audit_logs</code> با بدنه کامل درخواست/پاسخ، وضعیت HTTP، مدت زمان و کد ردیابی برای اشکال‌زدایی ثبت می‌شوند.
</p>
</div>
<div class="card card-blue">
<h3>اندپوینت‌های Lookup</h3>
<p class="note">همه زیر <code>https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/</code>. نتایج روی دیسک (به ازای کلید مشتری) و در مجموعه MongoDB <code>lookups</code> کش می‌شوند. تنانت پارسیان قبل از درخواست API از DB می‌خواند؛ دیگران ابتدا به API می‌روند.</p>
<table>
<tr><th>کاربرد</th><th>مسیر</th></tr>
<tr><td>گزینه‌های dropdown برای accidentReason (نگاشت شده به شناسه‌های محلی)</td><td><code>/car/base-info/accident-causes</code></td></tr>
<tr><td>گزینه‌های accidentWay</td><td><code>/car/code-list/accident-report-type</code></td></tr>
<tr><td>طبقه‌بندی استفاده از وسیله نقلیه</td><td><code>/car/base-info/vehicle-use-types</code></td></tr>
<tr><td>روش پرداخت خسارت</td><td><code>/car/code-list/dmg-pay-method</code></td></tr>
<tr><td>گزینه‌های نوع گواهینامه</td><td><code>/car/base-info/driving-licence-types</code></td></tr>
<tr><td>طبقه‌بندی طرف مقصر</td><td><code>/car/code-list/accident-culprit-type</code></td></tr>
<tr><td>گزینه‌های محل بازرسی</td><td><code>/car/code-list/inspection-place</code></td></tr>
<tr><td>کدهای وضعیت کاهش قیمت</td><td><code>/car/code-list/drop-amount-status</code></td></tr>
<tr><td>کاتالوگ کامپوننت (نگاشت به قطعات بیرونی/داخلی)</td><td><code>/car/base-info/car-components</code></td></tr>
<tr><td>گزینه‌های شدت تصادف</td><td><code>/car/code-list/accident-level</code></td></tr>
<tr><td>تطبیق <code>INSURANCE_CORP_ID</code> ← corpId فناوران</td><td><code>/common/code-list/insurance-corp</code></td></tr>
<tr><td>انتخابگرهای شهر/استان</td><td><code>/common/base-info/cities</code>، <code>/common/base-info/Provinces</code></td></tr>
<tr><td>دریافت بیمه‌نامه کامل بر اساس شناسه پس از استعلام</td><td><code>/car/third-party-car-policies/{policyId}</code></td></tr>
<tr><td>جستجوی وسیله نقلیه بر اساس VIN</td><td><code>/car/vehicles/inquiry-by-vin?vin=…</code></td></tr>
<tr><td>فهرست بیمه‌نامه‌ها برای یک کد ملی</td><td><code>/common/Policies/inquiry-my-policies</code></td></tr>
<tr><td>دریافت رکورد مشتری بر اساس شناسه</td><td><code>/common/customers/{customerId}</code></td></tr>
<tr><td>جستجوی طرف بر اساس کد ملی + تاریخ تولد</td><td><code>/common/parties/inquiry-by-unique-identifier</code></td></tr>
</table>
</div>
<div class="card card-blue">
<h3>مدیریت خطا و انعطاف‌پذیری</h3>
<table>
<tr><th>توضیح</th><th>مکانیزم</th></tr>
<tr><td>۳ تلاش، ۵۰۰ ms ← ۱۰۰۰ ms backoff نمایی در تمام فراخوانی‌های HTTP.</td><td>Retry</td></tr>
<tr><td>وقتی فناوران پیام فارسی "دوباره تلاش کنید" (یا tracking-code 500) برمی‌گرداند، یک مکث ۵ دقیقه‌ای در سطح تنانت فعال می‌شود. تمام فراخوانی‌ها در این پنجره بلافاصله <code>503 ServiceUnavailable</code> دریافت می‌کنند — بدون فشار.</td><td>Backoff گذرا</td></tr>
<tr><td>در ۴۰۱، توکن از حافظه و MongoDB پاک می‌شود؛ فراخوانی بعدی GetAppToken + Login تازه را فعال می‌کند.</td><td>ابطال توکن</td></tr>
<tr><td>درخواست‌های همزمان ورود برای همان تنانت به یک Promise در حال پرواز جمع می‌شوند.</td><td>حذف تکراری Inflight</td></tr>
<tr><td>هر مرحله (GET_APP_TOKEN، LOGIN و هر چهار مرحله ارسال) در <code>fanavaran_audit_logs</code> با وضعیت STARTED / SUCCESS / FAILURE، هدرهای کامل، بدنه و مدت زمان نوشته می‌شود.</td><td>لاگ Audit</td></tr>
<tr><td>۲۰–۳۰ ثانیه به ازای هر فراخوانی HTTP.</td><td>Timeout</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>پروفایل‌های تنانت (<code>FANAVARAN_CLIENT</code>)</h3>
<p class="section-intro" style="margin-top:6px; margin-bottom:8px;">سه پروفایل تنانت از پیش تعیین‌شده وجود دارد. پروفایل فعال توسط متغیر محیطی <code>FANAVARAN_CLIENT</code> انتخاب می‌شود. هر پروفایل <code>appName</code>، <code>secret</code>، <code>username</code>، <code>password</code>، <code>CorpId</code>، <code>ContractId</code> و <code>Location</code> هدرهای خود را به علاوه پیش‌فرض‌های payload (AccidentCityId و غیره) دارد.</p>
<table>
<tr><th>شرکت بیمه</th><th>کلید</th></tr>
<tr><td>بیمه پارسیان</td><td><code>parsian</code></td></tr>
<tr><td>بیمه تجارت نو</td><td><code>tejaratno</code></td></tr>
<tr><td>بیمه معلم</td><td><code>moallem</code></td></tr>
</table>
<p class="note" style="margin-top:8px;">
<code>INSURANCE_CORP_ID</code> یک رشته عنوان نمایشی است (مثلاً <em>"بیمه پارسیان"</em>) که در برابر فهرست زنده فناوران <code>insurance-corp</code> تطبیق داده می‌شود تا <code>corpId</code> عددی مورد استفاده در ارسال‌ها را تولید کند. شناسه تطبیق‌یافته روی دیسک کش می‌شود.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="sanhub">۳ — SandHub <span class="status-badge status-partial">قدیمی</span></h2>
<p class="section-intro">
SandHub درگاه پرس‌وجوی اصلی است. هنوز در کدبیس حضور دارد اما تمام جریان‌های
فعال تقصیر (V2+) به ارائه‌دهنده پرس‌وجوی تجارت منتقل شده‌اند. اندپوینت‌های
SandHub قابل فراخوانی هستند اما فقط از طریق مسیرهای قدیمی کد قابل دسترسی هستند.
حالت mock آن توسط همان تنظیم سیستم <code>sandHubUseLiveApi</code> کنترل می‌شود.
</p>
<div class="card card-gray">
<h3>احراز هویت</h3>
<p class="note">
<code>POST {SANHUB_BASE_URL}/user/login</code> با بدنه JSON نام کاربری + رمز عبور.
توکن در حافظه برای <strong>۵۵ دقیقه</strong> کش می‌شود. در ۴۰۱، توکن پاک می‌شود و یک تلاش مجدد انجام می‌شود.
۳ تلاش با ۱۰۰۰ ms ← ۲۰۰۰ ms backoff نمایی.
</p>
</div>
<div class="card card-gray">
<h3>اندپوینت‌ها</h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat</code></td><td>پرس‌وجوی بیمه‌نامه مبتنی بر پلاک (THIRD_PARTY). بدنه: <code>leftTwoDigits</code>، <code>serialLetter</code>، <code>threeDigits</code>، <code>rightTwoDigits</code>، <code>nationalCode</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat/badane</code></td><td>پرس‌وجوی بیمه‌نامه CAR_BODY. Timeout ۵۰ ثانیه (طولانی‌تر از استاندارد).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/personal-inquiry/tejarat-no</code></td><td>بررسی هویت شخصی. بدنه: <code>nationalCode</code> + <code>birthDate</code> <em>میلادی</em> (داخلی از جلالی تبدیل می‌شود).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/driver-license-check</code></td><td>اعتبارسنجی گواهینامه. پرچم <code>IsSucceed</code> را برمی‌گرداند.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/ownership</code></td><td>بررسی مالکیت وسیله نقلیه. پرچم <code>IsSuccess</code> را برمی‌گرداند.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/sheba/sheba-tejaratno</code></td><td>اعتبارسنجی شبا / حساب بانکی. <code>ReturnValue</code> + <code>HasError</code> را برمی‌گرداند.</td></tr>
</table>
<p class="note" style="margin-top:8px;">
تمام اندپوینت‌ها پاسخ‌های mock کامل را زمانی که <code>sandHubUseLiveApi=false</code> در تنظیمات سیستم (پیش‌فرض) پشتیبانی می‌کنند. داده‌های mock قطعی هستند و به‌صورت محلی بدون هیچ فراخوانی HTTP تولید می‌شوند.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="tejarat">۴ — پرس‌وجوی تجارت <span class="status-badge status-live">فعال</span></h2>
<p class="section-intro">
درگاه فعال block-inquiry برای تمام تنانت‌های غیر ESG. در هر فراخوانی V2+
<code>run-inquiries</code> که <code>CLIENT_ID ≠ 8</code> استفاده می‌شود.
URL پایه قابل پیکربندی است؛ در تولید به همان هاست SandHub اشاره می‌کند اما از
اعتبارنامه‌های جداگانه استفاده می‌کند.
</p>
<div class="card card-teal">
<h3>احراز هویت</h3>
<p class="note">
<code>POST {TEJARAT_INQUIRY_BASE_URL}/user/login</code> با بدنه JSON ایمیل + رمز عبور.
توکن برای <strong>۵۵ دقیقه</strong> کش می‌شود. ۲ تلاش با ۵۰۰ ms ← ۱۰۰۰ ms backoff.
جدا از اعتبارنامه‌های SandHub — از <code>TEJARAT_INQUIRY_EMAIL</code> / <code>TEJARAT_INQUIRY_PASSWORD</code> استفاده می‌کند.
</p>
</div>
<div class="card card-teal">
<h3>اندپوینت‌ها</h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat</code></td><td>پرس‌وجوی پلاک THIRD_PARTY. بدنه: فیلدهای پلاک + <code>nationalCode</code>. ابتدا داده آفلاین بررسی می‌شود.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat/badane</code></td><td>پرس‌وجوی پلاک CAR_BODY. بدنه: <code>part1–part4</code> (عددی) + <code>nationalCode</code>. همیشه زنده می‌شود (mock برای مسیر badane وجود ندارد).</td></tr>
</table>
<p class="note" style="margin-top:8px;">
وقتی <code>sandHubUseLiveApi=false</code>، مسیر THIRD_PARTY یک پاسخ mock بدون HTTP برمی‌گرداند. مسیر CAR_BODY همیشه API زنده را صرف‌نظر از این پرچم فراخوانی می‌کند.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="esg">۵ — ESG <span class="status-badge status-live">فعال (CLIENT_ID=8)</span></h2>
<p class="section-intro">
ESG یک درگاه API بیمه داخلی است که منحصراً توسط تنانت پارسیان
(<code>CLIENT_ID=8</code>) استفاده می‌شود. برای تمام انواع پرس‌وجو زمانی که
این تنانت فعال است، جایگزین تجارت/SandHub می‌شود. شکل پاسخ متفاوتی دارد،
TTL توکن پویا دارد و تاریخ تولد را در فرمت <strong>جلالی</strong> انتظار دارد
(نه میلادی، برخلاف SandHub/تجارت).
</p>
<div class="card card-purple">
<h3>احراز هویت</h3>
<p class="note">
<code>POST {ESG_URL}/auth/login</code> با بدنه JSON <code>{ username, password }</code>.
TTL توکن از فیلد <code>expiresIn</code> پاسخ خوانده می‌شود (پیش‌فرض ۱۴ دقیقه).
۲ تلاش با ۵۰۰ ms ← ۱۰۰۰ ms backoff. در ۴۰۱، توکن پاک و یک تلاش مجدد.
URL پیش‌فرض: <code>http://192.168.20.22:8085</code> (شبکه داخلی).
</p>
</div>
<div class="card card-purple">
<h3>اندپوینت‌ها</h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByPlate</code></td><td>جستجوی بیمه‌نامه مبتنی بر پلاک (THIRD_PARTY). بدنه: <code>nationalCode</code>، <code>plk1–plk4</code>. پاسخ قبل از ذخیره به فرمت قدیمی تجارت نگاشت می‌شود.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByChassis</code></td><td>جایگزین مبتنی بر VIN/شاسی برای پرس‌وجوی پلاک. توسط اندپوینت‌های <code>run-inquiries-vin</code> فراخوانی می‌شود. از جستجوی شاسی ESG استفاده می‌کند (نه مسیر SandHub). بدنه: <code>nationalCode</code>، <code>chassis</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/person</code></td><td>بررسی هویت شخصی. بدنه: <code>nationalCode</code>، <code>birthDate</code> (جلالی، نه میلادی).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/sheba</code></td><td>اعتبارسنجی شبا / حساب بانکی.</td></tr>
</table>
<p class="note" style="margin-top:8px;">
ESG هر پاسخ را به صورت <code>{ success: boolean, data: … }</code> می‌پیچد. یک بدنه <code>success=false</code> به یک خطای فارسی "استعلام در دسترس نیست" ترجمه می‌شود.
بررسی داده آفلاین-پرس‌وجو هنوز ابتدا اجرا می‌شود، قبل از هر فراخوانی HTTP ESG.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="sms">۶ — پیامک <span class="status-badge status-live">فعال</span></h2>
<p class="section-intro">
دو ارائه‌دهنده پیامک پشتیبانی می‌شوند: <strong>کاوه‌نگار</strong> (پیش‌فرض)
و <strong>درگاه پیامک پارسیان</strong>. ارائه‌دهنده فعال توسط متغیر محیطی
<code>SMS_PROVIDER</code> (یا <code>SMS</code>) انتخاب می‌شود. هر دو ارائه‌دهنده
رابط درگاه داخلی یکسانی را پیاده‌سازی می‌کنند بنابراین لایه ارکستراسیون
مستقل از ارائه‌دهنده است.
</p>
<div class="card card-green">
<h3>انتخاب ارائه‌دهنده</h3>
<table>
<tr><th>ارائه‌دهنده فعال</th><th>مقدار</th><th>متغیر محیطی</th></tr>
<tr><td>کاوه‌نگار — <code>api.kavenegar.com</code></td><td><code>kavenegar</code> (پیش‌فرض)</td><td><code>SMS_PROVIDER</code> (یا <code>SMS</code>)</td></tr>
<tr><td>درگاه پیامک پارسیان — <code>PARSIAN_SMS_URL</code></td><td><code>parsian</code></td><td><code>SMS_PROVIDER</code> (یا <code>SMS</code>)</td></tr>
</table>
</div>
<div class="card card-green">
<h3>اندپوینت‌های کاوه‌نگار</h3>
<p class="note">URL پایه: <code>https://api.kavenegar.com/v1/{SMS_API_KEY}/</code></p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>زمان استفاده</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>sms/send.json</code></td><td>پیام‌های متن ساده (مثلاً متن‌های اطلاع‌رسانی مبتنی بر کلید ذخیره‌شده در مجموعه <code>sms_texts</code>).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>verify/lookup.json</code></td><td>تمام پیام‌های مبتنی بر قالب (OTPها، لینک‌های دعوت، اطلاع‌رسانی کارشناس). پارامترها: <code>receptor</code>، <code>token</code>[، <code>token2</code>، <code>token3</code>، <code>token10</code>]، <code>template</code>.</td></tr>
</table>
</div>
<div class="card card-green">
<h3>درگاه پیامک پارسیان</h3>
<p class="note">URL پایه از <code>PARSIAN_SMS_URL</code>. احراز هویت: هدر <code>X-PACKAGE-API-KEY</code> + <code>Authorization: Basic {PARSIAN_BASIC_TOKEN}</code>. به‌صورت GET با پارامترهای URL-encoded <code>ReceiverNumbers</code> و <code>Message</code> ارسال می‌کند. پیام‌های قالب قبل از ارسال به یک بدنه متن ساده پیش‌رندر می‌شوند (معادل verify/lookup ندارد).</p>
</div>
<div class="card card-green">
<h3>قالب‌های پیامک در حال استفاده</h3>
<table>
<tr><th>توکن‌ها</th><th>ماشه</th><th>نام قالب</th></tr>
<tr><td><code>token</code> = کد OTP</td><td>ورود OTP کاربر / اکتور، فراموشی رمز، OTPهای طرف</td><td><code>AUTH_SMS_TEMPLATE</code> (محیطی)</td></tr>
<tr><td><code>token</code> = publicId، <code>token2</code> = لینک</td><td>طرف دوم لینک دعوت تقصیر را از طریق پیامک دریافت می‌کند</td><td><code>yara724-invite-link</code></td></tr>
<tr><td><code>token</code> = نوع فایل، <code>token2</code> = نام خانوادگی کارشناس، <code>token3</code> = لینک</td><td>کارشناس میدانی لینک را برای یک طرف ارسال می‌کند</td><td><code>yara-field-expert-link</code></td></tr>
<tr><td><code>token</code> = publicId، <code>token2</code> = لینک</td><td>اطلاع به طرف که طرف دیگر با رأی کارشناس موافقت کرده است</td><td><code>yara-blame-agreement</code></td></tr>
<tr><td><code>token</code> = publicId، <code>token2</code> = لینک</td><td>طرف زیان‌دیده مطلع می‌شود که جریان خسارت را پس از تکمیل تقصیر باز کند</td><td><code>yara-claim-link</code></td></tr>
<tr><td><code>token</code> = "تصادف"/"خسارت"، <code>token2</code> = publicId، <code>token3</code> = نام خانوادگی کارشناس</td><td>کارشناس یک فایل تقصیر یا خسارت را قفل می‌کند</td><td><code>yara-expert-lock</code></td></tr>
<tr><td><code>token</code> = نوع فایل، <code>token2</code> = publicId، <code>token3</code> = لینک</td><td>کارشناس درخواست ارسال مجدد اسناد می‌دهد</td><td><code>yara-resend-documents</code></td></tr>
<tr><td><code>token</code> = نوع فایل، <code>token2</code> = publicId، <code>token3</code> = نام خانوادگی کارشناس، <code>token10</code> = لینک</td><td>طرف مطلع می‌شود که ارزیابی خسارت کارشناس را امضا کند</td><td><code>yara-signature</code></td></tr>
<tr><td><code>token</code> = publicId، <code>token2</code> = claimId فناوران، <code>token3</code> = claimNo فناوران</td><td>ارسال فناوران تأیید شد — با شماره و شناسه خسارت فناوران برای مالک خسارت ارسال می‌شود</td><td><code>yara-fanavaran-claim</code></td></tr>
</table>
<p class="note" style="margin-top:8px;">
تمام فراخوانی‌های پیامک fire-and-forget هستند — هرگز throw نمی‌کنند. شکست‌ها log می‌شوند اما جریان اصلی را مسدود نمی‌کنند.
یک مجموعه MongoDB <code>sms_send_logs</code> هر پیام خروجی را با نوع آن (OTP در مقابل TEMPLATE)، ارائه‌دهنده، نام قالب و وضعیت موفقیت/شکست ثبت می‌کند.
پیام‌های متنی اطلاع‌رسانی (اختلاف طرفین، امضای یک طرف و غیره) در راه‌اندازی در مجموعه <code>sms_texts</code> seed می‌شوند و در زمان اجرا قابل ویرایش هستند.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="ai">۷ — سرویس هوش مصنوعی <span class="status-badge status-disabled">غیرفعال (کد موجود)</span></h2>
<p class="section-intro">
یک سرویس تشخیص خسارت خودرو مبتنی بر تصویر در کدبیس یکپارچه‌سازی شده است اما
فراخوانی‌های HTTP آن <strong>کاملاً comment شده‌اند</strong>. ماژول در راه‌اندازی
مقداردهی اولیه می‌شود، تلاش برای ورود می‌کند (در صورت شکست به صورت خاموش
بلعیده می‌شود)، و یک متد <code>aiRequestImage</code> را expose می‌کند — اما
فراخوانی‌های axios زیرین غیرفعال هستند. سرویس هیچ جریان تولیدی را تحت تأثیر
قرار نمی‌دهد.
</p>
<div class="card card-yellow">
<h3>رابط مورد نظر (زمانی که دوباره فعال شود)</h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>{AI_URL_V2}/auth/login</code></td><td>احراز هویت با نام کاربری + رمز عبور. <code>accessToken</code> را برمی‌گرداند.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>{AI_URL_V2}/auth/profile</code></td><td>دریافت <code>apiKey.key</code> مورد نیاز به‌عنوان هدر درخواست <code>gateway-api-key</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>{AI_URL_V2}/services/car-damage/detector?version=ai-v7</code></td><td>ارسال تصویر قطعه خودرو (multipart). <code>downloadLink</code> با نتیجه حاشیه‌نویسی‌شده را برمی‌گرداند.</td></tr>
</table>
<p class="warn" style="margin-top:8px;">
وضعیت: هر سه فراخوانی در بلوک‌های <code>axios.request(…)</code> comment-شده پیچیده شده‌اند.
<code>CW_URL</code> در <code>.env.example</code> نیست. برای فعال‌سازی مجدد، فراخوانی‌های axios login، getApiKey و aiRequestImage را uncomment کنید و <code>AI_URL_V2</code>، <code>AI_USERNAME</code>، <code>AI_PASSWORD</code> را پیکربندی کنید.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="car-pricing">۸ — سرویس قیمت خودرو <span class="status-badge status-partial">نیمه‌فعال</span></h2>
<p class="section-intro">
فقط در طول محاسبه کاهش قیمت کارشناس-خسارت استفاده می‌شود. وقتی یک کارشناس
مقادیر شدت برای هر قطعه ارائه می‌دهد، سیستم قیمت‌های بازار بلادرنگ برای مدل
خودروی آسیب‌دیده را دریافت می‌کند، سپس کاهش قیمت را با استفاده از فرمول
محاسبه می‌کند: <strong>قیمت خودرو × ضریب سال × مجموع ضرایب قطعات ÷ ۴۰۰</strong>.
سرویس دو منبع داده (اندپوینت) دارد که به‌صورت موازی امتحان می‌شوند.
</p>
<div class="card card-orange">
<h3>اندپوینت‌ها</h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>{CW_URL}price?akharin</code></td><td>دریافت قیمت‌های بازار خودرو از منبع "آخرین". آرایه <code>{ carName, marketPrice }</code> را برمی‌گرداند.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>{CW_URL}price?hamrah</code></td><td>دریافت قیمت‌های بازار خودرو از منبع "همراه". همان شکل پاسخ.</td></tr>
</table>
<p class="note" style="margin-top:8px;">
هر دو اندپوینت امتحان می‌شوند؛ نتایج ادغام و حذف تکراری می‌شوند. بهترین تطابق برای
نام خودروی آسیب‌دیده با استفاده از <strong>فاصله Levenshtein</strong> (تطابق رشته فازی) پیدا می‌شود.
اگر هر دو اندپوینت شکست بخورند یا خالی برگردانند، محاسبه کاهش قیمت رد می‌شود (ناقص علامت‌گذاری می‌شود) — ارسال خسارت را مسدود نمی‌کند.
</p>
<p class="warn" style="margin-top:6px;">
<strong><code>CW_URL</code> در <code>.env.example</code> مستندسازی نشده است.</strong>
این سرویس در صورت تنظیم نشدن متغیر، به‌صورت خاموش هیچ کاهش قیمتی تولید نخواهد کرد.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="offline-inquiry">۹ — پرس‌وجوی آفلاین <span class="status-badge status-internal">داخلی / fallback</span></h2>
<p class="section-intro">
لایه پرس‌وجوی آفلاین فراخوانی‌های پرس‌وجوی مبتنی بر پلاک را قبل از اینکه هر
HTTP خارجی انجام شود رهگیری می‌کند. عمدتاً برای توسعه و تست (پلاک‌های شناخته‌شده
از پیش seed شده) استفاده می‌شود اما همچنین به‌عنوان fallback انعطاف‌پذیری
زمانی که سرویس‌های پرس‌وجوی زنده در دسترس نیستند عمل می‌کند. توسط یک پرچم
پایگاه‌داده زمان اجرا کنترل می‌شود، نه یک متغیر محیطی.
</p>
<div class="card card-gray">
<h3>نحوه کار</h3>
<table>
<tr><th>جزئیات</th><th>جنبه</th></tr>
<tr><td>مجموعه MongoDB <code>offline-inquiries</code>. اسناد شامل <code>clientKey</code>، فیلدهای نرمال‌شده پلاک، <code>nationalCode</code> و پاسخ از پیش ساخته‌شده <code>raw</code> + <code>mapped</code> برای برگرداندن هستند.</td><td>ذخیره‌سازی</td></tr>
<tr><td><code>system_settings.offlineInquiry.enabled</code> — پیش‌فرض <code>true</code>. تغییر از طریق <code>PATCH /super-admin/system-settings/offline-inquiry</code>.</td><td>سوئیچ اصلی</td></tr>
<tr><td>پلاک نرمال‌شده (فقط ارقام، عربی→فارسی) + کد ملی + کلید مشتری فناوران باید همه مطابقت داشته باشند. اگر پیدا شد، بلافاصله برگردانده می‌شود؛ هیچ فراخوانی HTTP انجام نمی‌شود.</td><td>ترتیب جستجو</td></tr>
<tr><td>فقط برای پرس‌وجوی block مبتنی بر پلاک (THIRD_PARTY) اعمال می‌شود. پرس‌وجوی CAR_BODY (<code>/badane</code>) همیشه API زنده را می‌زند.</td><td>محدوده</td></tr>
<tr><td><code>system_settings.externalApis.sandHubUseLiveApi</code> — وقتی <code>false</code> (پیش‌فرض)، حتی اگر هیچ داده آفلاینی مطابقت نداشته باشد، یک پاسخ mock داخلی برگردانده می‌شود به جای فراخوانی تجارت/ESG.</td><td>پرچم API زنده</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="env-ref">۱۰ — مرجع متغیرهای محیطی</h2>
<p class="section-intro">
تمام متغیرهای محیطی در سراسر تمام یکپارچه‌سازی‌ها، گروه‌بندی‌شده بر اساس سرویس.
متغیرهای علامت‌گذاری‌شده با <strong>*</strong> در <code>.env.example</code> وجود ندارند.
</p>
<div class="card card-blue">
<h3>فناوران</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>کلید پروفایل تنانت فعال: <code>parsian</code> | <code>tejaratno</code> | <code>moallem</code></td><td><code>FANAVARAN_CLIENT</code></td></tr>
<tr><td>عنوان نمایشی شرکت بیمه‌گر (مثلاً <em>"بیمه پارسیان"</em>) — در راه‌اندازی در برابر فهرست insurance-corp فناوران به یک corpId عددی تطبیق داده می‌شود.</td><td><code>INSURANCE_CORP_ID</code></td></tr>
</table>
<p class="note" style="margin-top:8px;">اعتبارنامه‌های هر تنانت (appName، secret، username، password، CorpId، ContractId، Location) در <code>src/core/config/fanavaran-client.config.ts</code> زیر <code>SEED_FANAVARAN_CLIENT_PROFILES</code> hardcoded شده‌اند.</p>
</div>
<div class="card card-gray">
<h3>SandHub (قدیمی)</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>URL پایه برای SandHub. پیش‌فرض: <code>http://82.99.202.245:3027</code></td><td><code>SANHUB_BASE_URL</code></td></tr>
<tr><td>URL کامل ورود (معمولاً base + <code>/user/login</code>)</td><td><code>SANHUB_URL_LOGIN</code></td></tr>
<tr><td>ایمیل ورود SandHub</td><td><code>SANHUB_USERNAME</code></td></tr>
<tr><td>رمز عبور ورود SandHub</td><td><code>SANHUB_PASSWORD</code></td></tr>
</table>
</div>
<div class="card card-teal">
<h3>پرس‌وجوی تجارت</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>URL پایه. پیش‌فرض: <code>http://82.99.202.245:3027</code></td><td><code>TEJARAT_INQUIRY_BASE_URL</code></td></tr>
<tr><td>ایمیل ورود</td><td><code>TEJARAT_INQUIRY_EMAIL</code></td></tr>
<tr><td>رمز عبور ورود</td><td><code>TEJARAT_INQUIRY_PASSWORD</code></td></tr>
</table>
</div>
<div class="card card-purple">
<h3>ESG (فقط CLIENT_ID=8)</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>به <code>8</code> تنظیم کنید تا ارائه‌دهنده پرس‌وجوی ESG برای تنانت پارسیان فعال شود.</td><td><code>CLIENT_ID</code></td></tr>
<tr><td>URL پایه ESG. پیش‌فرض: <code>http://192.168.20.22:8085</code> (شبکه داخلی)</td><td><code>ESG_URL</code></td></tr>
<tr><td>نام کاربری ورود ESG</td><td><code>ESG_USERNAME</code></td></tr>
<tr><td>رمز عبور ورود ESG</td><td><code>ESG_PASSWORD</code></td></tr>
</table>
</div>
<div class="card card-green">
<h3>پیامک</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td><code>kavenegar</code> (پیش‌فرض) یا <code>parsian</code></td><td><code>SMS_PROVIDER</code> (یا <code>SMS</code>)</td></tr>
<tr><td>کلید API کاوه‌نگار (الزامی وقتی provider = kavenegar)</td><td><code>SMS_API_KEY</code></td></tr>
<tr><td>نام قالب کاوه‌نگار برای پیام‌های OTP (مثلاً <code>yara-otp</code>)</td><td><code>AUTH_SMS_TEMPLATE</code></td></tr>
<tr><td>URL پایه درگاه پیامک پارسیان (الزامی وقتی provider = parsian)</td><td><code>PARSIAN_SMS_URL</code></td></tr>
<tr><td>مقدار هدر پیامک پارسیان <code>X-PACKAGE-API-KEY</code></td><td><code>PARSIAN_API_KEY</code></td></tr>
<tr><td>اعتبارنامه‌های رمزگذاری‌شده Base64 برای هدر <code>Authorization: Basic …</code></td><td><code>PARSIAN_BASIC_TOKEN</code></td></tr>
<tr><td>URL پایه فرانت‌اند — برای ساخت تمام لینک‌های دعوت + خسارت تعبیه‌شده در پیام‌های پیامک استفاده می‌شود</td><td><code>URL</code></td></tr>
</table>
</div>
<div class="card card-yellow">
<h3>سرویس هوش مصنوعی</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>URL پایه درگاه هوش مصنوعی. پیش‌فرض: <code>https://ai-gw.ittalie.ir</code> (استفاده نشده — سرویس غیرفعال است)</td><td><code>AI_URL_V2</code></td></tr>
<tr><td>نام کاربری ورود سرویس هوش مصنوعی (استفاده نشده)</td><td><code>AI_USERNAME</code></td></tr>
<tr><td>رمز عبور ورود سرویس هوش مصنوعی (استفاده نشده)</td><td><code>AI_PASSWORD</code></td></tr>
</table>
</div>
<div class="card card-orange">
<h3>سرویس قیمت خودرو</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>URL پایه برای API قیمت بازار خودرو (مثلاً <code>https://…/</code>). در <code>.env.example</code> نیست. کاهش قیمت به‌صورت خاموش رد می‌شود اگر تنظیم نشده باشد.</td><td><code>CW_URL</code> *</td></tr>
</table>
</div>
<div class="card card-gray">
<h3>عمومی / برنامه</h3>
<table class="env-table">
<tr><th>شرح</th><th>متغیر</th></tr>
<tr><td>پورت HTTP (پیش‌فرض ۳۰۰۰). توسط fallback insurance-corp فناوران برای فراخوانی اندپوینت جستجوی محلی خودش استفاده می‌شود.</td><td><code>PORT</code></td></tr>
<tr><td><code>true</code> / <code>false</code> — چالش کپچای ورود را فعال/غیرفعال می‌کند. داخلی، بدون سرویس خارجی.</td><td><code>CAPTCHA_ENABLED</code></td></tr>
<tr><td>TTL چالش کپچا به دقیقه.</td><td><code>EXP_CAPTCHA_TIME</code></td></tr>
<tr><td>TTL کد یکبار مصرف به دقیقه.</td><td><code>EXP_OTP_TIME</code></td></tr>
</table>
</div>
<footer>Made by Sepehr</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,593 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>External Integrations Reference</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, "Segoe UI", system-ui, sans-serif;
font-size: 14px; line-height: 1.6;
background: #ffffff; color: #1f2328; padding: 24px;
}
h1 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.subtitle { font-size: 13px; color: #57606a; margin-bottom: 28px; }
h2 {
font-size: 15px; font-weight: 700;
margin-bottom: 10px; margin-top: 32px;
border-bottom: 1px solid #e5e7eb; padding-bottom: 6px;
}
h3 {
font-size: 12px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.05em;
color: #57606a; margin-bottom: 8px; margin-top: 14px;
}
.section-intro {
font-size: 13px; color: #57606a;
margin-bottom: 14px; line-height: 1.5;
}
.card {
border: 1px solid #e5e7eb; border-radius: 6px;
padding: 16px; background: #f7f8fa; margin-bottom: 16px;
}
.card.card-blue { border-left: 4px solid #3b82f6; }
.card.card-green { border-left: 4px solid #22c55e; }
.card.card-purple { border-left: 4px solid #8b5cf6; }
.card.card-orange { border-left: 4px solid #f97316; }
.card.card-teal { border-left: 4px solid #14b8a6; }
.card.card-indigo { border-left: 4px solid #6366f1; }
.card.card-gray { border-left: 4px solid #94a3b8; }
.card.card-red { border-left: 4px solid #ef4444; }
.card.card-yellow { border-left: 4px solid #eab308; }
table {
border-collapse: collapse; width: 100%;
font-size: 12px; margin-top: 4px;
}
th {
background: #f1f5f9; font-weight: 600;
text-align: left; padding: 5px 8px; border: 1px solid #e5e7eb;
}
td { padding: 4px 8px; border: 1px solid #e5e7eb; vertical-align: top; }
tr:nth-child(even) td { background: #ffffff; }
code { font-family: monospace; font-size: 11px; color: #3b82d4; }
.method {
font-family: monospace; font-size: 11px;
font-weight: 700; white-space: nowrap;
}
.method.get { color: #059669; }
.method.post { color: #2563eb; }
.method.put { color: #d97706; }
.method.patch { color: #7c3aed; }
.note { font-size: 11px; color: #57606a; font-style: italic; margin-top: 6px; }
.warn { font-size: 11px; color: #9a3412; font-style: italic; margin-top: 6px; }
.status-badge {
display: inline-block; font-size: 11px; font-weight: 600;
padding: 1px 7px; border-radius: 10px;
}
.status-live { background: #dcfce7; color: #166534; }
.status-partial { background: #ffedd5; color: #9a3412; }
.status-disabled { background: #fee2e2; color: #991b1b; }
.status-internal { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }
.toc {
background: #f7f8fa; border: 1px solid #e5e7eb;
border-radius: 6px; padding: 14px 18px; margin-bottom: 28px;
}
.toc-title { font-size: 13px; font-weight: 700; margin-bottom: 8px; }
.toc ol { padding-left: 18px; }
.toc li { font-size: 13px; margin-bottom: 3px; }
.toc a { color: #3b82d4; text-decoration: none; }
.toc a:hover { text-decoration: underline; }
footer {
text-align: center; font-size: 12px; color: #57606a;
border-top: 1px solid #e5e7eb; margin-top: 40px; padding-top: 12px;
}
.max-wrap { max-width: 760px; margin: 0 auto; }
.flow-box {
background: #ffffff; border: 1px solid #e5e7eb; border-radius: 4px;
padding: 10px 14px; font-size: 12px; margin-top: 8px;
}
.flow-step {
display: flex; gap: 10px; align-items: flex-start; margin-bottom: 6px;
}
.flow-num {
flex-shrink: 0; width: 20px; height: 20px; border-radius: 50%;
background: #3b82d4; color: #fff; font-size: 11px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.flow-text { flex: 1; padding-top: 2px; }
.decision-tree {
font-size: 12px; background: #ffffff;
border: 1px solid #e5e7eb; border-radius: 4px; padding: 12px 16px;
margin-top: 8px; line-height: 1.8;
}
.decision-tree ul { padding-left: 20px; }
.decision-tree li { margin-bottom: 2px; }
.env-table th:first-child { width: 220px; }
pre {
font-family: monospace; font-size: 11px;
background: #f1f5f9; border: 1px solid #e5e7eb;
border-radius: 4px; padding: 10px 12px;
white-space: pre-wrap; word-break: break-all;
margin-top: 6px; color: #1f2328;
}
</style>
</head>
<body>
<div class="max-wrap">
<h1>External Integrations Reference</h1>
<p class="subtitle">
Every outbound integration: what it does, when it fires, how auth works,
retry behaviour, fallbacks, and all environment variables. Internal-only
services (captcha, offline inquiry seed) are included for completeness.
</p>
<!-- TOC -->
<div class="toc">
<div class="toc-title">Contents</div>
<ol>
<li><a href="#inquiry-routing">Inquiry routing decision tree</a></li>
<li><a href="#fanavaran">Fanavaran — insurance claims platform</a></li>
<li><a href="#sanhub">SandHub — legacy inquiry gateway</a></li>
<li><a href="#tejarat">Tejarat inquiry — block-inquiry gateway (V2+)</a></li>
<li><a href="#esg">ESG — Parsian-tenant inquiry provider</a></li>
<li><a href="#sms">SMS — Kavenegar and Parsian gateways</a></li>
<li><a href="#ai">AI service — car damage detection</a></li>
<li><a href="#car-pricing">Car pricing service — market value lookup</a></li>
<li><a href="#offline-inquiry">Offline inquiry — fallback seed data</a></li>
<li><a href="#env-ref">Environment variable reference</a></li>
</ol>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="inquiry-routing">1 — Inquiry Routing Decision Tree</h2>
<p class="section-intro">
Every blame file starts with a "run-inquiries" call that fetches the
guilty party's insurance policy from an external provider. Which provider
is actually called depends on three factors: the tenant (<code>CLIENT_ID</code>),
the file type (THIRD_PARTY vs CAR_BODY), and whether live API mode is
enabled in system settings. The offline-inquiry seed layer sits in front
of all three providers.
</p>
<div class="card card-indigo">
<h3>Provider selection</h3>
<div class="decision-tree">
<strong>For every plate-based block inquiry:</strong>
<ul>
<li>1. Check offline-inquiry seeds (MongoDB) — if a matching seed exists, return it and skip all HTTP.</li>
<li>2. If <code>CLIENT_ID=8</code> (Parsian/ESG tenant) → route to <strong>ESG</strong> <code>/inquiry/policyByPlate</code> or <code>/inquiry/policyByChassis</code>.</li>
<li>3. Otherwise → route to <strong>Tejarat inquiry</strong> <code>/block-inquiry-tejarat</code> (THIRD_PARTY) or <code>/block-inquiry-tejarat/badane</code> (CAR_BODY).</li>
<li>4. If <code>system_settings.externalApis.sandHubUseLiveApi = false</code> (default) → return mock response instead of making HTTP calls.</li>
</ul>
<br>
<strong>For personal-identity, driving-licence, ownership, and Sheba checks:</strong>
<ul>
<li>If <code>CLIENT_ID=8</code> → ESG <code>/inquiry/person</code> and <code>/inquiry/sheba</code>.</li>
<li>Otherwise → Tejarat/SandHub <code>/personal-inquiry/tejarat-no</code>, <code>/driver-license-check</code>, <code>/ownership</code>, <code>/sheba/sheba-tejaratno</code>.</li>
</ul>
<br>
<strong>Key difference — birth date format:</strong>
SandHub/Tejarat expect a <em>Gregorian</em> birth date (converted internally from Jalali).
ESG expects the <em>Jalali</em> date directly.
</div>
<p class="note" style="margin-top:8px;">
SandHub endpoints are only used in legacy code paths. All active V2+ blame flows go through the Tejarat or ESG providers.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="fanavaran">2 — Fanavaran <span class="status-badge status-live">live</span></h2>
<p class="section-intro">
Fanavaran (<code>apimanager.iraneit.com</code>) is the national insurance
damage-case platform. After the damage expert submits their assessment,
the system auto-submits a structured claim to Fanavaran through a
four-step protocol. Fanavaran also serves as the lookup source for
code-lists (accident types, car components, city codes, etc.) used
across the platform.
</p>
<div class="card card-blue">
<h3>Authentication lifecycle</h3>
<div class="flow-box">
<div class="flow-step"><div class="flow-num">1</div><div class="flow-text"><strong>GET AppToken</strong> — <code>POST /EITAuthentication/GetAppToken</code> with <code>appname</code> + <code>secret</code> headers. Returns <code>apptoken</code> header.</div></div>
<div class="flow-step"><div class="flow-num">2</div><div class="flow-text"><strong>Login</strong> — <code>POST /EITAuthentication/Login</code> with <code>appToken</code> + <code>userName</code> + <code>password</code> headers. Returns <code>authenticationToken</code> header.</div></div>
<div class="flow-step"><div class="flow-num">3</div><div class="flow-text"><strong>Cache</strong> — token is cached in memory <em>and</em> persisted to MongoDB (<code>fanavaran_auth_tokens</code>). Valid until midnight <strong>Asia/Tehran</strong> — the first call after 00:00 fetches a fresh token.</div></div>
<div class="flow-step"><div class="flow-num">4</div><div class="flow-text"><strong>All subsequent calls</strong> include four headers: <code>authenticationToken</code>, <code>CorpId</code>, <code>ContractId</code>, <code>Location</code> — tenant-specific, hardcoded per <code>FANAVARAN_CLIENT</code> key.</div></div>
</div>
<p class="note" style="margin-top:8px;">
A config fingerprint (hash of appName + secret + username + password + corpId + contractId + location)
forces a fresh login when any credential changes, even before midnight.
</p>
</div>
<div class="card card-blue">
<h3>Claim submission protocol (4 steps)</h3>
<div class="flow-box">
<div class="flow-step"><div class="flow-num">1</div><div class="flow-text"><strong>Base claim (GEN.03)</strong> — <code>POST /car/third-party-car-financial-claims</code>. Sends owner, driver, insurance, vehicle, and accident data. Returns a Fanavaran <code>claimId</code> and <code>claimNo</code>. SMS is sent to the owner with both identifiers.</div></div>
<div class="flow-step"><div class="flow-num">2</div><div class="flow-text"><strong>Damage cases (GEN.05)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/dmg-cases</code>. One entry per damaged part with component ID, severity, and price. Cap: total ≤ 53 000 000 Toman.</div></div>
<div class="flow-step"><div class="flow-num">3</div><div class="flow-text"><strong>Attachments (GEN.07)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/files</code>. Documents, car-capture images, and videos referenced by file ID.</div></div>
<div class="flow-step"><div class="flow-num">4</div><div class="flow-text"><strong>Expertise (GEN.08)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/expertise</code>. Expert assessment metadata (expert role, date, result). Finalises the submission.</div></div>
</div>
<p class="note" style="margin-top:8px;">
All four steps are recorded in the <code>fanavaran_audit_logs</code> collection with full request/response bodies, HTTP status, duration, and tracking code for debugging.
</p>
</div>
<div class="card card-blue">
<h3>Lookup endpoints</h3>
<p class="note">All under <code>https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/</code>. Results are cached to disk (per client key) and in the <code>lookups</code> MongoDB collection. Parsian tenant reads DB before hitting the API; others go to the API first.</p>
<table>
<tr><th>Path</th><th>Used for</th></tr>
<tr><td><code>/car/base-info/accident-causes</code></td><td>accidentReason dropdown options (mapped to local IDs)</td></tr>
<tr><td><code>/car/code-list/accident-report-type</code></td><td>accidentWay options</td></tr>
<tr><td><code>/car/base-info/vehicle-use-types</code></td><td>vehicle usage classification</td></tr>
<tr><td><code>/car/code-list/dmg-pay-method</code></td><td>damage payment method</td></tr>
<tr><td><code>/car/base-info/driving-licence-types</code></td><td>licence type options</td></tr>
<tr><td><code>/car/code-list/accident-culprit-type</code></td><td>guilty-party classification</td></tr>
<tr><td><code>/car/code-list/inspection-place</code></td><td>inspection location options</td></tr>
<tr><td><code>/car/code-list/drop-amount-status</code></td><td>price-drop status codes</td></tr>
<tr><td><code>/car/base-info/car-components</code></td><td>component catalog (maps to outer/inner parts)</td></tr>
<tr><td><code>/car/code-list/accident-level</code></td><td>accident severity options</td></tr>
<tr><td><code>/common/code-list/insurance-corp</code></td><td>resolve <code>INSURANCE_CORP_ID</code> → Fanavaran corpId</td></tr>
<tr><td><code>/common/base-info/cities</code>, <code>/common/base-info/Provinces</code></td><td>city/province pickers</td></tr>
<tr><td><code>/car/third-party-car-policies/{policyId}</code></td><td>fetch full policy by ID after inquiry</td></tr>
<tr><td><code>/car/vehicles/inquiry-by-vin?vin=…</code></td><td>VIN-based vehicle lookup</td></tr>
<tr><td><code>/common/Policies/inquiry-my-policies</code></td><td>list policies for a national code</td></tr>
<tr><td><code>/common/customers/{customerId}</code></td><td>fetch customer record by ID</td></tr>
<tr><td><code>/common/parties/inquiry-by-unique-identifier</code></td><td>party lookup by national code + birth date</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>Error handling &amp; resilience</h3>
<table>
<tr><th>Mechanism</th><th>Detail</th></tr>
<tr><td>Retry</td><td>3 attempts, 500 ms → 1 000 ms exponential backoff on all HTTP calls.</td></tr>
<tr><td>Transient backoff</td><td>When Fanavaran returns the Persian "try again later" message (or tracking-code 500), a tenant-wide 5-minute pause is activated. All calls during this window get <code>503 ServiceUnavailable</code> immediately — no hammering.</td></tr>
<tr><td>Token invalidation</td><td>On 401, token is cleared from memory and MongoDB; next call triggers a fresh GetAppToken + Login.</td></tr>
<tr><td>Inflight de-dup</td><td>Concurrent login requests for the same tenant are collapsed to a single in-flight Promise.</td></tr>
<tr><td>Audit log</td><td>Every step (GET_APP_TOKEN, LOGIN, and all four submission steps) is written to <code>fanavaran_audit_logs</code> with STARTED / SUCCESS / FAILURE status, full headers, body, and duration.</td></tr>
<tr><td>Timeout</td><td>20–30 s per HTTP call.</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>Tenant profiles (<code>FANAVARAN_CLIENT</code>)</h3>
<p class="section-intro" style="margin-top:6px; margin-bottom:8px;">Three pre-seeded tenant profiles exist. The active one is chosen by the <code>FANAVARAN_CLIENT</code> env var. Each profile carries its own <code>appName</code>, <code>secret</code>, <code>username</code>, <code>password</code>, <code>CorpId</code>, <code>ContractId</code>, and <code>Location</code> headers, plus payload defaults (AccidentCityId, etc.).</p>
<table>
<tr><th>Key</th><th>Insurance company</th></tr>
<tr><td><code>parsian</code></td><td>Parsian Insurance</td></tr>
<tr><td><code>tejaratno</code></td><td>Tejaratno Insurance</td></tr>
<tr><td><code>moallem</code></td><td>Moallem Insurance</td></tr>
</table>
<p class="note" style="margin-top:8px;">
<code>INSURANCE_CORP_ID</code> is a display-caption string (e.g. <em>"بیمه پارسیان"</em>) that is resolved against the live Fanavaran <code>insurance-corp</code> list to produce the numeric <code>corpId</code> used in submissions. The resolved ID is cached to disk.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="sanhub">3 — SandHub <span class="status-badge status-partial">legacy</span></h2>
<p class="section-intro">
SandHub is the original inquiry gateway. It is still present in the
codebase but all active blame flows (V2+) have been migrated to the
Tejarat inquiry provider. SandHub endpoints remain callable but are
only reached through legacy code paths. Its mock mode is controlled
by the same <code>sandHubUseLiveApi</code> system setting.
</p>
<div class="card card-gray">
<h3>Auth</h3>
<p class="note">
<code>POST {SANHUB_BASE_URL}/user/login</code> with username + password JSON body.
Token cached in memory for <strong>55 minutes</strong>. On 401, token is cleared and one retry is made.
3 attempts with 1 000 ms → 2 000 ms exponential backoff.
</p>
</div>
<div class="card card-gray">
<h3>Endpoints</h3>
<table>
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat</code></td><td>Plate-based insurance policy inquiry (THIRD_PARTY). Body: <code>leftTwoDigits</code>, <code>serialLetter</code>, <code>threeDigits</code>, <code>rightTwoDigits</code>, <code>nationalCode</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat/badane</code></td><td>CAR_BODY policy inquiry. Timeout 50 s (longer than standard).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/personal-inquiry/tejarat-no</code></td><td>Personal identity check. Body: <code>nationalCode</code> + <em>Gregorian</em> <code>birthDate</code> (converted from Jalali internally).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/driver-license-check</code></td><td>Driving licence validation. Returns <code>IsSucceed</code> flag.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/ownership</code></td><td>Vehicle ownership check. Returns <code>IsSuccess</code> flag.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/sheba/sheba-tejaratno</code></td><td>Sheba / bank account validation. Returns <code>ReturnValue</code> + <code>HasError</code>.</td></tr>
</table>
<p class="note" style="margin-top:8px;">
All endpoints support full mock responses when <code>sandHubUseLiveApi=false</code> in system settings (default). Mock data is deterministic and produced locally without any HTTP calls.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="tejarat">4 — Tejarat Inquiry <span class="status-badge status-live">live</span></h2>
<p class="section-intro">
The active block-inquiry gateway for all non-ESG tenants. Used in every
V2+ <code>run-inquiries</code> call where <code>CLIENT_ID ≠ 8</code>.
The base URL is configurable; in production it points to the same host
as SandHub but uses separate credentials.
</p>
<div class="card card-teal">
<h3>Auth</h3>
<p class="note">
<code>POST {TEJARAT_INQUIRY_BASE_URL}/user/login</code> with email + password JSON body.
Token cached for <strong>55 minutes</strong>. 2 attempts with 500 ms → 1 000 ms backoff.
Separate from SandHub credentials — uses <code>TEJARAT_INQUIRY_EMAIL</code> / <code>TEJARAT_INQUIRY_PASSWORD</code>.
</p>
</div>
<div class="card card-teal">
<h3>Endpoints</h3>
<table>
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat</code></td><td>THIRD_PARTY plate inquiry. Body: plate fields + <code>nationalCode</code>. Offline seed checked first.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/block-inquiry-tejarat/badane</code></td><td>CAR_BODY plate inquiry. Body: <code>part1–part4</code> (numeric) + <code>nationalCode</code>. Always goes live (no mock for badane path).</td></tr>
</table>
<p class="note" style="margin-top:8px;">
When <code>sandHubUseLiveApi=false</code>, the THIRD_PARTY path returns a mock response without HTTP. The CAR_BODY path always calls the live API regardless of this flag.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="esg">5 — ESG <span class="status-badge status-live">live (CLIENT_ID=8)</span></h2>
<p class="section-intro">
ESG is an internal insurance API gateway used exclusively by the Parsian
tenant (<code>CLIENT_ID=8</code>). It replaces Tejarat/SandHub for all
inquiry types when this tenant is active. It has a different response
shape, a dynamic token TTL, and expects birth dates in <strong>Jalali</strong>
format (not Gregorian, unlike SandHub/Tejarat).
</p>
<div class="card card-purple">
<h3>Auth</h3>
<p class="note">
<code>POST {ESG_URL}/auth/login</code> with <code>{ username, password }</code> JSON body.
Token TTL is read from the response <code>expiresIn</code> field (default 14 min).
2 attempts with 500 ms → 1 000 ms backoff. On 401, token cleared and one retry.
Default URL: <code>http://192.168.20.22:8085</code> (internal network).
</p>
</div>
<div class="card card-purple">
<h3>Endpoints</h3>
<table>
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByPlate</code></td><td>Plate-based policy lookup (THIRD_PARTY). Body: <code>nationalCode</code>, <code>plk1–plk4</code>. Response is mapped to the old Tejarat format before being stored.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByChassis</code></td><td>VIN/chassis-based alternative to plate inquiry. Called by <code>run-inquiries-vin</code> endpoints. Uses ESG chassis lookup (not the SandHub path). Body: <code>nationalCode</code>, <code>chassis</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/person</code></td><td>Personal identity check. Body: <code>nationalCode</code>, <code>birthDate</code> (Jalali, NOT Gregorian).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/sheba</code></td><td>Sheba / bank account validation.</td></tr>
</table>
<p class="note" style="margin-top:8px;">
ESG wraps every response as <code>{ success: boolean, data: … }</code>. A <code>success=false</code> body is translated to a Persian "استعلام در دسترس نیست" (inquiry unavailable) error.
The offline-inquiry seed check still runs first, before any ESG HTTP call.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="sms">6 — SMS <span class="status-badge status-live">live</span></h2>
<p class="section-intro">
Two SMS providers are supported: <strong>Kavenegar</strong> (default)
and <strong>Parsian SMS Gateway</strong>. The active provider is chosen
by the <code>SMS_PROVIDER</code> (or <code>SMS</code>) env var. Both
providers implement the same internal gateway interface so the
orchestration layer is provider-agnostic.
</p>
<div class="card card-green">
<h3>Provider selection</h3>
<table>
<tr><th>Env var</th><th>Value</th><th>Active provider</th></tr>
<tr><td><code>SMS_PROVIDER</code> (or <code>SMS</code>)</td><td><code>kavenegar</code> (default)</td><td>Kavenegar — <code>api.kavenegar.com</code></td></tr>
<tr><td><code>SMS_PROVIDER</code> (or <code>SMS</code>)</td><td><code>parsian</code></td><td>Parsian SMS Gateway — <code>PARSIAN_SMS_URL</code></td></tr>
</table>
</div>
<div class="card card-green">
<h3>Kavenegar endpoints</h3>
<p class="note">Base URL: <code>https://api.kavenegar.com/v1/{SMS_API_KEY}/</code></p>
<table>
<tr><th style="width:70px">Method</th><th>Path</th><th>When used</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>sms/send.json</code></td><td>Plain-text messages (e.g. key-based notification texts stored in <code>sms_texts</code> collection).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>verify/lookup.json</code></td><td>All template-based messages (OTPs, invite links, expert notifications). Params: <code>receptor</code>, <code>token</code>[, <code>token2</code>, <code>token3</code>, <code>token10</code>], <code>template</code>.</td></tr>
</table>
</div>
<div class="card card-green">
<h3>Parsian SMS Gateway</h3>
<p class="note">Base URL from <code>PARSIAN_SMS_URL</code>. Auth: <code>X-PACKAGE-API-KEY</code> header + <code>Authorization: Basic {PARSIAN_BASIC_TOKEN}</code>. Sends as a GET with URL-encoded <code>ReceiverNumbers</code> and <code>Message</code> query params. Template messages are pre-rendered into a plain text body before sending (no verify/lookup equivalent).</p>
</div>
<div class="card card-green">
<h3>SMS templates in use</h3>
<table>
<tr><th>Template name</th><th>Trigger</th><th>Tokens</th></tr>
<tr><td><code>AUTH_SMS_TEMPLATE</code> (env)</td><td>User / actor OTP login, forget-password, party OTPs</td><td><code>token</code> = OTP code</td></tr>
<tr><td><code>yara724-invite-link</code></td><td>Second party receives blame invite link via SMS</td><td><code>token</code> = publicId, <code>token2</code> = link</td></tr>
<tr><td><code>yara-field-expert-link</code></td><td>Field expert sends link to a party</td><td><code>token</code> = file type, <code>token2</code> = expert surname, <code>token3</code> = link</td></tr>
<tr><td><code>yara-blame-agreement</code></td><td>Notify party that the other side agreed to the expert verdict</td><td><code>token</code> = publicId, <code>token2</code> = link</td></tr>
<tr><td><code>yara-claim-link</code></td><td>Damaged party notified to open claim flow after blame is complete</td><td><code>token</code> = publicId, <code>token2</code> = link</td></tr>
<tr><td><code>yara-expert-lock</code></td><td>Expert locks a blame or claim file</td><td><code>token</code> = "تصادف"/"خسارت", <code>token2</code> = publicId, <code>token3</code> = expert surname</td></tr>
<tr><td><code>yara-resend-documents</code></td><td>Expert requests document resend</td><td><code>token</code> = file kind, <code>token2</code> = publicId, <code>token3</code> = link</td></tr>
<tr><td><code>yara-signature</code></td><td>Party notified to sign the expert's damage assessment</td><td><code>token</code> = file kind, <code>token2</code> = publicId, <code>token3</code> = expert surname, <code>token10</code> = link</td></tr>
<tr><td><code>yara-fanavaran-claim</code></td><td>Fanavaran submission confirmed — sent to claim owner with Fanavaran claim number and ID</td><td><code>token</code> = publicId, <code>token2</code> = Fanavaran claimId, <code>token3</code> = Fanavaran claimNo</td></tr>
</table>
<p class="note" style="margin-top:8px;">
All SMS calls are fire-and-forget — they never throw. Failures are logged but do not block the main flow.
An <code>sms_send_logs</code> MongoDB collection records every outbound message with its kind (OTP vs TEMPLATE), provider, template name, and success/failure status.
Notification text messages (parties-disagree, one-party-signed, etc.) are seeded into the <code>sms_texts</code> collection on startup and editable at runtime.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="ai">7 — AI Service <span class="status-badge status-disabled">disabled (code present)</span></h2>
<p class="section-intro">
An image-based car damage detection service is integrated in the
codebase but its HTTP calls are <strong>fully commented out</strong>.
The module initialises on startup, attempts a login (silently swallowed
if it fails), and exposes an <code>aiRequestImage</code> method — but
the underlying axios calls are disabled. The service does not affect
any production flow.
</p>
<div class="card card-yellow">
<h3>Intended interface (when re-enabled)</h3>
<table>
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>{AI_URL_V2}/auth/login</code></td><td>Authenticate with username + password. Returns <code>accessToken</code>.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>{AI_URL_V2}/auth/profile</code></td><td>Fetch <code>apiKey.key</code> needed as the <code>gateway-api-key</code> request header.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>{AI_URL_V2}/services/car-damage/detector?version=ai-v7</code></td><td>Submit a car part image (multipart). Returns <code>downloadLink</code> with annotated result.</td></tr>
</table>
<p class="warn" style="margin-top:8px;">
Status: all three calls are wrapped in commented-out <code>axios.request(…)</code> blocks.
<code>CW_URL</code> is not in <code>.env.example</code>. To re-enable, uncomment the login, getApiKey, and aiRequestImage axios calls, and configure <code>AI_URL_V2</code>, <code>AI_USERNAME</code>, <code>AI_PASSWORD</code>.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="car-pricing">8 — Car Pricing Service <span class="status-badge status-partial">partially active</span></h2>
<p class="section-intro">
Used only during damage-expert price-drop calculation. When an expert
provides per-part severity values the system fetches real-time market
prices for the damaged car model, then computes the price-drop using
the formula: <strong>carPrice × yearCoefficient × sumOfPartCoefficients ÷ 400</strong>.
The service has two data sources (endpoints) that are tried in parallel.
</p>
<div class="card card-orange">
<h3>Endpoints</h3>
<table>
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>{CW_URL}price?akharin</code></td><td>Fetch car market prices from the "Akharin" source. Returns array of <code>{ carName, marketPrice }</code>.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>{CW_URL}price?hamrah</code></td><td>Fetch car market prices from the "Hamrah" source. Same response shape.</td></tr>
</table>
<p class="note" style="margin-top:8px;">
Both endpoints are tried; results are merged and de-duplicated. The best match for
the damaged car's name is found using <strong>Levenshtein distance</strong> (fuzzy string match).
If both endpoints fail or return empty, the price-drop calculation is skipped (marked incomplete) — it does not block claim submission.
</p>
<p class="warn" style="margin-top:6px;">
<strong><code>CW_URL</code> is not documented in <code>.env.example</code>.</strong>
This service will silently produce no price-drop if the variable is unset.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="offline-inquiry">9 — Offline Inquiry <span class="status-badge status-internal">internal / fallback</span></h2>
<p class="section-intro">
The offline inquiry layer intercepts plate-based inquiry calls before
any external HTTP is made. It is primarily used for development and
testing (pre-seeded known plates) but also acts as a resilience fallback
when live inquiry services are unavailable. It is controlled by a
runtime database flag, not an env var.
</p>
<div class="card card-gray">
<h3>How it works</h3>
<table>
<tr><th>Aspect</th><th>Detail</th></tr>
<tr><td>Storage</td><td>MongoDB collection <code>offline-inquiries</code>. Documents contain <code>clientKey</code>, normalised plate fields, <code>nationalCode</code>, and the pre-built <code>raw</code> + <code>mapped</code> response to return.</td></tr>
<tr><td>Master switch</td><td><code>system_settings.offlineInquiry.enabled</code> — defaults to <code>true</code>. Toggle via <code>PATCH /super-admin/system-settings/offline-inquiry</code>.</td></tr>
<tr><td>Lookup order</td><td>Normalised plate (digits-only, Arabic→Persian) + national code + Fanavaran client key must all match. If found, returned immediately; no HTTP call is made.</td></tr>
<tr><td>Scope</td><td>Only applies to plate-based block-inquiry (THIRD_PARTY). CAR_BODY inquiry (<code>/badane</code>) always hits the live API.</td></tr>
<tr><td>Live API flag</td><td><code>system_settings.externalApis.sandHubUseLiveApi</code> — when <code>false</code> (default), even if no offline seed matches, a built-in mock response is returned rather than calling Tejarat/ESG.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="env-ref">10 — Environment Variable Reference</h2>
<p class="section-intro">
All env vars across all integrations, grouped by service.
Variables marked <strong>*</strong> are not present in <code>.env.example</code>.
</p>
<div class="card card-blue">
<h3>Fanavaran</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>FANAVARAN_CLIENT</code></td><td>Active tenant profile key: <code>parsian</code> | <code>tejaratno</code> | <code>moallem</code></td></tr>
<tr><td><code>INSURANCE_CORP_ID</code></td><td>Display caption of the insurer company (e.g. <em>"بیمه پارسیان"</em>) — resolved to a numeric corpId at startup against the Fanavaran insurance-corp list.</td></tr>
</table>
<p class="note" style="margin-top:8px;">Per-tenant credentials (appName, secret, username, password, CorpId, ContractId, Location) are hardcoded in <code>src/core/config/fanavaran-client.config.ts</code> under <code>SEED_FANAVARAN_CLIENT_PROFILES</code>.</p>
</div>
<div class="card card-gray">
<h3>SandHub (legacy)</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>SANHUB_BASE_URL</code></td><td>Base URL for SandHub. Default: <code>http://82.99.202.245:3027</code></td></tr>
<tr><td><code>SANHUB_URL_LOGIN</code></td><td>Full login URL (usually base + <code>/user/login</code>)</td></tr>
<tr><td><code>SANHUB_USERNAME</code></td><td>SandHub login email</td></tr>
<tr><td><code>SANHUB_PASSWORD</code></td><td>SandHub login password</td></tr>
</table>
</div>
<div class="card card-teal">
<h3>Tejarat inquiry</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>TEJARAT_INQUIRY_BASE_URL</code></td><td>Base URL. Default: <code>http://82.99.202.245:3027</code></td></tr>
<tr><td><code>TEJARAT_INQUIRY_EMAIL</code></td><td>Login email</td></tr>
<tr><td><code>TEJARAT_INQUIRY_PASSWORD</code></td><td>Login password</td></tr>
</table>
</div>
<div class="card card-purple">
<h3>ESG (CLIENT_ID=8 only)</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>CLIENT_ID</code></td><td>Set to <code>8</code> to activate the ESG inquiry provider for the Parsian tenant.</td></tr>
<tr><td><code>ESG_URL</code></td><td>ESG base URL. Default: <code>http://192.168.20.22:8085</code> (internal network)</td></tr>
<tr><td><code>ESG_USERNAME</code></td><td>ESG login username</td></tr>
<tr><td><code>ESG_PASSWORD</code></td><td>ESG login password</td></tr>
</table>
</div>
<div class="card card-green">
<h3>SMS</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>SMS_PROVIDER</code> (or <code>SMS</code>)</td><td><code>kavenegar</code> (default) or <code>parsian</code></td></tr>
<tr><td><code>SMS_API_KEY</code></td><td>Kavenegar API key (required when provider = kavenegar)</td></tr>
<tr><td><code>AUTH_SMS_TEMPLATE</code></td><td>Kavenegar template name for OTP messages (e.g. <code>yara-otp</code>)</td></tr>
<tr><td><code>PARSIAN_SMS_URL</code></td><td>Parsian SMS Gateway base URL (required when provider = parsian)</td></tr>
<tr><td><code>PARSIAN_API_KEY</code></td><td>Parsian SMS <code>X-PACKAGE-API-KEY</code> header value</td></tr>
<tr><td><code>PARSIAN_BASIC_TOKEN</code></td><td>Base64-encoded credentials for <code>Authorization: Basic …</code> header</td></tr>
<tr><td><code>URL</code></td><td>Frontend base URL — used to build all invite + claim links embedded in SMS messages</td></tr>
</table>
</div>
<div class="card card-yellow">
<h3>AI service</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>AI_URL_V2</code></td><td>AI gateway base URL. Default: <code>https://ai-gw.ittalie.ir</code> (unused — service is disabled)</td></tr>
<tr><td><code>AI_USERNAME</code></td><td>AI service login username (unused)</td></tr>
<tr><td><code>AI_PASSWORD</code></td><td>AI service login password (unused)</td></tr>
</table>
</div>
<div class="card card-orange">
<h3>Car pricing service</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>CW_URL</code> *</td><td>Base URL for car market price API (e.g. <code>https://…/</code>). Not in <code>.env.example</code>. Price-drop silently skipped if unset.</td></tr>
</table>
</div>
<div class="card card-gray">
<h3>General / app</h3>
<table class="env-table">
<tr><th>Variable</th><th>Description</th></tr>
<tr><td><code>PORT</code></td><td>HTTP port (default 3000). Used by the Fanavaran insurance-corp fallback to call its own local lookup endpoint.</td></tr>
<tr><td><code>CAPTCHA_ENABLED</code></td><td><code>true</code> / <code>false</code> — enables/disables login CAPTCHA challenge. Internal, no external service.</td></tr>
<tr><td><code>EXP_CAPTCHA_TIME</code></td><td>CAPTCHA challenge TTL in minutes.</td></tr>
<tr><td><code>EXP_OTP_TIME</code></td><td>OTP TTL in minutes.</td></tr>
</table>
</div>
<footer>Made by Sepehr</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,624 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<title>مرجع نقش‌های پنل</title>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "Vazirmatn", "Tahoma", "Segoe UI", system-ui, sans-serif;
font-size: 14px;
line-height: 1.8;
background: #ffffff;
color: #1f2328;
padding: 24px;
}
h1 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.subtitle { font-size: 13px; color: #57606a; margin-bottom: 28px; }
h2 {
font-size: 15px; font-weight: 700;
margin-bottom: 10px; margin-top: 32px;
border-bottom: 1px solid #e5e7eb; padding-bottom: 6px;
}
h3 {
font-size: 12px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.03em;
color: #57606a; margin-bottom: 8px; margin-top: 14px;
}
.section-intro {
font-size: 13px; color: #57606a;
margin-bottom: 14px; line-height: 1.7;
}
/* Role header strip */
.role-header {
display: flex;
align-items: baseline;
gap: 10px;
margin-bottom: 6px;
}
.role-name {
font-size: 15px;
font-weight: 700;
}
.role-enum {
font-family: monospace;
font-size: 11px;
color: #3b82d4;
background: #f0f7ff;
border: 1px solid #bfdbfe;
border-radius: 4px;
padding: 1px 6px;
direction: ltr;
unicode-bidi: embed;
}
.badge {
display: inline-block;
font-size: 11px; font-weight: 600;
padding: 1px 7px; border-radius: 10px;
margin-left: 4px; margin-bottom: 3px;
}
.badge-blue { background: #dbeafe; color: #1d4ed8; }
.badge-green { background: #dcfce7; color: #166534; }
.badge-purple { background: #ede9fe; color: #5b21b6; }
.badge-orange { background: #ffedd5; color: #9a3412; }
.badge-teal { background: #ccfbf1; color: #0f766e; }
.badge-indigo { background: #e0e7ff; color: #3730a3; }
.badge-gray { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }
.badge-red { background: #fee2e2; color: #991b1b; }
/* Cards */
.card {
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 16px;
background: #f7f8fa;
margin-bottom: 16px;
}
.card.card-blue { border-right: 4px solid #3b82f6; }
.card.card-green { border-right: 4px solid #22c55e; }
.card.card-purple { border-right: 4px solid #8b5cf6; }
.card.card-orange { border-right: 4px solid #f97316; }
.card.card-teal { border-right: 4px solid #14b8a6; }
.card.card-indigo { border-right: 4px solid #6366f1; }
.card.card-gray { border-right: 4px solid #94a3b8; }
.card.card-red { border-right: 4px solid #ef4444; }
/* Endpoint tables */
table {
border-collapse: collapse;
width: 100%;
font-size: 12px;
margin-top: 4px;
direction: rtl;
}
th {
background: #f1f5f9; font-weight: 600;
text-align: right; padding: 5px 8px;
border: 1px solid #e5e7eb;
}
td {
padding: 4px 8px; border: 1px solid #e5e7eb;
vertical-align: top;
}
tr:nth-child(even) td { background: #ffffff; }
code {
font-family: monospace; font-size: 11px; color: #3b82d4;
direction: ltr; unicode-bidi: embed;
}
.method {
font-family: monospace; font-size: 11px;
font-weight: 700; white-space: nowrap;
direction: ltr; unicode-bidi: embed;
}
.method.get { color: #059669; }
.method.post { color: #2563eb; }
.method.put { color: #d97706; }
.method.patch { color: #7c3aed; }
.method.delete { color: #dc2626; }
.dep { color: #94a3b8; font-style: italic; font-size: 11px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 860px) { .grid-2 { grid-template-columns: 1fr; } }
.note {
font-size: 11px; color: #57606a; font-style: normal;
margin-top: 6px;
}
.toc {
background: #f7f8fa; border: 1px solid #e5e7eb;
border-radius: 6px; padding: 14px 18px;
margin-bottom: 28px;
}
.toc-title { font-size: 13px; font-weight: 700; margin-bottom: 8px; }
.toc ol { padding-right: 18px; padding-left: 0; }
.toc li { font-size: 13px; margin-bottom: 3px; }
.toc a { color: #3b82d4; text-decoration: none; }
.toc a:hover { text-decoration: underline; }
footer {
text-align: center; font-size: 12px; color: #57606a;
border-top: 1px solid #e5e7eb;
margin-top: 40px; padding-top: 12px;
}
.max-wrap { max-width: 760px; margin: 0 auto; }
/* Role overview table */
.overview-table { font-size: 12px; margin-bottom: 24px; }
.overview-table th { white-space: nowrap; }
.overview-table td:last-child { font-weight: 600; white-space: nowrap; }
</style>
</head>
<body>
<div class="max-wrap">
<h1>مرجع نقش‌های پنل</h1>
<p class="subtitle">
آنچه هر نقش می‌تواند ببیند و انجام دهد — اندپوینت‌ها، مسئولیت‌ها و
مراحل فرآیند. سوپر ادمین در این مستند نیست.
</p>
<!-- Table of Contents -->
<div class="toc">
<div class="toc-title">نقش‌های پوشش‌داده‌شده</div>
<ol>
<li><a href="#insurer">بیمه‌گر (COMPANY) — ادمین تنانت شرکت بیمه</a></li>
<li><a href="#blame-expert">کارشناس تقصیر (EXPERT) — صف بررسی اختلاف</a></li>
<li><a href="#damage-expert">کارشناس خسارت (DAMAGE_EXPERT) — قیمت‌گذاری خسارت</a></li>
<li><a href="#field-expert">کارشناس میدانی (FIELD_EXPERT) — ثبت حضوری در صحنه</a></li>
<li><a href="#file-maker">فایل‌ساز (FILE_MAKER) — روایت طرفین در V4/V5</a></li>
<li><a href="#file-reviewer">بازبین فایل (FILE_REVIEWER) — ارزیابی خسارت V4/V5</a></li>
<li><a href="#registrar">ثبات (REGISTRAR) — ثبت اداری حضوری</a></li>
<li><a href="#call-center">مرکز تماس (CALL_CENTER) — ثبت تلفنی V6</a></li>
</ol>
</div>
<!-- ── Role overview table ────────────────────────────────────── -->
<h2>نمای کلی نقش‌ها</h2>
<table class="overview-table">
<tr>
<th>وظیفه اصلی</th>
<th>محدوده</th>
<th>پنل ورود</th>
<th>enum نقش</th>
</tr>
<tr>
<td>مشاهده تمام فایل‌ها؛ مدیریت شعب و کارشناسان؛ گزارش‌گیری؛ امتیازدهی به کارشناسان</td>
<td>سطح تنانت</td>
<td>پورتال بیمه‌گر</td>
<td><code>company</code></td>
</tr>
<tr>
<td>قفل‌کردن پرونده‌های تقصیر، بررسی اسناد طرفین، صدور رأی یا درخواست ارسال مجدد</td>
<td>صف DISAGREEMENT تنانت</td>
<td>پنل کارشناس تقصیر</td>
<td><code>expert</code></td>
</tr>
<tr>
<td>قفل‌کردن خسارت، قیمت‌گذاری، اعتبارسنجی فاکتورها، درخواست ارسال مجدد/بازدید</td>
<td>صف خسارت تنانت</td>
<td>پنل خسارت</td>
<td><code>damage_expert</code></td>
</tr>
<tr>
<td>ثبت حضوری تقصیر + خسارت در V2/V3؛ دسترسی به پنل‌های تقصیر/خسارت</td>
<td>فایل‌های ساخته‌شده توسط خود</td>
<td>پنل کارشناس میدانی</td>
<td><code>field_expert</code></td>
</tr>
<tr>
<td>روایت طرفین V4/V5 (OTP، استعلام، جزئیات، امضا)؛ تأیید خسارت در V5</td>
<td>فایل‌های ساخته‌شده توسط خود</td>
<td>پنل فایل‌ساز</td>
<td><code>file_maker</code></td>
</tr>
<tr>
<td>ارزیابی خسارت V4/V5 (فیلدهای تصادف، قطعات، عکس‌ها، امضای مالک)</td>
<td>فایل‌های تخصیص‌یافته</td>
<td>پنل بازبین فایل</td>
<td><code>file_reviewer</code></td>
</tr>
<tr>
<td>ثبت حضوری اداری تقصیر + خسارت به نمایندگی از طرفین</td>
<td>فایل‌های ساخته‌شده توسط خود</td>
<td>پنل ثبات</td>
<td><code>registrar</code></td>
</tr>
<tr>
<td>ثبت تلفنی V6: اجرای استعلام، ارسال لینک؛ کاربر بقیه را تکمیل می‌کند</td>
<td>فایل‌های ساخته‌شده توسط خود</td>
<td>پنل مرکز تماس</td>
<td><code>call_center</code></td>
</tr>
</table>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="insurer">۱ — بیمه‌گر <span class="role-enum">company</span></h2>
<p class="section-intro">
به ازای هر تنانت شرکت بیمه یک اکتور <code>company</code> وجود دارد. پورتال بیمه‌گر
لایه مدیریتی است: می‌تواند همه چیز زیر تنانت خود را ببیند، لیست کارشناسان را مدیریت
کند، شعب را اداره کند، تنظیمات رسانه‌ای هر تنانت را پیکربندی کند و گزارش‌های آماری
استخراج کند. بیمه‌گر هرگز مستقیماً با مراحل تقصیر/خسارت درگیر نمی‌شود — فقط نظاره‌گر
و امتیازدهنده است.
</p>
<div class="card card-blue">
<h3>مدیریت فایل — <code>expert-insurer/</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files</code></td><td>فهرست تمام فایل‌های تقصیر + خسارت تنانت (ادغام‌شده بر اساس publicId). فیلترپذیر بر اساس وضعیت، نوع فایل، جستجو، مرتب‌سازی، صفحه.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files/:publicId</code></td><td>جزئیات کامل یک فایل بر اساس publicId.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files/:publicId/timeline</code></td><td>تایم‌لاین فعالیت به ترتیب زمانی (تمام رویدادهای تاریخچه: منبع، نوع، اکتور، متادیتا).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files/:publicId/report</code></td><td>داده‌های ساختاریافته گزارش برای تولید PDF (بخش‌های مالک، راننده، بیمه، خودرو، تصادف).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>expert-insurer/files/:publicId/rating</code></td><td>امتیازدهی به کارشناسان یک فایل (۱–۵ در هر بُعد: روش تصادف، به‌موقع‌بودن، دقت علت، دقت شناسایی مقصر، امتیاز ربات).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/report/unified-file-statuses</code></td><td>کاتالوگ وضعیت یکپارچه + تعداد به ازای هر وضعیت برای کل پرتفولیوی تنانت. فیلترپذیر بر اساس fileType و بازه تاریخ.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/report/status-counts</code></td><td class="dep">منسوخ‌شده — از unified-file-statuses استفاده کنید.</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>مدیریت شعب — <code>expert-insurer/branches</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/branches</code></td><td>فهرست تمام شعب این بیمه‌گر. پارامترها: جستجو، بازه تاریخ from/to، فیلتر isActive.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/branches</code></td><td>افزودن شعبه جدید (نام، کد، آدرس، شهر، تلفن و غیره).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>expert-insurer/branches/:branchId/status</code></td><td>فعال یا غیرفعال کردن یک شعبه.</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>مدیریت لیست کارشناسان — <code>expert-insurer/experts</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/blame</code></td><td>ایجاد حساب کارشناس تقصیر جدید زیر این بیمه‌گر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/claim</code></td><td>ایجاد حساب کارشناس خسارت جدید زیر این بیمه‌گر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/file-maker</code></td><td>ایجاد حساب فایل‌ساز جدید زیر این بیمه‌گر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/file-reviewer</code></td><td>ایجاد حساب بازبین فایل جدید زیر این بیمه‌گر.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/experts/list</code></td><td>فهرست صفحه‌بندی‌شده تمام حساب‌های کارشناس در این تنانت.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/experts/top</code></td><td>برترین کارشناسان تقصیر و خسارت رتبه‌بندی‌شده بر اساس میانگین امتیاز کلی (حداکثر ۱۰ نفر از هر نوع).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/top-experts</code></td><td>نام مستعار experts/top (سازگاری با فرانت‌اند).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/:expertId</code></td><td>فایل‌های رسیدگی‌شده توسط یک کارشناس (ردیف‌های خلاصه — تقصیر یا خسارت بسته به نوع کارشناس).</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>آمار و گزارش‌ها</h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/statistics</code></td><td>کارت‌های KPI: totalFilesReviewed، averageUserRating، inPersonCount، filesThisMonth، objectionPercentage و غیره. فیلترپذیر بر اساس بازه تاریخ.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/top-files</code></td><td>۱۰ فایل خسارت برتر بر اساس بالاترین امتیاز (ترکیبی از امتیاز بیمه‌گر + کاربر).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/expert-work-log</code></td><td>لاگ کاری هر کارشناس: totalHandled، currentlyChecking، distinctFilesCheckedInPeriod. فیلترپذیر بر اساس expertKind و بازه تاریخ.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/requests</code></td><td>تعداد خسارت + وضعیت تقصیر + تعداد فایل یکپارچه برای تنانت.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/per-month-requests</code></td><td>همان خلاصه، تفکیک‌شده بر اساس ۵ ماه تقویمی اخیر.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/checked-requests</code></td><td>همان خلاصه، فیلترشده بر اساس بازه زمانی اختیاری createdAt.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/expert-work-log</code></td><td>لاگ کاری کارشناسان (مجموعه‌های کارشناس تقصیر و خسارت، نه کارشناسان میدانی).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/expert-work-log/per-month</code></td><td>همان لاگ کاری تفکیک‌شده بر اساس ماه تقویمی (۵ ماه اخیر).</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>تنظیمات تنانت — <code>client-panel/</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>client-panel/settings</code></td><td>دریافت محدودیت‌های رسانه‌ای هر تنانت (حداکثر بایت ویدیو/تصویر/صوت) و پنجره زمانی تصادف CAR_BODY (روز).</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>client-panel/settings</code></td><td>به‌روزرسانی جزئی این تنظیمات. نمی‌تواند از سقف‌های سطح سیستم تجاوز کند.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="blame-expert">۲ — کارشناس تقصیر <span class="role-enum">expert</span></h2>
<p class="section-intro">
فایل‌های تقصیر در صف DISAGREEMENT را بررسی می‌کند — پرونده‌هایی که دو طرف درباره
مقصر بودن توافق ندارند. پس از بررسی اسناد و اظهارات طرفین، کارشناس پرونده را قفل
می‌کند، سپس یا رأی صادر می‌کند، درخواست ارسال مجدد اسناد می‌دهد، یا نتیجه بازدید
حضوری را ثبت می‌کند. تمام اندپوینت‌ها زیر <code>v2/expert-blame/</code> هستند.
</p>
<div class="card card-orange">
<h3>فرآیند</h3>
<p class="note">
۱ مرور فهرست ← ۲ تخصیص (قفل) پرونده ← ۳ بررسی مدارک طرفین (ویدیو، صدا، اسناد) ←
۴الف صدور رأی <em>یا</em> ۴ب درخواست ارسال مجدد اسناد <em>یا</em> ۴پ ثبت بازدید حضوری.
</p>
</div>
<div class="card card-orange">
<h3>اندپوینت‌ها — <code>v2/expert-blame/</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/</code></td><td>فهرست پرونده‌های تقصیر در صف DISAGREEMENT (موجود، قفل‌شده توسط من، یا تصمیم‌گرفته‌شده توسط من). پارامترها: search، sortBy، sortOrder، page، limit، unifiedStatus، fileType.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/:id</code></td><td>جزئیات کامل یک پرونده تقصیر (اظهارات، عکس، صدا، ویدیو طرفین).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>v2/expert-blame/:id/assign</code></td><td>بررسی در دسترس بودن و قفل پرونده برای این کارشناس. بازمی‌گرداند: <code>assigned</code>، <code>already_assigned_to_you</code>، یا ۴۰۹ در صورتی که شخص دیگری آن را نگه داشته باشد.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/reply/submit/:id</code></td><td>ارسال رأی (accidentWay، accidentReason، accidentType، تصمیم طرف مقصر). پرونده را آزاد می‌کند و به COMPLETED منتقل می‌کند.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/reply/resend/:id</code></td><td>درخواست از طرفین برای بارگذاری مجدد اسناد. تقصیر را به WAITING_FOR_RESEND تنظیم می‌کند. یک درخواست ارسال مجدد در هر چرخه عمر.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/reply/inPerson/:id</code></td><td>ثبت اینکه بازدید حضوری انجام شده و صدور رأی.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/report/unified-file-statuses</code></td><td>کاتالوگ وضعیت + تعداد به ازای هر وضعیت برای پرتفولیوی این کارشناس.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/report/status-counts</code></td><td class="dep">منسوخ‌شده — از unified-file-statuses استفاده کنید.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/lock/:id</code></td><td class="dep">اندپوینت قفل منسوخ‌شده — از POST assign استفاده کنید.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="damage-expert">۳ — کارشناس خسارت <span class="role-enum">damage_expert</span></h2>
<p class="section-intro">
فایل‌های خسارت را پس از ارسال مدارک خسارت توسط کاربر بررسی می‌کند. کارشناس
هر قطعه آسیب‌دیده را قیمت‌گذاری می‌کند، به‌صورت اختیاری کاهش قیمت (استهلاک)
محاسبه می‌کند، و می‌تواند از کاربر بخواهد مدارک را مجدداً ارسال کند، حضوری مراجعه
کند، یا فاکتورهای تعمیرگاه را هنگام نیاز به قیمت‌گذاری کارگاهی بارگذاری کند.
تمام اندپوینت‌ها زیر <code>v2/expert-claim/</code> هستند.
</p>
<div class="card card-red">
<h3>فرآیند</h3>
<p class="note">
۱ مرور فهرست ← ۲ تخصیص (قفل) خسارت ← ۳ بررسی عکس‌ها و اسناد خسارت ←
۴ ویرایش اختیاری قطعات انتخاب‌شده یا محاسبه کاهش قیمت ←
۵الف ارسال پاسخ قیمت‌گذاری‌شده <em>یا</em> ۵ب درخواست ارسال مجدد <em>یا</em> ۵پ درخواست بازدید حضوری ←
۶ در صورت وجود قطعات فاکتوردار: اعتبارسنجی فاکتورهای تعمیرگاه بارگذاری‌شده.
</p>
</div>
<div class="card card-red">
<h3>اندپوینت‌ها — <code>v2/expert-claim/</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/requests</code></td><td>فهرست خسارت‌ها در صف <code>WAITING_FOR_DAMAGE_EXPERT</code> + صف اعتبارسنجی فاکتور. پارامترها: search، sortBy، page، limit، unifiedStatus، fileType.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/request/:claimRequestId</code></td><td>جزئیات کامل خسارت: قطعات آسیب‌دیده، تصاویر گرفته‌شده، اسناد، priceDrop، داده طرف بلیم، آدرس‌های ویدیو.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>v2/expert-claim/assign/:claimRequestId</code></td><td>قفل خسارت برای این کارشناس. بازمی‌گرداند: <code>assigned</code>، <code>already_assigned_to_you</code>، یا ۴۰۹.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/request/:claimRequestId/price-drop</code></td><td>محتوای کاهش قیمت: برچسب‌های شدت، کاتالوگ ضریب، قطعات آسیب‌دیده + نگاشت، سال پیشنهادی خودرو از استعلام تقصیر.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/request/:claimRequestId/price-drop</code></td><td>محاسبه و ذخیره کاهش قیمت: قیمت خودرو × ضریب سال × مجموع ضرایب ÷ ۴۰۰.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/reply/submit/:claimRequestId</code></td><td>ارسال پاسخ ارزیابی خسارت (لیست قطعات قیمت‌گذاری‌شده، داغی، branchId). سقف: کل ≤ ۵۳،۰۰۰،۰۰۰ تومان. بسته به پرچم‌های factorNeeded، خسارت را به owner-sign، mixed-factors-pending، یا صف اعتبارسنجی فاکتور منتقل می‌کند.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/reply/resend/:claimRequestId</code></td><td>درخواست از کاربر برای ارسال مجدد اسناد/عکس‌ها. یک ارسال مجدد در هر چرخه خسارت؛ در صورت تکمیل قبلی ۴۲۲ برمی‌گرداند.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/:claimRequestId/visit</code></td><td>درخواست از کاربر برای مراجعه حضوری. خسارت را آزاد می‌کند، وضعیت claimStatus را به NEEDS_REVISION تنظیم می‌کند.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/validate-factors/:claimRequestId</code></td><td>اعتبارسنجی فاکتورهای تعمیرگاه بارگذاری‌شده. تأیید یا رد هر خط فاکتور با totalPayment. سقف برای تمام خطوط اعمال می‌شود (≤ ۵۳،۰۰۰،۰۰۰ تومان). پس از تصمیم‌گیری درباره تمام خطوط، به‌صورت خودکار تکمیل می‌شود.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/request/:claimRequestId/damaged-parts</code></td><td>ویرایش قطعات آسیب‌دیده انتخاب‌شده در حالی که خسارت توسط این کارشناس قفل است (EXPERT_REVIEWING).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/outer-parts-catalog</code></td><td>کاتالوگ قطعات بیرونی خودرو فناوران (مشترک با جریان کاربر).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/inner-parts-catalog</code></td><td>JSON ثابت کاتالوگ قطعات داخلی خودرو.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/branches</code></td><td>شعب بیمه‌گر برای تنانت این کارشناس (برای انتخاب داغی/شعبه در پیلود پاسخ).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/stream/:id/video</code></td><td>پخش ویدیوی خسارت (ویدیوی دور زدن خودرو یا ویدیوی تصادف). پارامتر: <code>query=car-capture|accident</code>.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/report/unified-file-statuses</code></td><td>کاتالوگ وضعیت + تعداد برای پرتفولیوی خسارت این کارشناس.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/report/status-counts</code></td><td class="dep">منسوخ‌شده — از unified-file-statuses استفاده کنید.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/lock/:claimRequestId</code></td><td class="dep">اندپوینت قفل منسوخ‌شده — از POST assign استفاده کنید.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="field-expert">۴ — کارشناس میدانی <span class="role-enum">field_expert</span></h2>
<p class="section-intro">
به صحنه تصادف می‌رود و فرم‌های هر دو طرف را حضوری پر می‌کند (جریان V2 mirror / V3).
کارشناس میدانی همچنین دسترسی خواندن به پنل‌های expert-blame و expert-claim دارد
(محدود به فایل‌های خودش). تنها نقشی است که هم <strong>ثبت تقصیر</strong> و هم
<strong>ثبت خسارت</strong> را در یک جلسه انجام می‌دهد.
</p>
<div class="card card-green">
<h3>ثبت تقصیر — <code>v2/expert-initiated/blame-request-management/</code></h3>
<p class="note">آینه‌ای از API تقصیر کاربر. فرانت‌اند همان صفحات را با تغییر فقط پیشوند مسیر بازاستفاده می‌کند.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>POST /</code></td><td>ایجاد فایل تقصیر IN_PERSON.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>send-party-otp/:id</code></td><td>ارسال OTP به یک طرف از طریق شماره تلفن (بدون لینک دعوت).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>verify-party-otp/:id</code></td><td>تأیید OTP یک طرف و اتصال حساب آنها.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>blame-confession/:id</code></td><td>ثبت اعتراف تقصیر طرف.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>car-body-form/:id</code></td><td>[فقط CAR_BODY] فرم نوع تصادف.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiries/:id</code> / <code>run-inquiries-vin/:id</code></td><td>فرم اولیه / استعلام پلاک یا VIN برای طرف فعلی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:id</code></td><td>بارگذاری ویدیوی طرف اول.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-detail-location/:id</code></td><td>افزودن موقعیت GPS برای طرف فعلی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-voice/:id</code></td><td>بارگذاری ضبط صوتی برای طرف فعلی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-detail-description/:id</code></td><td>افزودن توضیحات برای طرف فعلی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-second-party/:phone/:id/</code></td><td>پیشروی به طرف دوم (بدون ارسال لینک SMS).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>sign/:id</code></td><td>بارگذاری امضای طرف (اول سپس دوم، پارامتر partyRole).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:id</code></td><td>ذخیره فیلدهای تصادف و تکمیل فوری تقصیر (بدون صف کارشناس).</td></tr>
</table>
</div>
<div class="card card-green">
<h3>ثبت تقصیر + خسارت V3 — <code>v3/expert-initiated/blame-request-management/</code></h3>
<p class="note">ترتیب مراحل بازسازمان‌دهی‌شده: ابتدا تمام روایت طرفین، سپس ارزیابی خسارت. هم تقصیر هم خسارت در این کنترلر واحد مدیریت می‌شوند.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>POST /</code> ← <code>send-party-otp</code> ← <code>verify-party-otp</code> ← <code>run-inquiries</code> ← <code>add-detail-*</code> ← <code>sign</code> (×۲)</td><td>مرحله روایت طرفین (مراحل ۱–۸) — اندپوینت‌های یکسان با mirror، همان قرارداد.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:id</code></td><td>مرحله ۹: ذخیره فیلدهای تصادف پس از امضای هر دو طرف.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>مرحله ۱۰: دریافت شناسه خسارت ایجادشده به‌صورت خودکار.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>مرحله ۱۱: بارگذاری اسناد گواهینامه / کارت خودرو.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code> / <code>select-other-parts/:claimId</code></td><td>مراحل ۱۲–۱۳: انتخاب قطعات آسیب‌دیده.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>مرحله ۱۴: عکس‌برداری از قطعات + زوایا.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>مرحله ۱۵: ویدیوی دور زدن خودرو.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:requestId</code></td><td>مرحله ۱۶: ویدیوی تصادف تقصیر (نهایی) ← WAITING_FOR_EXPERT (THIRD_PARTY) یا COMPLETED (CAR_BODY).</td></tr>
</table>
</div>
<div class="card card-green">
<h3>دسترسی به پنل expert-blame + expert-claim (خواندن + اقدام روی فایل‌های خود)</h3>
<p class="note">
FIELD_EXPERT مسیر <code>v2/expert-blame/</code> را محدود به فایل‌های ساخته‌شده توسط خودش می‌بیند (نه صف اختلاف).
همچنین <code>v2/expert-claim/</code> را برای خسارت‌های مرتبط با فایل‌های تقصیرش می‌بیند.
اندپوینت‌های یکسان با پنل‌های کارشناس تقصیر و کارشناس خسارت در بالا.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="file-maker">۵ — فایل‌ساز <span class="role-enum">file_maker</span></h2>
<p class="section-intro">
اولین اکتور در تقسیم V4/V5. فایل‌ساز روایت طرفین را در محل انجام می‌دهد:
OTPها، استعلام‌ها، موقعیت/توضیحات/صدا و امضاها برای هر دو طرف.
همچنین اسناد اولیه خسارت (گواهینامه‌ها، کارت‌های خودرو) را بارگذاری می‌کند. پس از
امضای دوم، فایل برای تحویل به بازبین فایل «مهرومومه» می‌شود. در V5، فایل‌ساز
در انتها بازمی‌گردد تا خسارت تکمیل‌شده را قبل از ارسال به فناوران تأیید یا رد کند.
</p>
<div class="card card-purple">
<h3>ثبت تقصیر — <code>v4/file-maker/blame-request-management/</code> و <code>v5/…</code></h3>
<p class="note">اندپوینت‌های V4 و V5 یکسان هستند — فقط پیشوند تغییر می‌کند. V5 هنگام ایجاد <code>requiresFileMakerApproval=true</code> را تنظیم می‌کند.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>POST /</code></td><td>ایجاد فایل تقصیر IN_PERSON.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>فهرست تمام فایل‌های تقصیر ایجادشده توسط این فایل‌ساز.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files/:requestId</code></td><td>جزئیات کامل یک فایل (طرفین، گردش کار، شناسه خسارت مرتبط).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>دریافت شناسه خسارت ایجادشده به‌صورت خودکار پس از استعلام طرف مقصر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>send-party-otp/:id</code> / <code>verify-party-otp/:id</code></td><td>ارسال + تأیید OTP برای یک طرف در هر بار (ابتدا مقصر، سپس زیان‌دیده).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>car-body-form/:id</code></td><td>[فقط CAR_BODY] فرم نوع تصادف.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiries/:id</code> / <code>run-inquiries-vin/:id</code></td><td>اجرای استعلام پلاک یا VIN. فراخوانی اول = مقصر (+ خودکار خسارت ایجاد می‌کند). فراخوانی دوم = زیان‌دیده (فقط THIRD_PARTY).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-detail-location/:id</code> / <code>add-detail-description/:id</code> / <code>upload-voice/:id</code></td><td>افزودن موقعیت، توضیحات و صدا برای طرف فعلی (پارامتر partyRole، FIRST/SECOND را انتخاب می‌کند).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>sign/:id</code></td><td>بارگذاری امضای طرف (partyRole=FIRST سپس SECOND). پس از امضای دوم، فایل مهرومومه می‌شود.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>بارگذاری گواهینامه / کارت‌های خودرو روی خسارت ایجادشده به‌صورت خودکار.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>الزامات عکس‌برداری آگاه از مرحله (فازها: اسناد پیش از عکس‌برداری در مقابل قطعات آسیب‌دیده + شاسی/موتور).</td></tr>
</table>
</div>
<div class="card card-purple">
<h3>تأیید خسارت V5 — <code>v5/file-maker/claim-approval/</code></h3>
<p class="note">فقط در V5 استفاده می‌شود. پس از بررسی کارشناس خسارت و امضای مالک، خسارت وارد <code>WAITING_FOR_FILE_MAKER_APPROVAL</code> می‌شود.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>approve/:claimId</code></td><td>تأیید خسارت تکمیل‌شده ← ارسال SMS امضای مالک را فعال می‌کند ← پس از امضای مالک به فناوران ارسال می‌شود.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>reject/:claimId</code></td><td>رد به بازبین فایل ← خسارت به WAITING_FOR_DAMAGE_EXPERT برمی‌گردد. محدودیت: حداکثر ۲ رد در هر خسارت؛ تلاش سوم ۴۲۲ با کد <code>FILE_MAKER_REJECTION_LIMIT_EXCEEDED</code> برمی‌گرداند.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="file-reviewer">۶ — بازبین فایل <span class="role-enum">file_reviewer</span></h2>
<p class="section-intro">
دومین اکتور در تقسیم V4/V5. بازبین فایل فایل‌های مهرومومه‌شده (پس از اتمام کار
فایل‌ساز) را تحویل می‌گیرد و مرحله کامل ارزیابی خسارت را انجام می‌دهد: فیلدهای
تصادف، دریافت الزامات عکس‌برداری، بارگذاری اسناد (شاسی/موتور)، انتخاب قطعات،
عکس‌های قطعات، ویدیوی دور زدن خودرو، امضای مالک. تقصیر با car-capture به
COMPLETED علامت‌گذاری می‌شود. بازبین فایل همچنین دسترسی خواندن به پنل expert-claim
برای خسارت‌هایی که بررسی می‌کند دارد.
</p>
<div class="card card-teal">
<h3>ارزیابی خسارت — <code>v4/file-reviewer/blame-request-management/</code> و <code>v5/…</code></h3>
<p class="note">اندپوینت‌های V4 و V5 یکسان هستند — فقط پیشوند تغییر می‌کند.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>فهرست تمام فایل‌های تخصیص‌یافته به این بازبین فایل.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files/:requestId</code></td><td>جزئیات کامل یک فایل (طرفین، گردش کار، فیلدهای کارشناس، شناسه خسارت مرتبط).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>دریافت شناسه خسارت ایجادشده به‌صورت خودکار (از استعلام طرف مقصر فایل‌ساز).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:requestId</code></td><td>مرحله ۱ (بازبین): ذخیره فیلدهای تصادف (accidentWay، accidentReason، accidentType).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>الزامات عکس‌برداری آگاه از مرحله (فاز اسناد پیش از عکس‌برداری در مقابل فاز عکس‌برداری قطعات).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>بارگذاری اسناد شاسی / موتور / پلاک فلزی.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code></td><td>انتخاب قطعات آسیب‌دیده بیرونی (بدنه).</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-other-parts/:claimId</code></td><td>انتخاب سایر قطعات آسیب‌دیده (غیر بدنه).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>عکس‌برداری از قطعات + زوایا برای هر قطعه آسیب‌دیده انتخاب‌شده.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>ویدیوی دور زدن خودرو (آخرین مرحله عکس‌برداری بازبین). خسارت ← <code>WAITING_FOR_DAMAGE_EXPERT</code>، تقصیر ← <code>COMPLETED</code>.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>claim-sign/:claimId</code></td><td>ثبت امضای مالک روی قیمت‌گذاری کارشناس به نمایندگی از طرف زیان‌دیده (موافقت + branchId + تصویر امضا).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:requestId</code></td><td class="dep">در V4/V5 بی‌اثر است — تقصیر از قبل توسط car-capture COMPLETED شده. موفقیت idempotent برمی‌گرداند.</td></tr>
</table>
</div>
<div class="card card-teal">
<h3>دسترسی به پنل expert-claim</h3>
<p class="note">
FILE_REVIEWER در نقش‌های مجاز برای <code>v2/expert-claim/</code> است.
می‌تواند جزئیات خسارت را مشاهده کند و جریان assign/lock را برای خسارت‌های
مرتبط با فایل‌هایش اجرا کند. نمی‌تواند به‌طور مستقل درخواست ارسال مجدد
کارشناس خسارت را آغاز کند.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="registrar">۷ — ثبات <span class="role-enum">registrar</span></h2>
<p class="section-intro">
نقش اداری که تقصیر و خسارت حضوری را به نمایندگی از طرفین ثبت می‌کند.
از جریان OTP دسته‌ای استفاده می‌کند (OTPهای هر دو طرف به‌صورت همزمان ارسال و
تأیید می‌شوند) به جای OTP یک‌به‌یک که توسط کارشناسان میدانی استفاده می‌شود.
پس از تقصیر، ثبات آینه API خسارت کاربر را دنبال می‌کند تا انتخاب قطعات، اسناد
و عکس‌برداری را پر کند. سپس فایل وارد چرخه عادی بررسی کارشناس خسارت می‌شود.
</p>
<div class="card card-gray">
<h3>ثبت تقصیر — <code>registrar-initiated-blame/</code></h3>
<p class="note">توجه: <code>@ApiExcludeController</code> — مسیرها وجود دارند اما در مستندات Swagger نمایش داده نمی‌شوند.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/create</code></td><td>ایجاد فایل تقصیر IN_PERSON.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>registrar-initiated-blame/my-files</code></td><td>فهرست تمام فایل‌های تقصیر ایجادشده توسط این ثبات.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>registrar-initiated-blame/blame/:requestId</code></td><td>جزئیات کامل یک فایل تقصیر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/send-party-otps/:id</code></td><td>ارسال OTP به هر دو طرف به‌صورت همزمان.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/verify-party-otps/:id</code></td><td>تأیید OTPهای هر دو طرف در یک فراخوانی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/complete-blame-data/:id</code></td><td>ارسال تمام داده‌های فرم تقصیر هر دو طرف در یک پیلود.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/upload-video/:id</code></td><td>بارگذاری ویدیوی تقصیر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/upload-voice/:id</code></td><td>بارگذاری ضبط صوتی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/add-accident-fields/:id</code></td><td>ذخیره فیلدهای تصادف و تکمیل تقصیر.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/upload-party-signature/:id</code></td><td>بارگذاری امضای یک طرف (partyRole=FIRST/SECOND).</td></tr>
</table>
</div>
<div class="card card-gray">
<h3>ثبت خسارت — <code>v2/registrar/claim-request-management/</code></h3>
<p class="note">آینه‌ای از API خسارت کاربر. فرانت‌اند همان صفحات خسارت را با تغییر فقط پیشوند بازاستفاده می‌کند.</p>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>create-from-blame/:blameId</code></td><td>ایجاد خسارت از یک فایل تقصیر تکمیل‌شده.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>outer-parts-catalog</code> / <code>car-other-part</code></td><td>کاتالوگ قطعات (قطعات بیرونی بدنه + JSON سایر قطعات).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>branches/:insuranceId</code></td><td>فهرست شعب بیمه‌گر (برای انتخاب شعبه در مرحله امضای خسارت).</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code></td><td>انتخاب قطعات آسیب‌دیده بیرونی.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-other-parts/:claimId</code></td><td>انتخاب سایر قطعات آسیب‌دیده + اطلاعات بانکی.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>بارگذاری اسناد خسارت (گواهینامه، کارت خودرو).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>عکس‌برداری از قطعات + زوایا.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>ویدیوی دور زدن خودرو (مرحله نهایی) ← WAITING_FOR_DAMAGE_EXPERT.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="call-center">۸ — مرکز تماس <span class="role-enum">call_center</span></h2>
<p class="section-intro">
ثبت تقصیر تلفنی V6 را مدیریت می‌کند. اپراتور داده‌های طرف مقصر را از طریق تلفن
جمع‌آوری می‌کند، استعلام بیمه را اجرا می‌کند و لینک تقصیر را از طریق SMS ارسال
می‌کند. کاربر سپس بقیه فرم را از طریق جریان استاندارد V2 تکمیل می‌کند
(با رد شدن مرحله فرم اولیه/استعلام). کار اپراتور مرکز تماس پس از send-link
پایان می‌یابد؛ می‌تواند پیشرفت را از طریق اندپوینت‌های خواندن پایش کند.
</p>
<div class="card card-indigo">
<h3>اندپوینت‌ها — <code>v6/call-center-blame/</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>create</code></td><td>ایجاد فایل تقصیر LINK. بدنه: <code>{ type: "THIRD_PARTY" | "CAR_BODY" }</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiry/:requestId</code></td><td>اجرای استعلام بیمه پلاک + کد ملی برای طرف مقصر. نتیجه را روی سند تقصیر ذخیره می‌کند.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiry-vin/:requestId</code></td><td>VIN/شاسی جایگزین برای run-inquiry. از جستجوی شاسی ESG استفاده می‌کند.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>send-link/:requestId</code></td><td>در صورت لزوم کاربر را ثبت‌نام می‌کند، به‌عنوان طرف اول ذخیره می‌کند، لینک دعوت تقصیر را از طریق SMS ارسال می‌کند. بدنه: <code>{ phoneNumber }</code>.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>فهرست تمام فایل‌های تقصیر شروع‌شده توسط این اپراتور.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>blame/:requestId</code></td><td>وضعیت فعلی و مرحله گردش کار یک فایل (برای بررسی اینکه کاربر لینک را باز کرده و پیشرفت کرده است).</td></tr>
</table>
<p class="note" style="margin-top:8px;">
پس از <code>send-link</code>، کاربر فرم را از طریق
<code>v2/blame-request-management/</code> (جریان استاندارد V2) تکمیل می‌کند.
مرحله فرم اولیه / استعلام به‌صورت خودکار رد می‌شود
(<code>skipInitialFormStep=true</code>). جریان خسارت پایین‌دستی همان
جریان استاندارد خسارت V2 است.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2>مشترک: احراز هویت اکتورها</h2>
<p class="section-intro">
تمام اکتورهای پنل (هر نقش به جز <code>user</code>) از طریق همان اندپوینت
<code>POST actor/login</code> با کپچا احراز هویت می‌کنند. بازنشانی رمز عبور از
طریق OTP ایمیل است. خواندن و ویرایش پروفایل نیز مشترک است.
</p>
<div class="card card-gray">
<h3>اندپوینت‌ها — <code>actor/</code></h3>
<table>
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>actor/captcha</code></td><td>صدور یک چالش کپچای ورود جدید (captchaId + تصویر SVG را برمی‌گرداند).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>actor/login</code></td><td>احراز هویت هر نقش اکتور. بدنه: role، username/email/nationalCode، password، captchaId، captcha. توکن‌های JWT دسترسی + رفرش را برمی‌گرداند.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>actor/forget-password</code></td><td>ارسال OTP بازنشانی رمز عبور به ایمیل.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>actor/forget-password-verify</code></td><td>تأیید OTP و تنظیم رمز عبور جدید.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>actor/profile</code></td><td>دریافت پروفایل اکتور فعلی.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>actor/profile</code></td><td>به‌روزرسانی پروفایل اکتور فعلی.</td></tr>
</table>
</div>
<footer>Made by Sepehr</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,616 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Panel Roles Reference</title>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, "Segoe UI", system-ui, sans-serif;
font-size: 14px;
line-height: 1.6;
background: #ffffff;
color: #1f2328;
padding: 24px;
}
h1 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.subtitle { font-size: 13px; color: #57606a; margin-bottom: 28px; }
h2 {
font-size: 15px; font-weight: 700;
margin-bottom: 10px; margin-top: 32px;
border-bottom: 1px solid #e5e7eb; padding-bottom: 6px;
}
h3 {
font-size: 12px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.05em;
color: #57606a; margin-bottom: 8px; margin-top: 14px;
}
.section-intro {
font-size: 13px; color: #57606a;
margin-bottom: 14px; line-height: 1.5;
}
/* Role header strip */
.role-header {
display: flex;
align-items: baseline;
gap: 10px;
margin-bottom: 6px;
}
.role-name {
font-size: 15px;
font-weight: 700;
}
.role-enum {
font-family: monospace;
font-size: 11px;
color: #3b82d4;
background: #f0f7ff;
border: 1px solid #bfdbfe;
border-radius: 4px;
padding: 1px 6px;
}
.badge {
display: inline-block;
font-size: 11px; font-weight: 600;
padding: 1px 7px; border-radius: 10px;
margin-right: 4px; margin-bottom: 3px;
}
.badge-blue { background: #dbeafe; color: #1d4ed8; }
.badge-green { background: #dcfce7; color: #166534; }
.badge-purple { background: #ede9fe; color: #5b21b6; }
.badge-orange { background: #ffedd5; color: #9a3412; }
.badge-teal { background: #ccfbf1; color: #0f766e; }
.badge-indigo { background: #e0e7ff; color: #3730a3; }
.badge-gray { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }
.badge-red { background: #fee2e2; color: #991b1b; }
/* Cards */
.card {
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 16px;
background: #f7f8fa;
margin-bottom: 16px;
}
.card.card-blue { border-left: 4px solid #3b82f6; }
.card.card-green { border-left: 4px solid #22c55e; }
.card.card-purple { border-left: 4px solid #8b5cf6; }
.card.card-orange { border-left: 4px solid #f97316; }
.card.card-teal { border-left: 4px solid #14b8a6; }
.card.card-indigo { border-left: 4px solid #6366f1; }
.card.card-gray { border-left: 4px solid #94a3b8; }
/* Endpoint tables */
table {
border-collapse: collapse;
width: 100%;
font-size: 12px;
margin-top: 4px;
}
th {
background: #f1f5f9; font-weight: 600;
text-align: left; padding: 5px 8px;
border: 1px solid #e5e7eb;
}
td {
padding: 4px 8px; border: 1px solid #e5e7eb;
vertical-align: top;
}
tr:nth-child(even) td { background: #ffffff; }
code {
font-family: monospace; font-size: 11px; color: #3b82d4;
}
.method {
font-family: monospace; font-size: 11px;
font-weight: 700; white-space: nowrap;
}
.method.get { color: #059669; }
.method.post { color: #2563eb; }
.method.put { color: #d97706; }
.method.patch { color: #7c3aed; }
.method.delete { color: #dc2626; }
.dep { color: #94a3b8; font-style: italic; font-size: 11px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 860px) { .grid-2 { grid-template-columns: 1fr; } }
.note {
font-size: 11px; color: #57606a; font-style: italic;
margin-top: 6px;
}
.toc {
background: #f7f8fa; border: 1px solid #e5e7eb;
border-radius: 6px; padding: 14px 18px;
margin-bottom: 28px;
}
.toc-title { font-size: 13px; font-weight: 700; margin-bottom: 8px; }
.toc ol { padding-left: 18px; }
.toc li { font-size: 13px; margin-bottom: 3px; }
.toc a { color: #3b82d4; text-decoration: none; }
.toc a:hover { text-decoration: underline; }
footer {
text-align: center; font-size: 12px; color: #57606a;
border-top: 1px solid #e5e7eb;
margin-top: 40px; padding-top: 12px;
}
.max-wrap { max-width: 760px; margin: 0 auto; }
/* Role overview table */
.overview-table { font-size: 12px; margin-bottom: 24px; }
.overview-table th { white-space: nowrap; }
.overview-table td:first-child { font-weight: 600; white-space: nowrap; }
</style>
</head>
<body>
<div class="max-wrap">
<h1>Panel Roles Reference</h1>
<p class="subtitle">
What every actor role can see and do — endpoints, responsibilities, and
process steps. Super-admin excluded.
</p>
<!-- Table of Contents -->
<div class="toc">
<div class="toc-title">Roles covered</div>
<ol>
<li><a href="#insurer">Insurer (COMPANY) — the insurance-company tenant admin</a></li>
<li><a href="#blame-expert">Blame Expert (EXPERT) — disagreement review queue</a></li>
<li><a href="#damage-expert">Damage Expert (DAMAGE_EXPERT) — claim pricing</a></li>
<li><a href="#field-expert">Field Expert (FIELD_EXPERT) — on-scene in-person filing</a></li>
<li><a href="#file-maker">File Maker (FILE_MAKER) — V4/V5 party narrative</a></li>
<li><a href="#file-reviewer">File Reviewer (FILE_REVIEWER) — V4/V5 damage assessment</a></li>
<li><a href="#registrar">Registrar (REGISTRAR) — office-based filing</a></li>
<li><a href="#call-center">Call Center (CALL_CENTER) — V6 phone-initiated filing</a></li>
</ol>
</div>
<!-- ── Role overview table ────────────────────────────────────── -->
<h2>Role Overview</h2>
<table class="overview-table">
<tr>
<th>Role enum</th>
<th>Login panel</th>
<th>Scope</th>
<th>Primary job</th>
</tr>
<tr>
<td><code>company</code></td>
<td>Insurer portal</td>
<td>Tenant-wide</td>
<td>View all files; manage branches, experts; run reports; rate experts</td>
</tr>
<tr>
<td><code>expert</code></td>
<td>Blame expert panel</td>
<td>Tenant DISAGREEMENT queue</td>
<td>Lock blame cases, review party submissions, submit verdict or request resend</td>
</tr>
<tr>
<td><code>damage_expert</code></td>
<td>Claim/damage panel</td>
<td>Tenant claim queue</td>
<td>Lock claims, price damage, validate repair factors, request resend/visit</td>
</tr>
<tr>
<td><code>field_expert</code></td>
<td>Field expert panel</td>
<td>Own created files</td>
<td>V2/V3 in-person blame + claim filing; also sees blame/claim review panels</td>
</tr>
<tr>
<td><code>file_maker</code></td>
<td>FileMaker panel</td>
<td>Own created files</td>
<td>V4/V5 party narrative (OTPs, inquiries, details, signatures); V5 claim approval</td>
</tr>
<tr>
<td><code>file_reviewer</code></td>
<td>FileReviewer panel</td>
<td>Assigned files</td>
<td>V4/V5 damage assessment (accident fields, parts, captures, owner sign)</td>
</tr>
<tr>
<td><code>registrar</code></td>
<td>Registrar panel</td>
<td>Own created files</td>
<td>Office-based in-person blame + claim filing on behalf of parties</td>
</tr>
<tr>
<td><code>call_center</code></td>
<td>Call-center panel</td>
<td>Own created files</td>
<td>V6 phone-initiated blame: run inquiry, send link; user completes the rest</td>
</tr>
</table>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="insurer">1 — Insurer <span class="role-enum">company</span></h2>
<p class="section-intro">
One <code>company</code> actor per insurance-company tenant. The insurer portal is the
management layer: it can see everything under its tenant, manage the expert roster,
manage branches, configure per-tenant media settings, and pull statistical reports.
The insurer never touches blame/claim steps directly — it only observes and rates.
</p>
<div class="card card-blue">
<h3>File management — <code>expert-insurer/</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files</code></td><td>List all blame + claim files for the tenant (merged by publicId). Filterable by status, file type, search, sort, page.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files/:publicId</code></td><td>Full detail for one file by publicId.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files/:publicId/timeline</code></td><td>Chronological activity timeline (all history events: source, type, actor, metadata).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/files/:publicId/report</code></td><td>Structured report data for PDF generation (owner, driver, insurance, vehicle, accident sections).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>expert-insurer/files/:publicId/rating</code></td><td>Rate the experts on a file (1–5 per dimension: collision method, timeliness, cause accuracy, guilty-ID accuracy, bot rating).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/report/unified-file-statuses</code></td><td>Unified status catalog + per-status counts for the whole tenant portfolio. Filterable by fileType and date range.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/report/status-counts</code></td><td class="dep">Deprecated — prefer unified-file-statuses.</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>Branch management — <code>expert-insurer/branches</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/branches</code></td><td>List all branches for this insurer. Query: search, from/to date, isActive filter.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/branches</code></td><td>Add a new branch (name, code, address, city, phone, etc.).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>expert-insurer/branches/:branchId/status</code></td><td>Activate or deactivate a branch.</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>Expert roster management — <code>expert-insurer/experts</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/blame</code></td><td>Create a new blame-expert account under this insurer.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/claim</code></td><td>Create a new damage-expert (claim) account under this insurer.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/file-maker</code></td><td>Create a new FileMaker account under this insurer.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>expert-insurer/experts/file-reviewer</code></td><td>Create a new FileReviewer account under this insurer.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/experts/list</code></td><td>Paginated list of all expert accounts on this tenant.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/experts/top</code></td><td>Top blame vs claim experts ranked by overall average rating (up to 10 each).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/top-experts</code></td><td>Alias for experts/top (frontend compat).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/:expertId</code></td><td>Files handled by one expert (slim summary rows — blame or claim depending on expert type).</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>Statistics &amp; reports</h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/statistics</code></td><td>KPI cards: totalFilesReviewed, averageUserRating, inPersonCount, filesThisMonth, objectionPercentage, etc. Filterable by date range.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/top-files</code></td><td>Top 10 highest-rated claim files (combined insurer + user score).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>expert-insurer/expert-work-log</code></td><td>Per-expert work log: totalHandled, currentlyChecking, distinctFilesCheckedInPeriod. Filterable by expertKind and date range.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/requests</code></td><td>Claim + blame status bucket counts + unified file count for the tenant.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/per-month-requests</code></td><td>Same summary, broken down by the last 5 calendar months.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/checked-requests</code></td><td>Same summary filtered by optional createdAt date range.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/expert-work-log</code></td><td>Expert work log (blame + damage expert collections, not field experts).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>reports/report/insurer/expert-work-log/per-month</code></td><td>Same work log per calendar month (last 5).</td></tr>
</table>
</div>
<div class="card card-blue">
<h3>Tenant settings — <code>client-panel/</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>client-panel/settings</code></td><td>Get per-tenant media limits (video/image/voice maxBytes) and CAR_BODY accident window (days).</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>client-panel/settings</code></td><td>Update those settings (partial). Cannot exceed system-level route ceilings.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="blame-expert">2 — Blame Expert <span class="role-enum">expert</span></h2>
<p class="section-intro">
Reviews blame files in the DISAGREEMENT queue — cases where the two parties do not
agree on who is at fault. After reviewing submitted documents and party statements
the expert locks the case, then either submits a verdict, asks the parties to resend
documents, or records an in-person visit outcome. All endpoints are under
<code>v2/expert-blame/</code>.
</p>
<div class="card card-orange">
<h3>Process</h3>
<p class="note">
1 Browse list → 2 Assign (lock) the case → 3 Review party evidence (videos, voices, documents) →
4a Submit verdict <em>or</em> 4b Request document resend <em>or</em> 4c Record in-person visit.
</p>
</div>
<div class="card card-orange">
<h3>Endpoints — <code>v2/expert-blame/</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/</code></td><td>List blame cases in the DISAGREEMENT queue (available, locked by me, or decided by me). Query: search, sortBy, sortOrder, page, limit, unifiedStatus, fileType.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/:id</code></td><td>Full detail for one blame case (party statements, photos, voices, videos).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>v2/expert-blame/:id/assign</code></td><td>Check availability and lock the case to this expert. Returns <code>assigned</code>, <code>already_assigned_to_you</code>, or 409 if someone else holds it.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/reply/submit/:id</code></td><td>Submit verdict (accidentWay, accidentReason, accidentType, guilty party decision). Unlocks the case and moves it to COMPLETED.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/reply/resend/:id</code></td><td>Request parties to re-upload documents. Sets blame to WAITING_FOR_RESEND. One resend request per lifecycle.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/reply/inPerson/:id</code></td><td>Record that an in-person visit was made and submit verdict.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/report/unified-file-statuses</code></td><td>Status catalog + per-status counts for this expert's portfolio.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-blame/report/status-counts</code></td><td class="dep">Deprecated — prefer unified-file-statuses.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-blame/lock/:id</code></td><td class="dep">Deprecated lock endpoint — use POST assign.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="damage-expert">3 — Damage Expert <span class="role-enum">damage_expert</span></h2>
<p class="section-intro">
Reviews claim files after the user has submitted their damage evidence. The expert
prices each damaged part, optionally calculates a price-drop (depreciation), and
can ask the user to resend documents, come in person, or upload repair factor invoices
when workshop pricing is needed. All endpoints are under <code>v2/expert-claim/</code>.
</p>
<div class="card card-red">
<h3>Process</h3>
<p class="note">
1 Browse list → 2 Assign (lock) the claim → 3 Review damage photos and documents →
4 Optionally edit selected parts or calculate price-drop →
5a Submit priced reply <em>or</em> 5b Request resend <em>or</em> 5c Request in-person visit →
6 If factor parts present: validate uploaded factor invoices.
</p>
</div>
<div class="card card-red">
<h3>Endpoints — <code>v2/expert-claim/</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/requests</code></td><td>List claims in <code>WAITING_FOR_DAMAGE_EXPERT</code> queue + factor-validation queue. Query: search, sortBy, page, limit, unifiedStatus, fileType.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/request/:claimRequestId</code></td><td>Full claim detail: damaged parts, captured images, documents, priceDrop, blameCase party data, video URLs.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>v2/expert-claim/assign/:claimRequestId</code></td><td>Lock claim to this expert. Returns <code>assigned</code>, <code>already_assigned_to_you</code>, or 409.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/request/:claimRequestId/price-drop</code></td><td>Price-drop context: severity labels, coefficient catalog, damaged parts + mapping, suggested car year from blame inquiry.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/request/:claimRequestId/price-drop</code></td><td>Calculate and persist price-drop: carPrice × yearCoeff × sumOfCoeffs ÷ 400.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/reply/submit/:claimRequestId</code></td><td>Submit damage assessment reply (priced parts list, daghi, branchId). Cap: total ≤ 53 000 000 Toman. Depending on factorNeeded flags moves claim to owner-sign, mixed-factors-pending, or factor-validation queue.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/reply/resend/:claimRequestId</code></td><td>Request user to resend documents/photos. One resend per claim lifecycle; returns 422 if already fulfilled.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/:claimRequestId/visit</code></td><td>Ask user to come in person. Unlocks claim, sets claimStatus to NEEDS_REVISION.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/validate-factors/:claimRequestId</code></td><td>Validate uploaded repair factor invoices. Approve or reject each factor line with totalPayment. Cap applies across all lines (≤ 53 000 000 Toman). Auto-completes when all lines are decided.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/request/:claimRequestId/damaged-parts</code></td><td>Edit selected damaged parts while the claim is locked by this expert (EXPERT_REVIEWING).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/outer-parts-catalog</code></td><td>Fanavaran outer car-components catalog (shared with user flow).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/inner-parts-catalog</code></td><td>Static inner car-parts catalog JSON.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/branches</code></td><td>Insurer branches for this expert's tenant (for daghi/branch selection in reply payload).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/stream/:id/video</code></td><td>Stream claim video (car-capture walk-around or accident video). Query: <code>query=car-capture|accident</code>.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/report/unified-file-statuses</code></td><td>Status catalog + counts for this expert's claim portfolio.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/report/status-counts</code></td><td class="dep">Deprecated — prefer unified-file-statuses.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/lock/:claimRequestId</code></td><td class="dep">Deprecated lock endpoint — use POST assign.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="field-expert">4 — Field Expert <span class="role-enum">field_expert</span></h2>
<p class="section-intro">
Goes to the accident scene and fills both parties' forms in-person (V2 mirror / V3 flows).
The field expert also has read access to the expert-blame and expert-claim panels
(scoped to their own files). They are the only role that spans both
<strong>blame filing</strong> and <strong>claim filing</strong> in the same session.
</p>
<div class="card card-green">
<h3>Blame filing — <code>v2/expert-initiated/blame-request-management/</code></h3>
<p class="note">Mirror of the user blame API. Frontend reuses same pages by swapping prefix only.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>POST /</code></td><td>Create IN_PERSON blame file.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>send-party-otp/:id</code></td><td>Send OTP to one party by phone number (no invite link).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>verify-party-otp/:id</code></td><td>Verify one party's OTP and bind their account.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>blame-confession/:id</code></td><td>Record party's blame confession.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>car-body-form/:id</code></td><td>[CAR_BODY only] Accident type form.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiries/:id</code> / <code>run-inquiries-vin/:id</code></td><td>Initial form / plate or VIN inquiry for current party.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:id</code></td><td>Upload first-party video.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-detail-location/:id</code></td><td>Add GPS location for current party.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-voice/:id</code></td><td>Upload voice recording for current party.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-detail-description/:id</code></td><td>Add description for current party.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-second-party/:phone/:id/</code></td><td>Advance to second party (no SMS link sent).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>sign/:id</code></td><td>Upload party signature (FIRST then SECOND, partyRole param).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:id</code></td><td>Save accident fields and complete blame immediately (no expert queue).</td></tr>
</table>
</div>
<div class="card card-green">
<h3>V3 blame + claim filing — <code>v3/expert-initiated/blame-request-management/</code></h3>
<p class="note">Reorganised step order: all party narrative first, then damage assessment. Blame and claim both handled in this single controller.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>POST /</code> → <code>send-party-otp</code> → <code>verify-party-otp</code> → <code>run-inquiries</code> → <code>add-detail-*</code> → <code>sign</code> (×2)</td><td>Party narrative phase (steps 1–8) — identical endpoints to mirror, same contract.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:id</code></td><td>Step 9: save accident fields after both parties have signed.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>Step 10: get auto-created claim ID.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Step 11: upload licence / car card documents.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code> / <code>select-other-parts/:claimId</code></td><td>Steps 12–13: select damaged parts.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>Step 14: capture part photos + angles.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>Step 15: walk-around video.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:requestId</code></td><td>Step 16: blame accident video (final) → WAITING_FOR_EXPERT (THIRD_PARTY) or COMPLETED (CAR_BODY).</td></tr>
</table>
</div>
<div class="card card-green">
<h3>Expert-blame + expert-claim panel (read + action on own files)</h3>
<p class="note">
FIELD_EXPERT sees <code>v2/expert-blame/</code> scoped to their own created files (not the disagreement queue).
They also see <code>v2/expert-claim/</code> for claims linked to their blame files.
Same endpoints as blame-expert and damage-expert panels above.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="file-maker">5 — File Maker <span class="role-enum">file_maker</span></h2>
<p class="section-intro">
The first actor in the V4/V5 split. FileMaker handles the party narrative on-site:
OTPs, inquiries, location/description/voice, and signatures for both parties.
They also upload the initial claim documents (licences, car cards). After the second
signature the file is "sealed" for FileReviewer pickup. In V5, FileMaker comes
back at the end to approve or reject the completed claim before fanavaran submission.
</p>
<div class="card card-purple">
<h3>Blame filing — <code>v4/file-maker/blame-request-management/</code> and <code>v5/…</code></h3>
<p class="note">V4 and V5 endpoints are identical — only the prefix changes. V5 sets <code>requiresFileMakerApproval=true</code> at creation.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>POST /</code></td><td>Create IN_PERSON blame file.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>List all blame files created by this FileMaker.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files/:requestId</code></td><td>Full detail for one file (parties, workflow, linked claim ID).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>Get the auto-created claim ID after guilty-party run-inquiries.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>send-party-otp/:id</code> / <code>verify-party-otp/:id</code></td><td>Send + verify OTP for one party at a time (guilty first, then damaged).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>car-body-form/:id</code></td><td>[CAR_BODY only] Accident type form.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiries/:id</code> / <code>run-inquiries-vin/:id</code></td><td>Run plate or VIN inquiry. First call = guilty (+ auto-creates claim). Second call = damaged (THIRD_PARTY only).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>add-detail-location/:id</code> / <code>add-detail-description/:id</code> / <code>upload-voice/:id</code></td><td>Add location, description, and voice for current party (partyRole param selects FIRST/SECOND).</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>sign/:id</code></td><td>Upload party signature (partyRole=FIRST then SECOND). After second signature, file is sealed.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Upload licences / car cards against the auto-created claim.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>Step-aware capture requirements (phases: pre-capture docs vs damaged parts + chassis/engine).</td></tr>
</table>
</div>
<div class="card card-purple">
<h3>V5 claim approval — <code>v5/file-maker/claim-approval/</code></h3>
<p class="note">Used only in V5. After damage expert review and owner sign, claim enters <code>WAITING_FOR_FILE_MAKER_APPROVAL</code>.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>approve/:claimId</code></td><td>Approve the completed claim → triggers owner sign SMS → fanavaran submission after owner signs.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>reject/:claimId</code></td><td>Reject back to FileReviewer → claim returns to WAITING_FOR_DAMAGE_EXPERT. Limit: max 2 rejections per claim; 3rd attempt returns 422 <code>FILE_MAKER_REJECTION_LIMIT_EXCEEDED</code>.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="file-reviewer">6 — File Reviewer <span class="role-enum">file_reviewer</span></h2>
<p class="section-intro">
The second actor in the V4/V5 split. FileReviewer picks up sealed files (after
FileMaker is done) and performs the full damage assessment pass: accident fields,
capture requirements lookup, document upload (chassis/engine), part selection,
part photos, walk-around video, owner signature. The blame is marked COMPLETED
by car-capture. FileReviewer also has read access to the expert-claim panel for
claims they are reviewing.
</p>
<div class="card card-teal">
<h3>Damage assessment — <code>v4/file-reviewer/blame-request-management/</code> and <code>v5/…</code></h3>
<p class="note">V4 and V5 endpoints are identical — only the prefix changes.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>List all files assigned to this FileReviewer.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files/:requestId</code></td><td>Full detail for one file (parties, workflow, expert fields, linked claim ID).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>Get the auto-created claim ID (from FileMaker's guilty-party inquiry).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:requestId</code></td><td>Step 1 (FileReviewer): save accident fields (accidentWay, accidentReason, accidentType).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>Step-aware capture requirements (pre-capture docs phase vs capture-parts phase).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Upload chassis / engine / metal-plate documents.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code></td><td>Select outer (body) damaged parts.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-other-parts/:claimId</code></td><td>Select other (non-body) damaged parts.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>Capture part photos + angles for each selected damaged part.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>Walk-around video (final FileReviewer capture step). Claim → <code>WAITING_FOR_DAMAGE_EXPERT</code>, blame → <code>COMPLETED</code>.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>claim-sign/:claimId</code></td><td>Submit owner signature on expert pricing on behalf of the damaged party (agree + branchId + signature image).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:requestId</code></td><td class="dep">No-op in V4/V5 — blame already COMPLETED by car-capture. Returns idempotent success.</td></tr>
</table>
</div>
<div class="card card-teal">
<h3>Expert-claim panel access</h3>
<p class="note">
FILE_REVIEWER is in the allowed roles for <code>v2/expert-claim/</code>.
They can view claim details and run the assign/lock flow for claims associated
with their files. They cannot initiate a damage-expert resend independently.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="registrar">7 — Registrar <span class="role-enum">registrar</span></h2>
<p class="section-intro">
Office-based role that files in-person blame and claim on behalf of parties.
Uses a bulk-OTP flow (both parties' OTPs sent and verified in one call each)
rather than the one-at-a-time OTP used by field experts. After blame, the
registrar mirrors the user claim API to fill part selection, documents, and
captures. The file then enters the normal damage-expert review lifecycle.
</p>
<div class="card card-gray">
<h3>Blame filing — <code>registrar-initiated-blame/</code></h3>
<p class="note">Note: <code>@ApiExcludeController</code> — routes exist but not surfaced in Swagger docs.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/create</code></td><td>Create IN_PERSON blame file.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>registrar-initiated-blame/my-files</code></td><td>List all blame files created by this registrar.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>registrar-initiated-blame/blame/:requestId</code></td><td>Full detail for one blame file.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/send-party-otps/:id</code></td><td>Send OTPs to both parties simultaneously.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/verify-party-otps/:id</code></td><td>Verify both parties' OTPs in one call.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/complete-blame-data/:id</code></td><td>Submit all blame form data for both parties in one payload.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/upload-video/:id</code></td><td>Upload blame video.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/upload-voice/:id</code></td><td>Upload voice recording.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/add-accident-fields/:id</code></td><td>Save accident fields and complete blame.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>registrar-initiated-blame/upload-party-signature/:id</code></td><td>Upload a party's signature (partyRole=FIRST/SECOND).</td></tr>
</table>
</div>
<div class="card card-gray">
<h3>Claim filing — <code>v2/registrar/claim-request-management/</code></h3>
<p class="note">Mirror of the user claim API. Frontend reuses same claim pages by swapping prefix only.</p>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>create-from-blame/:blameId</code></td><td>Create claim from a completed blame file.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>outer-parts-catalog</code> / <code>car-other-part</code></td><td>Parts catalogs (outer body parts + other parts JSON).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>branches/:insuranceId</code></td><td>Insurer branch list (for branch selection in claim sign step).</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code></td><td>Select outer damaged parts.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>select-other-parts/:claimId</code></td><td>Select other damaged parts + bank info.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Upload claim documents (licences, car card).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>Capture part photos + angles.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>Walk-around video (final step) → WAITING_FOR_DAMAGE_EXPERT.</td></tr>
</table>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2 id="call-center">8 — Call Center <span class="role-enum">call_center</span></h2>
<p class="section-intro">
Handles V6 phone-initiated blame filing. The agent collects the guilty party's
data over the phone, runs the insurance inquiry, and sends the blame link via
SMS. The user then completes the rest of the form through the standard V2 flow
(with the initial-form/inquiry step skipped). The call-center agent's job ends
after send-link; they can monitor progress via the read endpoints.
</p>
<div class="card card-indigo">
<h3>Endpoints — <code>v6/call-center-blame/</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>create</code></td><td>Create a LINK blame file. Body: <code>{ type: "THIRD_PARTY" | "CAR_BODY" }</code>.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiry/:requestId</code></td><td>Run plate + national-code insurance inquiry for the guilty party. Stores result on blame document.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>run-inquiry-vin/:requestId</code></td><td>VIN/chassis alternative to run-inquiry. Uses ESG chassis lookup.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>send-link/:requestId</code></td><td>Register user if needed, store as first party, send blame invite link via SMS. Body: <code>{ phoneNumber }</code>.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>List all blame files started by this agent.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>blame/:requestId</code></td><td>Current status and workflow step for one file (to check if user has opened the link and progressed).</td></tr>
</table>
<p class="note" style="margin-top:8px;">
After <code>send-link</code> the user completes the form via
<code>v2/blame-request-management/</code> (standard V2 flow).
The initial-form / inquiry step is automatically skipped
(<code>skipInitialFormStep=true</code>). Downstream claim flow is the
standard V2 claim flow.
</p>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<h2>Shared: Actor Authentication</h2>
<p class="section-intro">
All panel actors (every role except <code>user</code>) authenticate through the same
<code>POST actor/login</code> endpoint with captcha. Password reset is via email OTP.
Profile reads and edits are also shared.
</p>
<div class="card card-gray">
<h3>Endpoints — <code>actor/</code></h3>
<table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>actor/captcha</code></td><td>Issue a new login captcha challenge (returns captchaId + SVG image).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>actor/login</code></td><td>Authenticate any actor role. Body: role, username/email/nationalCode, password, captchaId, captcha. Returns JWT access + refresh tokens.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>actor/forget-password</code></td><td>Send password-reset OTP to email.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>actor/forget-password-verify</code></td><td>Verify OTP and set new password.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>actor/profile</code></td><td>Get current actor's profile.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>actor/profile</code></td><td>Update current actor's profile.</td></tr>
</table>
</div>
<footer>Made by Sepehr</footer>
</div>
</body>
</html>

View File

@@ -10797,7 +10797,29 @@ export class ClaimRequestManagementService {
}
claims = [...byId.values()];
}
const list = (claims as any[]).map((c) => ({
const blameIdsForList = [
...new Set(
(claims as any[])
.map((c) => c.blameRequestId?.toString())
.filter((id): id is string => !!id),
),
];
const blamesForList =
blameIdsForList.length > 0
? ((await this.blameRequestDbService.find(
{ _id: { $in: blameIdsForList.map((id) => new Types.ObjectId(id)) } },
{ lean: true, select: "type creationMethod" },
)) as any[])
: [];
const blameByIdForList = new Map<string, any>(
blamesForList.map((b) => [String(b._id), b]),
);
const list = (claims as any[]).map((c) => {
const blameForItem = c.blameRequestId
? blameByIdForList.get(c.blameRequestId.toString())
: undefined;
return {
claimRequestId: c._id.toString(),
publicId: c.publicId,
requestNo: c.requestNo,
@@ -10806,7 +10828,10 @@ export class ClaimRequestManagementService {
currentStep: c.workflow?.currentStep || "",
createdAt: c.createdAt,
blameRequestId: c.blameRequestId?.toString(),
})) as ClaimListItemV2Dto[];
blameType: blameForItem?.type ?? undefined,
creationMethod: blameForItem?.creationMethod ?? undefined,
};
}) as ClaimListItemV2Dto[];
const paged = applyListQueryV2(
list,
@@ -10856,6 +10881,15 @@ export class ClaimRequestManagementService {
}
await this.assertActorCanViewClaimV2(claim, currentUserId, actor);
const blameForDetail = claim.blameRequestId
? ((
await this.blameRequestDbService.find(
{ _id: new Types.ObjectId(claim.blameRequestId.toString()) },
{ lean: true, select: "type creationMethod" },
)
)[0] ?? null)
: null;
const hasCapture = (data: any, key: string) =>
data && (data instanceof Map ? data.get(key) : data[key]);
@@ -11011,6 +11045,8 @@ export class ClaimRequestManagementService {
nextStep: claim.workflow?.nextStep,
blameRequestId: claim.blameRequestId?.toString(),
blameRequestNo: claim.blameRequestNo,
blameType: (blameForDetail as any)?.type ?? undefined,
creationMethod: (blameForDetail as any)?.creationMethod ?? undefined,
...(ownerData ? { owner: ownerData } : {}),
vehicle: claim.vehicle,
selectedParts: selectedNormDetails,

View File

@@ -122,6 +122,18 @@ export class ClaimDetailsV2ResponseDto {
@ApiPropertyOptional({ description: 'Blame request number' })
blameRequestNo?: string;
@ApiPropertyOptional({
description: 'Blame file type: THIRD_PARTY or CAR_BODY',
example: 'THIRD_PARTY',
})
blameType?: string;
@ApiPropertyOptional({
description: 'How the blame file was initiated: IN_PERSON or LINK',
example: 'IN_PERSON',
})
creationMethod?: string;
@ApiPropertyOptional({
description: 'Claim owner (damaged party): ids for the user and their insurer client scope',
})

View File

@@ -28,6 +28,18 @@ export class ClaimListItemV2Dto {
@ApiProperty({ description: 'Blame request ID this claim originated from' })
blameRequestId?: string;
@ApiPropertyOptional({
description: 'Blame file type: THIRD_PARTY or CAR_BODY',
example: 'THIRD_PARTY',
})
blameType?: string;
@ApiPropertyOptional({
description: 'How the blame file was initiated: IN_PERSON or LINK',
example: 'IN_PERSON',
})
creationMethod?: string;
}
export class GetMyClaimsV2ResponseDto {

View File

@@ -116,6 +116,8 @@ export class AllRequestDtoV2 {
lockFile: boolean;
lockTime: string | null;
type: string;
/** IN_PERSON or LINK — how the blame file was created */
creationMethod?: string;
blameStatus: string;
/** Calculated blame + linked claim lifecycle status */
unifiedFileStatus?: string;

View File

@@ -762,6 +762,7 @@ export class ExpertBlameService {
? new Date(workflow.lockedAt as string).toISOString()
: null,
type: String(doc.type ?? "THIRD_PARTY"),
creationMethod: doc.creationMethod ? String(doc.creationMethod) : undefined,
blameStatus: String(doc.blameStatus ?? ""),
partiesInitialForms: {
firstParty: statementToFormKey(firstParty?.statement) ?? "",

View File

@@ -52,6 +52,12 @@ export class ClaimDetailV2ResponseDto {
})
blameRequestType?: BlameRequestType;
@ApiPropertyOptional({
description: "How the blame file was initiated: IN_PERSON or LINK",
example: "IN_PERSON",
})
creationMethod?: string;
@ApiPropertyOptional({
description:
"CAR_BODY only: first-step flags — another car (`car`) and/or object (`object`)",

View File

@@ -49,6 +49,12 @@ export class ClaimListItemV2Dto {
})
blameRequestType?: BlameRequestType;
@ApiPropertyOptional({
description: 'How the blame file was initiated: IN_PERSON or LINK',
example: 'IN_PERSON',
})
creationMethod?: string;
@ApiPropertyOptional({
description:
'CAR_BODY only: first-step flags — accident involved another car (`car`) and/or fixed object (`object`)',

View File

@@ -629,6 +629,7 @@ export class ExpertClaimService {
*/
private blameFileContextForExpert(blame: any): {
blameRequestType?: BlameRequestType;
creationMethod?: string;
carBodyFirstForm?: { car?: boolean; object?: boolean };
blameStatus?: string;
} {
@@ -636,9 +637,11 @@ export class ExpertClaimService {
const blameRequestType = blame.type as BlameRequestType;
const out: {
blameRequestType?: BlameRequestType;
creationMethod?: string;
carBodyFirstForm?: { car?: boolean; object?: boolean };
blameStatus?: string;
} = { blameRequestType };
if (blame.creationMethod) out.creationMethod = blame.creationMethod;
out.blameStatus = blame.blameStatus;
if (blameRequestType !== BlameRequestType.CAR_BODY) return out;
@@ -4017,7 +4020,7 @@ export class ExpertClaimService {
blameIds.length > 0
? ((await this.blameRequestDbService.find(
{ _id: { $in: blameIds.map((id) => new Types.ObjectId(id)) } },
{ lean: true, select: "type parties expert.decision blameStatus status isMadeByFileMaker" },
{ lean: true, select: "type creationMethod parties expert.decision blameStatus status isMadeByFileMaker" },
)) as any[])
: [];
const blameById = new Map<string, any>(
@@ -4117,7 +4120,7 @@ export class ExpertClaimService {
blameIds.length > 0
? ((await this.blameRequestDbService.find(
{ _id: { $in: blameIds.map((id) => new Types.ObjectId(id)) } },
{ lean: true, select: "type parties expert.decision blameStatus status" },
{ lean: true, select: "type creationMethod parties expert.decision blameStatus status" },
)) as any[])
: [];
const blameById = new Map<string, any>(
@@ -4212,7 +4215,7 @@ export class ExpertClaimService {
{
lean: true,
select:
"_id type parties blameStatus status expert.decision assignedFileReviewerId requiresFileMakerApproval",
"_id type creationMethod parties blameStatus status expert.decision assignedFileReviewerId requiresFileMakerApproval",
},
)) as any[];
@@ -4303,7 +4306,7 @@ export class ExpertClaimService {
const makerBlames = (await this.blameRequestDbService.find(
{ isMadeByFileMaker: true, initiatedByFieldExpertId: makerOid },
{ lean: true, select: "_id type parties blameStatus status expert.decision assignedFileReviewerId requiresFileMakerApproval" },
{ lean: true, select: "_id type creationMethod parties blameStatus status expert.decision assignedFileReviewerId requiresFileMakerApproval" },
)) as any[];
if (makerBlames.length === 0) {

View File

@@ -1498,6 +1498,8 @@ export class ExpertInsurerService {
(blame as any)?.requestNumber ||
(claim as any)?.requestNumber,
type: (blame as any)?.type ?? (blameFull as any)?.type,
creationMethod:
(blame as any)?.creationMethod ?? (blameFull as any)?.creationMethod,
hasClaim: !!claim,
blameStatus: (blame as any)?.status ?? (blameFull as any)?.status,
claimStatus: (claim as any)?.status,

View File

@@ -6116,6 +6116,7 @@ export class RequestManagementService {
requestNo: plain.requestNo,
publicId: plain.publicId,
type: plain.type,
creationMethod: plain.creationMethod,
status: plain.status,
blameStatus: plain.blameStatus,
workflow: plain.workflow,