forked from Yara724/api
Compare commits
31 Commits
85576e7c5e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c422c9a43 | |||
|
|
c7c7709f0f | ||
| aaf0df1cc0 | |||
|
|
54d90b1c1b | ||
|
|
b758346af1 | ||
| 1a959ce8f6 | |||
| fc1c117b93 | |||
| 4f8fe04380 | |||
| 73ed37852b | |||
|
|
8ac185c861 | ||
| f07e510c5f | |||
|
|
8b65e43c60 | ||
| 985c28ed6b | |||
| 9dd0ec0c00 | |||
|
|
e06178f804 | ||
| a9adad3c1b | |||
| bbe5b7b77b | |||
| 1e13bb3a3c | |||
| a8d8df574b | |||
| cf07a33951 | |||
| 6f8a6fed69 | |||
| dbf46cfdda | |||
|
|
4ef53f2cc9 | ||
| b4c1d7bd5f | |||
|
|
4cd6e0ecb3 | ||
|
|
8071215803 | ||
| cd36a0f2d4 | |||
| ffaeb50c61 | |||
| 8afc1fabc4 | |||
| 1d9ef5e3d2 | |||
| be06bfe1b5 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -186,4 +186,3 @@ dist
|
|||||||
*.sh
|
*.sh
|
||||||
!scripts/fanavaran-flow-test.sh
|
!scripts/fanavaran-flow-test.sh
|
||||||
!scripts/fanavaran-auth.sh
|
!scripts/fanavaran-auth.sh
|
||||||
*.json
|
|
||||||
@@ -461,6 +461,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Optional document shared by every claim flow</div>
|
||||||
|
<p class="step-note">
|
||||||
|
Every V1–V6 document-upload step also offers
|
||||||
|
<code>accident_sketch</code> (کروکی). It is optional for both
|
||||||
|
<code>THIRD_PARTY</code> and <code>CAR_BODY</code>, may be omitted without
|
||||||
|
blocking progression, and is returned in expert and insurer case details when uploaded.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||||
<h2>Flow V1 / V2 — User-Initiated Blame & Claim</h2>
|
<h2>Flow V1 / V2 — User-Initiated Blame & Claim</h2>
|
||||||
<p class="section-intro">
|
<p class="section-intro">
|
||||||
|
|||||||
@@ -433,6 +433,23 @@ curl -X POST "$ESG_URL/inquiry/policyByPlate" \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Car By Chassis (Two-Factor VIN Inquiry)
|
||||||
|
|
||||||
|
Use the two-factor chassis route when the backend must match both the VIN and
|
||||||
|
the resolved third-party policyholder. Do not send `nationalCode` to the
|
||||||
|
one-factor `policyByChassis` route.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X POST "$ESG_URL/inquiry/carByChassis" \
|
||||||
|
-H "Authorization: Bearer $ESG_ACCESS_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
--data '{
|
||||||
|
"nationalCode": "0012345678",
|
||||||
|
"chassisNo": "NAAR03HFFRDE07024"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
### Person Inquiry
|
### Person Inquiry
|
||||||
|
|
||||||
ESG expects Jalali birth date, normalized as `YYYY-MM-DD`.
|
ESG expects Jalali birth date, normalized as `YYYY-MM-DD`.
|
||||||
|
|||||||
@@ -161,7 +161,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>درخواست همیشه با پلاک فعلی ارسالشده و بیمهگذار نهایی همان نوع بیمه انجام میشود. متادیتای انتقال اخیر هیچ استعلامی برای پلاک یا بیمهگذار قبلی ایجاد نمیکند.</li>
|
<li>درخواست همیشه با پلاک فعلی ارسالشده و بیمهگذار نهایی همان نوع بیمه انجام میشود. متادیتای انتقال اخیر هیچ استعلامی برای پلاک یا بیمهگذار قبلی ایجاد نمیکند.</li>
|
||||||
<li>۱. بررسی دادههای آفلاین (MongoDB) — اگر داده مطابق یافت شد، آن را برگردانده و تمام HTTP را رد کن.</li>
|
<li>۱. بررسی دادههای آفلاین (MongoDB) — اگر داده مطابق یافت شد، آن را برگردانده و تمام HTTP را رد کن.</li>
|
||||||
<li>۲. اگر <code>CLIENT_ID=8</code> (تنانت پارسیان/ESG) → مسیریابی به <strong>ESG</strong> <code>/inquiry/policyByPlate</code> یا <code>/inquiry/policyByChassis</code>.</li>
|
<li>۲. اگر <code>CLIENT_ID=8</code> (تنانت پارسیان/ESG) → برای پلاک به <strong>ESG</strong> <code>/inquiry/policyByPlate</code> و برای VIN/شاسی به مسیر دوعاملی <code>/inquiry/carByChassis</code> مسیریابی میشود.</li>
|
||||||
<li>۳. در غیر این صورت → مسیریابی به <strong>پرسوجوی تجارت</strong> <code>/block-inquiry-tejarat</code> (THIRD_PARTY) یا <code>/block-inquiry-tejarat/badane</code> (CAR_BODY).</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>
|
<li>۴. اگر <code>system_settings.externalApis.sandHubUseLiveApi = false</code> (پیشفرض) → پاسخ mock برگردانده شود به جای انجام فراخوانیهای HTTP.</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
<div class="card card-blue">
|
<div class="card card-blue">
|
||||||
<h3>پروتکل ارسال خسارت (۴ مرحله)</h3>
|
<h3>پروتکل ارسال خسارت (۴ مرحله)</h3>
|
||||||
<div class="flow-box">
|
<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.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.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.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 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>
|
||||||
@@ -359,7 +359,7 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
|
<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/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/carByChassis</code></td><td>جایگزین دوعاملی VIN/شاسی برای استعلام پلاک. توسط اندپوینتهای <code>run-inquiries-vin</code> فراخوانی میشود. بدنه: <code>nationalCode</code>، <code>chassisNo</code>. مسیر تکعاملی <code>policyByChassis</code> استفاده نمیشود، چون فیلد <code>nationalCode</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/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>
|
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/sheba</code></td><td>اعتبارسنجی شبا / حساب بانکی.</td></tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -415,7 +415,7 @@
|
|||||||
<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-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> = لینک</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> = نوع فایل، <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>
|
<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>
|
</table>
|
||||||
<p class="note" style="margin-top:8px;">
|
<p class="note" style="margin-top:8px;">
|
||||||
تمام فراخوانیهای پیامک fire-and-forget هستند — هرگز throw نمیکنند. شکستها log میشوند اما جریان اصلی را مسدود نمیکنند.
|
تمام فراخوانیهای پیامک fire-and-forget هستند — هرگز throw نمیکنند. شکستها log میشوند اما جریان اصلی را مسدود نمیکنند.
|
||||||
|
|||||||
@@ -156,7 +156,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>The request always uses the submitted current plate and the resolved policyholder for that policy type. Recent-transfer metadata never triggers a previous-plate or previous-policyholder lookup.</li>
|
<li>The request always uses the submitted current plate and the resolved policyholder for that policy type. Recent-transfer metadata never triggers a previous-plate or previous-policyholder lookup.</li>
|
||||||
<li>1. Check offline-inquiry seeds (MongoDB) — if a matching seed exists, return it and skip all HTTP.</li>
|
<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>2. If <code>CLIENT_ID=8</code> (Parsian/ESG tenant) → route to <strong>ESG</strong> <code>/inquiry/policyByPlate</code> for plates or the two-factor <code>/inquiry/carByChassis</code> for VIN/chassis inquiries.</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>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>
|
<li>4. If <code>system_settings.externalApis.sandHubUseLiveApi = false</code> (default) → return mock response instead of making HTTP calls.</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
<div class="card card-blue">
|
<div class="card card-blue">
|
||||||
<h3>Claim submission protocol (4 steps)</h3>
|
<h3>Claim submission protocol (4 steps)</h3>
|
||||||
<div class="flow-box">
|
<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">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>, which are persisted for panel display.</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">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">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 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>
|
||||||
@@ -356,7 +356,7 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
|
<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/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/carByChassis</code></td><td>Two-factor VIN/chassis alternative to the plate inquiry. Called by <code>run-inquiries-vin</code> endpoints. Body: <code>nationalCode</code>, <code>chassisNo</code>. The one-factor <code>policyByChassis</code> route is not used because it rejects <code>nationalCode</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/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>
|
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/sheba</code></td><td>Sheba / bank account validation.</td></tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -412,7 +412,7 @@
|
|||||||
<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-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-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-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>
|
<tr><td><code>yara-fanavaran-claim</code></td><td>Retained legacy template; automatic dispatch after the final Fanavaran stage is disabled</td><td><code>token</code> = publicId, <code>token2</code> = Fanavaran claimId, <code>token3</code> = Fanavaran claimNo</td></tr>
|
||||||
</table>
|
</table>
|
||||||
<p class="note" style="margin-top:8px;">
|
<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.
|
All SMS calls are fire-and-forget — they never throw. Failures are logged but do not block the main flow.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
last_updated: 2026-08-08
|
last_updated: 2026-08-08
|
||||||
tags: [fanavaran, api, write, gen03, gen07, gen08, gen12]
|
tags: [fanavaran, api, write, gen03, gen07, gen08, gen12, gen44]
|
||||||
source: fanavaran-module-docs
|
source: fanavaran-module-docs
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -115,7 +115,8 @@ Full shape: see skill reference sample and `buildFanavaranSubmitPayload` / `appl
|
|||||||
|
|
||||||
### Validation (YARA)
|
### Validation (YARA)
|
||||||
|
|
||||||
- Only `THIRD_PARTY` claims (not `CAR_BODY`)
|
- `THIRD_PARTY` → `third-party-car-financial-claims`; `CAR_BODY` → `vehicle-hull-claims`
|
||||||
|
- Hull skips `dmg-cases` (resource does not exist)
|
||||||
- `PolicyId` required on submit (`requirePolicyId: true`)
|
- `PolicyId` required on submit (`requirePolicyId: true`)
|
||||||
- Skip if local `claimId` / `claimNo` already set
|
- Skip if local `claimId` / `claimNo` already set
|
||||||
- `EstimateAmount` normalized to positive
|
- `EstimateAmount` normalized to positive
|
||||||
@@ -215,6 +216,26 @@ After local `SELECT_OUTER_PARTS` — before image upload.
|
|||||||
|
|
||||||
Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/damage-case/submit`.
|
Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/damage-case/submit`.
|
||||||
|
|
||||||
|
If `DriverId` is still missing, submit registers the damaged party with GEN.44 first (see below). Preview only warns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.1 GEN.44 — Other people (unknown person)
|
||||||
|
|
||||||
|
Doc: `CMN.GENR.APIH.GEN.44`
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|------|-------|
|
||||||
|
| نام عملیات | ثبت سایر اشخاص |
|
||||||
|
| Endpoint | `POST /Api/BimeApi/v2.0/common/other-people` |
|
||||||
|
| Method | `POST` |
|
||||||
|
|
||||||
|
Also: `GET/PUT .../common/other-people/{Id}`. Local GET: `GET /lookups/other-people/:personId`.
|
||||||
|
|
||||||
|
Used when parties inquiry has no row for the damaged party. Iranian create needs national code + Jalali birth date; Fanavaran fills name/gender from ثبت احوال. Lookup ids: `CityId`, `GenderId`, `IsIranian` (`ans`), `PersonKindId`, `NationalityId`, `MaritalStatus`.
|
||||||
|
|
||||||
|
YARA: `registerFanavaranOtherPerson` during damage-case submit. Store `Id` as `driverId` / `otherPersonId`. Best-effort; do not create on preview; do not block the local claim flow.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. GEN.07 — Attachments
|
## 5. GEN.07 — Attachments
|
||||||
@@ -308,7 +329,7 @@ Requires `claimId` + `dmgCaseId` (soft-ensures earlier stages).
|
|||||||
| `Id` | `claimCases.expertiseId` |
|
| `Id` | `claimCases.expertiseId` |
|
||||||
|
|
||||||
History: `FANAVARAN_EXPERTISE_AUTO_SUBMIT_SUCCEEDED`.
|
History: `FANAVARAN_EXPERTISE_AUTO_SUBMIT_SUCCEEDED`.
|
||||||
SMS owner via `SmsOrchestrationService` (deduped `fanavaranSync.expertise.smsNotifiedAt`).
|
No owner SMS is sent after expertise; the returned identifier is persisted only.
|
||||||
Proven Parsian: `expertiseId=403144`, `ClaimExpertId=29`.
|
Proven Parsian: `expertiseId=403144`, `ClaimExpertId=29`.
|
||||||
|
|
||||||
Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/expertise/submit`.
|
Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/expertise/submit`.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
last_updated: 2026-08-09
|
last_updated: 2026-09-14
|
||||||
tags: [fanavaran, api, read, lookups]
|
tags: [fanavaran, api, read, lookups]
|
||||||
source: fanavaran-module-docs
|
source: fanavaran-module-docs
|
||||||
---
|
---
|
||||||
@@ -66,6 +66,10 @@ Used when building GEN.12 to resolve `DriverId`.
|
|||||||
|
|
||||||
Prefer cache before live call.
|
Prefer cache before live call.
|
||||||
|
|
||||||
|
If inquiry returns no person, damage-case **submit** registers them with GEN.44 (`POST /common/other-people`). See [01-write-apis.md](./01-write-apis.md) and the skill reference `other-people.md`. Preview only warns; it does not create.
|
||||||
|
|
||||||
|
Local GET: `GET /lookups/inquiry-by-unique-identifier` and `GET /lookups/other-people/:personId`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Remote lookup catalogue
|
## 3. Remote lookup catalogue
|
||||||
@@ -80,6 +84,7 @@ Base: `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0`
|
|||||||
| dmg-pay-method | `/car/code-list/dmg-pay-method` | `GET /lookups/dmg-pay-method` | Pay method |
|
| dmg-pay-method | `/car/code-list/dmg-pay-method` | `GET /lookups/dmg-pay-method` | Pay method |
|
||||||
| driving-licence-types | `/car/base-info/driving-licence-types` | `GET /lookups/driving-licence-types` | Licence type |
|
| driving-licence-types | `/car/base-info/driving-licence-types` | `GET /lookups/driving-licence-types` | Licence type |
|
||||||
| accident-culprit-type | `/car/code-list/accident-culprit-type` | `GET /lookups/accident-culprit-type` | Culprit type |
|
| accident-culprit-type | `/car/code-list/accident-culprit-type` | `GET /lookups/accident-culprit-type` | Culprit type |
|
||||||
|
| detection-accident-culprits | `/car/base-info/detection-accident-culprits` | `GET /lookups/detection-accident-culprits` | GEN.03 `AuthorityCulpritId` |
|
||||||
| inspection-place | `/car/code-list/inspection-place` | `GET /lookups/inspection-place` | GEN.08 |
|
| inspection-place | `/car/code-list/inspection-place` | `GET /lookups/inspection-place` | GEN.08 |
|
||||||
| drop-amount-status | `/car/code-list/drop-amount-status` | `GET /lookups/drop-amount-status` | GEN.08 |
|
| drop-amount-status | `/car/code-list/drop-amount-status` | `GET /lookups/drop-amount-status` | GEN.08 |
|
||||||
| car-components | `/car/base-info/car-components` | `GET /lookups/car-components` | `DmgSectionId` |
|
| car-components | `/car/base-info/car-components` | `GET /lookups/car-components` | `DmgSectionId` |
|
||||||
@@ -95,6 +100,12 @@ Base: `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0`
|
|||||||
| dmg-history-status | `/car/code-list/dmg-case-history-status` | `GET /lookups/dmg-history-status` | GEN.12 |
|
| dmg-history-status | `/car/code-list/dmg-case-history-status` | `GET /lookups/dmg-history-status` | GEN.12 |
|
||||||
| used-place | `/car/code-list/used-place` | `GET /lookups/used-place` | Used place |
|
| used-place | `/car/code-list/used-place` | `GET /lookups/used-place` | Used place |
|
||||||
| dmg-business-line | `/car/code-list/dmg-business-line` | `GET /lookups/dmg-business-line` | Business line |
|
| dmg-business-line | `/car/code-list/dmg-business-line` | `GET /lookups/dmg-business-line` | Business line |
|
||||||
|
| gender | `/common/code-list/gender` | `GET /lookups/gender` | GEN.44 `GenderId` |
|
||||||
|
| marital-status | `/common/code-list/marital-status` | `GET /lookups/marital-status` | GEN.44 `MaritalStatus` |
|
||||||
|
| ans | `/common/code-list/ans` | `GET /lookups/ans` | GEN.44 `IsIranian` / yes-no |
|
||||||
|
| countries | `/common/base-info/countries` | `GET /lookups/countries` | GEN.44 `NationalityId` |
|
||||||
|
| person-kind | `/common/code-list/person-kind` | `GET /lookups/person-kind` | GEN.44 `PersonKindId` |
|
||||||
|
| cii-validation-status | `/common/code-list/cii-validation-status` | `GET /lookups/cii-validation-status` | GEN.44 CII status |
|
||||||
|
|
||||||
### Generic accessor
|
### Generic accessor
|
||||||
|
|
||||||
@@ -121,6 +132,7 @@ Auth: Bearer + `AuthGuard`. Active tenant from `FANAVARAN_CLIENT` / `CLIENT_ID`.
|
|||||||
| `GET /lookups/dmg-pay-method` | `/car/code-list/dmg-pay-method` |
|
| `GET /lookups/dmg-pay-method` | `/car/code-list/dmg-pay-method` |
|
||||||
| `GET /lookups/driving-licence-types` | `/car/base-info/driving-licence-types` |
|
| `GET /lookups/driving-licence-types` | `/car/base-info/driving-licence-types` |
|
||||||
| `GET /lookups/accident-culprit-type` | `/car/code-list/accident-culprit-type` |
|
| `GET /lookups/accident-culprit-type` | `/car/code-list/accident-culprit-type` |
|
||||||
|
| `GET /lookups/detection-accident-culprits` | `/car/base-info/detection-accident-culprits` |
|
||||||
| `GET /lookups/inspection-place` | `/car/code-list/inspection-place` |
|
| `GET /lookups/inspection-place` | `/car/code-list/inspection-place` |
|
||||||
| `GET /lookups/drop-amount-status` | `/car/code-list/drop-amount-status` |
|
| `GET /lookups/drop-amount-status` | `/car/code-list/drop-amount-status` |
|
||||||
| `GET /lookups/car-components` | `/car/base-info/car-components` |
|
| `GET /lookups/car-components` | `/car/base-info/car-components` |
|
||||||
@@ -135,6 +147,12 @@ Auth: Bearer + `AuthGuard`. Active tenant from `FANAVARAN_CLIENT` / `CLIENT_ID`.
|
|||||||
| `GET /lookups/dmg-history-status` | `/car/code-list/dmg-case-history-status` |
|
| `GET /lookups/dmg-history-status` | `/car/code-list/dmg-case-history-status` |
|
||||||
| `GET /lookups/used-place` | `/car/code-list/used-place` |
|
| `GET /lookups/used-place` | `/car/code-list/used-place` |
|
||||||
| `GET /lookups/dmg-business-line` | `/car/code-list/dmg-business-line` |
|
| `GET /lookups/dmg-business-line` | `/car/code-list/dmg-business-line` |
|
||||||
|
| `GET /lookups/gender` | `/common/code-list/gender` |
|
||||||
|
| `GET /lookups/marital-status` | `/common/code-list/marital-status` |
|
||||||
|
| `GET /lookups/ans` | `/common/code-list/ans` |
|
||||||
|
| `GET /lookups/countries` | `/common/base-info/countries` |
|
||||||
|
| `GET /lookups/person-kind` | `/common/code-list/person-kind` |
|
||||||
|
| `GET /lookups/cii-validation-status` | `/common/code-list/cii-validation-status` |
|
||||||
| `GET /lookups/fanavaran` | catalogue metadata (names + URLs) |
|
| `GET /lookups/fanavaran` | catalogue metadata (names + URLs) |
|
||||||
| `GET /lookups/fanavaran/{lookupName}` | any configured name (incl. `insurance-corp`) |
|
| `GET /lookups/fanavaran/{lookupName}` | any configured name (incl. `insurance-corp`) |
|
||||||
|
|
||||||
@@ -143,7 +161,11 @@ Auth: Bearer + `AuthGuard`. Active tenant from `FANAVARAN_CLIENT` / `CLIENT_ID`.
|
|||||||
| Nest route | Fanavaran path / notes |
|
| Nest route | Fanavaran path / notes |
|
||||||
|------------|------------------------|
|
|------------|------------------------|
|
||||||
| `GET /lookups/inquiry-by-vin?vin=` | `/car/vehicles/inquiry-by-vin` |
|
| `GET /lookups/inquiry-by-vin?vin=` | `/car/vehicles/inquiry-by-vin` |
|
||||||
|
| `GET /lookups/inquiry-by-unique-identifier?nationalCode=&birthday=` | `/common/parties/inquiry-by-unique-identifier` |
|
||||||
|
| `GET /lookups/other-people/:personId` | `/common/other-people/{Id}` (GEN.44) |
|
||||||
| `GET /lookups/my-policies?nationalCode=&insuranceLineId=` | `/common/Policies/inquiry-my-policies` |
|
| `GET /lookups/my-policies?nationalCode=&insuranceLineId=` | `/common/Policies/inquiry-my-policies` |
|
||||||
|
| `GET /lookups/processed-third-party-policy?nationalCode=&vin=&plaque…` | Last matching ثالث policy for that car + vehicle |
|
||||||
|
| `GET /lookups/processed-body-policy?nationalCode=&vin=&plaque…` | Last matching بدنه policy for that car + vehicle |
|
||||||
| `GET /lookups/third-party-policy/:policyId` | `/car/third-party-car-policies/{id}` |
|
| `GET /lookups/third-party-policy/:policyId` | `/car/third-party-car-policies/{id}` |
|
||||||
| `GET /lookups/body-policy/:policyId` | `/car/vehicle-hull-policies/{id}` |
|
| `GET /lookups/body-policy/:policyId` | `/car/vehicle-hull-policies/{id}` |
|
||||||
|
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ File: `claim-request-management.service.ts`
|
|||||||
|
|
||||||
| Name | Relevant values | Fanavaran use |
|
| Name | Relevant values | Fanavaran use |
|
||||||
|------|-----------------|---------------|
|
|------|-----------------|---------------|
|
||||||
| Claim type | `THIRD_PARTY` only | Fanavaran path enabled |
|
| Claim type | `THIRD_PARTY` | `third-party-car-financial-claims` (base → dmg-cases → files → expertise) |
|
||||||
| | `CAR_BODY` | **Not** submitted to Fanavaran |
|
| | `CAR_BODY` | `vehicle-hull-claims` (base → files → expertise; **no dmg-cases**) |
|
||||||
| `ClaimCaseStatus` | local workflow | Triggers stages indirectly |
|
| `ClaimCaseStatus` | local workflow | Triggers stages indirectly |
|
||||||
| History event types | `FANAVARAN_*_SUCCEEDED/FAILED` | Audit trail on claim |
|
| History event types | `FANAVARAN_*_SUCCEEDED/FAILED` | Audit trail on claim |
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Start local claim
|
|||||||
→ Upload docs/images locally
|
→ Upload docs/images locally
|
||||||
→ GEN.07 attachments (per file, best-effort)
|
→ GEN.07 attachments (per file, best-effort)
|
||||||
→ Expert pricing ready
|
→ Expert pricing ready
|
||||||
→ GEN.08 expertise → store expertiseId → SMS owner
|
→ GEN.08 expertise → store expertiseId (no SMS)
|
||||||
→ End (local completion independent of Fanavaran success)
|
→ End (local completion independent of Fanavaran success)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -60,6 +60,12 @@ sequenceDiagram
|
|||||||
User->>YARA: SELECT_OUTER_PARTS
|
User->>YARA: SELECT_OUTER_PARTS
|
||||||
YARA->>YARA: autoSubmitFanavaranDamageCase...
|
YARA->>YARA: autoSubmitFanavaranDamageCase...
|
||||||
Note over YARA: soft-ensure base if missing
|
Note over YARA: soft-ensure base if missing
|
||||||
|
alt DriverId missing
|
||||||
|
YARA->>FV: GET parties/inquiry-by-unique-identifier
|
||||||
|
FV-->>YARA: empty
|
||||||
|
YARA->>FV: POST common/other-people (GEN.44)
|
||||||
|
FV-->>YARA: person Id
|
||||||
|
end
|
||||||
YARA->>FV: POST .../dmg-cases (GEN.12)
|
YARA->>FV: POST .../dmg-cases (GEN.12)
|
||||||
FV-->>YARA: DmgCaseId
|
FV-->>YARA: DmgCaseId
|
||||||
YARA->>DB: dmgCaseId
|
YARA->>DB: dmgCaseId
|
||||||
@@ -76,7 +82,7 @@ sequenceDiagram
|
|||||||
YARA->>FV: POST .../expertise (GEN.08)
|
YARA->>FV: POST .../expertise (GEN.08)
|
||||||
FV-->>YARA: ExpertiseId
|
FV-->>YARA: ExpertiseId
|
||||||
YARA->>DB: expertiseId, history SUCCESS
|
YARA->>DB: expertiseId, history SUCCESS
|
||||||
YARA->>YARA: SMS owner (deduped)
|
Note over YARA: Post-expertise owner SMS is disabled
|
||||||
```
|
```
|
||||||
|
|
||||||
## Flow diagram (stages + soft-ensure)
|
## Flow diagram (stages + soft-ensure)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ source: fanavaran-module-docs
|
|||||||
| Tenant config | boot + System Settings | `fanavaran-client-config.service.ts`, `system-settings.*` |
|
| Tenant config | boot + System Settings | `fanavaran-client-config.service.ts`, `system-settings.*` |
|
||||||
| Orchestration | `ClaimRequestManagementModule` | `claim-request-management.service.ts` |
|
| Orchestration | `ClaimRequestManagementModule` | `claim-request-management.service.ts` |
|
||||||
| Policy select | same | `fanavaran-policy-selection.ts` |
|
| Policy select | same | `fanavaran-policy-selection.ts` |
|
||||||
| SMS after expertise | `SmsOrchestrationModule` | `sms-orchestration.service.ts` |
|
| SMS after expertise | Disabled by product decision | No message is dispatched |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ source: fanavaran-module-docs
|
|||||||
| Send | GEN.03 JSON + business headers |
|
| Send | GEN.03 JSON + business headers |
|
||||||
| Receive | `Id`, `ClaimNo` |
|
| Receive | `Id`, `ClaimNo` |
|
||||||
| Store | `claimId`, `claimNo`, `fanavaranSync.baseClaim.*`, history |
|
| Store | `claimId`, `claimNo`, `fanavaranSync.baseClaim.*`, history |
|
||||||
| Consumers | Damage/expertise soft-ensure; UI claim detail; SMS |
|
| Consumers | Damage/expertise soft-ensure; UI claim detail |
|
||||||
|
|
||||||
### C. Damage case
|
### C. Damage case
|
||||||
|
|
||||||
@@ -133,8 +133,9 @@ Expert reply path calls expertise auto-submit (~10576).
|
|||||||
| `claimCases.dmgCaseId` | GEN.12 |
|
| `claimCases.dmgCaseId` | GEN.12 |
|
||||||
| `claimCases.expertiseId` | GEN.08 |
|
| `claimCases.expertiseId` | GEN.08 |
|
||||||
| `claimCases.fanavaranSync.*` | All |
|
| `claimCases.fanavaranSync.*` | All |
|
||||||
|
| `claimCases.fanavaranSync.damageCase.otherPersonId` | GEN.44 |
|
||||||
| `claimCases.history[]` | Success/fail events |
|
| `claimCases.history[]` | Success/fail events |
|
||||||
| `blameCases.parties[].person.fanavaranDriverId` | GEN.12 driver resolve |
|
| `blameCases.parties[].person.fanavaranDriverId` | GEN.12 driver resolve / GEN.44 |
|
||||||
| `fanavaranAuditLogs` | Every real HTTP step |
|
| `fanavaranAuditLogs` | Every real HTTP step |
|
||||||
| `fanavaranAuthTokens` | Auth cache |
|
| `fanavaranAuthTokens` | Auth cache |
|
||||||
| `fanavaranClientConfigs` | Tenant profiles |
|
| `fanavaranClientConfigs` | Tenant profiles |
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ source: fanavaran-module-docs
|
|||||||
| YARA | Fanavaran |
|
| YARA | Fanavaran |
|
||||||
|------|-----------|
|
|------|-----------|
|
||||||
| `damage.selectedParts` labels | `Desc` (`/` joined) |
|
| `damage.selectedParts` labels | `Desc` (`/` joined) |
|
||||||
| Party `nationalCodeOfDriver` + birthday (+ insurer flag) → inquiry | `DriverId` |
|
| Party `nationalCodeOfDriver` + birthday (+ insurer flag) → inquiry, else GEN.44 create | `DriverId` |
|
||||||
| `claimCase.vehicle.carType` → vehicle-kinds | `VehicleKindId` |
|
| `claimCase.vehicle.carType` → vehicle-kinds | `VehicleKindId` |
|
||||||
| `INSURANCE_CORP_ID` caption → insurance-corp | `InsuranceCorpId` |
|
| `INSURANCE_CORP_ID` caption → insurance-corp | `InsuranceCorpId` |
|
||||||
| Inquiry `ShsNum` / Chassis* | `ChassisNo` |
|
| Inquiry `ShsNum` / Chassis* | `ChassisNo` |
|
||||||
@@ -43,6 +43,20 @@ source: fanavaran-module-docs
|
|||||||
| Tenant defaults | `DmgCaseTypeId`, `DmgHistoryStatus`, `PlaqueKindId`, `PlaqueSampleId`, `FaultPercent`, `AccidentVehicleUsedId`, `LicenceTypeId` |
|
| Tenant defaults | `DmgCaseTypeId`, `DmgHistoryStatus`, `PlaqueKindId`, `PlaqueSampleId`, `FaultPercent`, `AccidentVehicleUsedId`, `LicenceTypeId` |
|
||||||
| Provisional | `EstimateAmount` = 1000 |
|
| Provisional | `EstimateAmount` = 1000 |
|
||||||
|
|
||||||
|
## GEN.44 — Other people
|
||||||
|
|
||||||
|
| YARA | Fanavaran |
|
||||||
|
|------|-----------|
|
||||||
|
| Party national code | `NationalCode` |
|
||||||
|
| Party birthday (Jalali) | `BirthYear` / `BirthMonth` / `BirthDay` |
|
||||||
|
| `fullName` split | `Name` / `LastName` (registry may overwrite) |
|
||||||
|
| Party phone / user mobile | `Mobile` |
|
||||||
|
| User address / city / gender | `Address`, `CityId`, `GenderId` |
|
||||||
|
| `ans` lookup بله | `IsIranian` |
|
||||||
|
| `person-kind` حقیقی | `PersonKindId` |
|
||||||
|
|
||||||
|
Create only on GEN.12 submit when inquiry missed. Cache `otherPersonId` + `driverId`.
|
||||||
|
|
||||||
## GEN.07 — Attachments
|
## GEN.07 — Attachments
|
||||||
|
|
||||||
| YARA | Fanavaran |
|
| YARA | Fanavaran |
|
||||||
@@ -83,7 +97,7 @@ source: fanavaran-module-docs
|
|||||||
|
|
||||||
| YARA | Note |
|
| YARA | Note |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `CAR_BODY` claims | No Fanavaran submit |
|
| `CAR_BODY` dmg-cases | Hull has no GEN.12 resource; skip that stage |
|
||||||
| Videos | Not uploaded via GEN.07 in current code |
|
| Videos | Not uploaded via GEN.07 in current code |
|
||||||
| Full police report fields | Often null unless later enriched |
|
| Full police report fields | Often null unless later enriched |
|
||||||
|
|
||||||
|
|||||||
@@ -69,4 +69,4 @@ Per HTTP step:
|
|||||||
|
|
||||||
## SMS
|
## SMS
|
||||||
|
|
||||||
After successful expertise (last Fanavaran stage), owner SMS may be sent once (`fanavaranSync.expertise.smsNotifiedAt`). Failure to SMS should not roll back Fanavaran expertise submit; treat as separate notification concern. Claims already notified at base claim (`fanavaranSync.baseClaim.smsNotifiedAt`) are not re-notified.
|
The post-expertise owner SMS is disabled. A successful final Fanavaran response only persists the returned `expertiseId` and synchronization state; it does not dispatch a notification.
|
||||||
|
|||||||
442
docs/fanavaran/car-body-curl-flow.md
Normal file
442
docs/fanavaran/car-body-curl-flow.md
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
---
|
||||||
|
last_updated: 2026-09-16
|
||||||
|
tags: [fanavaran, car-body, hull, curl, vhud, parsian]
|
||||||
|
source: fanavaran-apis skill + VHUD GEN docs
|
||||||
|
---
|
||||||
|
|
||||||
|
# Car-body (بدنه) Fanavaran — manual curl flow
|
||||||
|
|
||||||
|
Direct Fanavaran BimeApi calls for **vehicle-hull-claims** (VHUD GEN.03 / GEN.05 / GEN.06).
|
||||||
|
Use this when debugging production without YARA, or to compare with YARA preview payloads.
|
||||||
|
|
||||||
|
Credentials below match **`PARSIAN_FANAVARAN_CONFIG`** in `claim-request-management.service.ts` and `scripts/fanavaran-auth.sh parsian`. Tokens (`APP_TOKEN`, `AUTH_TOKEN`) change daily — run auth curls first or `./scripts/fanavaran-auth.sh parsian` then `source files/fanavaran-auth/parsian/tokens.env`.
|
||||||
|
|
||||||
|
## Base URLs
|
||||||
|
|
||||||
|
| Purpose | URL |
|
||||||
|
|---------|-----|
|
||||||
|
| Auth | `https://apimanager.iraneit.com/BimeApiManager/api/EITAuthentication/...` |
|
||||||
|
| Business (BimeApi v2 car) | `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/...` |
|
||||||
|
|
||||||
|
## Variables (set after each step)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Parsian (ready values)
|
||||||
|
export FANAVARAN_APPNAME='ParsianService'
|
||||||
|
export FANAVARAN_SECRET='P@r30@n$erv!ce'
|
||||||
|
export FANAVARAN_USERNAME='ParsianServiceUser'
|
||||||
|
export FANAVARAN_PASSWORD='P@r30@n123'
|
||||||
|
export FANAVARAN_CORP_ID='543'
|
||||||
|
export FANAVARAN_CONTRACT_ID='28' # confirm hull ContractId with Fanavaran if files fail
|
||||||
|
export FANAVARAN_LOCATION='210050'
|
||||||
|
|
||||||
|
export APP_TOKEN='' # from GetAppToken response header apptoken
|
||||||
|
export AUTH_TOKEN='' # from Login response header/body
|
||||||
|
|
||||||
|
export HULL_CLAIM_ID='5023617' # GEN.03 response Id — کد رایانه (NOT ClaimNo); replace after POST
|
||||||
|
export HULL_CLAIM_NO='2268' # display only
|
||||||
|
export EXPERTISE_ID='' # GEN.06 response Id
|
||||||
|
```
|
||||||
|
|
||||||
|
### One-shot auth (Parsian) — copy/paste
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -D /tmp/fanavaran-apptoken.headers -o /tmp/fanavaran-apptoken.body \
|
||||||
|
-X POST 'https://apimanager.iraneit.com/BimeApiManager/api/EITAuthentication/GetAppToken' \
|
||||||
|
-H 'appname: ParsianService' \
|
||||||
|
-H 'secret: P@r30@n$erv!ce' \
|
||||||
|
-H 'Content-Length: 0'
|
||||||
|
|
||||||
|
export APP_TOKEN="$(awk -F': ' 'tolower($1)=="apptoken"{gsub(/\r/,"",$2);print $2;exit}' /tmp/fanavaran-apptoken.headers)"
|
||||||
|
|
||||||
|
curl -sS -D /tmp/fanavaran-login.headers -o /tmp/fanavaran-login.body \
|
||||||
|
-X POST 'https://apimanager.iraneit.com/BimeApiManager/api/EITAuthentication/Login' \
|
||||||
|
-H "appToken: ${APP_TOKEN}" \
|
||||||
|
-H 'userName: ParsianServiceUser' \
|
||||||
|
-H 'password: P@r30@n123' \
|
||||||
|
-H 'Content-Length: 0'
|
||||||
|
|
||||||
|
export AUTH_TOKEN="$(awk -F': ' 'tolower($1)=="authenticationtoken"{gsub(/\r/,"",$2);print $2;exit}' /tmp/fanavaran-login.headers)"
|
||||||
|
echo "APP_TOKEN=${APP_TOKEN}"
|
||||||
|
echo "AUTH_TOKEN=${AUTH_TOKEN}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from repo root: `./scripts/fanavaran-auth.sh parsian && source files/fanavaran-auth/parsian/tokens.env` (uses `AUTHENTICATION_TOKEN` — alias `export AUTH_TOKEN="$AUTHENTICATION_TOKEN"`).
|
||||||
|
|
||||||
|
### Id vs ClaimNo (critical)
|
||||||
|
|
||||||
|
| Field | Persian | Use in URL? |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| **`Id`** | کد رایانه پرونده | **Yes** — `/vehicle-hull-claims/{Id}/files` and `/expertise` |
|
||||||
|
| **`ClaimNo`** | شماره پرونده | **No** — do not put `2268` in the path |
|
||||||
|
|
||||||
|
Example: `Id=5023617`, `ClaimNo=2268` → use **`5023617`** everywhere below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auth — GetAppToken
|
||||||
|
|
||||||
|
**Doc:** API.GENR (EIT Authentication)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -D /tmp/fanavaran-apptoken.headers -o /tmp/fanavaran-apptoken.body \
|
||||||
|
-X POST 'https://apimanager.iraneit.com/BimeApiManager/api/EITAuthentication/GetAppToken' \
|
||||||
|
-H 'appname: ParsianService' \
|
||||||
|
-H 'secret: P@r30@n$erv!ce' \
|
||||||
|
-H 'Content-Length: 0'
|
||||||
|
```
|
||||||
|
|
||||||
|
Read **`apptoken`** (or `appToken`) from response **headers**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -i apptoken /tmp/fanavaran-apptoken.headers
|
||||||
|
export APP_TOKEN='<paste apptoken header value>'
|
||||||
|
```
|
||||||
|
|
||||||
|
Body is usually empty. No JSON body on this call.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auth — Login
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -D /tmp/fanavaran-login.headers -o /tmp/fanavaran-login.body \
|
||||||
|
-X POST 'https://apimanager.iraneit.com/BimeApiManager/api/EITAuthentication/Login' \
|
||||||
|
-H "appToken: ${APP_TOKEN}" \
|
||||||
|
-H 'userName: ParsianServiceUser' \
|
||||||
|
-H 'password: P@r30@n123' \
|
||||||
|
-H 'Content-Length: 0'
|
||||||
|
```
|
||||||
|
|
||||||
|
Read **`authenticationToken`** from response header or JSON body:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -i authenticationtoken /tmp/fanavaran-login.headers
|
||||||
|
# or
|
||||||
|
cat /tmp/fanavaran-login.body
|
||||||
|
export AUTH_TOKEN='<paste token>'
|
||||||
|
```
|
||||||
|
|
||||||
|
Token is typically valid until **Asia/Tehran midnight** (same as YARA `FanavaranAuthService`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business headers (all stages below)
|
||||||
|
|
||||||
|
Every BimeApi call needs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
authenticationToken: <AUTH_TOKEN>
|
||||||
|
CorpId: <FANAVARAN_CORP_ID>
|
||||||
|
ContractId: <FANAVARAN_CONTRACT_ID>
|
||||||
|
Location: <FANAVARAN_LOCATION>
|
||||||
|
Content-Type: application/json # except multipart files
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## YARA preview (copy JSON bodies from production)
|
||||||
|
|
||||||
|
Before curling Fanavaran directly, get the exact payload from Parsian production YARA (same tenant auth as above):
|
||||||
|
|
||||||
|
| Stage | YARA preview (Parsian) |
|
||||||
|
|-------|-------------------------|
|
||||||
|
| 1 Base claim | `GET /v2/fanavaran/parsian/claim-cases/{claimCaseId}/base-claim/preview` |
|
||||||
|
| 2 Damage case | **N/A for CAR_BODY** — skipped |
|
||||||
|
| 3 Attachments | `GET /v2/fanavaran/parsian/claim-cases/{claimCaseId}/attachments/preview` |
|
||||||
|
| 4 Expertise | `GET /v2/fanavaran/parsian/claim-cases/{claimCaseId}/expertise/preview` |
|
||||||
|
|
||||||
|
Expert UI BFF (if used): `POST /car-damage/expert/api/fanavaran` with `{"claimCaseId":"..."}` runs remaining stages via the app.
|
||||||
|
|
||||||
|
Save preview `payload` into a file, e.g. `stage1-body.json`, then use `@stage1-body.json` in curl.
|
||||||
|
Example hull-only GEN.03 body (proven Parsian case): [`stage1-hull-body.json`](./stage1-hull-body.json).
|
||||||
|
|
||||||
|
**Enrichment before POST (YARA does this automatically):**
|
||||||
|
|
||||||
|
| Field | Source |
|
||||||
|
|-------|--------|
|
||||||
|
| `ActualPremium` | `GET /lookups/body-policy/{PolicyId}` → `TotalPremium` |
|
||||||
|
| `AccidentCulpritId` | `GET /lookups/inquiry-by-unique-identifier?nationalCode=…&birthday=…` → `driverId` (fallback: policy `CustomerId`) |
|
||||||
|
| `CustomerFaultPercent` | **100** (required; درصد تقصير بيمه گذار) |
|
||||||
|
| `AuthorityCulpritId` | `GET /lookups/detection-accident-culprits` → بیمهگذار (required; مرجع تعيين مقصر) |
|
||||||
|
|
||||||
|
Success example: `Id=5026065`, `ClaimNo=1747` — use **`5026065`** for files/expertise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 1 — GEN.03 Create hull base claim
|
||||||
|
|
||||||
|
**Doc:** CAR.VHUD.APIH.GEN.03
|
||||||
|
**Method:** `POST`
|
||||||
|
**URL:** `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -X POST \
|
||||||
|
'https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims' \
|
||||||
|
-H "authenticationToken: ${AUTH_TOKEN}" \
|
||||||
|
-H "CorpId: ${FANAVARAN_CORP_ID}" \
|
||||||
|
-H "ContractId: ${FANAVARAN_CONTRACT_ID}" \
|
||||||
|
-H "Location: ${FANAVARAN_LOCATION}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d @stage1-body.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Body:** paste from YARA **base-claim preview** `payload`. Must be **hull GEN.03 shape** (include `AccidentTypeId`, `IsLicenseReplaced`, `CostSeparationToDmgSections`, `IsOwnerChanged`; do **not** send ثالث-only fields like `DamagedCount`, `HasOtherCulprit`, `CompensationReferenceId`, plaque block on base).
|
||||||
|
|
||||||
|
**GEN.03 required lookups (Parsian live):**
|
||||||
|
|
||||||
|
| Field | Lookup | Typical collision value |
|
||||||
|
|-------|--------|-------------------------|
|
||||||
|
| `AccidentTypeId` | `GET /lookups/vehicle-hull-accident-types` | **2** = تصادف(حادثه) |
|
||||||
|
| `CostSeparationToDmgSections` | `GET /lookups/ans` | **0** = خیر |
|
||||||
|
| `IsOwnerChanged` | `GET /lookups/ans` | **0** = خیر |
|
||||||
|
| `IsLicenseReplaced` | — | **`null`** (must be empty; do not send `0` or ثالث `IsLicenseReplacement`) |
|
||||||
|
| `CulpritTypeId` | `GET /lookups/vehicle-hull-accident-culprit-type` | **301** = حادثه مقصر شناخته شده دارد (not ثالث `337`) |
|
||||||
|
| `CustomerFaultPercent` | — | **100** |
|
||||||
|
| `AuthorityCulpritId` | `GET /lookups/detection-accident-culprits` | بیمهگذار (`Id` from lookup, fallback **1**) |
|
||||||
|
|
||||||
|
Fanavaran direct: `car/base-info/vehicle-hull-accident-types`, `common/code-list/ans`, `car/code-list/vehicle-hull-accident-culprit-type`.
|
||||||
|
|
||||||
|
**Success:** JSON includes at least:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "Id": 5023617, "ClaimNo": 2268, ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export HULL_CLAIM_ID='5023617' # use Id
|
||||||
|
export HULL_CLAIM_NO='2268' # do not use in nested URLs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional — GET base claim (verify Id exists)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -X GET \
|
||||||
|
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}" \
|
||||||
|
-H "authenticationToken: ${AUTH_TOKEN}" \
|
||||||
|
-H "CorpId: ${FANAVARAN_CORP_ID}" \
|
||||||
|
-H "ContractId: ${FANAVARAN_CONTRACT_ID}" \
|
||||||
|
-H "Location: ${FANAVARAN_LOCATION}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional — PUT hull shape (if files/expertise say پرونده خسارت بدنه یافت نشد)
|
||||||
|
|
||||||
|
If stage 1 was created with a wrong (ثالث-shaped) body, **PUT** the same hull GEN.03 field set onto the existing `Id` (body = preview payload after hull mapping):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -X PUT \
|
||||||
|
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}" \
|
||||||
|
-H "authenticationToken: ${AUTH_TOKEN}" \
|
||||||
|
-H "CorpId: ${FANAVARAN_CORP_ID}" \
|
||||||
|
-H "ContractId: ${FANAVARAN_CONTRACT_ID}" \
|
||||||
|
-H "Location: ${FANAVARAN_LOCATION}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d @stage1-body-hull.json
|
||||||
|
```
|
||||||
|
|
||||||
|
YARA does this automatically before attachments/expertise when product is CAR_BODY.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 2 — Damage case (ثالث GEN.12) — **does not exist for hull**
|
||||||
|
|
||||||
|
**Do not call:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST .../vehicle-hull-claims/{id}/dmg-cases ❌ Feature:url(...) not found
|
||||||
|
```
|
||||||
|
|
||||||
|
Third-party only:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST .../third-party-car-financial-claims/{id}/dmg-cases
|
||||||
|
```
|
||||||
|
|
||||||
|
For car-body YARA: damage-case preview/submit **returns skipped**. Flow is **1 → 3 → 4**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 3 — GEN.05 Attachments (one file per request)
|
||||||
|
|
||||||
|
**Doc:** CAR.VHUD.APIH.GEN.05
|
||||||
|
**Method:** `POST`
|
||||||
|
**URL:** `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/{HULL_CLAIM_ID}/files`
|
||||||
|
|
||||||
|
**Parsian `FileTypeId`:** `63` (سایر مدارک خسارت) — proven on ثالث; verify in `file-types` lookup for hull if rejected.
|
||||||
|
|
||||||
|
### JSON metadata (multipart part)
|
||||||
|
|
||||||
|
YARA sends this as multipart field **`Param`** (`application/json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"FileName": "photo_example.jpg",
|
||||||
|
"FileTypeId": 63,
|
||||||
|
"ClaimId": 5023617
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`FileName` must **exactly** match the uploaded file name. `ClaimId` = same as URL `{HULL_CLAIM_ID}` (`Id`, not `ClaimNo`).
|
||||||
|
|
||||||
|
### curl (matches YARA `postFanavaranMultipart`: Param + Param1)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export FILE_PATH='/path/to/photo_example.jpg'
|
||||||
|
export FILE_NAME='photo_example.jpg'
|
||||||
|
|
||||||
|
curl -sS -X POST \
|
||||||
|
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}/files" \
|
||||||
|
-H "authenticationToken: ${AUTH_TOKEN}" \
|
||||||
|
-H "CorpId: ${FANAVARAN_CORP_ID}" \
|
||||||
|
-H "ContractId: ${FANAVARAN_CONTRACT_ID}" \
|
||||||
|
-H "Location: ${FANAVARAN_LOCATION}" \
|
||||||
|
-F "Param={\"FileName\":\"${FILE_NAME}\",\"FileTypeId\":63,\"ClaimId\":${HULL_CLAIM_ID}};type=application/json" \
|
||||||
|
-F "Param1=@${FILE_PATH};filename=${FILE_NAME}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Repeat once per image (YARA does not batch hull uploads).
|
||||||
|
|
||||||
|
**Success example:** `{ "Id": 2008737, "Status": 2003, ... }`
|
||||||
|
|
||||||
|
### Optional — list files (GET)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -X GET \
|
||||||
|
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}/files" \
|
||||||
|
-H "authenticationToken: ${AUTH_TOKEN}" \
|
||||||
|
-H "CorpId: ${FANAVARAN_CORP_ID}" \
|
||||||
|
-H "ContractId: ${FANAVARAN_CONTRACT_ID}" \
|
||||||
|
-H "Location: ${FANAVARAN_LOCATION}"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 4 — GEN.06 Expertise
|
||||||
|
|
||||||
|
**Doc:** CAR.VHUD.APIH.GEN.06
|
||||||
|
**Method:** `POST`
|
||||||
|
**URL:** `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/{HULL_CLAIM_ID}/expertise`
|
||||||
|
|
||||||
|
**Body:** paste from YARA **expertise preview** `payload` (GEN.06 — **not** ثالث GEN.08).
|
||||||
|
|
||||||
|
| Field | Source (YARA) |
|
||||||
|
|-------|----------------|
|
||||||
|
| `VehicleCurrentValue` | `evaluation.priceDrop.carPrice` if set; else **`GET /lookups/body-policy/{PolicyId}` → `VehicleValue`** |
|
||||||
|
| `DmgSections[].DmgKindId` | **`5485`** (تخریب) from **`GET /lookups/vehicle-hull-dmg-kind`** |
|
||||||
|
| `DmgSectionCosts[].DmgCostKindId` | **`1`** (Id for «مجموع لوازم») from **`GET /lookups/vehicle-hull-dmg-cost-kinds`** — not `DmgCostCategoryId` |
|
||||||
|
|
||||||
|
Hull expects fields such as:
|
||||||
|
|
||||||
|
- `ClaimExpertId` (Parsian assessor: **29** = `ExpertiseClaimExpertId`)
|
||||||
|
- `Wage`, `ComponentReplacementCost`, `WasteValue`, `DropAmount`, `VehicleCurrentValue`
|
||||||
|
- `MotorNo`, `ChassisNo`, `VIN`, `PlaqueNo`, `PlaqueSerial`, `BuiltYear`
|
||||||
|
- `DmgSections[]` with `Count`, `Desc`, `AccessoryKindId`, `DmgSectionCosts[]`
|
||||||
|
- **No** `DmgCaseId`, `InspectionPlaceId`, `DropAmountStatus`, `RepairWage`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -X POST \
|
||||||
|
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}/expertise" \
|
||||||
|
-H "authenticationToken: ${AUTH_TOKEN}" \
|
||||||
|
-H "CorpId: ${FANAVARAN_CORP_ID}" \
|
||||||
|
-H "ContractId: ${FANAVARAN_CONTRACT_ID}" \
|
||||||
|
-H "Location: ${FANAVARAN_LOCATION}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d @stage4-expertise-body.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success:** `{ "Id": 71927, "ClaimId": 78776, ... }` → store as `expertiseId`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage map (YARA vs Fanavaran)
|
||||||
|
|
||||||
|
| YARA / user stage | Fanavaran doc | HTTP | Exists for بدنه? |
|
||||||
|
|-------------------|---------------|------|------------------|
|
||||||
|
| Auth | GetAppToken + Login | POST | Yes |
|
||||||
|
| 1 Base claim | GEN.03 | POST `/vehicle-hull-claims` | Yes |
|
||||||
|
| 1b Sync hull body | GEN.03 | PUT `/vehicle-hull-claims/{Id}` | Yes (fix row) |
|
||||||
|
| 2 Damage case | GEN.12 (ثالث) | POST `.../dmg-cases` | **No — skip** |
|
||||||
|
| 3 Attachments | GEN.05 | POST `.../{Id}/files` | Yes |
|
||||||
|
| 4 Expertise | GEN.06 | POST `.../{Id}/expertise` | Yes |
|
||||||
|
|
||||||
|
## Not used for normal collision car-body
|
||||||
|
|
||||||
|
| Doc | URL |
|
||||||
|
|-----|-----|
|
||||||
|
| GEN.01 bulk import | POST `/vehicle-hull-import-claims` |
|
||||||
|
| GEN.02 import files | POST `/vehicle-hull-import-claims/{PrvId}/files` |
|
||||||
|
| GEN.04 culprits | POST `.../{Id}/culprits` |
|
||||||
|
|
||||||
|
## Related docs
|
||||||
|
|
||||||
|
- Skill summary: `.agents/skills/fanavaran-apis/references/car-body-cases.md`
|
||||||
|
- VHUD extracts: `.agents/skills/fanavaran-apis/references/vehicle-hull/`
|
||||||
|
- Local auth script: `scripts/fanavaran-auth.sh parsian`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parsian — full flow script (after auth)
|
||||||
|
|
||||||
|
Run the **One-shot auth** block above first. Save YARA preview JSON to `stage1-body.json` and `stage4-expertise-body.json`. Set `FILE_PATH` / `FILE_NAME` for stage 3.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stage 1 — POST hull base claim
|
||||||
|
curl -sS -X POST \
|
||||||
|
'https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims' \
|
||||||
|
-H "authenticationToken: ${AUTH_TOKEN}" \
|
||||||
|
-H 'CorpId: 543' \
|
||||||
|
-H 'ContractId: 28' \
|
||||||
|
-H 'Location: 210050' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d @stage1-body.json
|
||||||
|
|
||||||
|
# Stage 1 optional — GET
|
||||||
|
curl -sS -X GET \
|
||||||
|
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}" \
|
||||||
|
-H "authenticationToken: ${AUTH_TOKEN}" \
|
||||||
|
-H 'CorpId: 543' \
|
||||||
|
-H 'ContractId: 28' \
|
||||||
|
-H 'Location: 210050'
|
||||||
|
|
||||||
|
# Stage 1 optional — PUT hull sync
|
||||||
|
curl -sS -X PUT \
|
||||||
|
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}" \
|
||||||
|
-H "authenticationToken: ${AUTH_TOKEN}" \
|
||||||
|
-H 'CorpId: 543' \
|
||||||
|
-H 'ContractId: 28' \
|
||||||
|
-H 'Location: 210050' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d @stage1-body-hull.json
|
||||||
|
|
||||||
|
# Stage 2 — (skip — no dmg-cases for hull)
|
||||||
|
|
||||||
|
# Stage 3 — POST one file
|
||||||
|
FILE_PATH='/path/to/photo.jpg'
|
||||||
|
FILE_NAME='photo.jpg'
|
||||||
|
curl -sS -X POST \
|
||||||
|
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}/files" \
|
||||||
|
-H "authenticationToken: ${AUTH_TOKEN}" \
|
||||||
|
-H 'CorpId: 543' \
|
||||||
|
-H 'ContractId: 28' \
|
||||||
|
-H 'Location: 210050' \
|
||||||
|
-F "Param={\"FileName\":\"${FILE_NAME}\",\"FileTypeId\":63,\"ClaimId\":${HULL_CLAIM_ID}};type=application/json" \
|
||||||
|
-F "Param1=@${FILE_PATH};filename=${FILE_NAME}"
|
||||||
|
|
||||||
|
# Stage 3 optional — GET files list
|
||||||
|
curl -sS -X GET \
|
||||||
|
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}/files" \
|
||||||
|
-H "authenticationToken: ${AUTH_TOKEN}" \
|
||||||
|
-H 'CorpId: 543' \
|
||||||
|
-H 'ContractId: 28' \
|
||||||
|
-H 'Location: 210050'
|
||||||
|
|
||||||
|
# Stage 4 — POST expertise
|
||||||
|
curl -sS -X POST \
|
||||||
|
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}/expertise" \
|
||||||
|
-H "authenticationToken: ${AUTH_TOKEN}" \
|
||||||
|
-H 'CorpId: 543' \
|
||||||
|
-H 'ContractId: 28' \
|
||||||
|
-H 'Location: 210050' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d @stage4-expertise-body.json
|
||||||
|
```
|
||||||
154
docs/fanavaran/car-body-stage-fields.md
Normal file
154
docs/fanavaran/car-body-stage-fields.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# Car-body (بدنه) — Fanavaran stage bodies (current YARA)
|
||||||
|
|
||||||
|
Snapshot of **what hull submit actually sends** after `toFanavaranHullBaseClaimPayload` / `toFanavaranHullExpertisePayload`.
|
||||||
|
|
||||||
|
Staged flow (VHUD):
|
||||||
|
|
||||||
|
1. Base: `POST /car/vehicle-hull-claims` (GEN.03)
|
||||||
|
2. **No dmg-cases** (`vehicle-hull-claims/{id}/dmg-cases` does not exist)
|
||||||
|
3. Attachments: `POST …/{Id}/files` (GEN.05)
|
||||||
|
4. Expertise: `POST …/{Id}/expertise` (GEN.06)
|
||||||
|
|
||||||
|
Use hull `ContractId` (`hullContractId`, else tenant `contractId`). Nested URLs use GEN.03 **`Id`**, never `ClaimNo`.
|
||||||
|
|
||||||
|
A PUT to `/vehicle-hull-claims/{Id}` may run before files/expertise; Parsian often has **no PUT route**. That PUT is best-effort and must not block GEN.05/06.
|
||||||
|
|
||||||
|
**Send rule:** send the **hull whitelist only**. Do not send ثالث-only keys (`DamagedCount`, `HasOtherCulprit`, `CompensationReferenceId`, `DocReceivedDate`, `IsLicenseReplacement`, plaque block on base, …).
|
||||||
|
|
||||||
|
**Source tags:** same as `third-party-stage-fields.md` (`system` / `lookup` / `inquiry` / `user` / `staff` / `derived`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 1 — Hull base claim (GEN.03)
|
||||||
|
|
||||||
|
Built by `buildFanavaranSubmitPayload` then `enrichCarBodyHullBaseClaimPayload` then `toFanavaranHullBaseClaimPayload`.
|
||||||
|
|
||||||
|
**Fanavaran has rejected as required (must not be null):**
|
||||||
|
`CustomerFaultPercent`, `AuthorityCulpritId`, `AccidentTypeId`, `CostSeparationToDmgSections`, `IsOwnerChanged`, `PolicyId`, `ClaimExpertId`, dates/time, `CulpritTypeId`.
|
||||||
|
|
||||||
|
**Must be null:** `IsLicenseReplaced` (error if `0` / mapped from ثالث `IsLicenseReplacement`).
|
||||||
|
|
||||||
|
| Field | Persian | Type | Null? | Current value / source |
|
||||||
|
|-------|---------|------|-------|------------------------|
|
||||||
|
| PolicyId | شناسه بیمهنامه بدنه | number | **no** | **inquiry** first-party hull policy (`processed-body-policy` / `body-policy/{id}` / stored inquiry `policyId`) |
|
||||||
|
| ClaimExpertId | کارشناس مسئول پرونده | number | **no** | **staff** FileMaker `CarBodyClaimExpertId` on V4/V5, else tenant |
|
||||||
|
| AccidentDate | تاریخ حادثه | string Jalali | **no** | **user** first-party `statement.accidentDate`, else createdAt |
|
||||||
|
| AccidentTime | ساعت حادثه | string `HH:mm` | **no** | **user** `statement.accidentTime` if set |
|
||||||
|
| AnnouncementDate | تاریخ اعلام | string Jalali | **no** | **derived** = AccidentDate |
|
||||||
|
| AccidentLocationAddress | نشانی محل حادثه | string | **no** | **system** `"استان تهران شهر تهران"` |
|
||||||
|
| AccidentCauseId | علت حادثه | number | **no** | **system** `6` · lookup `accident-causes` |
|
||||||
|
| AccidentCityId | شهر حادثه | number | **no** | **system** `701` · lookup `cities` |
|
||||||
|
| AccidentReportTypeId | نحوه اعلام | number | **no** | **system** `155` · lookup `accident-report-type` |
|
||||||
|
| AccidentTypeId | نوع حادثه بدنه | number | **no** | **system** `2` تصادف(حادثه) · **lookup** `GET /lookups/vehicle-hull-accident-types` |
|
||||||
|
| CulpritTypeId | نوع مقصر بدنه | number | **no** | **system** `300` (ثالث `337` remapped) · **lookup** `GET /lookups/vehicle-hull-accident-culprit-type` |
|
||||||
|
| AuthorityCulpritId | مرجع تعیین مقصر | number | **no** | **lookup** `GET /lookups/detection-accident-culprits` (caption بیمهگذار, fallback `1`) |
|
||||||
|
| AccidentCulpritId | شناسه مقصر فناوران | number | preferred | **inquiry** parties `inquiry-by-unique-identifier`, fallback hull policy `CustomerId` |
|
||||||
|
| CustomerFaultPercent | درصد تقصیر بیمهگذار | number | **no** | **system** `100` |
|
||||||
|
| CostSeparationToDmgSections | تفکیک هزینه به نقاط | number (ans) | **no** | **system** `0` خیر · lookup `ans` |
|
||||||
|
| IsOwnerChanged | مالک تغییر یافته؟ | number (ans) | **no** | **system** `0` · lookup `ans` |
|
||||||
|
| IsLicenseReplaced | گواهینامه تعویضی است؟ | number | **must be null** | never copy ثالث `IsLicenseReplacement` |
|
||||||
|
| ActualPremium | حق بیمه واقعی | number | preferred | **inquiry** hull policy `TotalPremium` via `GET /lookups/body-policy/{PolicyId}` |
|
||||||
|
| EstimateAmount | برآورد | number | **no** | **user** expert prices / provisional |
|
||||||
|
| CulpritLicenceNo | شماره گواهینامه | string | **no** | dummy / **user** |
|
||||||
|
| CulpritLicenceIssuDate | تاریخ صدور گواهینامه | string Jalali | **no** | **system** `"1394/10/13"` |
|
||||||
|
| CulpritLicenceTypeId | نوع گواهینامه | number | sent | tenant `2` · lookup `driving-licence-types` |
|
||||||
|
| AccidentVehicleUsedId | کاربری خودرو | number | **no** | **inquiry** VIN/policy, else tenant |
|
||||||
|
| PoliceOfficerId | افسر | number | sent | **system** `1` |
|
||||||
|
| IsAccidentOutOfBorder | خارج از مرز | number | sent | **system** `0` |
|
||||||
|
| ArchiveNo | بایگانی | string | yes | null |
|
||||||
|
| EntryDate | تاریخ ورود | string | yes | null |
|
||||||
|
| CulpritLicenceForeignCityName | شهر خارجی گواهینامه | string | yes | null |
|
||||||
|
| PoliceReportSeri / Serial / Desc | کروکی | string | yes | null until **user** |
|
||||||
|
| CulpritLevelTwoLicenceIssuDate | صدور گواهینامه پایه ۲ | string | yes | null |
|
||||||
|
| TrackingCode | کد پیگیری | string | yes | null |
|
||||||
|
| CourtArchiveNo | بایگانی دادگاه | string | yes | null |
|
||||||
|
| PlaqueReplacementDate | تاریخ تعویض پلاک | string | yes | null |
|
||||||
|
| StatusChangeDate | تغییر وضعیت | string | yes | null |
|
||||||
|
| ClaimCompletionDate | تکمیل پرونده | string | yes | null |
|
||||||
|
| DmgAssessorFirstCreationTime | زمان ارزیاب | string | yes | null |
|
||||||
|
| IsSurplusArticleEighthLaw | مازاد ماده ۸ | number | yes | null |
|
||||||
|
| GlassBreakReasonId | علت شکست شیشه | number | yes | null |
|
||||||
|
| CulpritLicenceCountryId / CityId | محل صدور گواهینامه | number | yes | null |
|
||||||
|
|
||||||
|
Also persisted locally (not a GEN.03 field): `claimCase.vehicle.carPrice` from hull policy **`VehicleValue`**.
|
||||||
|
|
||||||
|
**Inquiries at this stage**
|
||||||
|
|
||||||
|
- Hull policy: `GET /lookups/body-policy/{PolicyId}` → `TotalPremium`, `CustomerId`, `VehicleValue`, `VehicleId`
|
||||||
|
- Parties: `GET /lookups/inquiry-by-unique-identifier`
|
||||||
|
- Optional VIN / vehicle used-id
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 2 — Damage case
|
||||||
|
|
||||||
|
**Skipped.** YARA returns `fanavaranSync.damageCase.status = skipped`. Do not POST `dmg-cases`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 3 — Attachments (GEN.05)
|
||||||
|
|
||||||
|
Multipart `Param` + `Param1`. Proven `FileTypeId=63`.
|
||||||
|
|
||||||
|
| Field | Persian | Type | Null? | Source |
|
||||||
|
|-------|---------|------|-------|--------|
|
||||||
|
| FileName | نام فایل | string | **no** | **user** (exact match to binary) |
|
||||||
|
| FileTypeId | نوع مدرک | number | **no** | **lookup** `file-types` / tenant `ClaimFileTypeId` |
|
||||||
|
| ClaimId | کد رایانه پرونده بدنه | number | **no** | stage 1 `Id` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 4 — Expertise (GEN.06)
|
||||||
|
|
||||||
|
Not ثالث GEN.08: no `DmgCaseId`, `InspectionPlaceId`, `RepairWage` on the root. Uses `Wage` + vehicle identity.
|
||||||
|
|
||||||
|
`ClaimExpertId` = FileReviewer `CarBodyExpertiseClaim` on V4/V5, else tenant `ExpertiseClaimExpertId`.
|
||||||
|
|
||||||
|
| Field | Persian | Type | Null? | Current source |
|
||||||
|
|-------|---------|------|-------|----------------|
|
||||||
|
| ClaimExpertId | کارشناس ارزیاب بدنه | number | **no** | **staff** / tenant |
|
||||||
|
| DmgAssessmentDate | تاریخ بازدید | string Jalali | **no** | **derived** expert `submittedAt` |
|
||||||
|
| InspectionTime | ساعت بازدید | string | **no** | **derived** |
|
||||||
|
| MotorNo | شماره موتور | string | preferred | **inquiry** hull vehicle / party mapped `MtrNum` |
|
||||||
|
| ChassisNo | شماره شاسی | string | preferred | **inquiry** `ShsNum` |
|
||||||
|
| BuiltYear | سال ساخت | number | preferred | **inquiry** `BuiltYear` / `PrdDte` |
|
||||||
|
| VIN | VIN | string | preferred | **inquiry** |
|
||||||
|
| PlaqueNo | پلاک رشتهای | string | preferred | **inquiry** plk1+letter+plk3 |
|
||||||
|
| PlaqueSerial | سریال پلاک (ایران) | string | preferred | **inquiry** |
|
||||||
|
| ColorId | رنگ | number | preferred | **inquiry** `GET` vehicle by policy `VehicleId` |
|
||||||
|
| Wage | اجرت کل | number | **no** | **user** sum `salary` |
|
||||||
|
| ComponentReplacementCost | قطعه کل | number | **no** | **user** sum `price` |
|
||||||
|
| WasteValue | داغی کل | number | **no** | **user** daghi |
|
||||||
|
| RepairDuration | مدت تعمیر (روز) | number | **no** | **system** `3` |
|
||||||
|
| VehicleCurrentValue | ارزش روز خودرو | number | preferred | **user** `vehicle.carPrice` / `vehicle.price`, else **inquiry** policy `VehicleValue` |
|
||||||
|
| DropAmount | افت قیمت | number | **no** | **user** price-drop total or `0` |
|
||||||
|
| CarryAndRescueCost | حمل و نجات | number | yes | null |
|
||||||
|
| WreckHighestValue | بالاترین ارزش لاشه | number | yes | null |
|
||||||
|
| InspectionDeduction | کسر بازدید | number | yes | null |
|
||||||
|
| DmgAndWasteDesc | شرح خسارت و داغی | string | yes | null |
|
||||||
|
| WentDistanceByExpert | مسافت | number | yes | null |
|
||||||
|
| IsDestruction | اسقاط؟ | number | yes | null |
|
||||||
|
| PlaqueDesignId | طرح پلاک | number | yes | null |
|
||||||
|
| PlaqueCityId | شهر پلاک | number | yes | null |
|
||||||
|
| AccidentPercent | درصد حادثه | number | yes | null |
|
||||||
|
| TheftCases | موارد سرقت | array | empty `[]` | system |
|
||||||
|
| DmgSections[].Count | تعداد | number | **no** | **system** `1` |
|
||||||
|
| DmgSections[].Desc | شرح قطعه | string | **no** | **user** part label |
|
||||||
|
| DmgSections[].WasteValue | داغی ردیف | number | **no** | **user** |
|
||||||
|
| DmgSections[].AccessoryKindId | نوع قطعه کاتالوگ | number | preferred | **user** `partId` · lookup `car-components` |
|
||||||
|
| DmgSections[].DmgKindId | نوع آسیب بدنه | number | **no** | **lookup** `vehicle-hull-dmg-kind` caption تخریب, fallback `5485` |
|
||||||
|
| DmgSections[].VehicleHullAccessoryId | شناسه لوازم بدنه | number | **no** | **system** `3043330` (فابریک) for **every** row |
|
||||||
|
| DmgSections[].DmgSectionCosts[].Caption | عنوان هزینه | string | **no** | **user** label |
|
||||||
|
| DmgSections[].DmgSectionCosts[].Amount | مبلغ (قطعه+اجرت) | number | **no** | **user** |
|
||||||
|
| DmgSections[].DmgSectionCosts[].DmgCostKindId | نوع هزینه | number | **no** | **lookup** `vehicle-hull-dmg-cost-kinds` caption مجموع لوازم, fallback `1` |
|
||||||
|
|
||||||
|
**Lookups used:** `vehicle-hull-dmg-kind`, `vehicle-hull-dmg-cost-kinds`, `detection-accident-culprits`, `vehicle-hull-accident-types`, `vehicle-hull-accident-culprit-type`, `ans`, optional `vehicle-hull-policies/{id}/dmg-accessories` (no longer used for AccessoryId).
|
||||||
|
|
||||||
|
**Inquiries:** `body-policy/{PolicyId}`, `vehicleById` (color + identity), parties inquiry, VIN/plate inquiry on blame.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## YARA preview / submit
|
||||||
|
|
||||||
|
Same routes as ثالث (`/base-claim`, `/attachments`, `/expertise`).
|
||||||
|
`/damage-case` preview/submit **skips** for `CAR_BODY`.
|
||||||
201
docs/fanavaran/flow-design-user-vs-inquiry.md
Normal file
201
docs/fanavaran/flow-design-user-vs-inquiry.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# Fanavaran intake design — user vs inquiry (proposal)
|
||||||
|
|
||||||
|
This file is a **proposed product design**, not a description of every current default. Use it to decide which YARA screens should collect data and which calls should stay automatic.
|
||||||
|
|
||||||
|
Two intake channels only:
|
||||||
|
|
||||||
|
1. **Inquiries & lookups** (Fanavaran / ESG / SandHub — no extra typing when they succeed)
|
||||||
|
2. **User input** (owner, expert, file-maker, file-reviewer)
|
||||||
|
|
||||||
|
Persian titles match Fanavaran field captions where known. Types are what we send on the wire.
|
||||||
|
|
||||||
|
Related: [third-party-stage-fields.md](./third-party-stage-fields.md), [car-body-stage-fields.md](./car-body-stage-fields.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shared identity (both products)
|
||||||
|
|
||||||
|
Collect once on the blame file. Inquiries reuse these keys.
|
||||||
|
|
||||||
|
| Persian | Field / storage | Type | Channel | Why |
|
||||||
|
|---------|-----------------|------|---------|-----|
|
||||||
|
| کد ملی بیمهگذار | `nationalCodeOfInsurer` | string(10) | **user** | Policy inquiry key |
|
||||||
|
| تاریخ تولد (جلالی) | `birthday` | string `yyyy/mm/dd` or `yyyyMMdd` | **user** | Parties inquiry + GEN.44 |
|
||||||
|
| کد ملی راننده | `nationalCodeOfDriver` | string(10) | **user** if `driverIsInsurer=false` | Parties inquiry |
|
||||||
|
| راننده همان بیمهگذار است؟ | `driverIsInsurer` | boolean | **user** | Chooses which national code / GEN.44 |
|
||||||
|
| شماره موبایل | phone | string | **user** | OTP / SMS, not Fanavaran |
|
||||||
|
| VIN / شماره شاسی | vin, chassis | string | **inquiry first**, **user** if miss | VIN inquiry, vehicle GET |
|
||||||
|
| شماره موتور | motorNo | string | **inquiry**, else **user** | GEN.06 / GEN.12 |
|
||||||
|
| پلاک (۲ رقم، حرف، ۳ رقم، ایران) | plate parts | string/number | **user** (photo/OCR later) then **inquiry** confirm | Plaque + policy match |
|
||||||
|
| سال ساخت | builtYear | number | **inquiry**, else **user** | GEN.12 / GEN.06 |
|
||||||
|
| نوع بدنه خودرو (سواری/شاسیبلند/…) | `vehicle.carType` | enum string | **user** | Fallback `VehicleKindId` lookup |
|
||||||
|
|
||||||
|
**Auto after identity**
|
||||||
|
|
||||||
|
| Persian | Fanavaran field | Type | Call |
|
||||||
|
|---------|-----------------|------|------|
|
||||||
|
| شناسه بیمهنامه ثالث | `PolicyId` (ثالث) | number | `my-policies` / `processed-third-party-policy` |
|
||||||
|
| شناسه بیمهنامه بدنه | `PolicyId` (بدنه) | number | `processed-body-policy` / `body-policy/{id}` |
|
||||||
|
| شناسه شخص فناوران | `DriverId` / `AccidentCulpritId` | number | `inquiry-by-unique-identifier` → else GEN.44 |
|
||||||
|
| شناسه مشتری بیمهنامه | `CustomerId` | number | hull policy GET |
|
||||||
|
| حق بیمه | `ActualPremium` | number | hull `TotalPremium` |
|
||||||
|
| ارزش روز خودرو (بیمهنامه) | `VehicleCurrentValue` seed | number | hull `VehicleValue` → `vehicle.carPrice` |
|
||||||
|
| رنگ | `ColorId` | number | vehicle GET |
|
||||||
|
| نوع کاربری | `AccidentVehicleUsedId` | number | vehicle `UsedId` + `vehicle-use-types` |
|
||||||
|
| نوع خودرو فناوران | `VehicleKindId` | number | VIN `VehicleKindId` or `vehicle-kinds` |
|
||||||
|
| مشخصات پلاک فناوران | PlaqueKindId / SampleId / … | number/string | VIN / vehicle GET |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accident facts (both) — mostly user
|
||||||
|
|
||||||
|
| Persian | Field | Type | Channel |
|
||||||
|
|---------|-------|------|---------|
|
||||||
|
| تاریخ حادثه | AccidentDate | Jalali date | **user** |
|
||||||
|
| ساعت حادثه | AccidentTime | `HH:mm` | **user** |
|
||||||
|
| نشانی محل حادثه | AccidentLocationAddress | string | **user** (today hardcoded تهران) |
|
||||||
|
| شهر محل حادثه | AccidentCityId | number | **user** pick from `cities` (today `701`) |
|
||||||
|
| علت حادثه | AccidentCauseId | number | **user** pick from `accident-causes` (today `6`) |
|
||||||
|
| نحوه اعلام حادثه | AccidentReportTypeId | number | **user** or default from `accident-report-type` |
|
||||||
|
| کروکی — سری / سریال / شرح | PoliceReport* | string | **user** optional; null ok |
|
||||||
|
| خارج از مرز؟ | IsAccidentOutOfBorder | ans number | **user** default خیر |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Third-party only — proposed screens
|
||||||
|
|
||||||
|
### File create (مقصر + زیاندیده)
|
||||||
|
|
||||||
|
| Persian | Field | Type | Channel |
|
||||||
|
|---------|-------|------|---------|
|
||||||
|
| نقش طرفین | FIRST / SECOND | enum | **user** |
|
||||||
|
| مقصر کیست | guiltyPartyId | id | **user** expert decision |
|
||||||
|
| نوع مقصر | CulpritTypeId | number | **user** or default `accident-culprit-type` |
|
||||||
|
| شماره گواهینامه مقصر | CulpritLicenceNo | string | **user** (stop dummy when captured) |
|
||||||
|
| نوع گواهینامه مقصر | CulpritLicenceTypeId | number | **user** from `driving-licence-types` |
|
||||||
|
| تاریخ صدور گواهینامه | CulpritLicenceIssuDate | Jalali date | **user** |
|
||||||
|
| گواهینامه تعویضی؟ | IsLicenseReplacement | ans | **user** default خیر |
|
||||||
|
| قطعات آسیبدیده زیاندیده | selectedParts | catalog ids | **user** · `car-components` |
|
||||||
|
| نوع خودرو زیاندیده | carType | enum | **user** |
|
||||||
|
|
||||||
|
### Expert pricing (ثالث)
|
||||||
|
|
||||||
|
| Persian | Field | Type | Channel |
|
||||||
|
|---------|-------|------|---------|
|
||||||
|
| قیمت قطعه | parts[].price | money | **user** |
|
||||||
|
| اجرت | parts[].salary | money | **user** |
|
||||||
|
| داغی | parts[].daghi | money/object | **user** |
|
||||||
|
| شدت آسیب | AccidentLevel / typeOfDamage | number | **user** · `accident-level` |
|
||||||
|
| افت قیمت — دارد/ندارد + مبلغ | DropAmount* | number | **user** · `drop-amount-status` |
|
||||||
|
| محل بازدید | InspectionPlaceId | number | **user** or branch default · `inspection-place` |
|
||||||
|
| قیمت روز خودرو (اختیاری ثالث) | DamagedVehicleCurrentPrice | money | **user** |
|
||||||
|
|
||||||
|
### Staff (V4/V5)
|
||||||
|
|
||||||
|
| Persian | Field | Type | Channel |
|
||||||
|
|---------|-------|------|---------|
|
||||||
|
| کد کارشناس پرونده ثالث | FileMaker.ThirdPartyClaimExpertId | string/number | **staff admin** |
|
||||||
|
| کد ارزیاب ثالث | FileReviewer.ThirdPartyExpertiseClaim | string/number | **staff admin** |
|
||||||
|
|
||||||
|
Do not ask the owner for these.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Car-body only — proposed screens
|
||||||
|
|
||||||
|
Hull is **first party**. One vehicle, one policy.
|
||||||
|
|
||||||
|
### File create
|
||||||
|
|
||||||
|
| Persian | Field | Type | Channel |
|
||||||
|
|---------|-------|------|---------|
|
||||||
|
| نوع حادثه بدنه | AccidentTypeId | number | **user** from `vehicle-hull-accident-types` (default تصادف = 2) |
|
||||||
|
| نوع مقصر بدنه | CulpritTypeId | number | **user** from `vehicle-hull-accident-culprit-type` (default بیمهگذار = 300) |
|
||||||
|
| مرجع تعیین مقصر | AuthorityCulpritId | number | **user** from `detection-accident-culprits` (default بیمهگذار) |
|
||||||
|
| درصد تقصیر بیمهگذار | CustomerFaultPercent | number 0–100 | **user** if not 100%; today always 100 |
|
||||||
|
| تفکیک هزینه به نقاط | CostSeparationToDmgSections | ans | **user** default خیر |
|
||||||
|
| مالک تغییر یافته؟ | IsOwnerChanged | ans | **user** default خیر |
|
||||||
|
| گواهینامه تعویضی؟ | IsLicenseReplaced | ans or empty | **must stay empty/null** unless Fanavaran later requires a value |
|
||||||
|
| قطعات آسیبدیده | selectedParts | catalog ids | **user** |
|
||||||
|
|
||||||
|
### Expert pricing (بدنه)
|
||||||
|
|
||||||
|
| Persian | Field | Type | Channel |
|
||||||
|
|---------|-------|------|---------|
|
||||||
|
| قیمت قطعه / اجرت / داغی | price, salary, daghi | money | **user** |
|
||||||
|
| قیمت روز خودرو | vehicle.carPrice | money | **user** required; seed from policy `VehicleValue` |
|
||||||
|
| افت قیمت | DropAmount / priceDrop.total | money | **user** |
|
||||||
|
| مدت تعمیر (روز) | RepairDuration | number | **system** `3` unless product wants **user** |
|
||||||
|
| نوع آسیب ردیف | DmgKindId | number | default تخریب; optional **user** · `vehicle-hull-dmg-kind` |
|
||||||
|
| نوع هزینه ردیف | DmgCostKindId | number | default مجموع لوازم · `vehicle-hull-dmg-cost-kinds` |
|
||||||
|
| لوازم فابریک | VehicleHullAccessoryId | number | **system** `3043330` (do not ask user) |
|
||||||
|
|
||||||
|
### Staff (V4/V5)
|
||||||
|
|
||||||
|
| Persian | Field | Type | Channel |
|
||||||
|
|---------|-------|------|---------|
|
||||||
|
| کد کارشناس پرونده بدنه | FileMaker.CarBodyClaimExpertId | string/number | **staff admin** |
|
||||||
|
| کد ارزیاب بدنه | FileReviewer.CarBodyExpertiseClaim | string/number | **staff admin** |
|
||||||
|
|
||||||
|
If the matching code is missing, YARA must not create/review that product line.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documents (both) — user files
|
||||||
|
|
||||||
|
| Persian | Typical key | Channel |
|
||||||
|
|---------|-------------|---------|
|
||||||
|
| کارت ملی / گواهینامه / کارت خودرو / پلاک فلزی | requiredDocuments | **user** photos |
|
||||||
|
| عکس زاویه خودرو / قطعه | damage + car-angle captures | **user** |
|
||||||
|
| فاکتور تعمیر | factor images | **user** when `factorNeeded` |
|
||||||
|
|
||||||
|
`FileTypeId` is **lookup** (`file-types`), not typed by the owner.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lookups catalogue (for UI dropdowns)
|
||||||
|
|
||||||
|
Expose these as select lists; store the numeric `Id`.
|
||||||
|
|
||||||
|
| Persian UI | Lookup route | Used on |
|
||||||
|
|------------|--------------|---------|
|
||||||
|
| علت حادثه | `/lookups/accident-causes` | GEN.03 |
|
||||||
|
| نوع اعلام حادثه | `/lookups/accident-report-type` | GEN.03 |
|
||||||
|
| شهر | `/lookups/cities` | GEN.03 city |
|
||||||
|
| بله/خیر | `/lookups/ans` | hull flags |
|
||||||
|
| نوع مقصر ثالث | `/lookups/accident-culprit-type` | ثالث GEN.03 |
|
||||||
|
| نوع مقصر بدنه | `/lookups/vehicle-hull-accident-culprit-type` | بدنه GEN.03 |
|
||||||
|
| نوع حادثه بدنه | `/lookups/vehicle-hull-accident-types` | بدنه GEN.03 |
|
||||||
|
| مرجع تعیین مقصر | `/lookups/detection-accident-culprits` | بدنه GEN.03 |
|
||||||
|
| نوع گواهینامه | `/lookups/driving-licence-types` | GEN.03 / GEN.12 |
|
||||||
|
| کاربری خودرو | `/lookups/vehicle-use-types` | GEN.03 / GEN.12 |
|
||||||
|
| نوع خودرو | `/lookups/vehicle-kinds` | GEN.12 |
|
||||||
|
| قطعات | `/lookups/car-components` | parts / DmgSectionId |
|
||||||
|
| شدت آسیب | `/lookups/accident-level` | ثالث expertise |
|
||||||
|
| محل بازدید | `/lookups/inspection-place` | ثالث expertise |
|
||||||
|
| وضعیت افت | `/lookups/drop-amount-status` | ثالث expertise |
|
||||||
|
| نوع آسیب بدنه | `/lookups/vehicle-hull-dmg-kind` | بدنه expertise |
|
||||||
|
| نوع هزینه بدنه | `/lookups/vehicle-hull-dmg-cost-kinds` | بدنه expertise |
|
||||||
|
| نوع فایل | `/lookups/file-types` | attachments |
|
||||||
|
| جنسیت / تأهل / تابعیت / نوع شخص | gender, marital-status, countries, person-kind, ans | GEN.44 only if inquiry misses |
|
||||||
|
|
||||||
|
Inquiry helpers (not dropdowns): `inquiry-by-vin`, `inquiry-by-unique-identifier`, `my-policies`, `processed-third-party-policy`, `processed-body-policy`, `third-party-policy/:id`, `body-policy/:id`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suggested flow (what to collect, in order)
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. User: phone + national code + birthday + plate (and VIN if known)
|
||||||
|
2. Inquiry: policy + vehicle + person Id
|
||||||
|
3. If person missing → GEN.44 using user national code + birthday (submit time)
|
||||||
|
4. User: accident date/time/address + (بدنه) accident type / culprit authority
|
||||||
|
5. User: damaged parts
|
||||||
|
6. Expert: prices, daghi, carPrice (بدنه)
|
||||||
|
7. System: ClaimExpertId / expertise ClaimExpertId from staff profiles
|
||||||
|
8. System: POST stages in order (ثالث 1→2→3→4, بدنه 1→3→4)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Do not ask the user for:** Fanavaran `PolicyId`, `DriverId`, `CustomerId`, `ColorId`, `VehicleKindId`, `VehicleHullAccessoryId` (فابریک), `ActualPremium`, `FileTypeId`, expert numeric ids.
|
||||||
|
|
||||||
|
**Stop using dummies when the screen exists:** `CulpritLicenceNo`, `CulpritLicenceIssuDate`, `AccidentLocationAddress`, `AccidentCityId`, `AccidentCauseId`. Until then, current defaults keep Fanavaran from rejecting the create.
|
||||||
199
docs/fanavaran/third-party-stage-fields.md
Normal file
199
docs/fanavaran/third-party-stage-fields.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# Third-party (ثالث) — Fanavaran stage bodies (current YARA)
|
||||||
|
|
||||||
|
Snapshot of **what `ClaimRequestManagementService` actually sends** for `THIRD_PARTY` as of this version.
|
||||||
|
|
||||||
|
- Base: `POST /car/third-party-car-financial-claims` (GEN.03)
|
||||||
|
- Damage case: `POST …/{claimId}/dmg-cases` (GEN.12)
|
||||||
|
- Attachments: `POST …/{claimId}/files` (GEN.07, multipart)
|
||||||
|
- Expertise: `POST …/{claimId}/expertise` (GEN.08)
|
||||||
|
|
||||||
|
**Send rule:** keep keys in the JSON even when the value is `null`. Fanavaran treats omitted keys differently from explicit `null`.
|
||||||
|
|
||||||
|
**Source columns**
|
||||||
|
|
||||||
|
| Tag | Meaning |
|
||||||
|
|-----|---------|
|
||||||
|
| **system** | Hardcoded YARA default / tenant `fanavaranClientConfigs` |
|
||||||
|
| **lookup** | Fanavaran codebook (`GET /lookups/…`) |
|
||||||
|
| **inquiry** | Live Fanavaran inquiry (policies, VIN, parties, vehicle GET) |
|
||||||
|
| **user** | Collected in YARA UI (owner, expert, file-maker) |
|
||||||
|
| **staff** | FileMaker `ThirdPartyClaimExpertId` / FileReviewer `ThirdPartyExpertiseClaim` on V4/V5; else tenant defaults |
|
||||||
|
| **derived** | Computed from other fields (dates, sums) |
|
||||||
|
|
||||||
|
Parsian tenant defaults used below: `ClaimExpertId=154`, `ExpertiseClaimExpertId=29`, `ClaimFileTypeId=63`, `AccidentCityId=701`, `AccidentReportTypeId=155`, `CulpritTypeId=337`, `CulpritLicenceTypeId=2`, `CompensationReferenceId=167`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 1 — Base claim (GEN.03)
|
||||||
|
|
||||||
|
**Must send (non-null):** `PolicyId`, `ClaimExpertId`, `AccidentDate`, `AnnouncementDate`, `AccidentTime`, `AccidentCauseId`, `AccidentCityId`, `AccidentReportTypeId`, `CulpritTypeId`, `CulpritLicenceTypeId`, `CulpritLicenceNo`, `CulpritLicenceIssuDate`, `DamagedCount`, `EstimateAmount` (> 0), `AccidentLocationAddress`, `HasOtherCulprit`, `IsFatalAccident`, `IsAccidentOutOfBorder`, `IsPlaqueChanged`, `IsLicenseMatchWithVehicleKind`, `IsLicenseReplacement`, `PoliceOfficerId`, `AccidentVehicleUsedId`.
|
||||||
|
|
||||||
|
**May be null** (and currently are, unless noted): everything else in the table.
|
||||||
|
|
||||||
|
| Field | Persian | Type | Null? | Current source | Notes |
|
||||||
|
|-------|---------|------|-------|----------------|-------|
|
||||||
|
| PolicyId | شناسه بیمهنامه مقصر | number | **no** | **inquiry** `GET /lookups/my-policies` (guilty national code, line ثالث) | Cached on `fanavaranSync.baseClaim.policyId` |
|
||||||
|
| ClaimExpertId | کارشناس مسئول پرونده | number | **no** | **staff** FileMaker `ThirdPartyClaimExpertId` on V4/V5, else tenant | GEN.03 role, not expertise |
|
||||||
|
| AccidentDate | تاریخ حادثه | string (Jalali `yyyy/mm/dd`) | **no** | **user** accident datetime, else blame `createdAt` | |
|
||||||
|
| AnnouncementDate | تاریخ اعلام | string Jalali | **no** | **derived** = AccidentDate | |
|
||||||
|
| DocReceivedDate | تاریخ وصول مدارک | string Jalali | **no** | **derived** = AccidentDate | Third-party only (not on hull whitelist) |
|
||||||
|
| AccidentTime | ساعت حادثه | string `HH:mm` | **no** | **user** / **derived** from createdAt | |
|
||||||
|
| AccidentLocationAddress | نشانی محل حادثه | string | **no** | **system** `"استان تهران شهر تهران"` | Should later be **user** |
|
||||||
|
| AccidentCauseId | علت حادثه | number | **no** | **system** `6` | Lookup: `GET /lookups/accident-causes`. Expert `accidentReason.fanavaran` exists but is not yet wired as the sent id |
|
||||||
|
| AccidentCityId | شهر محل حادثه | number | **no** | **system** `701` تهران | Lookup: `GET /lookups/cities` |
|
||||||
|
| AccidentReportTypeId | نحوه اعلام حادثه | number | **no** | **system** `155` | Lookup: `GET /lookups/accident-report-type` |
|
||||||
|
| AccidentVehicleUsedId | نوع کاربری خودرو | number | **no** | **inquiry** VIN / policy vehicle `UsedId`, else tenant `1` | Lookup: `GET /lookups/vehicle-use-types` |
|
||||||
|
| CulpritTypeId | نوع مقصر | number | **no** | **system** `337` | Lookup: `GET /lookups/accident-culprit-type` (ثالث, not hull) |
|
||||||
|
| CulpritLicenceTypeId | نوع گواهینامه مقصر | number | **no** | **system** `2` | Lookup: `GET /lookups/driving-licence-types` |
|
||||||
|
| CulpritLicenceNo | شماره گواهینامه مقصر | string | **no** | **user** party licence if stored, else dummy `9705463515` | Must never be empty |
|
||||||
|
| CulpritLicenceIssuDate | تاریخ صدور گواهینامه | string Jalali | **no** | **system** `"1394/10/13"` | Should later be **user** |
|
||||||
|
| EstimateAmount | مبلغ برآورد | number | **no** | **user** expert part prices, else provisional `1000` | Must be > 0 |
|
||||||
|
| CompensationReferenceId | مرجع جبران | number | **no** | **system** `167` | Tenant default |
|
||||||
|
| DamagedCount | تعداد زیاندیدگان | number | **no** | **system** `1` | |
|
||||||
|
| HasOtherCulprit | مقصر دیگر دارد؟ | number (ans) | **no** | **system** `0` | Lookup `GET /lookups/ans` |
|
||||||
|
| IsFatalAccident | فوت دارد؟ | number | **no** | **system** `0` | |
|
||||||
|
| IsAccidentOutOfBorder | خارج از مرز؟ | number | **no** | **system** `0` | |
|
||||||
|
| IsPlaqueChanged | پلاک تعویضی؟ | number | **no** | **system** `0` | |
|
||||||
|
| IsLicenseMatchWithVehicleKind | گواهینامه متناسب؟ | number | **no** | **system** `1` | |
|
||||||
|
| IsLicenseReplacement | گواهینامه تعویضی؟ | number | **no** | **system** `0` | Third-party uses this name (hull uses `IsLicenseReplaced` and must be null) |
|
||||||
|
| PoliceOfficerId | افسر پلیس | number | **no** | **system** `1` | |
|
||||||
|
| ActualPremium | حق بیمه واقعی | number | yes | unused on ثالث create | |
|
||||||
|
| ArchiveNo | شماره بایگانی | string | yes | — | |
|
||||||
|
| AuthorityCulpritId | مرجع تعیین مقصر | number | yes | **null** on ثالث | Hull requires this |
|
||||||
|
| AccidentCulpritId | شناسه مقصر در فناوران | number | yes | **null** on ثالث | Hull fills from parties inquiry |
|
||||||
|
| ClaimCompletionDate | تاریخ تکمیل | string | yes | — | |
|
||||||
|
| CostSeparationToDmgSections | تفکیک هزینه به نقاط | number | yes | **null** on ثالث | Hull sends `0` |
|
||||||
|
| CouponNo | شماره کوپن | string | yes | — | |
|
||||||
|
| CourtArchiveNo | شماره بایگانی دادگاه | string | yes | — | |
|
||||||
|
| CustomerFaultPercent | درصد تقصیر بیمهگذار | number | yes | **null** on ثالث | Hull sends `100` |
|
||||||
|
| CulpritLicenceCityId | شهر صدور گواهینامه | number | yes | Lookup `cities` | |
|
||||||
|
| CulpritLicenceCountryId | کشور صدور گواهینامه | number | yes | Lookup `countries` | |
|
||||||
|
| CulpritLicenceForeignCityName | شهر خارجی صدور | string | yes | — | |
|
||||||
|
| DmgAssessorFirstCreationTime | زمان ایجاد ارزیاب | string | yes | — | |
|
||||||
|
| EntryDate | تاریخ ورود | string | yes | — | |
|
||||||
|
| GlassBreakReasonId | علت شکست شیشه | number | yes | — | |
|
||||||
|
| IsOwnerChanged | مالک تغییر کرده؟ | number | yes | **null** | Hull sends `0` |
|
||||||
|
| IsSurplusArticleEighthLaw | مازاد ماده ۸ | number | yes | — | |
|
||||||
|
| PlaqueCityId … PlaqueSerial | پلاک (بلوک پایه) | mixed | yes | **null** on GEN.03 ثالث | Plaque belongs on GEN.12 |
|
||||||
|
| PoliceReportDesc / Seri / Serial | مشخصات کروکی | string | yes | **user** later | |
|
||||||
|
| PreviousPolicyEndDate | پایان بیمهنامه قبلی | string | empty `""` | **system** | |
|
||||||
|
| StatusChangeDate | تاریخ تغییر وضعیت | string | yes | — | |
|
||||||
|
| TrackingCode | کد پیگیری | string | yes | — | |
|
||||||
|
| UnknownCulpritCauseId | علت مقصر ناشناس | number | yes | — | |
|
||||||
|
|
||||||
|
**Inquiries used at this stage**
|
||||||
|
|
||||||
|
- `GET /lookups/my-policies?nationalCode=&insuranceLineId=` (ثالث)
|
||||||
|
- Optional VIN → `AccidentVehicleUsedId`
|
||||||
|
|
||||||
|
**User input already needed (even if some are still defaulted)**
|
||||||
|
|
||||||
|
- Guilty party national code (to find PolicyId)
|
||||||
|
- Accident date/time
|
||||||
|
- Expert priced parts (EstimateAmount) when available
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 2 — Damage case (GEN.12)
|
||||||
|
|
||||||
|
Nested under Fanavaran `claimId` (`Id` from stage 1, never `ClaimNo`).
|
||||||
|
|
||||||
|
**Must send (non-null in proven Parsian):** `DriverId`, `VehicleKindId`, `AccidentVehicleUsedId`, `DmgCaseTypeId`, `DmgHistoryStatus`, `FaultPercent`, `InsuranceCorpId`, `LicenceNo`, `LicenceTypeId`, `LicenceIssuDate`, `EstimateAmount`, `Desc`, plaque block (`PlaqueKindId`, `PlaqueSampleId`, left/middle/right/serial), `BeginDate`/`EndDate` when inquiry has them.
|
||||||
|
|
||||||
|
If `DriverId` is missing, **submit** (not preview) registers GEN.44 `POST /common/other-people` and stores `otherPersonId`.
|
||||||
|
|
||||||
|
| Field | Persian | Type | Null? | Current source |
|
||||||
|
|-------|---------|------|-------|----------------|
|
||||||
|
| DriverId | شناسه راننده زیاندیده در فناوران | number | **no** on submit | **inquiry** `GET /lookups/inquiry-by-unique-identifier` (national code + Jalali birthday). Fallback **GEN.44** create |
|
||||||
|
| OwnerId | شناسه مالک | number | yes | currently `null` |
|
||||||
|
| VehicleKindId | نوع خودرو | number | **no** | **inquiry** VIN vehicle `VehicleKindId`, else **lookup** `GET /lookups/vehicle-kinds` matched to `claim.vehicle.carType` |
|
||||||
|
| AccidentVehicleUsedId | کاربری | number | **no** | **inquiry** vehicle `UsedId` / use-types, else tenant |
|
||||||
|
| VIN | شماره شاسی ۱۷ رقمی | string | preferred | **inquiry** plate/VIN inquiry |
|
||||||
|
| ChassisNo | شماره شاسی | string | preferred | **inquiry** |
|
||||||
|
| MotorNo | شماره موتور | string | preferred | **inquiry** |
|
||||||
|
| BuiltYear | سال ساخت | number | preferred | **inquiry** VIN / mapped `PrdDte` |
|
||||||
|
| PlaqueKindId | نوع پلاک | number | **no** | **inquiry** vehicle GET, else national-plate default |
|
||||||
|
| PlaqueSampleId | نمونه پلاک | number | **no** | same |
|
||||||
|
| PlaqueLeftNo | دو رقم سمت چپ | string | **no** | **user** plate / **inquiry** |
|
||||||
|
| PlaqueMiddleCodeId | حرف پلاک (کد فناوران) | number | **no** | **derived** from plate letter |
|
||||||
|
| PlaqueRightNo | سه رقم | string | **no** | **user** / **inquiry** |
|
||||||
|
| PlaqueSerial | ایران | string | **no** | **user** / **inquiry** |
|
||||||
|
| PlaqueNo | پلاک رشتهای | string | preferred | **derived** |
|
||||||
|
| PlaqueCityId | شهر پلاک | number | yes | **inquiry** vehicle |
|
||||||
|
| PolicyNo | شماره چاپی بیمهنامه زیاندیده | string | preferred | **inquiry** ESG/Tejarat mapped |
|
||||||
|
| PolicyCINumber | کد یکتای بیمهنامه | string | preferred | **inquiry** `PlcyUnqCod` |
|
||||||
|
| BeginDate | شروع پوشش | string Jalali | preferred | **inquiry** |
|
||||||
|
| EndDate | پایان پوشش | string Jalali | preferred | **inquiry** |
|
||||||
|
| PreviousPolicyEndDate | پایان بیمهنامه قبلی | string | empty ok | **derived** from EndDate |
|
||||||
|
| InsuranceCorpId | شرکت بیمه زیاندیده | number | **no** | **lookup** insurance-corp for tenant |
|
||||||
|
| DmgCaseTypeId | نوع مورد خسارت | number | **no** | **system** `175` · lookup `dmg-case-type` |
|
||||||
|
| DmgHistoryStatus | وضعیت سابقه خسارت | number | **no** | **system** `5214` · lookup `dmg-history-status` |
|
||||||
|
| FaultPercent | درصد تقصیر | number | **no** | **system** `100` |
|
||||||
|
| DriverIsOwner | راننده همان مالک؟ | number | **no** | **user** `driverIsInsurer` → 1/0 |
|
||||||
|
| LicenceNo | شماره گواهینامه زیاندیده | string | **no** | **user**, else dummy |
|
||||||
|
| LicenceTypeId | نوع گواهینامه | number | **no** | **system** same as CulpritLicenceTypeId |
|
||||||
|
| LicenceIssuDate | تاریخ صدور | string Jalali | **no** | **user** birthday used as stand-in, else dummy date |
|
||||||
|
| LicenceCityId / CountryId / ForeignCityName | محل صدور گواهینامه | mixed | yes | currently null |
|
||||||
|
| EstimateAmount | برآورد مورد خسارت | number | **no** | **system** provisional constant |
|
||||||
|
| Desc | شرح قطعات آسیبدیده | string | **no** | **user** selected outer parts labels joined with `/` |
|
||||||
|
| DmgCaseId | (response) | number | — | store as local `dmgCaseId` |
|
||||||
|
|
||||||
|
**Lookups:** `vehicle-kinds`, `vehicle-use-types`, `dmg-case-type`, `dmg-history-status`, `insurance-corp`, `file-types` (later), `driving-licence-types`, `ans`.
|
||||||
|
|
||||||
|
**Inquiries:** parties by national code + birthday; VIN inquiry; optional vehicle GET; plate inquiry on blame.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 3 — Attachments (GEN.07)
|
||||||
|
|
||||||
|
Multipart: field `Param` = JSON, field `Param1` = file bytes. One file per request. Parsian `FileTypeId=63` (سایر مدارک خسارت).
|
||||||
|
|
||||||
|
| Field | Persian | Type | Null? | Current source |
|
||||||
|
|-------|---------|------|-------|----------------|
|
||||||
|
| FileName | نام فایل | string | **no** | **user** upload name (must match binary name) |
|
||||||
|
| FileTypeId | نوع مدرک | number | **no** | **lookup** `GET /lookups/file-types` via tenant `ClaimFileTypeId` |
|
||||||
|
| Files[].FileName / FileTypeId | تکرار متادیتا | object | **no** | same |
|
||||||
|
| ClaimId | (hull only on Param) | number | hull yes | third-party URL already has claimId |
|
||||||
|
|
||||||
|
Files themselves are **user** (documents, damage photos, factors).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 4 — Expertise (GEN.08)
|
||||||
|
|
||||||
|
Requires local `dmgCaseId`. `ClaimExpertId` here is the **assessor** role (Parsian `29`), from FileReviewer `ThirdPartyExpertiseClaim` on V4/V5.
|
||||||
|
|
||||||
|
| Field | Persian | Type | Null? | Current source |
|
||||||
|
|-------|---------|------|-------|----------------|
|
||||||
|
| DmgCaseId | شناسه مورد خسارت | number | **no** | stage 2 response |
|
||||||
|
| ClaimExpertId | کارشناس ارزیاب | number | **no** | **staff** / tenant `ExpertiseClaimExpertId` |
|
||||||
|
| DmgAssessmentDate | تاریخ بازدید | string Jalali | **no** | **derived** expert reply `submittedAt` |
|
||||||
|
| InspectionTime | ساعت بازدید | string `HH:mm` | **no** | **derived** |
|
||||||
|
| InspectionPlaceId | محل بازدید | number | **no** | **system** `282` · lookup `GET /lookups/inspection-place` |
|
||||||
|
| RepairWage | اجرت کل | number | **no** | **user** sum of part `salary` |
|
||||||
|
| ComponentReplacementCost | قطعه کل | number | **no** | **user** sum of part `price` |
|
||||||
|
| WasteValue | داغی کل | number | **no** | **user** daghi prices |
|
||||||
|
| DropAmountStatus | وضعیت افت | number | **no** | **system** `5458` · lookup `drop-amount-status` |
|
||||||
|
| DropAmountAdditionsDeductions | مبلغ افت | number | **no** | **user** `evaluation.priceDrop.total` or `0` |
|
||||||
|
| ComponentReplacementTaxAndToll | مالیات قطعه | number | **no** | **system** `0` |
|
||||||
|
| DamagedVehicleCurrentPrice | قیمت روز خودرو | number | yes | **user** price-drop `carPrice` |
|
||||||
|
| WentDistanceByExpert | مسافت کارشناس | number | yes | currently `null` |
|
||||||
|
| DmgSections[].DmgSectionId | شناسه نقطه آسیب | number | **no** | **user** catalog `partId` · lookup `car-components` |
|
||||||
|
| DmgSections[].AccidentLevel | شدت آسیب | number | **no** | **user** `typeOfDamage` or default `5456` · lookup `accident-level` |
|
||||||
|
| DmgSections[].Desc | شرح | string | **no** | **user** |
|
||||||
|
| DmgSections[].ComponentReplacementCost | قیمت قطعه | number | **no** | **user** |
|
||||||
|
| DmgSections[].RepairWage | اجرت | number | **no** | **user** |
|
||||||
|
| DmgSections[].WasteValue | داغی | number | **no** | **user** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## YARA preview / submit
|
||||||
|
|
||||||
|
| Stage | Preview | Submit |
|
||||||
|
|-------|---------|--------|
|
||||||
|
| 1 | `GET /v2/fanavaran/{client}/claim-cases/{id}/base-claim/preview` | `POST …/base-claim/submit` |
|
||||||
|
| 2 | `GET …/damage-case/preview` | `POST …/damage-case/submit` |
|
||||||
|
| 3 | `GET …/attachments/preview` | `POST …/attachments/submit` |
|
||||||
|
| 4 | `GET …/expertise/preview` | `POST …/expertise/submit` |
|
||||||
|
|
||||||
|
Nested Fanavaran URLs always use stored `claimId` (کد رایانه), never `claimNo`.
|
||||||
@@ -462,7 +462,7 @@
|
|||||||
<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>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 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 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 post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Upload licences / car cards and the optional <code>accident_sketch</code> (کروکی) against the auto-created claim. The sketch never gates completion.</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>
|
<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>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -498,7 +498,7 @@
|
|||||||
<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 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 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 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 post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Upload chassis / engine / metal-plate documents; <code>accident_sketch</code> (کروکی) remains optional and does not affect completion.</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-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 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 post">POST</span></td><td><code>capture-part/:claimId</code></td><td>Capture part photos + angles for each selected damaged part.</td></tr>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
export enum ClaimRequiredDocumentType {
|
export enum ClaimRequiredDocumentType {
|
||||||
|
/** Optional police accident sketch (Persian: کروکی). Never gates claim progress. */
|
||||||
|
ACCIDENT_SKETCH = "accident_sketch",
|
||||||
|
|
||||||
// Car green card
|
// Car green card
|
||||||
CAR_GREEN_CARD = "car_green_card",
|
CAR_GREEN_CARD = "car_green_card",
|
||||||
CAR_CERTIFICATE = "car_certificate",
|
CAR_CERTIFICATE = "car_certificate",
|
||||||
@@ -34,4 +37,3 @@ export enum CarAngle {
|
|||||||
LEFT = "left",
|
LEFT = "left",
|
||||||
RIGHT = "right",
|
RIGHT = "right",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -483,6 +483,7 @@ describe("buildInsurerFileReport", () => {
|
|||||||
nationalCode: "0012345678",
|
nationalCode: "0012345678",
|
||||||
birthday: "1370/01/01",
|
birthday: "1370/01/01",
|
||||||
hasDrivingLicense: true,
|
hasDrivingLicense: true,
|
||||||
|
driverLicenseDate: "1400/01/01",
|
||||||
licenseNumber: "123456789",
|
licenseNumber: "123456789",
|
||||||
licenseType: "BASE_2",
|
licenseType: "BASE_2",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -732,7 +732,10 @@ function buildDriverSection(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: PR.licenseDate,
|
label: PR.licenseDate,
|
||||||
value: licenseDate ?? asString(person.driverLicense),
|
value:
|
||||||
|
asString(person.driverLicenseDate) ||
|
||||||
|
licenseDate ||
|
||||||
|
asString(person.driverLicense),
|
||||||
},
|
},
|
||||||
{ label: PR.phone, value: asString(person.phoneNumber) },
|
{ label: PR.phone, value: asString(person.phoneNumber) },
|
||||||
{ label: PR.nationalCode, value: asString(person.nationalCodeOfDriver) },
|
{ label: PR.nationalCode, value: asString(person.nationalCodeOfDriver) },
|
||||||
|
|||||||
45
src/claim-request-management/claim-optional-document.spec.ts
Normal file
45
src/claim-request-management/claim-optional-document.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
|
||||||
|
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||||
|
|
||||||
|
describe("optional accident sketch", () => {
|
||||||
|
const service = Object.create(
|
||||||
|
ClaimRequestManagementService.prototype,
|
||||||
|
) as ClaimRequestManagementService;
|
||||||
|
|
||||||
|
it("is excluded from legacy required-document completion", () => {
|
||||||
|
const requiredTypes = (service as any).getRequiredDocumentTypes({
|
||||||
|
blameFile: { type: "THIRD_PARTY" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requiredTypes).not.toContain(
|
||||||
|
ClaimRequiredDocumentType.ACCIDENT_SKETCH,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not gate V2 owner-document completion", () => {
|
||||||
|
const mandatoryKeys = (service as any).requiredDocumentKeysV2(false);
|
||||||
|
const requiredDocuments = Object.fromEntries(
|
||||||
|
[...mandatoryKeys, ClaimRequiredDocumentType.CAR_GREEN_CARD].map(
|
||||||
|
(key) => [key, { uploaded: true }],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(service as any).allV2OwnerDocumentsComplete(
|
||||||
|
{ requiredDocuments },
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
requiredDocuments[ClaimRequiredDocumentType.ACCIDENT_SKETCH] = {
|
||||||
|
uploaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(service as any).allV2OwnerDocumentsComplete(
|
||||||
|
{ requiredDocuments },
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -58,7 +58,6 @@ import { MediaPolicyModule } from "src/media-policy/media-policy.module";
|
|||||||
import { FanavaranAuditModule } from "src/fanavaran/fanavaran-audit.module";
|
import { FanavaranAuditModule } from "src/fanavaran/fanavaran-audit.module";
|
||||||
import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module";
|
import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module";
|
||||||
import { PlateNormalizerModule } from "src/utils/plate-normalizer/plate-normalizer.module";
|
import { PlateNormalizerModule } from "src/utils/plate-normalizer/plate-normalizer.module";
|
||||||
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -77,7 +76,6 @@ import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.
|
|||||||
SandHubModule,
|
SandHubModule,
|
||||||
ClientModule,
|
ClientModule,
|
||||||
MediaPolicyModule,
|
MediaPolicyModule,
|
||||||
SmsOrchestrationModule,
|
|
||||||
JwtModule.register({}),
|
JwtModule.register({}),
|
||||||
MongooseModule.forFeature([
|
MongooseModule.forFeature([
|
||||||
{ name: ClaimCase.name, schema: ClaimCaseSchema },
|
{ name: ClaimCase.name, schema: ClaimCaseSchema },
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -801,6 +801,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim)
|
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim)
|
||||||
|
|
||||||
**Upload one of the required documents** (12 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements):
|
**Upload one of the required documents** (12 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements):
|
||||||
|
- accident_sketch (optional; never blocks workflow completion)
|
||||||
- damaged_driving_license_front/back
|
- damaged_driving_license_front/back
|
||||||
- damaged_chassis_number, damaged_engine_photo
|
- damaged_chassis_number, damaged_engine_photo
|
||||||
- damaged_car_card_front/back, damaged_metal_plate
|
- damaged_car_card_front/back, damaged_metal_plate
|
||||||
@@ -836,6 +837,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
"guilty_car_card_front",
|
"guilty_car_card_front",
|
||||||
"guilty_car_card_back",
|
"guilty_car_card_back",
|
||||||
"guilty_metal_plate",
|
"guilty_metal_plate",
|
||||||
|
"accident_sketch",
|
||||||
],
|
],
|
||||||
example: "damaged_driving_license_front",
|
example: "damaged_driving_license_front",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ export class RequiredDocumentItem {
|
|||||||
example: true,
|
example: true,
|
||||||
})
|
})
|
||||||
preferUploadDuringCapture?: boolean;
|
preferUploadDuringCapture?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description:
|
||||||
|
'Whether this document is mandatory for workflow completion. Optional documents are uploadable but never included in remaining/progress counts.',
|
||||||
|
example: true,
|
||||||
|
})
|
||||||
|
required: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ export class ClaimVehicleSnapshot {
|
|||||||
|
|
||||||
@Prop({ type: String })
|
@Prop({ type: String })
|
||||||
price?: string;
|
price?: string;
|
||||||
|
|
||||||
|
/** Vehicle value in Rial from hull policy VehicleValue or expert assessment. */
|
||||||
|
@Prop({ type: Number })
|
||||||
|
carPrice?: number;
|
||||||
}
|
}
|
||||||
export const ClaimVehicleSnapshotSchema =
|
export const ClaimVehicleSnapshotSchema =
|
||||||
SchemaFactory.createForClass(ClaimVehicleSnapshot);
|
SchemaFactory.createForClass(ClaimVehicleSnapshot);
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export class FanavaranSyncStage {
|
|||||||
@Prop({ type: Date })
|
@Prop({ type: Date })
|
||||||
lastPayloadBuiltAt?: Date;
|
lastPayloadBuiltAt?: Date;
|
||||||
|
|
||||||
/** When we SMS'd the claim owner about Fanavaran claimId/ClaimNo (after expertise). */
|
/** Legacy marker from the retired post-expertise Fanavaran SMS flow. */
|
||||||
@Prop({ type: Date })
|
@Prop({ type: Date })
|
||||||
smsNotifiedAt?: Date;
|
smsNotifiedAt?: Date;
|
||||||
|
|
||||||
@@ -239,6 +239,10 @@ export class ClaimCase {
|
|||||||
@Prop({ type: Types.ObjectId, index: true })
|
@Prop({ type: Types.ObjectId, index: true })
|
||||||
initiatedByFieldExpertId?: Types.ObjectId;
|
initiatedByFieldExpertId?: Types.ObjectId;
|
||||||
|
|
||||||
|
/** Branch snapshot inherited from the originating expert-created blame. */
|
||||||
|
@Prop({ type: Types.ObjectId, index: true })
|
||||||
|
branchId?: Types.ObjectId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The damaged party's userId, resolved from the blame at claim-creation time.
|
* The damaged party's userId, resolved from the blame at claim-creation time.
|
||||||
* Stored here so view/list access does not require an extra blame lookup.
|
* Stored here so view/list access does not require an extra blame lookup.
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
"guilty_car_card_front",
|
"guilty_car_card_front",
|
||||||
"guilty_car_card_back",
|
"guilty_car_card_back",
|
||||||
"guilty_metal_plate",
|
"guilty_metal_plate",
|
||||||
|
"accident_sketch",
|
||||||
],
|
],
|
||||||
example: "damaged_driving_license_front",
|
example: "damaged_driving_license_front",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -107,6 +107,29 @@ describe("fanavaran claim product", () => {
|
|||||||
expect(nationalCode).toBe("0012345678");
|
expect(nationalCode).toBe("0012345678");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefers CAR_BODY_POLICYHOLDER participant over person.nationalCodeOfInsurer", () => {
|
||||||
|
const nationalCode = pickCarBodyPolicyNationalCode({
|
||||||
|
parties: [
|
||||||
|
{
|
||||||
|
role: PartyRole.FIRST,
|
||||||
|
person: { nationalCodeOfInsurer: "0012345678" },
|
||||||
|
participants: [
|
||||||
|
{
|
||||||
|
participantId: "CAR_BODY_POLICYHOLDER",
|
||||||
|
nationalCode: "0499370899",
|
||||||
|
birthday: "1368/05/20",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
participantRoles: {
|
||||||
|
carBodyPolicyholder: "CAR_BODY_POLICYHOLDER",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nationalCode).toBe("0499370899");
|
||||||
|
});
|
||||||
|
|
||||||
it("flattens car-body inquiry fields onto the damage-case aliases", () => {
|
it("flattens car-body inquiry fields onto the damage-case aliases", () => {
|
||||||
const { mapped } = fanavaranPartyInquirySources("car-body", {
|
const { mapped } = fanavaranPartyInquirySources("car-body", {
|
||||||
insurance: {
|
insurance: {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
parseFanavaranId,
|
parseFanavaranId,
|
||||||
type FanavaranCarPolicyProduct,
|
type FanavaranCarPolicyProduct,
|
||||||
} from "src/lookups/fanavaran-last-car-policy";
|
} from "src/lookups/fanavaran-last-car-policy";
|
||||||
|
import { resolvePolicyholderFromParty } from "./fanavaran-party-roles";
|
||||||
|
import type { InquiryParticipantRoleAssignments } from "src/request-management/inquiry-participant-resolver";
|
||||||
|
|
||||||
export type FanavaranClaimProduct = FanavaranCarPolicyProduct;
|
export type FanavaranClaimProduct = FanavaranCarPolicyProduct;
|
||||||
|
|
||||||
@@ -101,6 +103,8 @@ export function fanavaranInsuranceLineIdForProduct(
|
|||||||
type BlamePartyForCarBodyPolicy = {
|
type BlamePartyForCarBodyPolicy = {
|
||||||
role?: string;
|
role?: string;
|
||||||
person?: { nationalCodeOfInsurer?: unknown };
|
person?: { nationalCodeOfInsurer?: unknown };
|
||||||
|
participants?: Array<Record<string, any>>;
|
||||||
|
participantRoles?: Partial<InquiryParticipantRoleAssignments>;
|
||||||
insurance?: {
|
insurance?: {
|
||||||
carBodyInsurance?: {
|
carBodyInsurance?: {
|
||||||
policyId?: unknown;
|
policyId?: unknown;
|
||||||
@@ -168,6 +172,7 @@ export function pickCarBodyPolicyNationalCode(
|
|||||||
): string | null {
|
): string | null {
|
||||||
const party = firstCarBodyParty(blame);
|
const party = firstCarBodyParty(blame);
|
||||||
return (
|
return (
|
||||||
|
resolvePolicyholderFromParty(party, "car-body").nationalCode ??
|
||||||
nonEmptyText(party?.person?.nationalCodeOfInsurer) ??
|
nonEmptyText(party?.person?.nationalCodeOfInsurer) ??
|
||||||
nonEmptyText(party?.insurance?.carBodyInsurance?.ownerNationalCode) ??
|
nonEmptyText(party?.insurance?.carBodyInsurance?.ownerNationalCode) ??
|
||||||
nonEmptyText(party?.insurance?.carBodyInsurance?.insurerNationalCode)
|
nonEmptyText(party?.insurance?.carBodyInsurance?.insurerNationalCode)
|
||||||
|
|||||||
@@ -34,6 +34,21 @@ export function asPartyInquiryRows(value: unknown): FanavaranPartyInquiryRow[] {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectFanavaranIdByRoles(
|
||||||
|
rows: unknown,
|
||||||
|
preferredRoles: number[],
|
||||||
|
): number | null {
|
||||||
|
const parties = asPartyInquiryRows(rows);
|
||||||
|
if (parties.length === 0) return null;
|
||||||
|
|
||||||
|
for (const roleId of preferredRoles) {
|
||||||
|
const match = parties.find((row) => asPositiveId(row.RoleId) === roleId);
|
||||||
|
if (match) return asPositiveId(match.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return asPositiveId(parties[0].Id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DriverId for GEN.12 زیاندیده. Prefer insurer (161) or driver (166)
|
* DriverId for GEN.12 زیاندیده. Prefer insurer (161) or driver (166)
|
||||||
* based on driverIsInsurer, then any returned party id.
|
* based on driverIsInsurer, then any returned party id.
|
||||||
@@ -42,19 +57,27 @@ export function selectFanavaranDriverId(
|
|||||||
rows: unknown,
|
rows: unknown,
|
||||||
driverIsInsurer: boolean | undefined,
|
driverIsInsurer: boolean | undefined,
|
||||||
): number | null {
|
): number | null {
|
||||||
const parties = asPartyInquiryRows(rows);
|
|
||||||
if (parties.length === 0) return null;
|
|
||||||
|
|
||||||
const preferredRoles = driverIsInsurer
|
const preferredRoles = driverIsInsurer
|
||||||
? [FANAVARAN_INSURER_ROLE_ID, FANAVARAN_DRIVER_ROLE_ID, FANAVARAN_PLAQUE_OWNER_ROLE_ID]
|
? [
|
||||||
: [FANAVARAN_DRIVER_ROLE_ID, FANAVARAN_INSURER_ROLE_ID, FANAVARAN_PLAQUE_OWNER_ROLE_ID];
|
FANAVARAN_INSURER_ROLE_ID,
|
||||||
|
FANAVARAN_DRIVER_ROLE_ID,
|
||||||
|
FANAVARAN_PLAQUE_OWNER_ROLE_ID,
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
FANAVARAN_DRIVER_ROLE_ID,
|
||||||
|
FANAVARAN_INSURER_ROLE_ID,
|
||||||
|
FANAVARAN_PLAQUE_OWNER_ROLE_ID,
|
||||||
|
];
|
||||||
|
return selectFanavaranIdByRoles(rows, preferredRoles);
|
||||||
|
}
|
||||||
|
|
||||||
for (const roleId of preferredRoles) {
|
/** OwnerId: prefer plaque-owner role (163), then insurer / driver. */
|
||||||
const match = parties.find((row) => asPositiveId(row.RoleId) === roleId);
|
export function selectFanavaranOwnerId(rows: unknown): number | null {
|
||||||
if (match) return asPositiveId(match.Id);
|
return selectFanavaranIdByRoles(rows, [
|
||||||
}
|
FANAVARAN_PLAQUE_OWNER_ROLE_ID,
|
||||||
|
FANAVARAN_INSURER_ROLE_ID,
|
||||||
return asPositiveId(parties[0].Id);
|
FANAVARAN_DRIVER_ROLE_ID,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeNationalCode(value: unknown): string | null {
|
export function normalizeNationalCode(value: unknown): string | null {
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ describe("fanavaran hull base claim", () => {
|
|||||||
expect(fanavaranHullNestedClaimId({ ClaimNo: 2268 })).toBeNull();
|
expect(fanavaranHullNestedClaimId({ ClaimNo: 2268 })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps IsLicenseReplaced null even when ثالث IsLicenseReplacement is 0", () => {
|
||||||
|
const hull = toFanavaranHullBaseClaimPayload({
|
||||||
|
PolicyId: 1,
|
||||||
|
IsLicenseReplacement: 0,
|
||||||
|
});
|
||||||
|
expect(hull.IsLicenseReplaced).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("strips ثالث-only fields from a live hull create echo", () => {
|
it("strips ثالث-only fields from a live hull create echo", () => {
|
||||||
const hull = toFanavaranHullBaseClaimPayload({
|
const hull = toFanavaranHullBaseClaimPayload({
|
||||||
AccidentCauseId: 6,
|
AccidentCauseId: 6,
|
||||||
@@ -68,7 +76,7 @@ describe("fanavaran hull base claim", () => {
|
|||||||
expect(hull.IsLicenseReplaced).toBeNull();
|
expect(hull.IsLicenseReplaced).toBeNull();
|
||||||
expect(hull.CostSeparationToDmgSections).toBe(0);
|
expect(hull.CostSeparationToDmgSections).toBe(0);
|
||||||
expect(hull.IsOwnerChanged).toBe(0);
|
expect(hull.IsOwnerChanged).toBe(0);
|
||||||
expect(hull.CulpritTypeId).toBe(301);
|
expect(hull.CulpritTypeId).toBe(300);
|
||||||
expect(hull.DamagedCount).toBeUndefined();
|
expect(hull.DamagedCount).toBeUndefined();
|
||||||
expect(hull.HasOtherCulprit).toBeUndefined();
|
expect(hull.HasOtherCulprit).toBeUndefined();
|
||||||
expect(hull.CompensationReferenceId).toBeUndefined();
|
expect(hull.CompensationReferenceId).toBeUndefined();
|
||||||
|
|||||||
@@ -51,7 +51,17 @@ export const FANAVARAN_DEFAULT_HULL_ACCIDENT_TYPE_ID = 2;
|
|||||||
export const FANAVARAN_HULL_ANS_NO = 0;
|
export const FANAVARAN_HULL_ANS_NO = 0;
|
||||||
|
|
||||||
/** GEN.03 hull culprit type when third-party is at fault. Lookup: vehicle-hull-accident-culprit-type */
|
/** GEN.03 hull culprit type when third-party is at fault. Lookup: vehicle-hull-accident-culprit-type */
|
||||||
export const FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID = 301;
|
export const FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID = 300;
|
||||||
|
|
||||||
|
/** GEN.03 required: درصد تقصير بيمه گذار. Proven Parsian submit uses 100. */
|
||||||
|
export const FANAVARAN_DEFAULT_HULL_CUSTOMER_FAULT_PERCENT = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GEN.03 required: مرجع تعيين مقصر.
|
||||||
|
* Lookup: GET /lookups/detection-accident-culprits. Fallback is بیمهگذار.
|
||||||
|
*/
|
||||||
|
export const FANAVARAN_DEFAULT_HULL_AUTHORITY_CULPRIT_ID = 1;
|
||||||
|
export const FANAVARAN_HULL_AUTHORITY_CULPRIT_CAPTION = "بيمه گذار";
|
||||||
|
|
||||||
/** GEN.03 ActualPremium from GET car/vehicle-hull-policies/{PolicyId} → TotalPremium. */
|
/** GEN.03 ActualPremium from GET car/vehicle-hull-policies/{PolicyId} → TotalPremium. */
|
||||||
export function actualPremiumFromHullPolicyRecord(
|
export function actualPremiumFromHullPolicyRecord(
|
||||||
@@ -63,6 +73,41 @@ export function actualPremiumFromHullPolicyRecord(
|
|||||||
return Number.isFinite(n) && n >= 0 ? n : null;
|
return Number.isFinite(n) && n >= 0 ? n : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeHullCaption(value: unknown): string {
|
||||||
|
return String(value ?? "")
|
||||||
|
.replace(/ي/g, "ی")
|
||||||
|
.replace(/ك/g, "ک")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** AuthorityCulpritId from GET /lookups/detection-accident-culprits. */
|
||||||
|
export function authorityCulpritIdFromDetectionRows(
|
||||||
|
rows: unknown,
|
||||||
|
): number {
|
||||||
|
const list = Array.isArray(rows) ? rows : [];
|
||||||
|
const wanted = normalizeHullCaption(
|
||||||
|
FANAVARAN_HULL_AUTHORITY_CULPRIT_CAPTION,
|
||||||
|
);
|
||||||
|
for (const row of list) {
|
||||||
|
if (!row || typeof row !== "object") continue;
|
||||||
|
const caption = normalizeHullCaption(
|
||||||
|
(row as { Caption?: unknown }).Caption,
|
||||||
|
);
|
||||||
|
if (!caption.includes(wanted)) continue;
|
||||||
|
const raw = (row as { Id?: unknown }).Id;
|
||||||
|
const n = typeof raw === "number" ? raw : Number(raw);
|
||||||
|
if (Number.isFinite(n) && n > 0) return n;
|
||||||
|
}
|
||||||
|
for (const row of list) {
|
||||||
|
if (!row || typeof row !== "object") continue;
|
||||||
|
const raw = (row as { Id?: unknown }).Id;
|
||||||
|
const n = typeof raw === "number" ? raw : Number(raw);
|
||||||
|
if (Number.isFinite(n) && n > 0) return n;
|
||||||
|
}
|
||||||
|
return FANAVARAN_DEFAULT_HULL_AUTHORITY_CULPRIT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
/** Fallback AccidentCulpritId when parties inquiry is unavailable. */
|
/** Fallback AccidentCulpritId when parties inquiry is unavailable. */
|
||||||
export function customerIdFromHullPolicyRecord(
|
export function customerIdFromHullPolicyRecord(
|
||||||
policy: Record<string, unknown> | null | undefined,
|
policy: Record<string, unknown> | null | undefined,
|
||||||
@@ -79,8 +124,8 @@ export function toFanavaranHullBaseClaimPayload(
|
|||||||
const next: Record<string, unknown> = {};
|
const next: Record<string, unknown> = {};
|
||||||
for (const key of FANAVARAN_HULL_BASE_CLAIM_KEYS) {
|
for (const key of FANAVARAN_HULL_BASE_CLAIM_KEYS) {
|
||||||
if (key === "IsLicenseReplaced") {
|
if (key === "IsLicenseReplaced") {
|
||||||
next[key] =
|
// GEN.03: must be empty/null. Do not map ثالث IsLicenseReplacement (default 0).
|
||||||
built.IsLicenseReplaced ?? built.IsLicenseReplacement ?? null;
|
next[key] = null;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (key === "AccidentTypeId") {
|
if (key === "AccidentTypeId") {
|
||||||
@@ -108,15 +153,14 @@ export function toFanavaranHullBaseClaimPayload(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (key === "CustomerFaultPercent") {
|
if (key === "CustomerFaultPercent") {
|
||||||
const culpritType =
|
next[key] =
|
||||||
built.CulpritTypeId ?? FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID;
|
built.CustomerFaultPercent ??
|
||||||
if (built.CustomerFaultPercent != null) {
|
FANAVARAN_DEFAULT_HULL_CUSTOMER_FAULT_PERCENT;
|
||||||
next[key] = built.CustomerFaultPercent;
|
continue;
|
||||||
} else if (culpritType === 300) {
|
}
|
||||||
next[key] = 100;
|
if (key === "AuthorityCulpritId") {
|
||||||
} else {
|
next[key] =
|
||||||
next[key] = null;
|
built.AuthorityCulpritId ?? FANAVARAN_DEFAULT_HULL_AUTHORITY_CULPRIT_ID;
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
next[key] = Object.prototype.hasOwnProperty.call(built, key)
|
next[key] = Object.prototype.hasOwnProperty.call(built, key)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export const FANAVARAN_HULL_DMG_COST_KIND_CAPTION = "مجموع لوازم";
|
|||||||
export const FANAVARAN_DEFAULT_HULL_DMG_KIND_ID = 5485;
|
export const FANAVARAN_DEFAULT_HULL_DMG_KIND_ID = 5485;
|
||||||
/** Fallback when lookup fetch fails (`vehicle-hull-dmg-cost-kinds` Id). */
|
/** Fallback when lookup fetch fails (`vehicle-hull-dmg-cost-kinds` Id). */
|
||||||
export const FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID = 1;
|
export const FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID = 1;
|
||||||
|
/** GEN.06 factory (فابریک) accessory row. Used for every hull DmgSection. */
|
||||||
|
export const FANAVARAN_DEFAULT_HULL_VEHICLE_ACCESSORY_ID = 3043330;
|
||||||
|
|
||||||
export type FanavaranHullDmgAccessoryRow = {
|
export type FanavaranHullDmgAccessoryRow = {
|
||||||
Id?: unknown;
|
Id?: unknown;
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ export {
|
|||||||
FANAVARAN_HULL_DMG_KIND_CAPTION,
|
FANAVARAN_HULL_DMG_KIND_CAPTION,
|
||||||
} from "./fanavaran-hull-expertise-lookups";
|
} from "./fanavaran-hull-expertise-lookups";
|
||||||
|
|
||||||
|
/** GEN.06 RepairDuration in days for every hull expertise. */
|
||||||
|
export const FANAVARAN_DEFAULT_HULL_REPAIR_DURATION = 3;
|
||||||
|
|
||||||
/** GEN.06 VehicleCurrentValue from GET car/vehicle-hull-policies/{PolicyId} → VehicleValue. */
|
/** GEN.06 VehicleCurrentValue from GET car/vehicle-hull-policies/{PolicyId} → VehicleValue. */
|
||||||
export function vehicleCurrentValueFromHullPolicyRecord(
|
export function vehicleCurrentValueFromHullPolicyRecord(
|
||||||
policy: Record<string, unknown> | null | undefined,
|
policy: Record<string, unknown> | null | undefined,
|
||||||
@@ -117,7 +120,8 @@ export function toFanavaranHullExpertisePayload(input: {
|
|||||||
ComponentReplacementCost: input.componentReplacementCost,
|
ComponentReplacementCost: input.componentReplacementCost,
|
||||||
WasteValue: input.wasteValue,
|
WasteValue: input.wasteValue,
|
||||||
CarryAndRescueCost: null,
|
CarryAndRescueCost: null,
|
||||||
RepairDuration: input.repairDuration ?? null,
|
RepairDuration:
|
||||||
|
input.repairDuration ?? FANAVARAN_DEFAULT_HULL_REPAIR_DURATION,
|
||||||
VehicleCurrentValue: input.vehicleCurrentValue,
|
VehicleCurrentValue: input.vehicleCurrentValue,
|
||||||
WreckHighestValue: null,
|
WreckHighestValue: null,
|
||||||
InspectionDeduction: null,
|
InspectionDeduction: null,
|
||||||
|
|||||||
125
src/claim-request-management/fanavaran-party-roles.spec.ts
Normal file
125
src/claim-request-management/fanavaran-party-roles.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { InquiryParticipantRole } from "src/common/dto/inquiry-participants.dto";
|
||||||
|
import {
|
||||||
|
FANAVARAN_DUMMY_LICENCE_ISSU_DATE,
|
||||||
|
formatFanavaranLicenceIssuDate,
|
||||||
|
resolveDriverFromParty,
|
||||||
|
resolveDriverIsOwner,
|
||||||
|
resolveFanavaranLicenceIssuDate,
|
||||||
|
resolveFanavaranLicenceNo,
|
||||||
|
resolveOwnerFromParty,
|
||||||
|
resolvePolicyholderFromParty,
|
||||||
|
} from "./fanavaran-party-roles";
|
||||||
|
|
||||||
|
describe("fanavaran-party-roles", () => {
|
||||||
|
const partyWithDistinctRoles = {
|
||||||
|
participants: [
|
||||||
|
{
|
||||||
|
participantId: "DRIVER",
|
||||||
|
nationalCode: "0012345678",
|
||||||
|
birthday: "1370/01/15",
|
||||||
|
licenseNumber: "1234567890",
|
||||||
|
licenseType: "1",
|
||||||
|
driverLicenseDate: "1398/06/01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
participantId: "VEHICLE_OWNER",
|
||||||
|
nationalCode: "0499370899",
|
||||||
|
birthday: "1368/05/20",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
participantId: "THIRD_PARTY_POLICYHOLDER",
|
||||||
|
nationalCode: "0080086519",
|
||||||
|
birthday: "1364/06/28",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
participantRoles: {
|
||||||
|
driver: "DRIVER",
|
||||||
|
vehicleOwner: "VEHICLE_OWNER",
|
||||||
|
thirdPartyPolicyholder: "THIRD_PARTY_POLICYHOLDER",
|
||||||
|
},
|
||||||
|
person: {
|
||||||
|
nationalCodeOfDriver: "0012345678",
|
||||||
|
nationalCodeOfInsurer: "0080086519",
|
||||||
|
driverLicense: "1234567890",
|
||||||
|
driverIsInsurer: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it("resolves driver licence and driverLicenseDate from participants", () => {
|
||||||
|
const driver = resolveDriverFromParty(partyWithDistinctRoles);
|
||||||
|
expect(driver.nationalCode).toBe("0012345678");
|
||||||
|
expect(driver.licenseNo).toBe("1234567890");
|
||||||
|
expect(driver.licenseIssuDate).toBe("1398/06/01");
|
||||||
|
expect(resolveFanavaranLicenceNo(driver, "9705463515")).toBe("1234567890");
|
||||||
|
expect(resolveFanavaranLicenceIssuDate(driver)).toBe("1398/06/01");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to dummy licence issue date when driverLicenseDate is missing", () => {
|
||||||
|
const driver = resolveDriverFromParty({
|
||||||
|
participants: [
|
||||||
|
{
|
||||||
|
participantId: InquiryParticipantRole.DRIVER,
|
||||||
|
nationalCode: "0012345678",
|
||||||
|
birthday: "1370/01/15",
|
||||||
|
licenseNumber: "999",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
participantRoles: { driver: InquiryParticipantRole.DRIVER },
|
||||||
|
});
|
||||||
|
expect(driver.licenseIssuDate).toBeNull();
|
||||||
|
expect(resolveFanavaranLicenceIssuDate(driver)).toBe(
|
||||||
|
FANAVARAN_DUMMY_LICENCE_ISSU_DATE,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks DriverIsOwner false when driver and owner differ", () => {
|
||||||
|
expect(resolveDriverIsOwner(partyWithDistinctRoles)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks DriverIsOwner true when driver and owner share a national code", () => {
|
||||||
|
expect(
|
||||||
|
resolveDriverIsOwner({
|
||||||
|
participants: [
|
||||||
|
{
|
||||||
|
participantId: "SAME",
|
||||||
|
nationalCode: "0012345678",
|
||||||
|
birthday: "1370/01/15",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
participantRoles: {
|
||||||
|
driver: "SAME",
|
||||||
|
vehicleOwner: "SAME",
|
||||||
|
thirdPartyPolicyholder: "SAME",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves owner and policyholder independently of the driver", () => {
|
||||||
|
expect(resolveOwnerFromParty(partyWithDistinctRoles).nationalCode).toBe(
|
||||||
|
"0499370899",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
resolvePolicyholderFromParty(partyWithDistinctRoles, "third-party")
|
||||||
|
.nationalCode,
|
||||||
|
).toBe("0080086519");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats compact Jalali driverLicenseDate", () => {
|
||||||
|
expect(formatFanavaranLicenceIssuDate(13980601)).toBe("1398/06/01");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to legacy person.driverLicense when participants are absent", () => {
|
||||||
|
const driver = resolveDriverFromParty({
|
||||||
|
person: {
|
||||||
|
nationalCodeOfDriver: "0011111111",
|
||||||
|
driverBirthday: "13700101",
|
||||||
|
driverLicense: "5555555555",
|
||||||
|
driverLicenseDate: "1390/01/01",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(driver.nationalCode).toBe("0011111111");
|
||||||
|
expect(driver.licenseNo).toBe("5555555555");
|
||||||
|
expect(driver.licenseIssuDate).toBe("1390/01/01");
|
||||||
|
});
|
||||||
|
});
|
||||||
218
src/claim-request-management/fanavaran-party-roles.ts
Normal file
218
src/claim-request-management/fanavaran-party-roles.ts
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import { InquiryParticipantRole } from "src/common/dto/inquiry-participants.dto";
|
||||||
|
import type { InquiryParticipantRoleAssignments } from "src/request-management/inquiry-participant-resolver";
|
||||||
|
import { participantForStoredPartyRole } from "src/request-management/inquiry-participant-resolver";
|
||||||
|
import {
|
||||||
|
normalizeNationalCode,
|
||||||
|
parseJalaliDateParts,
|
||||||
|
} from "./fanavaran-driver-inquiry";
|
||||||
|
import type { FanavaranClaimProduct } from "./fanavaran-claim-product";
|
||||||
|
|
||||||
|
/** Fanavaran dummy licence issue date when the user did not supply one. */
|
||||||
|
export const FANAVARAN_DUMMY_LICENCE_ISSU_DATE = "1394/10/13";
|
||||||
|
|
||||||
|
export type FanavaranRoleIdentity = {
|
||||||
|
nationalCode: string | null;
|
||||||
|
birthday: string | number | null;
|
||||||
|
/** Digits-only licence number when known. */
|
||||||
|
licenseNo: string | null;
|
||||||
|
licenseType: string | null;
|
||||||
|
/**
|
||||||
|
* Jalali `YYYY/MM/DD` from `driverLicenseDate` when present.
|
||||||
|
* Not a persisted model field — read opportunistically from party/person/participant.
|
||||||
|
*/
|
||||||
|
licenseIssuDate: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PartyLike = {
|
||||||
|
participants?: Array<Record<string, any>>;
|
||||||
|
participantRoles?: Partial<InquiryParticipantRoleAssignments>;
|
||||||
|
person?: Record<string, any> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function nonEmptyText(value: unknown): string | null {
|
||||||
|
if (value == null) return null;
|
||||||
|
const text = String(value).trim();
|
||||||
|
if (!text || text.toLowerCase() === "null" || text.toLowerCase() === "undefined") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Digits-only licence number, or null when empty / non-digit. */
|
||||||
|
export function extractLicenceDigits(value: unknown): string | null {
|
||||||
|
const text = nonEmptyText(value);
|
||||||
|
if (!text) return null;
|
||||||
|
const digits = text.replace(/[^\d]/g, "");
|
||||||
|
return digits || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a stored `driverLicenseDate` (or similar) to Fanavaran Jalali `YYYY/MM/DD`.
|
||||||
|
* Returns null when missing / unparseable so callers can fall back to the dummy.
|
||||||
|
*/
|
||||||
|
export function formatFanavaranLicenceIssuDate(value: unknown): string | null {
|
||||||
|
const parsed = parseJalaliDateParts(value);
|
||||||
|
if (!parsed) return null;
|
||||||
|
const month = String(parsed.month).padStart(2, "0");
|
||||||
|
const day = String(parsed.day).padStart(2, "0");
|
||||||
|
return `${parsed.year}/${month}/${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDriverLicenseDate(
|
||||||
|
...sources: Array<Record<string, any> | null | undefined>
|
||||||
|
): string | null {
|
||||||
|
for (const source of sources) {
|
||||||
|
if (!source) continue;
|
||||||
|
const formatted = formatFanavaranLicenceIssuDate(
|
||||||
|
source.driverLicenseDate ?? source.licenseDate ?? source.licenceIssuDate,
|
||||||
|
);
|
||||||
|
if (formatted) return formatted;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromParticipant(
|
||||||
|
participant: Record<string, any> | undefined,
|
||||||
|
): FanavaranRoleIdentity {
|
||||||
|
if (!participant) {
|
||||||
|
return {
|
||||||
|
nationalCode: null,
|
||||||
|
birthday: null,
|
||||||
|
licenseNo: null,
|
||||||
|
licenseType: null,
|
||||||
|
licenseIssuDate: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nationalCode: normalizeNationalCode(participant.nationalCode),
|
||||||
|
birthday: nonEmptyText(participant.birthday) ?? participant.birthday ?? null,
|
||||||
|
licenseNo: extractLicenceDigits(participant.licenseNumber),
|
||||||
|
licenseType: nonEmptyText(participant.licenseType),
|
||||||
|
licenseIssuDate: readDriverLicenseDate(participant),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Damaged/guilty party DRIVER: participants first, then legacy person.* fields.
|
||||||
|
* Licence number prefers real user values; callers fall back to the Fanavaran dummy.
|
||||||
|
*/
|
||||||
|
export function resolveDriverFromParty(party: PartyLike | null | undefined): FanavaranRoleIdentity {
|
||||||
|
const fromRole = fromParticipant(
|
||||||
|
participantForStoredPartyRole(
|
||||||
|
party ?? {},
|
||||||
|
InquiryParticipantRole.DRIVER,
|
||||||
|
) as Record<string, any> | undefined,
|
||||||
|
);
|
||||||
|
const person = party?.person ?? undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
nationalCode:
|
||||||
|
fromRole.nationalCode ??
|
||||||
|
normalizeNationalCode(person?.nationalCodeOfDriver) ??
|
||||||
|
normalizeNationalCode(person?.nationalCode),
|
||||||
|
birthday:
|
||||||
|
(fromRole.birthday != null && String(fromRole.birthday).trim() !== ""
|
||||||
|
? fromRole.birthday
|
||||||
|
: null) ??
|
||||||
|
nonEmptyText(person?.driverBirthday) ??
|
||||||
|
nonEmptyText(person?.birthday) ??
|
||||||
|
person?.driverBirthday ??
|
||||||
|
person?.birthday ??
|
||||||
|
null,
|
||||||
|
licenseNo:
|
||||||
|
fromRole.licenseNo ??
|
||||||
|
extractLicenceDigits(person?.driverLicense) ??
|
||||||
|
extractLicenceDigits(person?.insurerLicense),
|
||||||
|
licenseType: fromRole.licenseType ?? nonEmptyText(person?.licenseType),
|
||||||
|
licenseIssuDate:
|
||||||
|
fromRole.licenseIssuDate ?? readDriverLicenseDate(person),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vehicle owner for OwnerId / DriverIsOwner / Sheba national-code routing. */
|
||||||
|
export function resolveOwnerFromParty(party: PartyLike | null | undefined): FanavaranRoleIdentity {
|
||||||
|
const fromRole = fromParticipant(
|
||||||
|
participantForStoredPartyRole(
|
||||||
|
party ?? {},
|
||||||
|
InquiryParticipantRole.VEHICLE_OWNER,
|
||||||
|
) as Record<string, any> | undefined,
|
||||||
|
);
|
||||||
|
if (fromRole.nationalCode || fromRole.birthday) {
|
||||||
|
return fromRole;
|
||||||
|
}
|
||||||
|
// Legacy parties had no dedicated owner fields.
|
||||||
|
return {
|
||||||
|
nationalCode: null,
|
||||||
|
birthday: null,
|
||||||
|
licenseNo: null,
|
||||||
|
licenseType: null,
|
||||||
|
licenseIssuDate: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Policyholder national code for GEN.03 PolicyId inquiry:
|
||||||
|
* THIRD_PARTY → third-party policyholder; CAR_BODY → car-body policyholder.
|
||||||
|
*/
|
||||||
|
export function resolvePolicyholderFromParty(
|
||||||
|
party: PartyLike | null | undefined,
|
||||||
|
product: FanavaranClaimProduct,
|
||||||
|
): FanavaranRoleIdentity {
|
||||||
|
const role =
|
||||||
|
product === "car-body"
|
||||||
|
? InquiryParticipantRole.CAR_BODY_POLICYHOLDER
|
||||||
|
: InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER;
|
||||||
|
const fromRole = fromParticipant(
|
||||||
|
participantForStoredPartyRole(party ?? {}, role) as
|
||||||
|
| Record<string, any>
|
||||||
|
| undefined,
|
||||||
|
);
|
||||||
|
const person = party?.person ?? undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
nationalCode:
|
||||||
|
fromRole.nationalCode ??
|
||||||
|
normalizeNationalCode(person?.nationalCodeOfInsurer) ??
|
||||||
|
normalizeNationalCode(person?.nationalCode),
|
||||||
|
birthday:
|
||||||
|
(fromRole.birthday != null && String(fromRole.birthday).trim() !== ""
|
||||||
|
? fromRole.birthday
|
||||||
|
: null) ??
|
||||||
|
nonEmptyText(person?.insurerBirthday) ??
|
||||||
|
person?.insurerBirthday ??
|
||||||
|
null,
|
||||||
|
licenseNo:
|
||||||
|
fromRole.licenseNo ?? extractLicenceDigits(person?.insurerLicense),
|
||||||
|
licenseType: fromRole.licenseType ?? null,
|
||||||
|
licenseIssuDate: fromRole.licenseIssuDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GEN.12 `DriverIsOwner`: driver national code equals vehicle-owner national code.
|
||||||
|
* Falls back to legacy `person.driverIsInsurer` when owner role is absent.
|
||||||
|
*/
|
||||||
|
export function resolveDriverIsOwner(party: PartyLike | null | undefined): boolean {
|
||||||
|
const driver = resolveDriverFromParty(party);
|
||||||
|
const owner = resolveOwnerFromParty(party);
|
||||||
|
if (driver.nationalCode && owner.nationalCode) {
|
||||||
|
return driver.nationalCode === owner.nationalCode;
|
||||||
|
}
|
||||||
|
return party?.person?.driverIsInsurer === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Licence number for Fanavaran; never empty — uses `dummy` when user value is missing. */
|
||||||
|
export function resolveFanavaranLicenceNo(
|
||||||
|
identity: Pick<FanavaranRoleIdentity, "licenseNo">,
|
||||||
|
dummy: string,
|
||||||
|
): string {
|
||||||
|
return identity.licenseNo || dummy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Licence issue date for Fanavaran; uses dummy when `driverLicenseDate` is absent. */
|
||||||
|
export function resolveFanavaranLicenceIssuDate(
|
||||||
|
identity: Pick<FanavaranRoleIdentity, "licenseIssuDate">,
|
||||||
|
dummy: string = FANAVARAN_DUMMY_LICENCE_ISSU_DATE,
|
||||||
|
): string {
|
||||||
|
return identity.licenseIssuDate || dummy;
|
||||||
|
}
|
||||||
@@ -373,6 +373,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
"guilty_car_card_front",
|
"guilty_car_card_front",
|
||||||
"guilty_car_card_back",
|
"guilty_car_card_back",
|
||||||
"guilty_metal_plate",
|
"guilty_metal_plate",
|
||||||
|
"accident_sketch",
|
||||||
],
|
],
|
||||||
example: "damaged_driving_license_front",
|
example: "damaged_driving_license_front",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export class ExternalInquiryFlagsDto implements ExternalInquiryFlags {
|
|||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description:
|
description:
|
||||||
"ESG VIN/chassis-number inquiry (`/inquiry/policyByChassis`). Required for the VIN initial-form path.",
|
"ESG two-factor VIN/chassis inquiry (`/inquiry/carByChassis`). Required for the VIN initial-form path.",
|
||||||
example: false,
|
example: false,
|
||||||
})
|
})
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
|||||||
@@ -81,6 +81,14 @@ export class InquiryParticipantInputDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
licenseType?: string;
|
licenseType?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Driver licence issue date. Required only for a driver who has a licence; Jalali YYYY/MM/DD is recommended.",
|
||||||
|
example: "1400/01/15",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
driverLicenseDate?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InquiryVehicleInputDto {
|
export class InquiryVehicleInputDto {
|
||||||
@@ -168,6 +176,9 @@ export class InquiryParticipantFieldsDto {
|
|||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
driverLicense?: string;
|
driverLicense?: string;
|
||||||
|
|
||||||
|
@ApiHideProperty()
|
||||||
|
driverLicenseDate?: string;
|
||||||
|
|
||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
licenseType?: string;
|
licenseType?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,26 @@ export class ListQueryV2Dto {
|
|||||||
@MaxLength(64)
|
@MaxLength(64)
|
||||||
search?: string;
|
search?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Filter cases by a phone number found on any party.",
|
||||||
|
example: "09121234567",
|
||||||
|
maxLength: 32,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(32)
|
||||||
|
phoneNumber?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Filter cases by a national code found on any party.",
|
||||||
|
example: "0012345678",
|
||||||
|
maxLength: 32,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(32)
|
||||||
|
nationalCode?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
enum: UNIFIED_FILE_STATUS_KEYS,
|
enum: UNIFIED_FILE_STATUS_KEYS,
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -614,9 +614,24 @@ export class ExpertBlameService {
|
|||||||
)) as Record<string, unknown>[];
|
)) as Record<string, unknown>[];
|
||||||
|
|
||||||
// Scope to this reviewer's insurance company via blame party clientId
|
// Scope to this reviewer's insurance company via blame party clientId
|
||||||
const visibleCases = allSealed.filter((doc) =>
|
const tenantVisibleCases = allSealed.filter((doc) =>
|
||||||
blameCaseTouchesClient(doc, clientKey),
|
blameCaseTouchesClient(doc, clientKey),
|
||||||
);
|
);
|
||||||
|
const visibleCases = (
|
||||||
|
await Promise.all(
|
||||||
|
tenantVisibleCases.map(async (doc) => {
|
||||||
|
try {
|
||||||
|
await this.requestManagementService.assertFileReviewerBranchAccess(
|
||||||
|
doc,
|
||||||
|
actor.sub,
|
||||||
|
);
|
||||||
|
return doc;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).filter((doc): doc is Record<string, unknown> => doc != null);
|
||||||
|
|
||||||
const pagedResult = await this.paginateBlameListV2(visibleCases, query);
|
const pagedResult = await this.paginateBlameListV2(visibleCases, query);
|
||||||
return pagedResult;
|
return pagedResult;
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ describe("ExpertClaimService expert-reply pricing", () => {
|
|||||||
expect(findByIdAndUpdate).toHaveBeenCalledTimes(1);
|
expect(findByIdAndUpdate).toHaveBeenCalledTimes(1);
|
||||||
expect(findByIdAndUpdate.mock.calls[0][1]).toMatchObject({
|
expect(findByIdAndUpdate.mock.calls[0][1]).toMatchObject({
|
||||||
"vehicle.price": "1250000000",
|
"vehicle.price": "1250000000",
|
||||||
|
"vehicle.carPrice": 1250000000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -514,6 +515,9 @@ describe("ExpertClaimService FileReviewer assignment", () => {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
service.fanavaranLocationService = {
|
||||||
|
assertMakerReviewerBranchCompatible: jest.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
service.assertExpertActorOnClaim = jest.fn();
|
service.assertExpertActorOnClaim = jest.fn();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -530,3 +534,28 @@ describe("ExpertClaimService FileReviewer assignment", () => {
|
|||||||
expect(service.assertExpertActorOnClaim).not.toHaveBeenCalled();
|
expect(service.assertExpertActorOnClaim).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ExpertClaimService branch-scoped claim views", () => {
|
||||||
|
it("shows branch-scoped claims only to damage experts in that branch", async () => {
|
||||||
|
const service = createService() as any;
|
||||||
|
const branchId = new Types.ObjectId();
|
||||||
|
const otherBranchId = new Types.ObjectId();
|
||||||
|
service.damageExpertDbService = {
|
||||||
|
findById: jest.fn().mockResolvedValue({ branchId }),
|
||||||
|
};
|
||||||
|
service.blameRequestDbService = {
|
||||||
|
find: jest.fn().mockResolvedValue([]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const inBranch = { _id: "in-branch", branchId };
|
||||||
|
const otherBranch = { _id: "other-branch", branchId: otherBranchId };
|
||||||
|
const legacyUnscoped = { _id: "legacy-unscoped" };
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.filterDamageExpertClaimsByBranch(
|
||||||
|
[inBranch, otherBranch, legacyUnscoped],
|
||||||
|
{ sub: V2_EXPERT_ID, role: RoleEnum.DAMAGE_EXPERT },
|
||||||
|
),
|
||||||
|
).resolves.toEqual([inBranch, legacyUnscoped]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -418,14 +418,145 @@ export class ExpertClaimService {
|
|||||||
claim: any,
|
claim: any,
|
||||||
actor: any,
|
actor: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const blame = await this.loadBlameForClaim(claim);
|
||||||
// FILE_REVIEWER: by the time we reach this method the caller has already
|
// FILE_REVIEWER: by the time we reach this method the caller has already
|
||||||
// verified that actor.sub === blame.assignedFileReviewerId (Phase 2 of
|
// verified that actor.sub === blame.assignedFileReviewerId (Phase 2 of
|
||||||
// assignClaimForReviewV2). The generic tenant-scope check would incorrectly
|
// assignClaimForReviewV2). The generic tenant-scope check would incorrectly
|
||||||
// reject them because initiatedByFieldExpertId belongs to the FileMaker, not
|
// reject them because initiatedByFieldExpertId belongs to the FileMaker, not
|
||||||
// the reviewer. Skip it — the assignment check is the access proof.
|
// the reviewer. Skip it — the assignment check is the access proof.
|
||||||
if ((actor as any).role === RoleEnum.FILE_REVIEWER) return;
|
if ((actor as any).role === RoleEnum.FILE_REVIEWER) {
|
||||||
const blame = await this.loadBlameForClaim(claim);
|
await this.assertActorBranchAccess(claim, actor, blame);
|
||||||
|
return;
|
||||||
|
}
|
||||||
assertClaimCaseForExpertActor(claim, actor, blame);
|
assertClaimCaseForExpertActor(claim, actor, blame);
|
||||||
|
await this.assertActorBranchAccess(claim, actor, blame);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertActorBranchAccess(
|
||||||
|
claim: any,
|
||||||
|
actor: any,
|
||||||
|
blame?: any,
|
||||||
|
): Promise<void> {
|
||||||
|
const caseBranchId = claim?.branchId
|
||||||
|
? String(claim.branchId)
|
||||||
|
: blame?.branchId
|
||||||
|
? String(blame.branchId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (actor?.role === RoleEnum.FILE_REVIEWER) {
|
||||||
|
await this.fanavaranLocationService.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: blame?.initiatedByFieldExpertId
|
||||||
|
? String(blame.initiatedByFieldExpertId)
|
||||||
|
: null,
|
||||||
|
fileReviewerId: String(actor.sub),
|
||||||
|
caseBranchId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor?.role === RoleEnum.FILE_MAKER) {
|
||||||
|
const actorBranchId = await this.fanavaranLocationService.fileMakerBranchId(
|
||||||
|
String(actor.sub),
|
||||||
|
);
|
||||||
|
if (!actorBranchId || (caseBranchId && caseBranchId !== actorBranchId)) {
|
||||||
|
throw new ForbiddenException("This file belongs to another branch.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor?.role === RoleEnum.FIELD_EXPERT) return;
|
||||||
|
|
||||||
|
const damageExpert = await this.damageExpertDbService.findById(
|
||||||
|
String(actor.sub),
|
||||||
|
);
|
||||||
|
const actorBranchId = (damageExpert as any)?.branchId
|
||||||
|
? String((damageExpert as any).branchId)
|
||||||
|
: undefined;
|
||||||
|
if (!actorBranchId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"DamageExpert account is not assigned to a branch.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Ordinary user-created legacy claims have no branch snapshot. Preserve
|
||||||
|
// their existing tenant queue; branch-created claims must match exactly.
|
||||||
|
if (caseBranchId && caseBranchId !== actorBranchId) {
|
||||||
|
throw new ForbiddenException("This file belongs to another branch.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async filterDamageExpertClaimsByBranch(
|
||||||
|
claims: any[],
|
||||||
|
actor: any,
|
||||||
|
): Promise<any[]> {
|
||||||
|
const damageExpert = await this.damageExpertDbService.findById(
|
||||||
|
String(actor.sub),
|
||||||
|
);
|
||||||
|
const actorBranchId = (damageExpert as any)?.branchId
|
||||||
|
? String((damageExpert as any).branchId)
|
||||||
|
: undefined;
|
||||||
|
if (!actorBranchId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"DamageExpert account is not assigned to a branch.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingBranchBlameIds = [
|
||||||
|
...new Set(
|
||||||
|
claims
|
||||||
|
.filter((claim) => !claim?.branchId && claim?.blameRequestId)
|
||||||
|
.map((claim) => String(claim.blameRequestId)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
const blames = missingBranchBlameIds.length
|
||||||
|
? ((await this.blameRequestDbService.find(
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
$in: missingBranchBlameIds.map((id) => new Types.ObjectId(id)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lean: true,
|
||||||
|
select:
|
||||||
|
"_id branchId isMadeByFileMaker initiatedByFieldExpertId",
|
||||||
|
},
|
||||||
|
)) as any[])
|
||||||
|
: [];
|
||||||
|
const blameById = new Map(
|
||||||
|
blames.map((blame) => [String(blame._id), blame] as const),
|
||||||
|
);
|
||||||
|
const makerBranchById = new Map<string, string | undefined>();
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
...new Set(
|
||||||
|
blames
|
||||||
|
.filter(
|
||||||
|
(blame) =>
|
||||||
|
blame.isMadeByFileMaker &&
|
||||||
|
!blame.branchId &&
|
||||||
|
blame.initiatedByFieldExpertId,
|
||||||
|
)
|
||||||
|
.map((blame) => String(blame.initiatedByFieldExpertId)),
|
||||||
|
),
|
||||||
|
].map(async (makerId) => {
|
||||||
|
makerBranchById.set(
|
||||||
|
makerId,
|
||||||
|
await this.fanavaranLocationService.fileMakerBranchId(makerId),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return claims.filter((claim) => {
|
||||||
|
let caseBranchId = claim?.branchId ? String(claim.branchId) : undefined;
|
||||||
|
if (!caseBranchId && claim?.blameRequestId) {
|
||||||
|
const blame = blameById.get(String(claim.blameRequestId));
|
||||||
|
caseBranchId = blame?.branchId
|
||||||
|
? String(blame.branchId)
|
||||||
|
: blame?.isMadeByFileMaker && blame?.initiatedByFieldExpertId
|
||||||
|
? makerBranchById.get(String(blame.initiatedByFieldExpertId))
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
return !caseBranchId || caseBranchId === actorBranchId;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fieldExpertOwnsClaim(
|
private async fieldExpertOwnsClaim(
|
||||||
@@ -647,6 +778,7 @@ export class ExpertClaimService {
|
|||||||
creationMethod?: string;
|
creationMethod?: string;
|
||||||
carBodyFirstForm?: { car?: boolean; object?: boolean };
|
carBodyFirstForm?: { car?: boolean; object?: boolean };
|
||||||
blameStatus?: string;
|
blameStatus?: string;
|
||||||
|
partyIdentities?: Array<Record<string, unknown>>;
|
||||||
} {
|
} {
|
||||||
if (!blame?.type) return {};
|
if (!blame?.type) return {};
|
||||||
const blameRequestType = blame.type as BlameRequestType;
|
const blameRequestType = blame.type as BlameRequestType;
|
||||||
@@ -655,12 +787,30 @@ export class ExpertClaimService {
|
|||||||
creationMethod?: string;
|
creationMethod?: string;
|
||||||
carBodyFirstForm?: { car?: boolean; object?: boolean };
|
carBodyFirstForm?: { car?: boolean; object?: boolean };
|
||||||
blameStatus?: string;
|
blameStatus?: string;
|
||||||
|
partyIdentities?: Array<Record<string, unknown>>;
|
||||||
} = { blameRequestType };
|
} = { blameRequestType };
|
||||||
if (blame.creationMethod) out.creationMethod = blame.creationMethod;
|
if (blame.creationMethod) out.creationMethod = blame.creationMethod;
|
||||||
out.blameStatus = blame.blameStatus;
|
out.blameStatus = blame.blameStatus;
|
||||||
if (blameRequestType !== BlameRequestType.CAR_BODY) return out;
|
|
||||||
|
|
||||||
const parties = blame.parties;
|
const parties = blame.parties;
|
||||||
|
if (Array.isArray(parties) && parties.length > 0) {
|
||||||
|
out.partyIdentities = parties.map((party: any) => ({
|
||||||
|
phoneNumber: party?.person?.phoneNumber,
|
||||||
|
nationalCode: party?.person?.nationalCode,
|
||||||
|
nationalCodeOfInsurer: party?.person?.nationalCodeOfInsurer,
|
||||||
|
nationalCodeOfDriver: party?.person?.nationalCodeOfDriver,
|
||||||
|
participants: (
|
||||||
|
Array.isArray(party?.participants)
|
||||||
|
? party.participants
|
||||||
|
: party?.inquiryParticipants
|
||||||
|
)?.map((participant: any) => ({
|
||||||
|
phoneNumber: participant?.phoneNumber,
|
||||||
|
nationalCode: participant?.nationalCode,
|
||||||
|
nationalCodeOfInsurer: participant?.nationalCodeOfInsurer,
|
||||||
|
nationalCodeOfDriver: participant?.nationalCodeOfDriver,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (blameRequestType !== BlameRequestType.CAR_BODY) return out;
|
||||||
if (!Array.isArray(parties) || parties.length === 0) return out;
|
if (!Array.isArray(parties) || parties.length === 0) return out;
|
||||||
|
|
||||||
const first =
|
const first =
|
||||||
@@ -2600,6 +2750,15 @@ export class ExpertClaimService {
|
|||||||
"This file does not belong to your organization.",
|
"This file does not belong to your organization.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await this.fanavaranLocationService.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: (blame as any)?.initiatedByFieldExpertId
|
||||||
|
? String((blame as any).initiatedByFieldExpertId)
|
||||||
|
: null,
|
||||||
|
fileReviewerId: actor.sub,
|
||||||
|
caseBranchId: (blame as any)?.branchId
|
||||||
|
? String((blame as any).branchId)
|
||||||
|
: null,
|
||||||
|
});
|
||||||
if (
|
if (
|
||||||
!(blame as any).isMadeByFileMaker ||
|
!(blame as any).isMadeByFileMaker ||
|
||||||
!(blame as any).expertInitiated ||
|
!(blame as any).expertInitiated ||
|
||||||
@@ -2732,6 +2891,15 @@ export class ExpertClaimService {
|
|||||||
"This file does not belong to your organization.",
|
"This file does not belong to your organization.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await this.fanavaranLocationService.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: (reviewerBlame as any).initiatedByFieldExpertId
|
||||||
|
? String((reviewerBlame as any).initiatedByFieldExpertId)
|
||||||
|
: null,
|
||||||
|
fileReviewerId: actor.sub,
|
||||||
|
caseBranchId: (reviewerBlame as any).branchId
|
||||||
|
? String((reviewerBlame as any).branchId)
|
||||||
|
: null,
|
||||||
|
});
|
||||||
const assignedReviewerId = (reviewerBlame as any).assignedFileReviewerId
|
const assignedReviewerId = (reviewerBlame as any).assignedFileReviewerId
|
||||||
? String((reviewerBlame as any).assignedFileReviewerId)
|
? String((reviewerBlame as any).assignedFileReviewerId)
|
||||||
: null;
|
: null;
|
||||||
@@ -3341,9 +3509,10 @@ export class ExpertClaimService {
|
|||||||
|
|
||||||
const carPriceWasProvided = reply.carPrice != null;
|
const carPriceWasProvided = reply.carPrice != null;
|
||||||
let normalizedCurrentCarPrice: string | undefined;
|
let normalizedCurrentCarPrice: string | undefined;
|
||||||
|
let parsedCurrentCarPrice: number | undefined;
|
||||||
|
|
||||||
if (blame.type === BlameRequestType.CAR_BODY) {
|
if (blame.type === BlameRequestType.CAR_BODY) {
|
||||||
const parsedCurrentCarPrice = parseMoneyAmountToman(reply.carPrice);
|
parsedCurrentCarPrice = parseMoneyAmountToman(reply.carPrice) ?? undefined;
|
||||||
if (
|
if (
|
||||||
parsedCurrentCarPrice == null ||
|
parsedCurrentCarPrice == null ||
|
||||||
!Number.isSafeInteger(parsedCurrentCarPrice) ||
|
!Number.isSafeInteger(parsedCurrentCarPrice) ||
|
||||||
@@ -3600,8 +3769,11 @@ export class ExpertClaimService {
|
|||||||
"evaluation.ownerInsurerApproval": "",
|
"evaluation.ownerInsurerApproval": "",
|
||||||
"evaluation.ownerPricedPartsApproval": "",
|
"evaluation.ownerPricedPartsApproval": "",
|
||||||
},
|
},
|
||||||
...(normalizedCurrentCarPrice
|
...(normalizedCurrentCarPrice && parsedCurrentCarPrice != null
|
||||||
? { "vehicle.price": normalizedCurrentCarPrice }
|
? {
|
||||||
|
"vehicle.price": normalizedCurrentCarPrice,
|
||||||
|
"vehicle.carPrice": parsedCurrentCarPrice,
|
||||||
|
}
|
||||||
: {}),
|
: {}),
|
||||||
"workflow.currentStep": currentStep,
|
"workflow.currentStep": currentStep,
|
||||||
"workflow.nextStep": nextWorkflowStep,
|
"workflow.nextStep": nextWorkflowStep,
|
||||||
@@ -4117,9 +4289,13 @@ export class ExpertClaimService {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const filtered = (claims as any[]).filter((c) =>
|
const tenantFiltered = (claims as any[]).filter((c) =>
|
||||||
claimCaseTouchesClient(c, clientKey),
|
claimCaseTouchesClient(c, clientKey),
|
||||||
);
|
);
|
||||||
|
const filtered = await this.filterDamageExpertClaimsByBranch(
|
||||||
|
tenantFiltered,
|
||||||
|
actor,
|
||||||
|
);
|
||||||
|
|
||||||
// Reconcile stale locks — if no decision was made, also clear assignedForReviewBy
|
// Reconcile stale locks — if no decision was made, also clear assignedForReviewBy
|
||||||
const staleLockToReconcile = filtered.filter(
|
const staleLockToReconcile = filtered.filter(
|
||||||
@@ -4236,6 +4412,8 @@ export class ExpertClaimService {
|
|||||||
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
||||||
: undefined,
|
: undefined,
|
||||||
...fileCtx,
|
...fileCtx,
|
||||||
|
partyIdentities:
|
||||||
|
fileCtx.partyIdentities ?? c.parties ?? c.snapshot?.parties,
|
||||||
blameRequestId: c.blameRequestId?.toString(),
|
blameRequestId: c.blameRequestId?.toString(),
|
||||||
createdAt: c.createdAt,
|
createdAt: c.createdAt,
|
||||||
awaitingFactorValidation,
|
awaitingFactorValidation,
|
||||||
@@ -4329,6 +4507,8 @@ export class ExpertClaimService {
|
|||||||
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
||||||
: undefined,
|
: undefined,
|
||||||
...fileCtx,
|
...fileCtx,
|
||||||
|
partyIdentities:
|
||||||
|
fileCtx.partyIdentities ?? c.parties ?? c.snapshot?.parties,
|
||||||
blameRequestId: c.blameRequestId?.toString(),
|
blameRequestId: c.blameRequestId?.toString(),
|
||||||
createdAt: c.createdAt,
|
createdAt: c.createdAt,
|
||||||
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
|
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
|
||||||
@@ -4385,7 +4565,7 @@ export class ExpertClaimService {
|
|||||||
{
|
{
|
||||||
lean: true,
|
lean: true,
|
||||||
select:
|
select:
|
||||||
"_id type creationMethod parties blameStatus status expert.decision assignedFileReviewerId requiresFileMakerApproval",
|
"_id branchId initiatedByFieldExpertId type creationMethod parties blameStatus status expert.decision assignedFileReviewerId requiresFileMakerApproval",
|
||||||
},
|
},
|
||||||
)) as any[];
|
)) as any[];
|
||||||
|
|
||||||
@@ -4394,9 +4574,31 @@ export class ExpertClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scope to this reviewer's insurer via blame party clientId
|
// Scope to this reviewer's insurer via blame party clientId
|
||||||
const scopedBlames = blames.filter((b) =>
|
const tenantScopedBlames = blames.filter((b) =>
|
||||||
blameCaseTouchesClient(b, clientKey),
|
blameCaseTouchesClient(b, clientKey),
|
||||||
);
|
);
|
||||||
|
const scopedBlames = (
|
||||||
|
await Promise.all(
|
||||||
|
tenantScopedBlames.map(async (blame) => {
|
||||||
|
try {
|
||||||
|
await this.fanavaranLocationService.assertMakerReviewerBranchCompatible(
|
||||||
|
{
|
||||||
|
fileMakerId: blame.initiatedByFieldExpertId
|
||||||
|
? String(blame.initiatedByFieldExpertId)
|
||||||
|
: null,
|
||||||
|
fileReviewerId: actor.sub,
|
||||||
|
caseBranchId: blame.branchId
|
||||||
|
? String(blame.branchId)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return blame;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).filter((blame): blame is any => blame != null);
|
||||||
|
|
||||||
if (scopedBlames.length === 0) {
|
if (scopedBlames.length === 0) {
|
||||||
return this.paginateClaimListV2([], query);
|
return this.paginateClaimListV2([], query);
|
||||||
@@ -4446,6 +4648,8 @@ export class ExpertClaimService {
|
|||||||
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
||||||
: undefined,
|
: undefined,
|
||||||
...fileCtx,
|
...fileCtx,
|
||||||
|
partyIdentities:
|
||||||
|
fileCtx.partyIdentities ?? c.parties ?? c.snapshot?.parties,
|
||||||
blameRequestId: c.blameRequestId?.toString(),
|
blameRequestId: c.blameRequestId?.toString(),
|
||||||
createdAt: c.createdAt,
|
createdAt: c.createdAt,
|
||||||
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
|
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
|
||||||
@@ -4474,10 +4678,25 @@ export class ExpertClaimService {
|
|||||||
): Promise<GetClaimListV2ResponseDto> {
|
): Promise<GetClaimListV2ResponseDto> {
|
||||||
const makerOid = new Types.ObjectId(actor.sub);
|
const makerOid = new Types.ObjectId(actor.sub);
|
||||||
|
|
||||||
const makerBlames = (await this.blameRequestDbService.find(
|
const ownMakerBlames = (await this.blameRequestDbService.find(
|
||||||
{ isMadeByFileMaker: true, initiatedByFieldExpertId: makerOid },
|
{ isMadeByFileMaker: true, initiatedByFieldExpertId: makerOid },
|
||||||
{ lean: true, select: "_id type creationMethod parties blameStatus status expert.decision assignedFileReviewerId requiresFileMakerApproval" },
|
{ lean: true, select: "_id branchId type creationMethod parties blameStatus status expert.decision assignedFileReviewerId requiresFileMakerApproval" },
|
||||||
)) as any[];
|
)) as any[];
|
||||||
|
const hasBranchScopedFiles = ownMakerBlames.some(
|
||||||
|
(blame) => !!blame.branchId,
|
||||||
|
);
|
||||||
|
const makerBranchId = hasBranchScopedFiles
|
||||||
|
? await this.fanavaranLocationService.fileMakerBranchId(actor.sub)
|
||||||
|
: undefined;
|
||||||
|
if (hasBranchScopedFiles && !makerBranchId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"FileMaker account is not assigned to a branch.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const makerBlames = ownMakerBlames.filter(
|
||||||
|
(blame) =>
|
||||||
|
!blame.branchId || String(blame.branchId) === makerBranchId,
|
||||||
|
);
|
||||||
|
|
||||||
if (makerBlames.length === 0) {
|
if (makerBlames.length === 0) {
|
||||||
return this.paginateClaimListV2([], query);
|
return this.paginateClaimListV2([], query);
|
||||||
@@ -4526,6 +4745,8 @@ export class ExpertClaimService {
|
|||||||
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
? { carName: v.carName, carModel: v.carModel, carType: v.carType }
|
||||||
: undefined,
|
: undefined,
|
||||||
...fileCtx,
|
...fileCtx,
|
||||||
|
partyIdentities:
|
||||||
|
fileCtx.partyIdentities ?? c.parties ?? c.snapshot?.parties,
|
||||||
blameRequestId: c.blameRequestId?.toString(),
|
blameRequestId: c.blameRequestId?.toString(),
|
||||||
createdAt: c.createdAt,
|
createdAt: c.createdAt,
|
||||||
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
|
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
|
||||||
@@ -4916,6 +5137,7 @@ export class ExpertClaimService {
|
|||||||
if (actor.role !== RoleEnum.FILE_REVIEWER) {
|
if (actor.role !== RoleEnum.FILE_REVIEWER) {
|
||||||
assertClaimCaseForExpertActor(claim, actor, linkedBlame);
|
assertClaimCaseForExpertActor(claim, actor, linkedBlame);
|
||||||
}
|
}
|
||||||
|
await this.assertActorBranchAccess(claim, actor, linkedBlame);
|
||||||
|
|
||||||
// Variables used both in the gate block and in the detail-build section below
|
// Variables used both in the gate block and in the detail-build section below
|
||||||
const isDamageExpertPhase =
|
const isDamageExpertPhase =
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ExpertInsurerService } from "./expert-insurer.service";
|
import { ExpertInsurerService } from "./expert-insurer.service";
|
||||||
|
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||||
|
|
||||||
describe("insurer expert file summaries", () => {
|
describe("insurer expert file summaries", () => {
|
||||||
const service = Object.create(
|
const service = Object.create(
|
||||||
@@ -49,4 +50,25 @@ describe("insurer expert file summaries", () => {
|
|||||||
|
|
||||||
expect(summary.reviewDurationMinutes).toBe(0);
|
expect(summary.reviewDurationMinutes).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("exposes the four Fanavaran claim references in insurer claim detail", async () => {
|
||||||
|
const detail = await (service as any).buildInsurerClaimDetail(
|
||||||
|
{
|
||||||
|
_id: "6aa50ce70545fc1906433c86",
|
||||||
|
status: ClaimCaseStatus.COMPLETED,
|
||||||
|
claimId: 101,
|
||||||
|
claimNo: 102,
|
||||||
|
dmgCaseId: 103,
|
||||||
|
expertiseId: 104,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(detail.fanavaran).toEqual({
|
||||||
|
claimId: 101,
|
||||||
|
claimNo: 102,
|
||||||
|
dmgCaseId: 103,
|
||||||
|
expertiseId: 104,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import {
|
|||||||
} from "./helper/timeline-fa-labels";
|
} from "./helper/timeline-fa-labels";
|
||||||
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
|
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
|
||||||
import { serializeDamagedPartSelectionHistory } from "src/helpers/claim-damaged-part-audit";
|
import { serializeDamagedPartSelectionHistory } from "src/helpers/claim-damaged-part-audit";
|
||||||
|
import { fanavaranClaimReferences } from "src/claim-request-management/fanavaran-claim-references";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExpertInsurerService {
|
export class ExpertInsurerService {
|
||||||
@@ -786,6 +787,7 @@ export class ExpertInsurerService {
|
|||||||
publicId: claim.publicId,
|
publicId: claim.publicId,
|
||||||
status: claim.status,
|
status: claim.status,
|
||||||
claimStatus: claim.claimStatus,
|
claimStatus: claim.claimStatus,
|
||||||
|
fanavaran: fanavaranClaimReferences(claim),
|
||||||
blameDocumentResendPending: claim.blameDocumentResendPending,
|
blameDocumentResendPending: claim.blameDocumentResendPending,
|
||||||
workflow: claim.workflow,
|
workflow: claim.workflow,
|
||||||
owner: ownerOut,
|
owner: ownerOut,
|
||||||
|
|||||||
55
src/fanavaran/fanavaran-location.service.spec.ts
Normal file
55
src/fanavaran/fanavaran-location.service.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { ForbiddenException } from "@nestjs/common";
|
||||||
|
import { FanavaranLocationService } from "./fanavaran-location.service";
|
||||||
|
|
||||||
|
describe("FanavaranLocationService branch scope", () => {
|
||||||
|
const makerId = "maker-1";
|
||||||
|
const reviewerId = "reviewer-1";
|
||||||
|
|
||||||
|
const createService = (makerBranch?: string, reviewerBranch?: string) =>
|
||||||
|
new FanavaranLocationService(
|
||||||
|
{
|
||||||
|
findById: jest.fn().mockResolvedValue(
|
||||||
|
makerBranch ? { branchId: makerBranch } : {},
|
||||||
|
),
|
||||||
|
} as any,
|
||||||
|
{
|
||||||
|
findById: jest.fn().mockResolvedValue(
|
||||||
|
reviewerBranch ? { branchId: reviewerBranch } : {},
|
||||||
|
),
|
||||||
|
} as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
it("allows a reviewer to view a file from the same branch", async () => {
|
||||||
|
const service = createService("branch-1", "branch-1");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: makerId,
|
||||||
|
fileReviewerId: reviewerId,
|
||||||
|
}),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a reviewer from another branch", async () => {
|
||||||
|
const service = createService("branch-1", "branch-2");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: makerId,
|
||||||
|
fileReviewerId: reviewerId,
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(ForbiddenException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers the immutable case branch snapshot", async () => {
|
||||||
|
const service = createService("old-branch", "branch-2");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: makerId,
|
||||||
|
fileReviewerId: reviewerId,
|
||||||
|
caseBranchId: "branch-2",
|
||||||
|
}),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -113,6 +113,54 @@ export class FanavaranLocationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fileMakerBranchId(
|
||||||
|
fileMakerId: string | null | undefined,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (!fileMakerId) return undefined;
|
||||||
|
const doc = await this.fileMakerDbService.findById(String(fileMakerId));
|
||||||
|
const branchId = (doc as any)?.branchId;
|
||||||
|
return branchId ? String(branchId) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fileReviewerBranchId(
|
||||||
|
fileReviewerId: string | null | undefined,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (!fileReviewerId) return undefined;
|
||||||
|
const doc = await this.fileReviewerDbService.findById(
|
||||||
|
String(fileReviewerId),
|
||||||
|
);
|
||||||
|
const branchId = (doc as any)?.branchId;
|
||||||
|
return branchId ? String(branchId) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V4/V5 case visibility is branch-scoped by the FileMaker who created it.
|
||||||
|
* Missing branch assignments are denied instead of widening visibility.
|
||||||
|
*/
|
||||||
|
async assertMakerReviewerBranchCompatible(input: {
|
||||||
|
fileMakerId?: string | null;
|
||||||
|
fileReviewerId?: string | null;
|
||||||
|
caseBranchId?: string | null;
|
||||||
|
}): Promise<void> {
|
||||||
|
const reviewerBranchId = await this.fileReviewerBranchId(
|
||||||
|
input.fileReviewerId,
|
||||||
|
);
|
||||||
|
const caseBranchId =
|
||||||
|
(input.caseBranchId ? String(input.caseBranchId) : undefined) ??
|
||||||
|
(await this.fileMakerBranchId(input.fileMakerId));
|
||||||
|
|
||||||
|
if (!reviewerBranchId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"FileReviewer account is not assigned to a branch.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!caseBranchId || caseBranchId !== reviewerBranchId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"This file belongs to another branch.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async loadPrimaryLocationId(
|
private async loadPrimaryLocationId(
|
||||||
userId: string | null | undefined,
|
userId: string | null | undefined,
|
||||||
kind: "maker" | "reviewer",
|
kind: "maker" | "reviewer",
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [
|
|||||||
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/vehicle-hull-accident-culprit-type`,
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/vehicle-hull-accident-culprit-type`,
|
||||||
cacheFile: "vehicle-hull-accident-culprit-type.json",
|
cacheFile: "vehicle-hull-accident-culprit-type.json",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "detection-accident-culprits",
|
||||||
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/detection-accident-culprits`,
|
||||||
|
cacheFile: "detection-accident-culprits.json",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "vehicle-hull-dmg-kind",
|
name: "vehicle-hull-dmg-kind",
|
||||||
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/vehicle-hull-dmg-kind`,
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/vehicle-hull-dmg-kind`,
|
||||||
@@ -167,8 +172,41 @@ export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [
|
|||||||
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/cii-validation-status`,
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/cii-validation-status`,
|
||||||
cacheFile: "cii-validation-status.json",
|
cacheFile: "cii-validation-status.json",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "vehicle-groups",
|
||||||
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/vehicle-groups`,
|
||||||
|
cacheFile: "vehicle-groups.json",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Safe Fanavaran lookup slug: lowercase segments separated by hyphens. */
|
||||||
|
const FANAVARAN_LOOKUP_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a remote lookup definition.
|
||||||
|
* Known names use the catalogue; unknown slugs fall back to
|
||||||
|
* `car/base-info/{lookupName}` so `/lookups/fanavaran/{name}` works without
|
||||||
|
* a dedicated route or config entry (e.g. vehicle-groups).
|
||||||
|
*/
|
||||||
|
export function resolveFanavaranRemoteLookup(
|
||||||
|
name: string,
|
||||||
|
): FanavaranRemoteLookupDefinition | null {
|
||||||
|
const configured = FANAVARAN_REMOTE_LOOKUPS.find((item) => item.name === name);
|
||||||
|
if (configured) {
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FANAVARAN_LOOKUP_NAME_PATTERN.test(name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/${name}`,
|
||||||
|
cacheFile: `${name}.json`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const TEJARAT_STATIC_ACCIDENT_FILES = {
|
export const TEJARAT_STATIC_ACCIDENT_FILES = {
|
||||||
accidentReason: "ACCIDENT_REASON.json",
|
accidentReason: "ACCIDENT_REASON.json",
|
||||||
accidentWay: "ACCIDENT_WAY.json",
|
accidentWay: "ACCIDENT_WAY.json",
|
||||||
|
|||||||
@@ -18,9 +18,22 @@ describe("parseIranLocalDateTime", () => {
|
|||||||
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
|
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("applies a separately supplied time to an explicit-offset date", () => {
|
||||||
|
const instant = parseIranLocalDateTime(
|
||||||
|
"2025-05-16T00:00:00.000Z",
|
||||||
|
"18:28",
|
||||||
|
);
|
||||||
|
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
|
||||||
|
});
|
||||||
|
|
||||||
it("combines Date calendar day in Iran with accidentTime", () => {
|
it("combines Date calendar day in Iran with accidentTime", () => {
|
||||||
const dateOnly = new Date("2025-05-16T00:00:00.000Z");
|
const dateOnly = new Date("2025-05-16T00:00:00.000Z");
|
||||||
const instant = parseIranLocalDateTime(dateOnly, "18:28");
|
const instant = parseIranLocalDateTime(dateOnly, "18:28");
|
||||||
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
|
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects out-of-range hours and minutes", () => {
|
||||||
|
expect(parseIranLocalDateTime("2025-05-16", "24:00")).toBeNull();
|
||||||
|
expect(parseIranLocalDateTime("2025-05-16", "12:60")).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ function normalizeTimeToHms(time: string): string | null {
|
|||||||
const hh = m[1].padStart(2, "0");
|
const hh = m[1].padStart(2, "0");
|
||||||
const mm = m[2];
|
const mm = m[2];
|
||||||
const ss = m[3] ?? "00";
|
const ss = m[3] ?? "00";
|
||||||
|
if (Number(hh) > 23 || Number(mm) > 59 || Number(ss) > 59) return null;
|
||||||
return `${hh}:${mm}:${ss}`;
|
return `${hh}:${mm}:${ss}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +57,12 @@ export function parseIranLocalDateTime(
|
|||||||
|
|
||||||
if (/[zZ]|[+-]\d{2}:?\d{2}$/.test(raw)) {
|
if (/[zZ]|[+-]\d{2}:?\d{2}$/.test(raw)) {
|
||||||
const d = new Date(raw);
|
const d = new Date(raw);
|
||||||
return Number.isNaN(d.getTime()) ? null : d;
|
if (Number.isNaN(d.getTime())) return null;
|
||||||
|
const cleanTime = (time ?? "").trim();
|
||||||
|
if (!cleanTime) return d;
|
||||||
|
const timeHms = normalizeTimeToHms(cleanTime);
|
||||||
|
if (!timeHms) return null;
|
||||||
|
return parseIranLocalIso(gregorianDateInIran(d), timeHms);
|
||||||
}
|
}
|
||||||
|
|
||||||
const naive = raw.match(NAIVE_ISO_DATETIME_RE);
|
const naive = raw.match(NAIVE_ISO_DATETIME_RE);
|
||||||
|
|||||||
@@ -26,6 +26,61 @@ describe("applyListQueryV2", () => {
|
|||||||
expect(r.list[0].publicId).toBe("A200");
|
expect(r.list[0].publicId).toBe("A200");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("filters nested party identities and normalizes Persian digits", () => {
|
||||||
|
const identityRows = [
|
||||||
|
{
|
||||||
|
publicId: "PHONE",
|
||||||
|
createdAt: "2026-01-01",
|
||||||
|
status: "OPEN",
|
||||||
|
parties: [
|
||||||
|
{
|
||||||
|
person: {
|
||||||
|
phoneNumber: "+98 912 123 4567",
|
||||||
|
nationalCodeOfDriver: "۰۰۱۲۳۴۵۶۷۸",
|
||||||
|
},
|
||||||
|
participants: [
|
||||||
|
{
|
||||||
|
phoneNumber: "0913-765-4321",
|
||||||
|
nationalCode: "۰۰۹۸۷۶۵۴۳۲۱",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
publicId: "OTHER",
|
||||||
|
createdAt: "2026-01-02",
|
||||||
|
status: "OPEN",
|
||||||
|
parties: [{ person: { phoneNumber: "09351234567", nationalCode: "1111111111" } }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const identityGetters = {
|
||||||
|
publicId: (r: (typeof identityRows)[0]) => r.publicId,
|
||||||
|
createdAt: (r: (typeof identityRows)[0]) => r.createdAt,
|
||||||
|
status: (r: (typeof identityRows)[0]) => r.status,
|
||||||
|
};
|
||||||
|
|
||||||
|
const byPhone = applyListQueryV2(identityRows, identityGetters, {
|
||||||
|
phoneNumber: "۰۹۱۲۱۲۳۴۵۶۷",
|
||||||
|
});
|
||||||
|
expect(byPhone.list.map((row) => row.publicId)).toEqual(["PHONE"]);
|
||||||
|
|
||||||
|
const byNationalCode = applyListQueryV2(identityRows, identityGetters, {
|
||||||
|
nationalCode: "0012345678",
|
||||||
|
});
|
||||||
|
expect(byNationalCode.list.map((row) => row.publicId)).toEqual(["PHONE"]);
|
||||||
|
|
||||||
|
const byParticipantPhone = applyListQueryV2(identityRows, identityGetters, {
|
||||||
|
phoneNumber: "09137654321",
|
||||||
|
});
|
||||||
|
expect(byParticipantPhone.list.map((row) => row.publicId)).toEqual(["PHONE"]);
|
||||||
|
|
||||||
|
const byParticipantNationalCode = applyListQueryV2(identityRows, identityGetters, {
|
||||||
|
nationalCode: "۰۰۹۸۷۶۵۴۳۲۱",
|
||||||
|
});
|
||||||
|
expect(byParticipantNationalCode.list.map((row) => row.publicId)).toEqual(["PHONE"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("sorts by publicId ascending", () => {
|
it("sorts by publicId ascending", () => {
|
||||||
const r = applyListQueryV2(rows, getters, {
|
const r = applyListQueryV2(rows, getters, {
|
||||||
sortBy: "publicId",
|
sortBy: "publicId",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
import type { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
import { BadRequestException } from "@nestjs/common";
|
import { BadRequestException } from "@nestjs/common";
|
||||||
|
import { normalizeIranMobile } from "src/helpers/iran-mobile";
|
||||||
|
|
||||||
const MAX_LIMIT = 100;
|
const MAX_LIMIT = 100;
|
||||||
const DEFAULT_LIMIT_WHEN_PAGE_SET = 20;
|
const DEFAULT_LIMIT_WHEN_PAGE_SET = 20;
|
||||||
@@ -53,6 +54,53 @@ function normalizeSearch(raw?: string): string | undefined {
|
|||||||
return t || undefined;
|
return t || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeIdentityValue(raw?: unknown): string {
|
||||||
|
return String(raw ?? "")
|
||||||
|
.trim()
|
||||||
|
.replace(/[۰-۹]/g, (digit) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(digit)))
|
||||||
|
.replace(/[٠-٩]/g, (digit) => String("٠١٢٣٤٥٦٧٨٩".indexOf(digit)))
|
||||||
|
.replace(/\D/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPhoneKey(key: string): boolean {
|
||||||
|
return /phone|mobile|tel/i.test(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNationalCodeKey(key: string): boolean {
|
||||||
|
return /national[_-]?(code|id)|code[_-]?mell?i|mell?i[_-]?code/i.test(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePhoneSearchValue(raw: unknown): string {
|
||||||
|
const digits = normalizeIdentityValue(raw);
|
||||||
|
return normalizeIranMobile(digits) ?? digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectIdentityValues(
|
||||||
|
value: unknown,
|
||||||
|
matchKey: (key: string) => boolean,
|
||||||
|
): string[] {
|
||||||
|
const values: string[] = [];
|
||||||
|
const seen = new WeakSet<object>();
|
||||||
|
const visit = (current: unknown, depth: number) => {
|
||||||
|
if (current == null || depth > 8 || typeof current !== "object") return;
|
||||||
|
const object = current as Record<string, unknown>;
|
||||||
|
if (seen.has(object)) return;
|
||||||
|
seen.add(object);
|
||||||
|
for (const [key, child] of Object.entries(object)) {
|
||||||
|
if (
|
||||||
|
matchKey(key) &&
|
||||||
|
(typeof child === "string" || typeof child === "number")
|
||||||
|
) {
|
||||||
|
const normalized = normalizeIdentityValue(child);
|
||||||
|
if (normalized) values.push(normalized);
|
||||||
|
}
|
||||||
|
if (child && typeof child === "object") visit(child, depth + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(value, 0);
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
function toTime(v: Date | string | number | undefined): number {
|
function toTime(v: Date | string | number | undefined): number {
|
||||||
if (v == null) return 0;
|
if (v == null) return 0;
|
||||||
const n = v instanceof Date ? v.getTime() : new Date(v as string | number).getTime();
|
const n = v instanceof Date ? v.getTime() : new Date(v as string | number).getTime();
|
||||||
@@ -75,12 +123,28 @@ export function applyListQueryV2<T>(
|
|||||||
totalPages?: number;
|
totalPages?: number;
|
||||||
} {
|
} {
|
||||||
const search = normalizeSearch(query.search);
|
const search = normalizeSearch(query.search);
|
||||||
|
const phoneNumber = normalizePhoneSearchValue(query.phoneNumber);
|
||||||
|
const nationalCode = normalizeIdentityValue(query.nationalCode);
|
||||||
let filtered = rows;
|
let filtered = rows;
|
||||||
if (query.fileType && getters.fileType) {
|
if (query.fileType && getters.fileType) {
|
||||||
filtered = filtered.filter(
|
filtered = filtered.filter(
|
||||||
(row) => getters.fileType!(row) === query.fileType,
|
(row) => getters.fileType!(row) === query.fileType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (phoneNumber) {
|
||||||
|
filtered = filtered.filter((row) =>
|
||||||
|
collectIdentityValues(row, isPhoneKey).some((value) =>
|
||||||
|
normalizePhoneSearchValue(value).includes(phoneNumber),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (nationalCode) {
|
||||||
|
filtered = filtered.filter((row) =>
|
||||||
|
collectIdentityValues(row, isNationalCodeKey).some((value) =>
|
||||||
|
value.includes(nationalCode),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (search) {
|
if (search) {
|
||||||
filtered = filtered.filter((row) => {
|
filtered = filtered.filter((row) => {
|
||||||
const chunks: string[] = [];
|
const chunks: string[] = [];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
|||||||
import { buildCoefficientsFromPartSeverities } from "./claim-price-drop";
|
import { buildCoefficientsFromPartSeverities } from "./claim-price-drop";
|
||||||
import {
|
import {
|
||||||
catalogPartIdFromSelectedPart,
|
catalogPartIdFromSelectedPart,
|
||||||
|
normalizeDamageSelectedParts,
|
||||||
resolveCatalogPartByPartId,
|
resolveCatalogPartByPartId,
|
||||||
resolvePartForExpertReply,
|
resolvePartForExpertReply,
|
||||||
resolveSelectedPartByPartId,
|
resolveSelectedPartByPartId,
|
||||||
@@ -86,6 +87,31 @@ describe("resolveSelectedPartByPartId", () => {
|
|||||||
expect(catalogPartIdFromSelectedPart(row)).toBeNull();
|
expect(catalogPartIdFromSelectedPart(row)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves a live Fanavaran id that is newer than the static snapshot", () => {
|
||||||
|
const [row] = normalizeDamageSelectedParts(
|
||||||
|
[{
|
||||||
|
id: 96,
|
||||||
|
name: "96",
|
||||||
|
side: "",
|
||||||
|
label_fa: "تیوتر",
|
||||||
|
catalogKey: "96",
|
||||||
|
}],
|
||||||
|
ClaimVehicleTypeV2.SEDAN,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(row).toEqual({
|
||||||
|
id: 96,
|
||||||
|
name: "96",
|
||||||
|
side: "",
|
||||||
|
label_fa: "تیوتر",
|
||||||
|
catalogKey: "96",
|
||||||
|
});
|
||||||
|
expect(catalogPartIdFromSelectedPart(row)).toBe(96);
|
||||||
|
expect(resolvePartForExpertReply(96, [row], ClaimVehicleTypeV2.SEDAN)).toBe(
|
||||||
|
row,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("recovers outer part wrongly stored as internal with Persian label", () => {
|
it("recovers outer part wrongly stored as internal with Persian label", () => {
|
||||||
const fixed = sanitizeDamageSelectedPartV2(
|
const fixed = sanitizeDamageSelectedPartV2(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -466,6 +466,28 @@ export function sanitizeDamageSelectedPartV2(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fanavaran serves this catalog live and can add ids that do not exist in
|
||||||
|
// our fallback snapshot yet. Live rows are persisted with their numeric id
|
||||||
|
// repeated as `name`/`catalogKey` and an empty side. Preserve that identity
|
||||||
|
// instead of downgrading the row to an internal free-text part. Otherwise a
|
||||||
|
// newly-added Fanavaran part (for example id 96) disappears from the expert
|
||||||
|
// reply payload and later fails with PART_NOT_ON_CLAIM.
|
||||||
|
const numericCatalogIdentity =
|
||||||
|
id != null &&
|
||||||
|
Number.isFinite(id) &&
|
||||||
|
Number(id) > 0 &&
|
||||||
|
sideTrim === "" &&
|
||||||
|
(nameTrim === String(id) || String(catalogKey ?? "").trim() === String(id));
|
||||||
|
if (numericCatalogIdentity) {
|
||||||
|
return {
|
||||||
|
id: Number(id),
|
||||||
|
name: String(id),
|
||||||
|
side: "",
|
||||||
|
label_fa: labelTrim || nameTrim || String(id),
|
||||||
|
catalogKey: String(id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (isInternalDamageSide(side)) {
|
if (isInternalDamageSide(side)) {
|
||||||
return {
|
return {
|
||||||
id: null,
|
id: null,
|
||||||
|
|||||||
@@ -64,6 +64,20 @@ export class LookupsController {
|
|||||||
return await this.lookupsService.getVehicleHullAccidentCulpritType();
|
return await this.lookupsService.getVehicleHullAccidentCulpritType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("detection-accident-culprits")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Fanavaran GEN.03 hull authority that determined the culprit",
|
||||||
|
description:
|
||||||
|
"Returns values for base-claim field AuthorityCulpritId from car/base-info/detection-accident-culprits.",
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Returns Fanavaran detection-accident-culprits rows",
|
||||||
|
schema: { type: "array", items: { type: "object" } },
|
||||||
|
})
|
||||||
|
async getDetectionAccidentCulprits() {
|
||||||
|
return await this.lookupsService.getDetectionAccidentCulprits();
|
||||||
|
}
|
||||||
|
|
||||||
@Get("vehicle-hull-dmg-kind")
|
@Get("vehicle-hull-dmg-kind")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Fanavaran GEN.06 hull damage kind lookup",
|
summary: "Fanavaran GEN.06 hull damage kind lookup",
|
||||||
@@ -568,7 +582,8 @@ export class LookupsController {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "List configured Fanavaran remote lookups",
|
summary: "List configured Fanavaran remote lookups",
|
||||||
description:
|
description:
|
||||||
"Returns the lookup names available through /lookups/fanavaran/{lookupName}.",
|
"Returns catalogue entries for /lookups/fanavaran/{lookupName}. " +
|
||||||
|
"Unlisted car/base-info slugs still work via the same route (fallback).",
|
||||||
})
|
})
|
||||||
async listFanavaranRemoteLookups() {
|
async listFanavaranRemoteLookups() {
|
||||||
return this.lookupsService.listFanavaranRemoteLookups().map((lookup) => ({
|
return this.lookupsService.listFanavaranRemoteLookups().map((lookup) => ({
|
||||||
@@ -582,12 +597,14 @@ export class LookupsController {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Fetch a Fanavaran remote lookup by name",
|
summary: "Fetch a Fanavaran remote lookup by name",
|
||||||
description:
|
description:
|
||||||
"Generic cached Fanavaran lookup fetcher for configured lookup names, including GEN.08 expertise lookups.",
|
"Generic cached Fanavaran lookup fetcher. Configured names use their catalogue URL; " +
|
||||||
|
"unlisted slugs (e.g. vehicle-groups) fall back to car/base-info/{lookupName}.",
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "lookupName",
|
name: "lookupName",
|
||||||
description:
|
description:
|
||||||
"Configured Fanavaran lookup name, for example inspection-place, drop-amount-status, car-components, accident-level, expert-status",
|
"Fanavaran lookup slug, for example vehicle-groups, inspection-place, car-components. " +
|
||||||
|
"Unconfigured names are fetched from car/base-info/{lookupName}.",
|
||||||
})
|
})
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
description: "Returns cached or live Fanavaran lookup data",
|
description: "Returns cached or live Fanavaran lookup data",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "src/core/config/fanavaran-client.config";
|
} from "src/core/config/fanavaran-client.config";
|
||||||
import {
|
import {
|
||||||
FANAVARAN_REMOTE_LOOKUPS,
|
FANAVARAN_REMOTE_LOOKUPS,
|
||||||
|
resolveFanavaranRemoteLookup,
|
||||||
type FanavaranRemoteLookupDefinition,
|
type FanavaranRemoteLookupDefinition,
|
||||||
TEJARAT_STATIC_ACCIDENT_FILES,
|
TEJARAT_STATIC_ACCIDENT_FILES,
|
||||||
} from "src/fanavaran/fanavaran-lookup.config";
|
} from "src/fanavaran/fanavaran-lookup.config";
|
||||||
@@ -66,7 +67,7 @@ export class LookupsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private findRemoteLookup(name: string) {
|
private findRemoteLookup(name: string) {
|
||||||
const lookup = FANAVARAN_REMOTE_LOOKUPS.find((item) => item.name === name);
|
const lookup = resolveFanavaranRemoteLookup(name);
|
||||||
if (!lookup) {
|
if (!lookup) {
|
||||||
throw new NotFoundException(`Unknown Fanavaran remote lookup: ${name}`);
|
throw new NotFoundException(`Unknown Fanavaran remote lookup: ${name}`);
|
||||||
}
|
}
|
||||||
@@ -114,6 +115,10 @@ export class LookupsService {
|
|||||||
return await this.getClientRemoteLookup("vehicle-hull-accident-culprit-type");
|
return await this.getClientRemoteLookup("vehicle-hull-accident-culprit-type");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDetectionAccidentCulprits(): Promise<any> {
|
||||||
|
return await this.getClientRemoteLookup("detection-accident-culprits");
|
||||||
|
}
|
||||||
|
|
||||||
async getVehicleHullDmgKind(): Promise<any> {
|
async getVehicleHullDmgKind(): Promise<any> {
|
||||||
return await this.getClientRemoteLookup("vehicle-hull-dmg-kind");
|
return await this.getClientRemoteLookup("vehicle-hull-dmg-kind");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class CallCenterBlameV6Controller {
|
|||||||
description:
|
description:
|
||||||
"VIN alternative to `run-inquiry`. " +
|
"VIN alternative to `run-inquiry`. " +
|
||||||
"The agent supplies the chassis number and personal data collected from the caller. " +
|
"The agent supplies the chassis number and personal data collected from the caller. " +
|
||||||
"ESG chassis lookup (`policyByChassis`) is executed and the result is stored on the " +
|
"ESG two-factor chassis lookup (`carByChassis`) is executed and the result is stored on the " +
|
||||||
"blame document under `vehicle.vin` (plateId is left empty). " +
|
"blame document under `vehicle.vin` (plateId is left empty). " +
|
||||||
"Identical eligibility guards and insurer-company validation as the plate variant. " +
|
"Identical eligibility guards and insurer-company validation as the plate variant. " +
|
||||||
"After this call, proceed to `send-link` exactly as in the plate flow.",
|
"After this call, proceed to `send-link` exactly as in the plate flow.",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
|
import { IsOptional, IsString } from "class-validator";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
|
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
|
||||||
import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
|
import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
|
||||||
@@ -51,8 +51,13 @@ export class FirstPartyFileDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class DescriptionDto {
|
export class DescriptionDto {
|
||||||
@ApiProperty({ description: "Accident description (required for all types)" })
|
@ApiPropertyOptional({
|
||||||
desc: string;
|
description:
|
||||||
|
"Accident description. Optional for THIRD_PARTY; CAR_BODY requires at least 100 non-whitespace characters.",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
desc?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: "CAR_BODY only. Ignored for THIRD_PARTY.",
|
description: "CAR_BODY only. Ignored for THIRD_PARTY.",
|
||||||
@@ -94,10 +99,13 @@ export class DescriptionDto {
|
|||||||
* front-end can display them consistently regardless of blame type.
|
* front-end can display them consistently regardless of blame type.
|
||||||
*/
|
*/
|
||||||
export class DescriptionV4Dto {
|
export class DescriptionV4Dto {
|
||||||
@ApiProperty({ description: "Accident description" })
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Accident description. Optional for THIRD_PARTY; CAR_BODY requires at least 100 non-whitespace characters.",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
desc?: string;
|
||||||
desc: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import {
|
||||||
|
ArrayMinSize,
|
||||||
|
ArrayUnique,
|
||||||
|
IsArray,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
} from "class-validator";
|
||||||
import { CarDamagePartDto } from "src/claim-request-management/dto/car-part.dto";
|
import { CarDamagePartDto } from "src/claim-request-management/dto/car-part.dto";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6,6 +13,19 @@ import { CarDamagePartDto } from "src/claim-request-management/dto/car-part.dto"
|
|||||||
* All fields optional so expert can submit in one or two steps (e.g. car parts first, then sheba/other).
|
* All fields optional so expert can submit in one or two steps (e.g. car parts first, then sheba/other).
|
||||||
*/
|
*/
|
||||||
export class ExpertCompleteClaimDataDto {
|
export class ExpertCompleteClaimDataDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Selected damaged-part IDs from the live Fanavaran car-components catalog.",
|
||||||
|
example: [9, 10, 30],
|
||||||
|
type: [Number],
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray({ message: "selectedPartIds must be an array" })
|
||||||
|
@ArrayMinSize(1, { message: "At least one part ID must be selected" })
|
||||||
|
@ArrayUnique({ message: "Duplicate part IDs are not allowed" })
|
||||||
|
@IsInt({ each: true, message: "Each selected part ID must be an integer" })
|
||||||
|
selectedPartIds?: number[];
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: "Car part damage selection (same as selectCarPartDamage). Required for first claim data step.",
|
description: "Car part damage selection (same as selectCarPartDamage). Required for first claim data step.",
|
||||||
type: CarDamagePartDto,
|
type: CarDamagePartDto,
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ export class BlameRequest {
|
|||||||
@Prop({ type: Types.ObjectId })
|
@Prop({ type: Types.ObjectId })
|
||||||
initiatedByFieldExpertId?: Types.ObjectId;
|
initiatedByFieldExpertId?: Types.ObjectId;
|
||||||
|
|
||||||
|
/** Branch snapshot of the expert/FileMaker who created this file. */
|
||||||
|
@Prop({ type: Types.ObjectId, index: true })
|
||||||
|
branchId?: Types.ObjectId;
|
||||||
|
|
||||||
/** True when this blame was created by a registrar. */
|
/** True when this blame was created by a registrar. */
|
||||||
@Prop({ default: false })
|
@Prop({ default: false })
|
||||||
registrarInitiated?: boolean;
|
registrarInitiated?: boolean;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export class Person {
|
|||||||
@Prop() nationalCodeOfDriver?: string;
|
@Prop() nationalCodeOfDriver?: string;
|
||||||
@Prop() insurerLicense?: string;
|
@Prop() insurerLicense?: string;
|
||||||
@Prop() driverLicense?: string;
|
@Prop() driverLicense?: string;
|
||||||
|
@Prop() driverLicenseDate?: string;
|
||||||
/** Driving licence type code from the Fanavaran lookup (GET /lookups/driving-licence-types). */
|
/** Driving licence type code from the Fanavaran lookup (GET /lookups/driving-licence-types). */
|
||||||
@Prop() licenseType?: string;
|
@Prop() licenseType?: string;
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ export class InquiryParticipant {
|
|||||||
@Prop({ type: Boolean }) hasDrivingLicense?: boolean;
|
@Prop({ type: Boolean }) hasDrivingLicense?: boolean;
|
||||||
@Prop() licenseNumber?: string;
|
@Prop() licenseNumber?: string;
|
||||||
@Prop() licenseType?: string;
|
@Prop() licenseType?: string;
|
||||||
|
@Prop() driverLicenseDate?: string;
|
||||||
}
|
}
|
||||||
export const InquiryParticipantSchema =
|
export const InquiryParticipantSchema =
|
||||||
SchemaFactory.createForClass(InquiryParticipant);
|
SchemaFactory.createForClass(InquiryParticipant);
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export class CarDetail {
|
|||||||
insurerBirthday?: string;
|
insurerBirthday?: string;
|
||||||
|
|
||||||
driverBirthday?: string | null;
|
driverBirthday?: string | null;
|
||||||
|
driverLicenseDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InsuranceDetail {
|
export class InsuranceDetail {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ describe("inquiry participant persistence", () => {
|
|||||||
nationalCode: "0022324224",
|
nationalCode: "0022324224",
|
||||||
birthday: "1378/04/26",
|
birthday: "1378/04/26",
|
||||||
hasDrivingLicense: true,
|
hasDrivingLicense: true,
|
||||||
|
driverLicenseDate: "1400/01/01",
|
||||||
licenseNumber: "1001011111",
|
licenseNumber: "1001011111",
|
||||||
licenseType: "10",
|
licenseType: "10",
|
||||||
},
|
},
|
||||||
@@ -60,6 +61,9 @@ describe("inquiry participant persistence", () => {
|
|||||||
expect(request.validateSync()).toBeUndefined();
|
expect(request.validateSync()).toBeUndefined();
|
||||||
expect(request.parties[0].person.insurerBirthday).toBe("1378/06/27");
|
expect(request.parties[0].person.insurerBirthday).toBe("1378/06/27");
|
||||||
expect(request.parties[0].person.driverBirthday).toBe("1378/04/26");
|
expect(request.parties[0].person.driverBirthday).toBe("1378/04/26");
|
||||||
|
expect(request.parties[0].participants?.[0].driverLicenseDate).toBe(
|
||||||
|
"1400/01/01",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("recasts populated party subdocuments during an inquiry-audit update", () => {
|
it("recasts populated party subdocuments during an inquiry-audit update", () => {
|
||||||
@@ -89,6 +93,7 @@ describe("inquiry participant persistence", () => {
|
|||||||
nationalCode: "0022324224",
|
nationalCode: "0022324224",
|
||||||
birthday: "1378/04/26",
|
birthday: "1378/04/26",
|
||||||
hasDrivingLicense: true,
|
hasDrivingLicense: true,
|
||||||
|
driverLicenseDate: "1400/01/01",
|
||||||
licenseNumber: "1001011111",
|
licenseNumber: "1001011111",
|
||||||
licenseType: "10",
|
licenseType: "10",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,15 +14,60 @@ import {
|
|||||||
resolveInquiryVehicle,
|
resolveInquiryVehicle,
|
||||||
runCurrentPlateInquiry,
|
runCurrentPlateInquiry,
|
||||||
sanitizeStoredInquiryParticipants,
|
sanitizeStoredInquiryParticipants,
|
||||||
|
validateDriverLicenseDate,
|
||||||
} from "./inquiry-participant-resolver";
|
} from "./inquiry-participant-resolver";
|
||||||
|
|
||||||
describe("inquiry participant resolver", () => {
|
describe("inquiry participant resolver", () => {
|
||||||
|
it("accepts a driver licence date within the ten-year window", () => {
|
||||||
|
expect(validateDriverLicenseDate("2016-09-26", "2026-09-26")).toBe(
|
||||||
|
"2016-09-26",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects future and older-than-ten-year driver licence dates", () => {
|
||||||
|
expect(() =>
|
||||||
|
validateDriverLicenseDate("2026-09-27", "2026-09-26"),
|
||||||
|
).toThrow("نمیتواند در آینده");
|
||||||
|
expect(() =>
|
||||||
|
validateDriverLicenseDate("2016-09-25", "2026-09-26"),
|
||||||
|
).toThrow("بیشتر از ۱۰ سال قبل");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires the date only for a licensed driver", () => {
|
||||||
|
expect(() =>
|
||||||
|
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
|
||||||
|
driver: {
|
||||||
|
nationalCode: "0012345678",
|
||||||
|
birthday: "1370/01/01",
|
||||||
|
hasDrivingLicense: true,
|
||||||
|
licenseNumber: "123456789",
|
||||||
|
licenseType: "1",
|
||||||
|
},
|
||||||
|
vehicleOwner: { sameAs: InquiryParticipantRole.DRIVER },
|
||||||
|
thirdPartyPolicyholder: { sameAs: InquiryParticipantRole.DRIVER },
|
||||||
|
}),
|
||||||
|
).toThrow("تاریخ صدور گواهینامه");
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
|
||||||
|
driver: {
|
||||||
|
nationalCode: "0012345678",
|
||||||
|
birthday: "1370/01/01",
|
||||||
|
hasDrivingLicense: false,
|
||||||
|
},
|
||||||
|
vehicleOwner: { sameAs: InquiryParticipantRole.DRIVER },
|
||||||
|
thirdPartyPolicyholder: { sameAs: InquiryParticipantRole.DRIVER },
|
||||||
|
}),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it("links several roles to one explicitly shared person", () => {
|
it("links several roles to one explicitly shared person", () => {
|
||||||
const resolved = resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
|
const resolved = resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, {
|
||||||
driver: {
|
driver: {
|
||||||
nationalCode: "0012345678",
|
nationalCode: "0012345678",
|
||||||
birthday: "1370/01/01",
|
birthday: "1370/01/01",
|
||||||
hasDrivingLicense: true,
|
hasDrivingLicense: true,
|
||||||
|
driverLicenseDate: "1400/01/01",
|
||||||
licenseNumber: "123456789",
|
licenseNumber: "123456789",
|
||||||
licenseType: "1",
|
licenseType: "1",
|
||||||
},
|
},
|
||||||
@@ -231,6 +276,7 @@ describe("inquiry participant resolver", () => {
|
|||||||
nationalCode: "0012345678",
|
nationalCode: "0012345678",
|
||||||
birthday: "1370/01/01",
|
birthday: "1370/01/01",
|
||||||
hasDrivingLicense: true,
|
hasDrivingLicense: true,
|
||||||
|
driverLicenseDate: "1400/01/01",
|
||||||
licenseNumber: "D-1",
|
licenseNumber: "D-1",
|
||||||
licenseType: "1",
|
licenseType: "1",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface ResolvedInquiryParticipant {
|
|||||||
hasDrivingLicense?: boolean;
|
hasDrivingLicense?: boolean;
|
||||||
licenseNumber?: string;
|
licenseNumber?: string;
|
||||||
licenseType?: string;
|
licenseType?: string;
|
||||||
|
driverLicenseDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InquiryParticipantRoleAssignments {
|
export interface InquiryParticipantRoleAssignments {
|
||||||
@@ -38,6 +39,7 @@ export interface NormalizedInquirySubmission<T extends Record<string, any>> {
|
|||||||
nationalCodeOfDriver: string;
|
nationalCodeOfDriver: string;
|
||||||
driverBirthday: string;
|
driverBirthday: string;
|
||||||
driverLicense?: string;
|
driverLicense?: string;
|
||||||
|
driverLicenseDate?: string;
|
||||||
licenseType?: string;
|
licenseType?: string;
|
||||||
userNoCertificate?: boolean;
|
userNoCertificate?: boolean;
|
||||||
nationalCodeOfInsurer: string;
|
nationalCodeOfInsurer: string;
|
||||||
@@ -215,6 +217,29 @@ function requiredIdentity(
|
|||||||
"شماره و نوع گواهینامه برای راننده دارای گواهینامه الزامی است.",
|
"شماره و نوع گواهینامه برای راننده دارای گواهینامه الزامی است.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
role !== InquiryParticipantRole.DRIVER &&
|
||||||
|
Object.prototype.hasOwnProperty.call(input, "driverLicenseDate")
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"تاریخ صدور گواهینامه فقط برای راننده قابل ثبت است.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const driverLicenseDate =
|
||||||
|
role === InquiryParticipantRole.DRIVER && input.hasDrivingLicense === true
|
||||||
|
? validateDriverLicenseDate(input.driverLicenseDate)
|
||||||
|
: undefined;
|
||||||
|
if (
|
||||||
|
role === InquiryParticipantRole.DRIVER &&
|
||||||
|
input.hasDrivingLicense === false &&
|
||||||
|
Object.prototype.hasOwnProperty.call(input, "driverLicenseDate") &&
|
||||||
|
input.driverLicenseDate != null &&
|
||||||
|
String(input.driverLicenseDate).trim() !== ""
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"تاریخ صدور گواهینامه برای راننده فاقد گواهینامه قابل ثبت نیست.",
|
||||||
|
);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
participantId: role,
|
participantId: role,
|
||||||
nationalCode,
|
nationalCode,
|
||||||
@@ -225,9 +250,38 @@ function requiredIdentity(
|
|||||||
: {}),
|
: {}),
|
||||||
...(input.licenseNumber ? { licenseNumber: input.licenseNumber } : {}),
|
...(input.licenseNumber ? { licenseNumber: input.licenseNumber } : {}),
|
||||||
...(input.licenseType ? { licenseType: input.licenseType } : {}),
|
...(input.licenseType ? { licenseType: input.licenseType } : {}),
|
||||||
|
...(driverLicenseDate ? { driverLicenseDate } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Validate a driver licence issue date against the current Iran date. */
|
||||||
|
export function validateDriverLicenseDate(
|
||||||
|
value: unknown,
|
||||||
|
todayGregorian: string = gregorianDateInIran(new Date()),
|
||||||
|
): string {
|
||||||
|
const raw = String(value ?? "").trim();
|
||||||
|
const parsed = jalaliToGregorianDate(raw);
|
||||||
|
if (!parsed) {
|
||||||
|
throw new BadRequestException("تاریخ صدور گواهینامه راننده نامعتبر است.");
|
||||||
|
}
|
||||||
|
const today = jalaliToGregorianDate(todayGregorian) ?? todayGregorian;
|
||||||
|
const todayDate = new Date(`${today}T00:00:00Z`);
|
||||||
|
const minimumDate = new Date(todayDate);
|
||||||
|
minimumDate.setUTCFullYear(minimumDate.getUTCFullYear() - 10);
|
||||||
|
const minimum = minimumDate.toISOString().slice(0, 10);
|
||||||
|
if (parsed > today) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"تاریخ صدور گواهینامه راننده نمیتواند در آینده باشد.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (parsed < minimum) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"تاریخ صدور گواهینامه راننده نباید بیشتر از ۱۰ سال قبل باشد.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveInquiryParticipants(
|
export function resolveInquiryParticipants(
|
||||||
caseType: BlameRequestType,
|
caseType: BlameRequestType,
|
||||||
input: Partial<InquiryParticipantFieldsDto> & Record<string, any>,
|
input: Partial<InquiryParticipantFieldsDto> & Record<string, any>,
|
||||||
@@ -413,6 +467,7 @@ const LEGACY_INQUIRY_FIELDS = [
|
|||||||
"nationalCodeOfDriver",
|
"nationalCodeOfDriver",
|
||||||
"driverBirthday",
|
"driverBirthday",
|
||||||
"driverLicense",
|
"driverLicense",
|
||||||
|
"driverLicenseDate",
|
||||||
"licenseType",
|
"licenseType",
|
||||||
"nationalCodeOfInsurer",
|
"nationalCodeOfInsurer",
|
||||||
"insurerBirthday",
|
"insurerBirthday",
|
||||||
@@ -555,6 +610,7 @@ export function normalizeInquirySubmission<T extends Record<string, any>>(
|
|||||||
nationalCodeOfDriver: driver.nationalCode,
|
nationalCodeOfDriver: driver.nationalCode,
|
||||||
driverBirthday: driver.birthday,
|
driverBirthday: driver.birthday,
|
||||||
driverLicense: driver.licenseNumber,
|
driverLicense: driver.licenseNumber,
|
||||||
|
driverLicenseDate: driver.driverLicenseDate,
|
||||||
licenseType: driver.licenseType,
|
licenseType: driver.licenseType,
|
||||||
userNoCertificate:
|
userNoCertificate:
|
||||||
driver.hasDrivingLicense == null
|
driver.hasDrivingLicense == null
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ describe("RequestManagementService FileReviewer inbox", () => {
|
|||||||
(service as any).claimCaseDbService = {
|
(service as any).claimCaseDbService = {
|
||||||
find: jest.fn().mockResolvedValue([]),
|
find: jest.fn().mockResolvedValue([]),
|
||||||
};
|
};
|
||||||
|
(service as any).fanavaranLocationService = {
|
||||||
|
assertMakerReviewerBranchCompatible: jest.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
return { service, blameRequestDbService };
|
return { service, blameRequestDbService };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +92,22 @@ describe("RequestManagementService FileReviewer inbox", () => {
|
|||||||
expect(result.list).toEqual([]);
|
expect(result.list).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not list an open file from another branch", async () => {
|
||||||
|
const { service } = createService([sealedFile]);
|
||||||
|
(service as any).fanavaranLocationService
|
||||||
|
.assertMakerReviewerBranchCompatible.mockRejectedValue(
|
||||||
|
new Error("This file belongs to another branch."),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await service.getMyFileReviewerFiles({
|
||||||
|
sub: String(reviewerId),
|
||||||
|
role: RoleEnum.FILE_REVIEWER,
|
||||||
|
clientKey: String(clientId),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.list).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("sorts and paginates the reviewer inbox with the shared list contract", async () => {
|
it("sorts and paginates the reviewer inbox with the shared list contract", async () => {
|
||||||
const olderFile = {
|
const olderFile = {
|
||||||
...sealedFile,
|
...sealedFile,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
|
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||||
import { RequestManagementService } from "./request-management.service";
|
import { RequestManagementService } from "./request-management.service";
|
||||||
|
|
||||||
describe("RequestManagementService policyholder inquiry routing", () => {
|
describe("RequestManagementService policyholder inquiry routing", () => {
|
||||||
@@ -138,7 +139,7 @@ describe("RequestManagementService policyholder inquiry routing", () => {
|
|||||||
it("queries VIN with the third-party policyholder", async () => {
|
it("queries VIN with the third-party policyholder", async () => {
|
||||||
const service = Object.create(RequestManagementService.prototype) as any;
|
const service = Object.create(RequestManagementService.prototype) as any;
|
||||||
service.sandHubService = {
|
service.sandHubService = {
|
||||||
getPolicyByChassisInquiry: jest.fn().mockResolvedValue({
|
getCarByChassisInquiry: jest.fn().mockResolvedValue({
|
||||||
raw: {},
|
raw: {},
|
||||||
mapped: { CompanyName: "پارسیان" },
|
mapped: { CompanyName: "پارسیان" },
|
||||||
}),
|
}),
|
||||||
@@ -151,16 +152,74 @@ describe("RequestManagementService policyholder inquiry routing", () => {
|
|||||||
await service.getThirdPartyVinInquiry(submission);
|
await service.getThirdPartyVinInquiry(submission);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
service.sandHubService.getPolicyByChassisInquiry,
|
service.sandHubService.getCarByChassisInquiry,
|
||||||
).toHaveBeenCalledTimes(1);
|
).toHaveBeenCalledTimes(1);
|
||||||
expect(
|
expect(
|
||||||
service.sandHubService.getPolicyByChassisInquiry,
|
service.sandHubService.getCarByChassisInquiry,
|
||||||
).toHaveBeenCalledWith(
|
).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
nationalCode: "0022222222",
|
nationalCode: "0022222222",
|
||||||
chassis: vehicle.vin,
|
chassisNo: vehicle.vin,
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("runs both third-party and car-body inquiries for a car-body VIN", async () => {
|
||||||
|
const service = Object.create(RequestManagementService.prototype) as any;
|
||||||
|
service.getThirdPartyVinInquiry = jest.fn().mockResolvedValue({
|
||||||
|
raw: { success: true },
|
||||||
|
mapped: {
|
||||||
|
CompanyCode: "8",
|
||||||
|
CompanyName: "بیمه پارسیان",
|
||||||
|
PrntPlcyCmpDocNo: "TP-1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
service.sandHubService = {
|
||||||
|
getCarBodyInquiry: jest.fn().mockResolvedValue({
|
||||||
|
source: "ESG_CAR_BODY_VIN_INQUIRY",
|
||||||
|
raw: { policyId: 123 },
|
||||||
|
mapped: {
|
||||||
|
policyNumber: "BODY-1",
|
||||||
|
companyId: "8",
|
||||||
|
CompanyName: "بیمه پارسیان",
|
||||||
|
VinNumberField: vehicle.vin,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
service.clientService = {
|
||||||
|
findOrCreateClientByCompanyCode: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ _id: "client-third-party" }),
|
||||||
|
};
|
||||||
|
service.resolveCarBodyPolicyClientId = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue("client-car-body");
|
||||||
|
service.runParticipantPersonalInquiries = jest.fn().mockResolvedValue(undefined);
|
||||||
|
service.runVehicleOwnershipInquiry = jest.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const req: any = {
|
||||||
|
_id: "car-body-vin-request",
|
||||||
|
type: BlameRequestType.CAR_BODY,
|
||||||
|
inquiries: {},
|
||||||
|
};
|
||||||
|
const party: any = { person: {}, vehicle: {}, insurance: {} };
|
||||||
|
|
||||||
|
await service.runPartyInquiriesVinV3Internal(
|
||||||
|
req,
|
||||||
|
participantInput(BlameRequestType.CAR_BODY),
|
||||||
|
PartyRole.FIRST,
|
||||||
|
party,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(service.getThirdPartyVinInquiry).toHaveBeenCalledTimes(1);
|
||||||
|
expect(service.sandHubService.getCarBodyInquiry).toHaveBeenCalledWith({
|
||||||
|
nationalCodeOfInsurer: "0033333333",
|
||||||
|
plate: vehicle.vin,
|
||||||
|
});
|
||||||
|
expect(req.inquiries.thirdParty.has).toBe(true);
|
||||||
|
expect(req.inquiries.carBody.has).toBe(true);
|
||||||
|
expect(party.insurance.policyNumber).toBe("TP-1");
|
||||||
|
expect(party.insurance.carBodyInsurance.policyNumber).toBe("BODY-1");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.
|
|||||||
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
|
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
|
||||||
import { isOtpExpiryActive } from "src/helpers/user-otp-expiry";
|
import { isOtpExpiryActive } from "src/helpers/user-otp-expiry";
|
||||||
import { parseIranLocalDateTime } from "src/helpers/iran-datetime";
|
import { parseIranLocalDateTime } from "src/helpers/iran-datetime";
|
||||||
|
import { jalaliToGregorianDate } from "src/helpers/date-jalali";
|
||||||
import {
|
import {
|
||||||
applyListQueryV2,
|
applyListQueryV2,
|
||||||
isInListDateRange,
|
isInListDateRange,
|
||||||
@@ -210,6 +211,19 @@ export class RequestManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Keep eligibility rejections distinct from an unavailable CAR_BODY provider. */
|
/** Keep eligibility rejections distinct from an unavailable CAR_BODY provider. */
|
||||||
|
private assertCarBodyDescriptionMinimum(
|
||||||
|
requestType: BlameRequestType | string | undefined,
|
||||||
|
description: unknown,
|
||||||
|
): void {
|
||||||
|
if (requestType !== BlameRequestType.CAR_BODY) return;
|
||||||
|
const length = String(description ?? "").trim().length;
|
||||||
|
if (length < 100) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"CAR_BODY description must contain at least 100 non-whitespace characters.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private throwCarBodyInquiryFailure(
|
private throwCarBodyInquiryFailure(
|
||||||
err: unknown,
|
err: unknown,
|
||||||
context: "carBodyPlate" | "carBodyVin" = "carBodyPlate",
|
context: "carBodyPlate" | "carBodyVin" = "carBodyPlate",
|
||||||
@@ -465,10 +479,10 @@ export class RequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const subjects = resolveInquirySubjects(submission);
|
const subjects = resolveInquirySubjects(submission);
|
||||||
const result = await this.sandHubService.getPolicyByChassisInquiry(
|
const result = await this.sandHubService.getCarByChassisInquiry(
|
||||||
{
|
{
|
||||||
nationalCode: subjects.thirdPartyPolicyNationalCode,
|
nationalCode: subjects.thirdPartyPolicyNationalCode,
|
||||||
chassis,
|
chassisNo: chassis,
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
@@ -1125,6 +1139,22 @@ export class RequestManagementService {
|
|||||||
private readonly fileReviewerDbService: FileReviewerDbService,
|
private readonly fileReviewerDbService: FileReviewerDbService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async assertFileReviewerBranchAccess(
|
||||||
|
file: {
|
||||||
|
initiatedByFieldExpertId?: unknown;
|
||||||
|
branchId?: unknown;
|
||||||
|
},
|
||||||
|
fileReviewerId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.fanavaranLocationService.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: file?.initiatedByFieldExpertId
|
||||||
|
? String(file.initiatedByFieldExpertId)
|
||||||
|
: null,
|
||||||
|
fileReviewerId,
|
||||||
|
caseBranchId: file?.branchId ? String(file.branchId) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reject CAR_BODY submissions whose accident is older than the per-client
|
* Reject CAR_BODY submissions whose accident is older than the per-client
|
||||||
* window (see `ClientService.getCarBodyAccidentMaxAgeDays`). The check is
|
* window (see `ClientService.getCarBodyAccidentMaxAgeDays`). The check is
|
||||||
@@ -1171,7 +1201,8 @@ export class RequestManagementService {
|
|||||||
if (ageMs < 0) {
|
if (ageMs < 0) {
|
||||||
throw new BadRequestException({
|
throw new BadRequestException({
|
||||||
code: "CAR_BODY_ACCIDENT_DATE_IN_FUTURE",
|
code: "CAR_BODY_ACCIDENT_DATE_IN_FUTURE",
|
||||||
message: "Accident date cannot be in the future.",
|
message: "Accident date and time cannot be in the future.",
|
||||||
|
messageFa: "تاریخ و ساعت حادثه نمیتواند در آینده باشد.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1202,7 +1233,47 @@ export class RequestManagementService {
|
|||||||
date: Date | string,
|
date: Date | string,
|
||||||
time?: string,
|
time?: string,
|
||||||
): Date | null {
|
): Date | null {
|
||||||
return parseIranLocalDateTime(date, time);
|
const normalizedDate =
|
||||||
|
typeof date === "string" ? jalaliToGregorianDate(date) ?? date : date;
|
||||||
|
return parseIranLocalDateTime(normalizedDate, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Universal write guard for accident timestamps. UI restrictions are only a
|
||||||
|
* convenience; every endpoint that persists accidentDate/accidentTime must
|
||||||
|
* reject an invalid or future Iran-local timestamp as well.
|
||||||
|
*/
|
||||||
|
private assertAccidentDateTimeNotInFuture(params: {
|
||||||
|
accidentDate: Date | string | null | undefined;
|
||||||
|
accidentTime?: string | null;
|
||||||
|
}): void {
|
||||||
|
const { accidentDate, accidentTime } = params;
|
||||||
|
if (accidentDate == null && !accidentTime) return;
|
||||||
|
|
||||||
|
if (accidentDate == null || !String(accidentTime ?? "").trim()) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
code: "ACCIDENT_DATE_TIME_INVALID",
|
||||||
|
message: "A valid accident date and time are required together.",
|
||||||
|
messageFa: "تاریخ و ساعت معتبر حادثه باید با هم وارد شوند.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const instant = this.parseAccidentInstant(accidentDate, accidentTime!);
|
||||||
|
if (!instant || Number.isNaN(instant.getTime())) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
code: "ACCIDENT_DATE_TIME_INVALID",
|
||||||
|
message: "Invalid accident date or time.",
|
||||||
|
messageFa: "تاریخ یا ساعت حادثه معتبر نیست.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instant.getTime() > Date.now()) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
code: "ACCIDENT_DATE_TIME_IN_FUTURE",
|
||||||
|
message: "Accident date and time cannot be in the future.",
|
||||||
|
messageFa: "تاریخ و ساعت حادثه نمیتواند در آینده باشد.",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1963,6 +2034,7 @@ export class RequestManagementService {
|
|||||||
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
||||||
party.person.insurerLicense = body.insurerLicense;
|
party.person.insurerLicense = body.insurerLicense;
|
||||||
party.person.driverLicense = body.driverLicense;
|
party.person.driverLicense = body.driverLicense;
|
||||||
|
party.person.driverLicenseDate = body.driverLicenseDate;
|
||||||
party.person.driverIsInsurer = body.driverIsInsurer;
|
party.person.driverIsInsurer = body.driverIsInsurer;
|
||||||
party.person.isNewCar = body.isNewCar;
|
party.person.isNewCar = body.isNewCar;
|
||||||
party.person.userNoCertificate = body.userNoCertificate;
|
party.person.userNoCertificate = body.userNoCertificate;
|
||||||
@@ -2142,7 +2214,7 @@ export class RequestManagementService {
|
|||||||
* V2 initial-form submitted with a VIN/chassis number instead of a plate.
|
* V2 initial-form submitted with a VIN/chassis number instead of a plate.
|
||||||
*
|
*
|
||||||
* Performs:
|
* Performs:
|
||||||
* 1. ESG `/inquiry/policyByChassis` (or its mock when `vinChassis` is off)
|
* 1. ESG two-factor `/inquiry/carByChassis` (or its mock when `vinChassis` is off)
|
||||||
* 2. Personal identity inquiry (same as the plate path)
|
* 2. Personal identity inquiry (same as the plate path)
|
||||||
*
|
*
|
||||||
* The inquiry result is mapped through the same `mapEsgPolicyByPlateToOldFormat`
|
* The inquiry result is mapped through the same `mapEsgPolicyByPlateToOldFormat`
|
||||||
@@ -2236,7 +2308,7 @@ export class RequestManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- External inquiry: ESG policyByChassis ----
|
// ---- External inquiry: ESG two-factor carByChassis ----
|
||||||
const inquiryClientId = party.person?.clientId
|
const inquiryClientId = party.person?.clientId
|
||||||
? String(party.person.clientId)
|
? String(party.person.clientId)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -2256,10 +2328,10 @@ export class RequestManagementService {
|
|||||||
inquiryRaw = inquiry.raw;
|
inquiryRaw = inquiry.raw;
|
||||||
inquiryMapped = inquiry.mapped;
|
inquiryMapped = inquiry.mapped;
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[ESG] policyByChassis raw for request=${req._id}: ${JSON.stringify(inquiryRaw)}`,
|
`[ESG] carByChassis raw for request=${req._id}: ${JSON.stringify(inquiryRaw)}`,
|
||||||
);
|
);
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[ESG] policyByChassis mapped for request=${req._id}: ${JSON.stringify(inquiryMapped)}`,
|
`[ESG] carByChassis mapped for request=${req._id}: ${JSON.stringify(inquiryMapped)}`,
|
||||||
);
|
);
|
||||||
this.recordPartyCaseInquiryStatus(
|
this.recordPartyCaseInquiryStatus(
|
||||||
req,
|
req,
|
||||||
@@ -2274,11 +2346,11 @@ export class RequestManagementService {
|
|||||||
);
|
);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`[ESG] policyByChassis failed for request=${req._id}: ${err?.message || err}`,
|
`[ESG] carByChassis failed for request=${req._id}: ${err?.message || err}`,
|
||||||
);
|
);
|
||||||
if (err?.response) {
|
if (err?.response) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`[ESG] policyByChassis response for request=${req._id}: status=${
|
`[ESG] carByChassis response for request=${req._id}: status=${
|
||||||
err.response.status
|
err.response.status
|
||||||
}, data=${JSON.stringify(err.response.data)}`,
|
}, data=${JSON.stringify(err.response.data)}`,
|
||||||
);
|
);
|
||||||
@@ -2300,7 +2372,7 @@ export class RequestManagementService {
|
|||||||
|
|
||||||
if (inquiryMapped?.Error) {
|
if (inquiryMapped?.Error) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`[ESG] policyByChassis error for request=${req._id}: ${JSON.stringify(inquiryMapped.Error)}`,
|
`[ESG] carByChassis error for request=${req._id}: ${JSON.stringify(inquiryMapped.Error)}`,
|
||||||
);
|
);
|
||||||
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, false, {
|
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, false, {
|
||||||
source: "ESG_VIN_INQUIRY",
|
source: "ESG_VIN_INQUIRY",
|
||||||
@@ -2390,6 +2462,7 @@ export class RequestManagementService {
|
|||||||
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
||||||
party.person.insurerLicense = body.insurerLicense;
|
party.person.insurerLicense = body.insurerLicense;
|
||||||
party.person.driverLicense = body.driverLicense;
|
party.person.driverLicense = body.driverLicense;
|
||||||
|
party.person.driverLicenseDate = body.driverLicenseDate;
|
||||||
party.person.driverIsInsurer = body.driverIsInsurer;
|
party.person.driverIsInsurer = body.driverIsInsurer;
|
||||||
party.person.isNewCar = body.isNewCar;
|
party.person.isNewCar = body.isNewCar;
|
||||||
party.person.userNoCertificate = body.userNoCertificate;
|
party.person.userNoCertificate = body.userNoCertificate;
|
||||||
@@ -2759,6 +2832,8 @@ export class RequestManagementService {
|
|||||||
this.isBlameOnBehalfActor(req, user),
|
this.isBlameOnBehalfActor(req, user),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.assertCarBodyDescriptionMinimum(req.type, body.desc);
|
||||||
|
|
||||||
// Set userId for party if not already set (important for second party)
|
// Set userId for party if not already set (important for second party)
|
||||||
if (!party.person) party.person = {} as any;
|
if (!party.person) party.person = {} as any;
|
||||||
if (
|
if (
|
||||||
@@ -4062,6 +4137,8 @@ export class RequestManagementService {
|
|||||||
throw new NotFoundException("Request not found");
|
throw new NotFoundException("Request not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.assertCarBodyDescriptionMinimum(request.type, body.desc);
|
||||||
|
|
||||||
// --- First Party Flow ---
|
// --- First Party Flow ---
|
||||||
if (request.firstPartyDetails.firstPartyId == user.sub) {
|
if (request.firstPartyDetails.firstPartyId == user.sub) {
|
||||||
// Build base update payload with proper MongoDB operators
|
// Build base update payload with proper MongoDB operators
|
||||||
@@ -4106,6 +4183,10 @@ export class RequestManagementService {
|
|||||||
|
|
||||||
// Add CAR_BODY specific fields if type is CAR_BODY
|
// Add CAR_BODY specific fields if type is CAR_BODY
|
||||||
if (request.type === "CAR_BODY") {
|
if (request.type === "CAR_BODY") {
|
||||||
|
this.assertAccidentDateTimeNotInFuture({
|
||||||
|
accidentDate: body.accidentDate,
|
||||||
|
accidentTime: body.accidentTime,
|
||||||
|
});
|
||||||
if (body.accidentDate) {
|
if (body.accidentDate) {
|
||||||
updatePayload.$set["firstPartyDetails.firstPartyFile.accidentDate"] =
|
updatePayload.$set["firstPartyDetails.firstPartyFile.accidentDate"] =
|
||||||
body.accidentDate;
|
body.accidentDate;
|
||||||
@@ -5378,6 +5459,15 @@ export class RequestManagementService {
|
|||||||
throw new ForbiddenException("FileReviewer account not found.");
|
throw new ForbiddenException("FileReviewer account not found.");
|
||||||
}
|
}
|
||||||
assertFileReviewerCanReviewBlameType(fileReviewer, req.type);
|
assertFileReviewerCanReviewBlameType(fileReviewer, req.type);
|
||||||
|
await this.fanavaranLocationService.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: req?.initiatedByFieldExpertId
|
||||||
|
? String(req.initiatedByFieldExpertId)
|
||||||
|
: null,
|
||||||
|
fileReviewerId: String(expert.sub),
|
||||||
|
caseBranchId: (req as any)?.branchId
|
||||||
|
? String((req as any).branchId)
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
|
||||||
if (!assignedId) {
|
if (!assignedId) {
|
||||||
// Atomically claim — ignore if another reviewer won the race (they would have
|
// Atomically claim — ignore if another reviewer won the race (they would have
|
||||||
@@ -5687,12 +5777,19 @@ export class RequestManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isFileMakerRole = (expert as any)?.role === RoleEnum.FILE_MAKER;
|
const isFileMakerRole = (expert as any)?.role === RoleEnum.FILE_MAKER;
|
||||||
|
let expertBranchId: Types.ObjectId | undefined;
|
||||||
if (isFileMakerRole) {
|
if (isFileMakerRole) {
|
||||||
const fileMaker = await this.fileMakerDbService.findById(String(expert.sub));
|
const fileMaker = await this.fileMakerDbService.findById(String(expert.sub));
|
||||||
if (!fileMaker) {
|
if (!fileMaker) {
|
||||||
throw new ForbiddenException("FileMaker account not found.");
|
throw new ForbiddenException("FileMaker account not found.");
|
||||||
}
|
}
|
||||||
assertFileMakerCanCreateBlameType(fileMaker, type);
|
assertFileMakerCanCreateBlameType(fileMaker, type);
|
||||||
|
if (!fileMaker.branchId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"FileMaker account is not assigned to a branch.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expertBranchId = new Types.ObjectId(String(fileMaker.branchId));
|
||||||
}
|
}
|
||||||
const created = await this.blameRequestDbService.create({
|
const created = await this.blameRequestDbService.create({
|
||||||
publicId,
|
publicId,
|
||||||
@@ -5708,6 +5805,7 @@ export class RequestManagementService {
|
|||||||
history: [],
|
history: [],
|
||||||
expertInitiated: true,
|
expertInitiated: true,
|
||||||
initiatedByFieldExpertId: expertId,
|
initiatedByFieldExpertId: expertId,
|
||||||
|
...(expertBranchId ? { branchId: expertBranchId } : {}),
|
||||||
creationMethod: dto.creationMethod,
|
creationMethod: dto.creationMethod,
|
||||||
filledBy:
|
filledBy:
|
||||||
dto.creationMethod === CreationMethod.IN_PERSON
|
dto.creationMethod === CreationMethod.IN_PERSON
|
||||||
@@ -6532,11 +6630,30 @@ export class RequestManagementService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const obj = req.toObject();
|
const obj = req.toObject();
|
||||||
|
const partyIdentities = Array.isArray(req.parties)
|
||||||
|
? req.parties.map((entry: any) => ({
|
||||||
|
phoneNumber: entry?.person?.phoneNumber,
|
||||||
|
nationalCode: entry?.person?.nationalCode,
|
||||||
|
nationalCodeOfInsurer: entry?.person?.nationalCodeOfInsurer,
|
||||||
|
nationalCodeOfDriver: entry?.person?.nationalCodeOfDriver,
|
||||||
|
participants: (
|
||||||
|
Array.isArray(entry?.participants)
|
||||||
|
? entry.participants
|
||||||
|
: entry?.inquiryParticipants
|
||||||
|
)?.map((participant: any) => ({
|
||||||
|
phoneNumber: participant?.phoneNumber,
|
||||||
|
nationalCode: participant?.nationalCode,
|
||||||
|
nationalCodeOfInsurer: participant?.nationalCodeOfInsurer,
|
||||||
|
nationalCodeOfDriver: participant?.nationalCodeOfDriver,
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
delete obj.parties;
|
delete obj.parties;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...obj,
|
...obj,
|
||||||
|
partyIdentities,
|
||||||
userSide: party?.role ?? null,
|
userSide: party?.role ?? null,
|
||||||
initiatedByMe: isInitiator,
|
initiatedByMe: isInitiator,
|
||||||
unifiedFileStatus: resolveUnifiedFileStatus({
|
unifiedFileStatus: resolveUnifiedFileStatus({
|
||||||
@@ -7012,9 +7129,10 @@ export class RequestManagementService {
|
|||||||
if (!formData.carBodyForm) {
|
if (!formData.carBodyForm) {
|
||||||
throw new BadRequestException("carBodyForm is required.");
|
throw new BadRequestException("carBodyForm is required.");
|
||||||
}
|
}
|
||||||
if (!formData?.expertDescription?.desc) {
|
this.assertCarBodyDescriptionMinimum(
|
||||||
throw new BadRequestException("expertDescription.desc is required.");
|
req.type,
|
||||||
}
|
formData?.expertDescription?.desc,
|
||||||
|
);
|
||||||
|
|
||||||
const firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
const firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
||||||
formData.firstPartyPhoneNumber,
|
formData.firstPartyPhoneNumber,
|
||||||
@@ -7193,6 +7311,7 @@ export class RequestManagementService {
|
|||||||
nationalCodeOfDriver: firstPartyPlate.nationalCodeOfDriver,
|
nationalCodeOfDriver: firstPartyPlate.nationalCodeOfDriver,
|
||||||
insurerLicense: firstPartyPlate.insurerLicense,
|
insurerLicense: firstPartyPlate.insurerLicense,
|
||||||
driverLicense: firstPartyPlate.driverLicense,
|
driverLicense: firstPartyPlate.driverLicense,
|
||||||
|
driverLicenseDate: firstPartyPlate.driverLicenseDate,
|
||||||
driverIsInsurer: firstPartyPlate.driverIsInsurer,
|
driverIsInsurer: firstPartyPlate.driverIsInsurer,
|
||||||
isNewCar: firstPartyPlate.isNewCar,
|
isNewCar: firstPartyPlate.isNewCar,
|
||||||
userNoCertificate: firstPartyPlate.userNoCertificate,
|
userNoCertificate: firstPartyPlate.userNoCertificate,
|
||||||
@@ -7481,6 +7600,7 @@ export class RequestManagementService {
|
|||||||
nationalCodeOfDriver: plateDto.nationalCodeOfDriver,
|
nationalCodeOfDriver: plateDto.nationalCodeOfDriver,
|
||||||
insurerLicense: plateDto.insurerLicense,
|
insurerLicense: plateDto.insurerLicense,
|
||||||
driverLicense: plateDto.driverLicense,
|
driverLicense: plateDto.driverLicense,
|
||||||
|
driverLicenseDate: plateDto.driverLicenseDate,
|
||||||
driverIsInsurer: plateDto.driverIsInsurer,
|
driverIsInsurer: plateDto.driverIsInsurer,
|
||||||
isNewCar: plateDto.isNewCar,
|
isNewCar: plateDto.isNewCar,
|
||||||
userNoCertificate: plateDto.userNoCertificate,
|
userNoCertificate: plateDto.userNoCertificate,
|
||||||
@@ -8207,6 +8327,7 @@ export class RequestManagementService {
|
|||||||
carName: sandHubReport?.MapTypNam || sandHubReport?.CarName,
|
carName: sandHubReport?.MapTypNam || sandHubReport?.CarName,
|
||||||
insurerBirthday: firstPartyPlate.insurerBirthday,
|
insurerBirthday: firstPartyPlate.insurerBirthday,
|
||||||
driverBirthday: firstPartyPlate.driverBirthday,
|
driverBirthday: firstPartyPlate.driverBirthday,
|
||||||
|
driverLicenseDate: firstPartyPlate.driverLicenseDate,
|
||||||
carType: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`,
|
carType: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`,
|
||||||
isNewCar: firstPartyPlate.isNewCar,
|
isNewCar: firstPartyPlate.isNewCar,
|
||||||
};
|
};
|
||||||
@@ -8553,6 +8674,16 @@ export class RequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.assertCarBodyDescriptionMinimum(
|
||||||
|
request.type,
|
||||||
|
formData.firstPartyDescription?.desc,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.assertAccidentDateTimeNotInFuture({
|
||||||
|
accidentDate: formData.firstPartyDescription?.accidentDate,
|
||||||
|
accidentTime: formData.firstPartyDescription?.accidentTime,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get or create user for first party phone number
|
// Get or create user for first party phone number
|
||||||
const firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
const firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
||||||
@@ -8688,6 +8819,7 @@ export class RequestManagementService {
|
|||||||
carName: sandHubReport?.MapTypNam || sandHubReport?.CarName,
|
carName: sandHubReport?.MapTypNam || sandHubReport?.CarName,
|
||||||
insurerBirthday: firstPartyPlate.insurerBirthday,
|
insurerBirthday: firstPartyPlate.insurerBirthday,
|
||||||
driverBirthday: firstPartyPlate.driverBirthday,
|
driverBirthday: firstPartyPlate.driverBirthday,
|
||||||
|
driverLicenseDate: firstPartyPlate.driverLicenseDate,
|
||||||
carType: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`,
|
carType: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`,
|
||||||
isNewCar: firstPartyPlate.isNewCar,
|
isNewCar: firstPartyPlate.isNewCar,
|
||||||
};
|
};
|
||||||
@@ -10023,6 +10155,8 @@ export class RequestManagementService {
|
|||||||
party.person.insurerLicense = partyData.insurerLicense;
|
party.person.insurerLicense = partyData.insurerLicense;
|
||||||
if (partyData.driverLicense)
|
if (partyData.driverLicense)
|
||||||
party.person.driverLicense = partyData.driverLicense;
|
party.person.driverLicense = partyData.driverLicense;
|
||||||
|
if ((partyData as any).driverLicenseDate)
|
||||||
|
party.person.driverLicenseDate = (partyData as any).driverLicenseDate;
|
||||||
if ((partyData as any).licenseType)
|
if ((partyData as any).licenseType)
|
||||||
(party.person as any).licenseType = (partyData as any).licenseType;
|
(party.person as any).licenseType = (partyData as any).licenseType;
|
||||||
|
|
||||||
@@ -10645,7 +10779,7 @@ export class RequestManagementService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* VIN variant of `runInquiriesV3`.
|
* VIN variant of `runInquiriesV3`.
|
||||||
* Identical flow, but calls `getPolicyByChassisInquiry` instead of
|
* Identical flow, but calls the two-factor `getCarByChassisInquiry` instead of
|
||||||
* `getTejaratBlockInquiry` for the primary policy lookup.
|
* `getTejaratBlockInquiry` for the primary policy lookup.
|
||||||
*/
|
*/
|
||||||
async runInquiriesVinV3(
|
async runInquiriesVinV3(
|
||||||
@@ -10788,7 +10922,7 @@ export class RequestManagementService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* VIN variant of `runPartyInquiriesV3Internal`.
|
* VIN variant of `runPartyInquiriesV3Internal`.
|
||||||
* Calls `getPolicyByChassisInquiry` (ESG chassis lookup) instead of
|
* Calls `getCarByChassisInquiry` (ESG two-factor chassis lookup) instead of
|
||||||
* `getTejaratBlockInquiry` (plate-based). All other inquiries (personal,
|
* `getTejaratBlockInquiry` (plate-based). All other inquiries (personal,
|
||||||
* driving licence, car-body for CAR_BODY) are unchanged.
|
* driving licence, car-body for CAR_BODY) are unchanged.
|
||||||
*/
|
*/
|
||||||
@@ -10931,6 +11065,8 @@ export class RequestManagementService {
|
|||||||
party.person.insurerLicense = partyData.insurerLicense;
|
party.person.insurerLicense = partyData.insurerLicense;
|
||||||
if (partyData.driverLicense)
|
if (partyData.driverLicense)
|
||||||
party.person.driverLicense = partyData.driverLicense;
|
party.person.driverLicense = partyData.driverLicense;
|
||||||
|
if ((partyData as any).driverLicenseDate)
|
||||||
|
party.person.driverLicenseDate = (partyData as any).driverLicenseDate;
|
||||||
if ((partyData as any).licenseType)
|
if ((partyData as any).licenseType)
|
||||||
(party.person as any).licenseType = (partyData as any).licenseType;
|
(party.person as any).licenseType = (partyData as any).licenseType;
|
||||||
|
|
||||||
@@ -11693,6 +11829,16 @@ export class RequestManagementService {
|
|||||||
const role = this.resolvePartyRoleV3(req, partyRole);
|
const role = this.resolvePartyRoleV3(req, partyRole);
|
||||||
this.assertBlameV3PartyDetailPhase(req, role);
|
this.assertBlameV3PartyDetailPhase(req, role);
|
||||||
|
|
||||||
|
// V3/V5 clients can include CAR_BODY accident fields even though this
|
||||||
|
// endpoint only persists the description. Never accept a future timestamp
|
||||||
|
// merely because those extra fields are not part of the V3 statement step.
|
||||||
|
if (body.accidentDate != null || body.accidentTime != null) {
|
||||||
|
this.assertAccidentDateTimeNotInFuture({
|
||||||
|
accidentDate: body.accidentDate,
|
||||||
|
accidentTime: body.accidentTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const idx = this.getPartyIndex(req, role);
|
const idx = this.getPartyIndex(req, role);
|
||||||
if (idx === -1) throw new BadRequestException(`${role} party not found`);
|
if (idx === -1) throw new BadRequestException(`${role} party not found`);
|
||||||
|
|
||||||
@@ -11735,6 +11881,7 @@ export class RequestManagementService {
|
|||||||
await this.verifyExpertAccessForBlameV2(req, actor);
|
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||||
const role = this.resolvePartyRoleV3(req, partyRole);
|
const role = this.resolvePartyRoleV3(req, partyRole);
|
||||||
this.assertBlameV3PartyDetailPhase(req, role);
|
this.assertBlameV3PartyDetailPhase(req, role);
|
||||||
|
this.assertCarBodyDescriptionMinimum(req.type, body.desc);
|
||||||
|
|
||||||
// accidentDate and accidentTime are only required for the first (guilty)
|
// accidentDate and accidentTime are only required for the first (guilty)
|
||||||
// party — the second (damaged) party can skip them.
|
// party — the second (damaged) party can skip them.
|
||||||
@@ -11751,6 +11898,11 @@ export class RequestManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.assertAccidentDateTimeNotInFuture({
|
||||||
|
accidentDate: body.accidentDate,
|
||||||
|
accidentTime: body.accidentTime,
|
||||||
|
});
|
||||||
|
|
||||||
const idx = this.getPartyIndex(req, role);
|
const idx = this.getPartyIndex(req, role);
|
||||||
if (idx === -1) throw new BadRequestException(`${role} party not found`);
|
if (idx === -1) throw new BadRequestException(`${role} party not found`);
|
||||||
|
|
||||||
@@ -12958,10 +13110,31 @@ export class RequestManagementService {
|
|||||||
throw new ForbiddenException("Only FileMakers can use this endpoint.");
|
throw new ForbiddenException("Only FileMakers can use this endpoint.");
|
||||||
}
|
}
|
||||||
const makerId = new Types.ObjectId(fileMaker.sub);
|
const makerId = new Types.ObjectId(fileMaker.sub);
|
||||||
const files = await this.blameRequestDbService.find({
|
const ownFiles = await this.blameRequestDbService.find({
|
||||||
isMadeByFileMaker: true,
|
isMadeByFileMaker: true,
|
||||||
initiatedByFieldExpertId: makerId,
|
initiatedByFieldExpertId: makerId,
|
||||||
});
|
});
|
||||||
|
const hasBranchScopedFiles = (ownFiles || []).some(
|
||||||
|
(file: any) => !!file.branchId,
|
||||||
|
);
|
||||||
|
let makerBranchId: string | undefined;
|
||||||
|
if (hasBranchScopedFiles) {
|
||||||
|
const fileMakerProfile = await this.fileMakerDbService.findById(
|
||||||
|
String(fileMaker.sub),
|
||||||
|
);
|
||||||
|
makerBranchId = fileMakerProfile?.branchId
|
||||||
|
? String(fileMakerProfile.branchId)
|
||||||
|
: undefined;
|
||||||
|
if (!makerBranchId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"FileMaker account is not assigned to a branch.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const files = (ownFiles || []).filter(
|
||||||
|
(file: any) =>
|
||||||
|
!file.branchId || String(file.branchId) === makerBranchId,
|
||||||
|
);
|
||||||
const blameIds = (files || []).map((f: any) => f._id).filter(Boolean);
|
const blameIds = (files || []).map((f: any) => f._id).filter(Boolean);
|
||||||
const claims =
|
const claims =
|
||||||
blameIds.length > 0
|
blameIds.length > 0
|
||||||
@@ -13021,6 +13194,17 @@ export class RequestManagementService {
|
|||||||
}
|
}
|
||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const req = await this.blameRequestDbService.findById(requestId);
|
||||||
if (!req) throw new NotFoundException("Blame request not found");
|
if (!req) throw new NotFoundException("Blame request not found");
|
||||||
|
if ((req as any).branchId) {
|
||||||
|
const fileMakerProfile = await this.fileMakerDbService.findById(
|
||||||
|
String(fileMaker.sub),
|
||||||
|
);
|
||||||
|
const makerBranchId = fileMakerProfile?.branchId
|
||||||
|
? String(fileMakerProfile.branchId)
|
||||||
|
: undefined;
|
||||||
|
if (!makerBranchId || String((req as any).branchId) !== makerBranchId) {
|
||||||
|
throw new ForbiddenException("This file belongs to another branch.");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!req.isMadeByFileMaker ||
|
!req.isMadeByFileMaker ||
|
||||||
String((req as any).initiatedByFieldExpertId) !== String(fileMaker.sub)
|
String((req as any).initiatedByFieldExpertId) !== String(fileMaker.sub)
|
||||||
@@ -13067,6 +13251,7 @@ export class RequestManagementService {
|
|||||||
driverBirthday: p.person?.driverBirthday,
|
driverBirthday: p.person?.driverBirthday,
|
||||||
insurerLicense: p.person?.insurerLicense,
|
insurerLicense: p.person?.insurerLicense,
|
||||||
driverLicense: p.person?.driverLicense,
|
driverLicense: p.person?.driverLicense,
|
||||||
|
driverLicenseDate: p.person?.driverLicenseDate,
|
||||||
userId: p.person?.userId ? String(p.person.userId) : undefined,
|
userId: p.person?.userId ? String(p.person.userId) : undefined,
|
||||||
clientId: p.person?.clientId ? String(p.person.clientId) : undefined,
|
clientId: p.person?.clientId ? String(p.person.clientId) : undefined,
|
||||||
licenseType: p.person?.licenseType ?? undefined,
|
licenseType: p.person?.licenseType ?? undefined,
|
||||||
@@ -13149,7 +13334,7 @@ export class RequestManagementService {
|
|||||||
{ assignedFileReviewerId: reviewerId },
|
{ assignedFileReviewerId: reviewerId },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const visibleFiles = (files || []).filter((file: any) => {
|
const tenantAndAssignmentVisible = (files || []).filter((file: any) => {
|
||||||
const assignedReviewerId = file.assignedFileReviewerId
|
const assignedReviewerId = file.assignedFileReviewerId
|
||||||
? String(file.assignedFileReviewerId)
|
? String(file.assignedFileReviewerId)
|
||||||
: null;
|
: null;
|
||||||
@@ -13163,6 +13348,26 @@ export class RequestManagementService {
|
|||||||
(isOpen || isAssignedToReviewer)
|
(isOpen || isAssignedToReviewer)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
const visibleFiles = (
|
||||||
|
await Promise.all(
|
||||||
|
tenantAndAssignmentVisible.map(async (file: any) => {
|
||||||
|
try {
|
||||||
|
await this.fanavaranLocationService.assertMakerReviewerBranchCompatible(
|
||||||
|
{
|
||||||
|
fileMakerId: file.initiatedByFieldExpertId
|
||||||
|
? String(file.initiatedByFieldExpertId)
|
||||||
|
: null,
|
||||||
|
fileReviewerId: String(fileReviewer.sub),
|
||||||
|
caseBranchId: file.branchId ? String(file.branchId) : null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return file;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).filter((file): file is any => file != null);
|
||||||
|
|
||||||
const visibleBlameIds = visibleFiles
|
const visibleBlameIds = visibleFiles
|
||||||
.map((f: any) => f._id)
|
.map((f: any) => f._id)
|
||||||
@@ -13230,6 +13435,15 @@ export class RequestManagementService {
|
|||||||
"This file does not belong to your organization.",
|
"This file does not belong to your organization.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await this.fanavaranLocationService.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: (req as any).initiatedByFieldExpertId
|
||||||
|
? String((req as any).initiatedByFieldExpertId)
|
||||||
|
: null,
|
||||||
|
fileReviewerId: String(fileReviewer.sub),
|
||||||
|
caseBranchId: (req as any).branchId
|
||||||
|
? String((req as any).branchId)
|
||||||
|
: null,
|
||||||
|
});
|
||||||
const assignedId = (req as any).assignedFileReviewerId
|
const assignedId = (req as any).assignedFileReviewerId
|
||||||
? String((req as any).assignedFileReviewerId)
|
? String((req as any).assignedFileReviewerId)
|
||||||
: null;
|
: null;
|
||||||
@@ -13280,6 +13494,7 @@ export class RequestManagementService {
|
|||||||
driverBirthday: p.person?.driverBirthday,
|
driverBirthday: p.person?.driverBirthday,
|
||||||
insurerLicense: p.person?.insurerLicense,
|
insurerLicense: p.person?.insurerLicense,
|
||||||
driverLicense: p.person?.driverLicense,
|
driverLicense: p.person?.driverLicense,
|
||||||
|
driverLicenseDate: p.person?.driverLicenseDate,
|
||||||
userId: p.person?.userId ? String(p.person.userId) : undefined,
|
userId: p.person?.userId ? String(p.person.userId) : undefined,
|
||||||
clientId: p.person?.clientId ? String(p.person.clientId) : undefined,
|
clientId: p.person?.clientId ? String(p.person.clientId) : undefined,
|
||||||
licenseType: p.person?.licenseType ?? undefined,
|
licenseType: p.person?.licenseType ?? undefined,
|
||||||
|
|||||||
@@ -189,9 +189,9 @@ describe("SandHubService inquiry mocks", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.getPolicyByChassisInquiry({
|
const result = await service.getCarByChassisInquiry({
|
||||||
nationalCode: "0012345678",
|
nationalCode: "0012345678",
|
||||||
chassis: "NAAR03HFFRDE07024",
|
chassisNo: "NAAR03HFFRDE07024",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.mapped.Error.Message).toBe(
|
expect(result.mapped.Error.Message).toBe(
|
||||||
@@ -239,10 +239,10 @@ describe("SandHubService inquiry mocks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.getPolicyByChassisInquiry(
|
service.getCarByChassisInquiry(
|
||||||
{
|
{
|
||||||
nationalCode: "1234567890",
|
nationalCode: "1234567890",
|
||||||
chassis: "NAAR03HFFRDE07024",
|
chassisNo: "NAAR03HFFRDE07024",
|
||||||
},
|
},
|
||||||
{ enforceDeploymentClientMatch: true },
|
{ enforceDeploymentClientMatch: true },
|
||||||
),
|
),
|
||||||
@@ -256,16 +256,16 @@ describe("SandHubService inquiry mocks", () => {
|
|||||||
data: { CmpCod: "8", CmpNam: "بیمه پارسیان" },
|
data: { CmpCod: "8", CmpNam: "بیمه پارسیان" },
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.getPolicyByChassisInquiry({
|
await service.getCarByChassisInquiry({
|
||||||
nationalCode: "0012345678",
|
nationalCode: "0012345678",
|
||||||
chassis: "NAAR03HFFRDE07024",
|
chassisNo: "NAAR03HFFRDE07024",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(esg).toHaveBeenCalledWith(
|
expect(esg).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("/inquiry/policyByChassis"),
|
expect.stringContaining("/inquiry/carByChassis"),
|
||||||
{
|
{
|
||||||
nationalCode: "0012345678",
|
nationalCode: "0012345678",
|
||||||
chassis: "NAAR03HFFRDE07024",
|
chassisNo: "NAAR03HFFRDE07024",
|
||||||
},
|
},
|
||||||
"vinChassis",
|
"vinChassis",
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
@@ -1187,7 +1187,7 @@ export class SandHubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ESG VIN/chassis-number inquiry (`/inquiry/policyByChassis`).
|
* ESG two-factor VIN/chassis inquiry (`/inquiry/carByChassis`).
|
||||||
*
|
*
|
||||||
* When `vinChassis` inquiry is disabled (mock mode) the response shape mirrors
|
* When `vinChassis` inquiry is disabled (mock mode) the response shape mirrors
|
||||||
* `buildMockPlateInquiryRaw` so the downstream mapper (`mapEsgPolicyByPlateToOldFormat`)
|
* `buildMockPlateInquiryRaw` so the downstream mapper (`mapEsgPolicyByPlateToOldFormat`)
|
||||||
@@ -1196,15 +1196,15 @@ export class SandHubService {
|
|||||||
* @param identity - policyholder national code and 17-character VIN/chassis
|
* @param identity - policyholder national code and 17-character VIN/chassis
|
||||||
* @param options - optional per-tenant client scope
|
* @param options - optional per-tenant client scope
|
||||||
*/
|
*/
|
||||||
async getPolicyByChassisInquiry(
|
async getCarByChassisInquiry(
|
||||||
identity: { nationalCode: string; chassis: string },
|
identity: { nationalCode: string; chassisNo: string },
|
||||||
options?: SandHubInquiryOptions,
|
options?: SandHubInquiryOptions,
|
||||||
): Promise<{ raw: any; mapped: any }> {
|
): Promise<{ raw: any; mapped: any }> {
|
||||||
const baseUrl = process.env.ESG_URL ?? "http://192.168.20.22:8085";
|
const baseUrl = process.env.ESG_URL ?? "http://192.168.20.22:8085";
|
||||||
const requestUrl = `${baseUrl}/inquiry/policyByChassis`;
|
const requestUrl = `${baseUrl}/inquiry/carByChassis`;
|
||||||
const requestPayload = {
|
const requestPayload = {
|
||||||
nationalCode: String(identity.nationalCode),
|
nationalCode: String(identity.nationalCode),
|
||||||
chassis: String(identity.chassis),
|
chassisNo: String(identity.chassisNo),
|
||||||
};
|
};
|
||||||
|
|
||||||
const live = await this.isInquiryLive("vinChassis", options);
|
const live = await this.isInquiryLive("vinChassis", options);
|
||||||
@@ -1213,7 +1213,7 @@ export class SandHubService {
|
|||||||
const ctx = await this.mockCompanyContext(options);
|
const ctx = await this.mockCompanyContext(options);
|
||||||
const raw = this.buildMockPlateInquiryRaw(ctx);
|
const raw = this.buildMockPlateInquiryRaw(ctx);
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`[MOCK] getPolicyByChassisInquiry nationalCode=${identity.nationalCode} chassis=${identity.chassis}`,
|
`[MOCK] getCarByChassisInquiry nationalCode=${identity.nationalCode} chassisNo=${identity.chassisNo}`,
|
||||||
);
|
);
|
||||||
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw, "thirdPartyVin");
|
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw, "thirdPartyVin");
|
||||||
if (!mapped?.Error) {
|
if (!mapped?.Error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user