Compare commits

...

17 Commits

Author SHA1 Message Date
985c28ed6b docs 2026-09-21 10:05:49 +03:30
9dd0ec0c00 Merge pull request 'fix claim validation and expert branch scoping' (#335) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#335
2026-09-20 15:03:22 +03:30
SepehrYahyaee
e06178f804 fix claim validation and expert branch scoping 2026-09-20 15:00:46 +03:30
a9adad3c1b Merge pull request 'VehicleHullAccessoryId' (#334) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#334
2026-09-20 12:32:48 +03:30
bbe5b7b77b VehicleHullAccessoryId 2026-09-20 12:31:39 +03:30
1e13bb3a3c Merge pull request 'Repair Duration' (#333) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#333
2026-09-20 12:07:11 +03:30
a8d8df574b Repair Duration 2026-09-20 12:06:20 +03:30
cf07a33951 Merge pull request 'hat PUT is now best-effort. If it 502s, YARA logs it and continues with POST …/vehicle-hull-claims/5032516/expertise.' (#332) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#332
2026-09-20 11:07:02 +03:30
6f8a6fed69 hat PUT is now best-effort. If it 502s, YARA logs it and continues with POST …/vehicle-hull-claims/5032516/expertise. 2026-09-20 11:05:07 +03:30
dbf46cfdda Merge pull request 'fix ESG chassis inquiry payload' (#331) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#331
2026-09-19 16:51:31 +03:30
SepehrYahyaee
4ef53f2cc9 fix ESG chassis inquiry payload 2026-09-19 16:50:34 +03:30
b4c1d7bd5f Merge pull request 'main' (#330) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#330
2026-09-19 16:12:46 +03:30
SepehrYahyaee
4cd6e0ecb3 test: align hull culprit default 2026-09-19 16:09:50 +03:30
SepehrYahyaee
8071215803 fix inquiry integration and insurer case details 2026-09-19 16:08:28 +03:30
cd36a0f2d4 Merge pull request 'main' (#329) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#329
2026-09-19 15:38:51 +03:30
ffaeb50c61 merge upstream 2026-09-19 15:38:13 +03:30
1d9ef5e3d2 Merge pull request 'update the culprit licenses chnged and ActualPremium' (#328) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#328
2026-09-19 13:10:09 +03:30
39 changed files with 1882 additions and 248 deletions

1
.gitignore vendored
View File

@@ -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

View File

@@ -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`.

View File

@@ -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 می‌شوند اما جریان اصلی را مسدود نمی‌کنند.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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}` |

View File

@@ -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 |

View File

@@ -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)

View File

@@ -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 |

View File

@@ -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 |

View File

@@ -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.

View 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
```

View 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`.

View 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.

View 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`.

View File

@@ -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 },

View File

@@ -66,15 +66,15 @@ import {
toFanavaranHullExpertiseDmgSection, toFanavaranHullExpertiseDmgSection,
toFanavaranHullExpertisePayload, toFanavaranHullExpertisePayload,
vehicleCurrentValueFromHullPolicyRecord, vehicleCurrentValueFromHullPolicyRecord,
FANAVARAN_DEFAULT_HULL_REPAIR_DURATION,
} from "./fanavaran-hull-expertise"; } from "./fanavaran-hull-expertise";
import { import {
asHullDmgAccessoryRows,
FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID, FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID,
FANAVARAN_DEFAULT_HULL_DMG_KIND_ID, FANAVARAN_DEFAULT_HULL_DMG_KIND_ID,
FANAVARAN_DEFAULT_HULL_VEHICLE_ACCESSORY_ID,
FANAVARAN_HULL_DMG_COST_KIND_CAPTION, FANAVARAN_HULL_DMG_COST_KIND_CAPTION,
FANAVARAN_HULL_DMG_KIND_CAPTION, FANAVARAN_HULL_DMG_KIND_CAPTION,
findLookupIdByCaption, findLookupIdByCaption,
resolveVehicleHullAccessoryId,
} from "./fanavaran-hull-expertise-lookups"; } from "./fanavaran-hull-expertise-lookups";
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service"; import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto"; import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto";
@@ -249,7 +249,6 @@ import {
FanavaranAuditStatus, FanavaranAuditStatus,
FanavaranAuditStep, FanavaranAuditStep,
} from "src/fanavaran/schema/fanavaran-audit-log.schema"; } from "src/fanavaran/schema/fanavaran-audit-log.schema";
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
import { selectLatestActiveFanavaranPolicy } from "./fanavaran-policy-selection"; import { selectLatestActiveFanavaranPolicy } from "./fanavaran-policy-selection";
import { import {
selectAccidentVehicleUsedId, selectAccidentVehicleUsedId,
@@ -487,7 +486,6 @@ export class ClaimRequestManagementService {
private readonly fanavaranAuditService: FanavaranAuditService, private readonly fanavaranAuditService: FanavaranAuditService,
private readonly fanavaranAuthService: FanavaranAuthService, private readonly fanavaranAuthService: FanavaranAuthService,
private readonly fanavaranLookupService: FanavaranLookupService, private readonly fanavaranLookupService: FanavaranLookupService,
private readonly smsOrchestrationService: SmsOrchestrationService,
private readonly fileMakerDbService: FileMakerDbService, private readonly fileMakerDbService: FileMakerDbService,
private readonly fileReviewerDbService: FileReviewerDbService, private readonly fileReviewerDbService: FileReviewerDbService,
private readonly fieldExpertDbService: FieldExpertDbService, private readonly fieldExpertDbService: FieldExpertDbService,
@@ -1228,6 +1226,7 @@ export class ClaimRequestManagementService {
claimRequestId: string, claimRequestId: string,
expert: any, expert: any,
dto: { dto: {
selectedPartIds?: number[];
carPartDamage?: CarDamagePartDto; carPartDamage?: CarDamagePartDto;
sheba?: string; sheba?: string;
nationalCodeOfInsurer?: string; nationalCodeOfInsurer?: string;
@@ -1264,12 +1263,34 @@ export class ClaimRequestManagementService {
); );
} }
if (dto.carPartDamage) { if (dto.selectedPartIds?.length || dto.carPartDamage) {
if (claim.carPartDamage && (claim.carPartDamage as any[]).length > 0) { if (claim.carPartDamage && (claim.carPartDamage as any[]).length > 0) {
throw new BadRequestException("Car part damage is already set."); throw new BadRequestException("Car part damage is already set.");
} }
this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage); let trueItems: Array<Record<string, unknown>>;
const trueItems = this.findTrueItems(dto.carPartDamage); if (dto.selectedPartIds?.length) {
const liveCatalog = await this.getLiveFanavaranCatalogItems();
const byId = new Map<number, OuterPartCatalogItem>(
liveCatalog.map((part) => [part.id, part]),
);
trueItems = dto.selectedPartIds.map((id) => {
const item = byId.get(id);
if (!item) {
throw new BadRequestException(
`Invalid outer part id: ${id}. Use GET outer-parts-catalog to see valid IDs.`,
);
}
return {
side: item.side || "carDamage",
part: item.titleFa,
id: item.id,
label_fa: item.titleFa,
};
});
} else {
this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage!);
trueItems = this.findTrueItems(dto.carPartDamage!);
}
const claimDoc = (await this.claimDbService.findOne( const claimDoc = (await this.claimDbService.findOne(
claimRequestId, claimRequestId,
)) as any; )) as any;
@@ -1304,6 +1325,7 @@ export class ClaimRequestManagementService {
claimRequestId: string, claimRequestId: string,
expert: any, expert: any,
dto: { dto: {
selectedPartIds?: number[];
carPartDamage?: CarDamagePartDto; carPartDamage?: CarDamagePartDto;
sheba?: string; sheba?: string;
nationalCodeOfInsurer?: string; nationalCodeOfInsurer?: string;
@@ -1315,7 +1337,7 @@ export class ClaimRequestManagementService {
let working = claimCase; let working = claimCase;
if (dto.carPartDamage) { if (dto.selectedPartIds?.length || dto.carPartDamage) {
if ( if (
working.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OUTER_PARTS working.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OUTER_PARTS
) { ) {
@@ -1329,20 +1351,41 @@ export class ClaimRequestManagementService {
) { ) {
throw new BadRequestException("Car part damage is already set."); throw new BadRequestException("Car part damage is already set.");
} }
this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage); let selectedPartObjs: DamageSelectedPartV2[];
if (dto.selectedPartIds?.length) {
const liveCatalog = await this.getLiveFanavaranCatalogItems();
const byId = new Map<number, OuterPartCatalogItem>(
liveCatalog.map((part) => [part.id, part]),
);
const selectedItems = dto.selectedPartIds.map((id) => {
const item = byId.get(id);
if (!item) {
throw new BadRequestException(
`Invalid outer part id: ${id}. Use GET outer-parts-catalog to see valid IDs.`,
);
}
return item;
});
selectedPartObjs = selectedItems.map((part) =>
catalogItemToSelectedPart(part, liveCatalog),
);
} else {
// Backward compatibility for older expert-panel clients.
this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage!);
const selectedSlugStrings = this.carDamagePartDtoToOuterPartSlugs( const selectedSlugStrings = this.carDamagePartDtoToOuterPartSlugs(
dto.carPartDamage, dto.carPartDamage!,
); );
if (!selectedSlugStrings.length) { if (!selectedSlugStrings.length) {
throw new BadRequestException( throw new BadRequestException(
"No outer damaged parts could be derived from carPartDamage. Check part selections.", "No outer damaged parts could be derived from carPartDamage. Check part selections.",
); );
} }
const selectedPartObjs = normalizeDamageSelectedParts( selectedPartObjs = normalizeDamageSelectedParts(
selectedSlugStrings, selectedSlugStrings,
working.vehicle?.carType as ClaimVehicleTypeV2 | undefined, working.vehicle?.carType as ClaimVehicleTypeV2 | undefined,
undefined, undefined,
); );
}
const damagedPartsStubs = selectedPartObjs.map((p) => ({ const damagedPartsStubs = selectedPartObjs.map((p) => ({
id: p.id ?? undefined, id: p.id ?? undefined,
name: p.name, name: p.name,
@@ -4437,7 +4480,7 @@ export class ClaimRequestManagementService {
), ),
}; };
const [dmgKindRows, dmgCostKindRows, policyRecord, accessoriesRaw] = const [dmgKindRows, dmgCostKindRows, policyRecord] =
await Promise.all([ await Promise.all([
this.getFanavaranLookupRows(input.clientKey, "vehicle-hull-dmg-kind"), this.getFanavaranLookupRows(input.clientKey, "vehicle-hull-dmg-kind"),
this.getFanavaranLookupRows( this.getFanavaranLookupRows(
@@ -4451,13 +4494,6 @@ export class ClaimRequestManagementService {
contractOptions, contractOptions,
) )
: Promise.resolve(null), : Promise.resolve(null),
policyId != null
? this.fanavaranLookupService.vehicleHullDmgAccessoriesByPolicyId(
input.clientKey,
policyId,
contractOptions,
)
: Promise.resolve([]),
]); ]);
const dmgKindId = const dmgKindId =
@@ -4469,13 +4505,6 @@ export class ClaimRequestManagementService {
FANAVARAN_HULL_DMG_COST_KIND_CAPTION, FANAVARAN_HULL_DMG_COST_KIND_CAPTION,
) ?? FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID; ) ?? FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID;
const accessories = asHullDmgAccessoryRows(accessoriesRaw);
if (policyId != null && accessories.length === 0) {
input.warnings.push(
`No hull dmg-accessories for policy ${policyId}; VehicleHullAccessoryId may be missing.`,
);
}
let vehicle = pickHullVehicleIdentity(input.blame); let vehicle = pickHullVehicleIdentity(input.blame);
let colorId: number | null = null; let colorId: number | null = null;
const policy = asObjectRecord(policyRecord); const policy = asObjectRecord(policyRecord);
@@ -4519,16 +4548,6 @@ export class ClaimRequestManagementService {
); );
const hullSections = input.parts.map((part: any) => { const hullSections = input.parts.map((part: any) => {
const accessoryKindId = parseCatalogPartIdInput(part?.partId);
const vehicleHullAccessoryId = resolveVehicleHullAccessoryId(
accessories,
accessoryKindId,
);
if (vehicleHullAccessoryId == null) {
input.warnings.push(
`No VehicleHullAccessoryId for part "${this.fanavaranPartLabel(part)}" (AccessoryKindId=${accessoryKindId ?? "none"}).`,
);
}
return toFanavaranHullExpertiseDmgSection({ return toFanavaranHullExpertiseDmgSection({
partId: part?.partId, partId: part?.partId,
desc: this.fanavaranPartLabel(part), desc: this.fanavaranPartLabel(part),
@@ -4538,7 +4557,7 @@ export class ClaimRequestManagementService {
this.parseFanavaranMoney(part?.salary), this.parseFanavaranMoney(part?.salary),
dmgKindId, dmgKindId,
dmgCostKindId, dmgCostKindId,
vehicleHullAccessoryId, vehicleHullAccessoryId: FANAVARAN_DEFAULT_HULL_VEHICLE_ACCESSORY_ID,
}); });
}); });
@@ -4551,10 +4570,7 @@ export class ClaimRequestManagementService {
vehicleCurrentValue = vehicleCurrentValueFromHullPolicyRecord(policy); vehicleCurrentValue = vehicleCurrentValueFromHullPolicyRecord(policy);
} }
const repairDuration = const repairDuration = FANAVARAN_DEFAULT_HULL_REPAIR_DURATION;
typeof input.profile.defaults.HullExpertiseRepairDuration === "number"
? input.profile.defaults.HullExpertiseRepairDuration
: null;
return { return {
replyKey: input.replyKey, replyKey: input.replyKey,
@@ -7395,13 +7411,7 @@ export class ClaimRequestManagementService {
this.logger.log( this.logger.log(
`[executeFanavaranExpertiseSubmit] Already have expertiseId=${claimCase.expertiseId}; skipping Fanavaran POST`, `[executeFanavaranExpertiseSubmit] Already have expertiseId=${claimCase.expertiseId}; skipping Fanavaran POST`,
); );
// Catch-up / deduped: SMS may still be pending if expertise predated this notify. // Product decision: completing the final Fanavaran stage must not send SMS.
await this.notifyClaimOwnerFanavaranClaimRegistered({
claimCaseId,
claimId: claimCase.claimId,
claimNo: claimCase.claimNo,
logPrefix: `[Fanavaran ${clientKey} V2 Expertise]`,
});
return ( return (
(claimCase as any)?.fanavaranSync?.expertise?.response ?? { (claimCase as any)?.fanavaranSync?.expertise?.response ?? {
Id: claimCase.expertiseId, Id: claimCase.expertiseId,
@@ -7477,13 +7487,7 @@ export class ClaimRequestManagementService {
}, },
}); });
// Best-effort: SMS after last Fanavaran stage (expertise) succeeds // Product decision: completing the final Fanavaran stage must not send SMS.
await this.notifyClaimOwnerFanavaranClaimRegistered({
claimCaseId,
claimId: claimCase.claimId,
claimNo: claimCase.claimNo,
logPrefix: `[Fanavaran ${clientKey} V2 Expertise]`,
});
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -7521,12 +7525,7 @@ export class ClaimRequestManagementService {
}; };
} }
if (claimCase.expertiseId != null) { if (claimCase.expertiseId != null) {
await this.notifyClaimOwnerFanavaranClaimRegistered({ // Product decision: an already-completed Fanavaran stage must not send SMS.
claimCaseId,
claimId: claimCase.claimId,
claimNo: claimCase.claimNo,
logPrefix,
});
return { return {
attempted: false, attempted: false,
submitted: false, submitted: false,
@@ -7872,7 +7871,18 @@ export class ClaimRequestManagementService {
this.logger.log( this.logger.log(
`[Fanavaran hull sync] PUT ${url} keys=${Object.keys(hull).join(",")}`, `[Fanavaran hull sync] PUT ${url} keys=${Object.keys(hull).join(",")}`,
); );
try {
await this.putFanavaranJson(url, hull, clientKey, undefined, claimCaseId); await this.putFanavaranJson(url, hull, clientKey, undefined, claimCaseId);
} catch (error) {
// Parsian bonyan often has no PUT route on vehicle-hull-claims/{Id}
// ("No HTTP resource was found.../vehicle-hull-claims/{Id}"). Do not
// block GEN.05 files / GEN.06 expertise on that.
this.logger.warn(
`[Fanavaran hull sync] PUT ${url} failed; continuing with nested stages: ${
error instanceof Error ? error.message : error
}`,
);
}
} }
private async postFanavaranMultipart( private async postFanavaranMultipart(
@@ -8695,111 +8705,6 @@ export class ClaimRequestManagementService {
return true; return true;
} }
/**
* Phone for the claim owner / damaged party (same recipient as other claim SMS).
*/
private async resolveFanavaranClaimOwnerPhone(
claimCase: any,
): Promise<string | undefined> {
const notifyUserId =
claimCase?.damagedPartyUserId ?? claimCase?.owner?.userId;
if (!notifyUserId) return undefined;
const ownerUserId = String(notifyUserId);
if (claimCase.blameRequestId) {
const blame = await this.blameRequestDbService.findById(
String(claimCase.blameRequestId),
);
const ownerParty = (blame?.parties || []).find(
(p: any) =>
p?.person?.userId && String(p.person.userId) === ownerUserId,
);
const fromParty = ownerParty?.person?.phoneNumber;
if (typeof fromParty === "string" && fromParty.trim()) {
return fromParty.trim();
}
}
const user = await this.userDbService.findOne({
_id: new Types.ObjectId(ownerUserId),
});
const mobile = user?.mobile;
if (typeof mobile === "string" && mobile.trim()) return mobile.trim();
return undefined;
}
/**
* After Fanavaran expertise (last stage) succeeds, SMS the claim owner with
* publicId + claimId + ClaimNo. Uses SmsOrchestrationService → same provider
* as login for this deployment (`SMS` / `SMS_PROVIDER`). Never throws.
*/
private async notifyClaimOwnerFanavaranClaimRegistered(input: {
claimCaseId: string;
claimId?: number | string | null;
claimNo?: number | string | null;
logPrefix: string;
}): Promise<void> {
try {
if (input.claimId == null && input.claimNo == null) {
this.logger.warn(
`${input.logPrefix} Skip Fanavaran SMS: no claimId/ClaimNo on response`,
);
return;
}
const claimCase = await this.claimCaseDbService.findById(
input.claimCaseId,
);
if (!claimCase) return;
const sync = (claimCase as any)?.fanavaranSync;
// Dedup: expertise stage, or older base-claim notify from before the move.
if (sync?.expertise?.smsNotifiedAt || sync?.baseClaim?.smsNotifiedAt) {
this.logger.log(
`${input.logPrefix} Fanavaran SMS already sent; skipping duplicate`,
);
return;
}
const phone = await this.resolveFanavaranClaimOwnerPhone(claimCase);
if (!phone) {
this.logger.warn(
`${input.logPrefix} Skip Fanavaran SMS: no phone for claim owner`,
);
return;
}
const publicId =
typeof claimCase.publicId === "string" && claimCase.publicId.trim()
? claimCase.publicId.trim()
: String(input.claimCaseId);
const sent =
await this.smsOrchestrationService.sendFanavaranClaimRegisteredNotice({
receptor: phone,
publicId,
claimNo: input.claimNo ?? claimCase.claimNo,
claimId: input.claimId ?? claimCase.claimId,
});
if (sent) {
await this.claimCaseDbService.findByIdAndUpdate(input.claimCaseId, {
$set: {
"fanavaranSync.expertise.smsNotifiedAt": new Date(),
},
});
this.logger.log(
`${input.logPrefix} Fanavaran claim SMS sent to claim owner phone=${phone} publicId=${publicId} claimNo=${input.claimNo ?? claimCase.claimNo ?? "-"} claimId=${input.claimId ?? claimCase.claimId ?? "-"}`,
);
}
} catch (error) {
this.logger.error(
`${input.logPrefix} Fanavaran claim SMS failed (non-fatal)`,
error,
);
}
}
private async executeFanavaranV2Submit( private async executeFanavaranV2Submit(
claimCaseId: string, claimCaseId: string,
clientKey: FanavaranClientKey, clientKey: FanavaranClientKey,
@@ -9442,6 +9347,9 @@ export class ClaimRequestManagementService {
blameRequestId: new Types.ObjectId(blameRequestId), blameRequestId: new Types.ObjectId(blameRequestId),
blameRequestNo: blameRequest.requestNo, blameRequestNo: blameRequest.requestNo,
initiatedByFieldExpertId: new Types.ObjectId(expert.sub), initiatedByFieldExpertId: new Types.ObjectId(expert.sub),
...((blameRequest as any).branchId
? { branchId: new Types.ObjectId(String((blameRequest as any).branchId)) }
: {}),
status: ClaimCaseStatus.SELECTING_OUTER_PARTS, status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
claimStatus: ClaimStatus.PENDING, claimStatus: ClaimStatus.PENDING,
inquiries: (blameRequest as any).inquiries ?? {}, inquiries: (blameRequest as any).inquiries ?? {},

View File

@@ -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.

View File

@@ -76,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();

View File

@@ -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;

View File

@@ -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,

View File

@@ -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()

View File

@@ -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;

View File

@@ -515,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(
@@ -531,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]);
});
});

View File

@@ -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(
@@ -2600,6 +2731,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 +2872,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;
@@ -4121,9 +4270,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(
@@ -4389,7 +4542,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[];
@@ -4398,9 +4551,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);
@@ -4478,10 +4653,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);
@@ -4920,6 +5110,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 =

View File

@@ -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,
});
});
}); });

View File

@@ -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,

View 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();
});
});

View File

@@ -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",

View File

@@ -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();
});
}); });

View File

@@ -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);

View File

@@ -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.",

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,

View File

@@ -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");
});
}); });

View File

@@ -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,
@@ -465,10 +466,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 +1126,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 +1188,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 +1220,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: "تاریخ و ساعت حادثه نمی‌تواند در آینده باشد.",
});
}
} }
/** /**
@@ -2142,7 +2200,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 +2294,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 +2314,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 +2332,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 +2358,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",
@@ -4106,6 +4164,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 +5440,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 +5758,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 +5786,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
@@ -8553,6 +8632,11 @@ export class RequestManagementService {
); );
} }
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(
@@ -10645,7 +10729,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 +10872,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.
*/ */
@@ -11693,6 +11777,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`);
@@ -11751,6 +11845,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 +13057,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 +13141,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)
@@ -13149,7 +13280,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 +13294,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 +13381,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;

View File

@@ -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,

View File

@@ -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) {