forked from Yara724/api
Compare commits
22 Commits
406b139b3d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ac185c861 | ||
| f07e510c5f | |||
|
|
8b65e43c60 | ||
| 9dd0ec0c00 | |||
|
|
e06178f804 | ||
| a9adad3c1b | |||
| bbe5b7b77b | |||
| 1e13bb3a3c | |||
| a8d8df574b | |||
| cf07a33951 | |||
| 6f8a6fed69 | |||
| dbf46cfdda | |||
|
|
4ef53f2cc9 | ||
| b4c1d7bd5f | |||
|
|
4cd6e0ecb3 | ||
|
|
8071215803 | ||
| cd36a0f2d4 | |||
| ffaeb50c61 | |||
| 8afc1fabc4 | |||
| 1d9ef5e3d2 | |||
| be06bfe1b5 | |||
| 85576e7c5e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -186,4 +186,3 @@ dist
|
|||||||
*.sh
|
*.sh
|
||||||
!scripts/fanavaran-flow-test.sh
|
!scripts/fanavaran-flow-test.sh
|
||||||
!scripts/fanavaran-auth.sh
|
!scripts/fanavaran-auth.sh
|
||||||
*.json
|
|
||||||
@@ -461,6 +461,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Optional document shared by every claim flow</div>
|
||||||
|
<p class="step-note">
|
||||||
|
Every V1–V6 document-upload step also offers
|
||||||
|
<code>accident_sketch</code> (کروکی). It is optional for both
|
||||||
|
<code>THIRD_PARTY</code> and <code>CAR_BODY</code>, may be omitted without
|
||||||
|
blocking progression, and is returned in expert and insurer case details when uploaded.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||||
<h2>Flow V1 / V2 — User-Initiated Blame & Claim</h2>
|
<h2>Flow V1 / V2 — User-Initiated Blame & Claim</h2>
|
||||||
<p class="section-intro">
|
<p class="section-intro">
|
||||||
|
|||||||
@@ -433,6 +433,23 @@ curl -X POST "$ESG_URL/inquiry/policyByPlate" \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Car By Chassis (Two-Factor VIN Inquiry)
|
||||||
|
|
||||||
|
Use the two-factor chassis route when the backend must match both the VIN and
|
||||||
|
the resolved third-party policyholder. Do not send `nationalCode` to the
|
||||||
|
one-factor `policyByChassis` route.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X POST "$ESG_URL/inquiry/carByChassis" \
|
||||||
|
-H "Authorization: Bearer $ESG_ACCESS_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
--data '{
|
||||||
|
"nationalCode": "0012345678",
|
||||||
|
"chassisNo": "NAAR03HFFRDE07024"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
### Person Inquiry
|
### Person Inquiry
|
||||||
|
|
||||||
ESG expects Jalali birth date, normalized as `YYYY-MM-DD`.
|
ESG expects Jalali birth date, normalized as `YYYY-MM-DD`.
|
||||||
|
|||||||
@@ -161,7 +161,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>درخواست همیشه با پلاک فعلی ارسالشده و بیمهگذار نهایی همان نوع بیمه انجام میشود. متادیتای انتقال اخیر هیچ استعلامی برای پلاک یا بیمهگذار قبلی ایجاد نمیکند.</li>
|
<li>درخواست همیشه با پلاک فعلی ارسالشده و بیمهگذار نهایی همان نوع بیمه انجام میشود. متادیتای انتقال اخیر هیچ استعلامی برای پلاک یا بیمهگذار قبلی ایجاد نمیکند.</li>
|
||||||
<li>۱. بررسی دادههای آفلاین (MongoDB) — اگر داده مطابق یافت شد، آن را برگردانده و تمام HTTP را رد کن.</li>
|
<li>۱. بررسی دادههای آفلاین (MongoDB) — اگر داده مطابق یافت شد، آن را برگردانده و تمام HTTP را رد کن.</li>
|
||||||
<li>۲. اگر <code>CLIENT_ID=8</code> (تنانت پارسیان/ESG) → مسیریابی به <strong>ESG</strong> <code>/inquiry/policyByPlate</code> یا <code>/inquiry/policyByChassis</code>.</li>
|
<li>۲. اگر <code>CLIENT_ID=8</code> (تنانت پارسیان/ESG) → برای پلاک به <strong>ESG</strong> <code>/inquiry/policyByPlate</code> و برای VIN/شاسی به مسیر دوعاملی <code>/inquiry/carByChassis</code> مسیریابی میشود.</li>
|
||||||
<li>۳. در غیر این صورت → مسیریابی به <strong>پرسوجوی تجارت</strong> <code>/block-inquiry-tejarat</code> (THIRD_PARTY) یا <code>/block-inquiry-tejarat/badane</code> (CAR_BODY).</li>
|
<li>۳. در غیر این صورت → مسیریابی به <strong>پرسوجوی تجارت</strong> <code>/block-inquiry-tejarat</code> (THIRD_PARTY) یا <code>/block-inquiry-tejarat/badane</code> (CAR_BODY).</li>
|
||||||
<li>۴. اگر <code>system_settings.externalApis.sandHubUseLiveApi = false</code> (پیشفرض) → پاسخ mock برگردانده شود به جای انجام فراخوانیهای HTTP.</li>
|
<li>۴. اگر <code>system_settings.externalApis.sandHubUseLiveApi = false</code> (پیشفرض) → پاسخ mock برگردانده شود به جای انجام فراخوانیهای HTTP.</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
<div class="card card-blue">
|
<div class="card card-blue">
|
||||||
<h3>پروتکل ارسال خسارت (۴ مرحله)</h3>
|
<h3>پروتکل ارسال خسارت (۴ مرحله)</h3>
|
||||||
<div class="flow-box">
|
<div class="flow-box">
|
||||||
<div class="flow-step"><div class="flow-num">۱</div><div class="flow-text"><strong>خسارت پایه (GEN.03)</strong> — <code>POST /car/third-party-car-financial-claims</code>. دادههای مالک، راننده، بیمه، وسیله نقلیه و تصادف را ارسال میکند. یک <code>claimId</code> و <code>claimNo</code> فناوران برمیگرداند. پیامک با هر دو شناسه برای مالک ارسال میشود.</div></div>
|
<div class="flow-step"><div class="flow-num">۱</div><div class="flow-text"><strong>خسارت پایه (GEN.03)</strong> — <code>POST /car/third-party-car-financial-claims</code>. دادههای مالک، راننده، بیمه، وسیله نقلیه و تصادف را ارسال میکند. یک <code>claimId</code> و <code>claimNo</code> فناوران برمیگرداند که برای نمایش در پنل ذخیره میشوند.</div></div>
|
||||||
<div class="flow-step"><div class="flow-num">۲</div><div class="flow-text"><strong>موارد خسارت (GEN.05)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/dmg-cases</code>. یک ورودی به ازای هر قطعه آسیبدیده با شناسه کامپوننت، شدت و قیمت. سقف: کل ≤ ۵۳،۰۰۰،۰۰۰ تومان.</div></div>
|
<div class="flow-step"><div class="flow-num">۲</div><div class="flow-text"><strong>موارد خسارت (GEN.05)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/dmg-cases</code>. یک ورودی به ازای هر قطعه آسیبدیده با شناسه کامپوننت، شدت و قیمت. سقف: کل ≤ ۵۳،۰۰۰،۰۰۰ تومان.</div></div>
|
||||||
<div class="flow-step"><div class="flow-num">۳</div><div class="flow-text"><strong>پیوستها (GEN.07)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/files</code>. اسناد، تصاویر car-capture و ویدیوها که با شناسه فایل ارجاع داده شدهاند.</div></div>
|
<div class="flow-step"><div class="flow-num">۳</div><div class="flow-text"><strong>پیوستها (GEN.07)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/files</code>. اسناد، تصاویر car-capture و ویدیوها که با شناسه فایل ارجاع داده شدهاند.</div></div>
|
||||||
<div class="flow-step"><div class="flow-num">۴</div><div class="flow-text"><strong>کارشناسی (GEN.08)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/expertise</code>. متادیتای ارزیابی کارشناس (نقش کارشناس، تاریخ، نتیجه). ارسال را نهایی میکند.</div></div>
|
<div class="flow-step"><div class="flow-num">۴</div><div class="flow-text"><strong>کارشناسی (GEN.08)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/expertise</code>. متادیتای ارزیابی کارشناس (نقش کارشناس، تاریخ، نتیجه). ارسال را نهایی میکند.</div></div>
|
||||||
@@ -359,7 +359,7 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
|
<tr><th style="width:70px">متد</th><th>مسیر</th><th>توضیح</th></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByPlate</code></td><td>جستجوی بیمهنامه مبتنی بر پلاک (THIRD_PARTY). بدنه: <code>nationalCode</code>، <code>plk1–plk4</code>. پاسخ قبل از ذخیره به فرمت قدیمی تجارت نگاشت میشود.</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByPlate</code></td><td>جستجوی بیمهنامه مبتنی بر پلاک (THIRD_PARTY). بدنه: <code>nationalCode</code>، <code>plk1–plk4</code>. پاسخ قبل از ذخیره به فرمت قدیمی تجارت نگاشت میشود.</td></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByChassis</code></td><td>جایگزین مبتنی بر VIN/شاسی برای پرسوجوی پلاک. توسط اندپوینتهای <code>run-inquiries-vin</code> فراخوانی میشود. از جستجوی شاسی ESG استفاده میکند (نه مسیر SandHub). بدنه: <code>nationalCode</code>، <code>chassis</code>.</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/carByChassis</code></td><td>جایگزین دوعاملی VIN/شاسی برای استعلام پلاک. توسط اندپوینتهای <code>run-inquiries-vin</code> فراخوانی میشود. بدنه: <code>nationalCode</code>، <code>chassisNo</code>. مسیر تکعاملی <code>policyByChassis</code> استفاده نمیشود، چون فیلد <code>nationalCode</code> را نمیپذیرد.</td></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/person</code></td><td>بررسی هویت شخصی. بدنه: <code>nationalCode</code>، <code>birthDate</code> (جلالی، نه میلادی).</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/person</code></td><td>بررسی هویت شخصی. بدنه: <code>nationalCode</code>، <code>birthDate</code> (جلالی، نه میلادی).</td></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/sheba</code></td><td>اعتبارسنجی شبا / حساب بانکی.</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/sheba</code></td><td>اعتبارسنجی شبا / حساب بانکی.</td></tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -415,7 +415,7 @@
|
|||||||
<tr><td><code>token</code> = "تصادف"/"خسارت"، <code>token2</code> = publicId، <code>token3</code> = نام خانوادگی کارشناس</td><td>کارشناس یک فایل تقصیر یا خسارت را قفل میکند</td><td><code>yara-expert-lock</code></td></tr>
|
<tr><td><code>token</code> = "تصادف"/"خسارت"، <code>token2</code> = publicId، <code>token3</code> = نام خانوادگی کارشناس</td><td>کارشناس یک فایل تقصیر یا خسارت را قفل میکند</td><td><code>yara-expert-lock</code></td></tr>
|
||||||
<tr><td><code>token</code> = نوع فایل، <code>token2</code> = publicId، <code>token3</code> = لینک</td><td>کارشناس درخواست ارسال مجدد اسناد میدهد</td><td><code>yara-resend-documents</code></td></tr>
|
<tr><td><code>token</code> = نوع فایل، <code>token2</code> = publicId، <code>token3</code> = لینک</td><td>کارشناس درخواست ارسال مجدد اسناد میدهد</td><td><code>yara-resend-documents</code></td></tr>
|
||||||
<tr><td><code>token</code> = نوع فایل، <code>token2</code> = publicId، <code>token3</code> = نام خانوادگی کارشناس، <code>token10</code> = لینک</td><td>طرف مطلع میشود که ارزیابی خسارت کارشناس را امضا کند</td><td><code>yara-signature</code></td></tr>
|
<tr><td><code>token</code> = نوع فایل، <code>token2</code> = publicId، <code>token3</code> = نام خانوادگی کارشناس، <code>token10</code> = لینک</td><td>طرف مطلع میشود که ارزیابی خسارت کارشناس را امضا کند</td><td><code>yara-signature</code></td></tr>
|
||||||
<tr><td><code>token</code> = publicId، <code>token2</code> = claimId فناوران، <code>token3</code> = claimNo فناوران</td><td>ارسال فناوران تأیید شد — با شماره و شناسه خسارت فناوران برای مالک خسارت ارسال میشود</td><td><code>yara-fanavaran-claim</code></td></tr>
|
<tr><td><code>token</code> = publicId، <code>token2</code> = claimId فناوران، <code>token3</code> = claimNo فناوران</td><td>قالب قدیمی نگهداری شده است؛ ارسال خودکار پس از آخرین مرحله فناوران غیرفعال است</td><td><code>yara-fanavaran-claim</code></td></tr>
|
||||||
</table>
|
</table>
|
||||||
<p class="note" style="margin-top:8px;">
|
<p class="note" style="margin-top:8px;">
|
||||||
تمام فراخوانیهای پیامک fire-and-forget هستند — هرگز throw نمیکنند. شکستها log میشوند اما جریان اصلی را مسدود نمیکنند.
|
تمام فراخوانیهای پیامک fire-and-forget هستند — هرگز throw نمیکنند. شکستها log میشوند اما جریان اصلی را مسدود نمیکنند.
|
||||||
|
|||||||
@@ -156,7 +156,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>The request always uses the submitted current plate and the resolved policyholder for that policy type. Recent-transfer metadata never triggers a previous-plate or previous-policyholder lookup.</li>
|
<li>The request always uses the submitted current plate and the resolved policyholder for that policy type. Recent-transfer metadata never triggers a previous-plate or previous-policyholder lookup.</li>
|
||||||
<li>1. Check offline-inquiry seeds (MongoDB) — if a matching seed exists, return it and skip all HTTP.</li>
|
<li>1. Check offline-inquiry seeds (MongoDB) — if a matching seed exists, return it and skip all HTTP.</li>
|
||||||
<li>2. If <code>CLIENT_ID=8</code> (Parsian/ESG tenant) → route to <strong>ESG</strong> <code>/inquiry/policyByPlate</code> or <code>/inquiry/policyByChassis</code>.</li>
|
<li>2. If <code>CLIENT_ID=8</code> (Parsian/ESG tenant) → route to <strong>ESG</strong> <code>/inquiry/policyByPlate</code> for plates or the two-factor <code>/inquiry/carByChassis</code> for VIN/chassis inquiries.</li>
|
||||||
<li>3. Otherwise → route to <strong>Tejarat inquiry</strong> <code>/block-inquiry-tejarat</code> (THIRD_PARTY) or <code>/block-inquiry-tejarat/badane</code> (CAR_BODY).</li>
|
<li>3. Otherwise → route to <strong>Tejarat inquiry</strong> <code>/block-inquiry-tejarat</code> (THIRD_PARTY) or <code>/block-inquiry-tejarat/badane</code> (CAR_BODY).</li>
|
||||||
<li>4. If <code>system_settings.externalApis.sandHubUseLiveApi = false</code> (default) → return mock response instead of making HTTP calls.</li>
|
<li>4. If <code>system_settings.externalApis.sandHubUseLiveApi = false</code> (default) → return mock response instead of making HTTP calls.</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
<div class="card card-blue">
|
<div class="card card-blue">
|
||||||
<h3>Claim submission protocol (4 steps)</h3>
|
<h3>Claim submission protocol (4 steps)</h3>
|
||||||
<div class="flow-box">
|
<div class="flow-box">
|
||||||
<div class="flow-step"><div class="flow-num">1</div><div class="flow-text"><strong>Base claim (GEN.03)</strong> — <code>POST /car/third-party-car-financial-claims</code>. Sends owner, driver, insurance, vehicle, and accident data. Returns a Fanavaran <code>claimId</code> and <code>claimNo</code>. SMS is sent to the owner with both identifiers.</div></div>
|
<div class="flow-step"><div class="flow-num">1</div><div class="flow-text"><strong>Base claim (GEN.03)</strong> — <code>POST /car/third-party-car-financial-claims</code>. Sends owner, driver, insurance, vehicle, and accident data. Returns a Fanavaran <code>claimId</code> and <code>claimNo</code>, which are persisted for panel display.</div></div>
|
||||||
<div class="flow-step"><div class="flow-num">2</div><div class="flow-text"><strong>Damage cases (GEN.05)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/dmg-cases</code>. One entry per damaged part with component ID, severity, and price. Cap: total ≤ 53 000 000 Toman.</div></div>
|
<div class="flow-step"><div class="flow-num">2</div><div class="flow-text"><strong>Damage cases (GEN.05)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/dmg-cases</code>. One entry per damaged part with component ID, severity, and price. Cap: total ≤ 53 000 000 Toman.</div></div>
|
||||||
<div class="flow-step"><div class="flow-num">3</div><div class="flow-text"><strong>Attachments (GEN.07)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/files</code>. Documents, car-capture images, and videos referenced by file ID.</div></div>
|
<div class="flow-step"><div class="flow-num">3</div><div class="flow-text"><strong>Attachments (GEN.07)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/files</code>. Documents, car-capture images, and videos referenced by file ID.</div></div>
|
||||||
<div class="flow-step"><div class="flow-num">4</div><div class="flow-text"><strong>Expertise (GEN.08)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/expertise</code>. Expert assessment metadata (expert role, date, result). Finalises the submission.</div></div>
|
<div class="flow-step"><div class="flow-num">4</div><div class="flow-text"><strong>Expertise (GEN.08)</strong> — <code>POST /car/third-party-car-financial-claims/{claimId}/expertise</code>. Expert assessment metadata (expert role, date, result). Finalises the submission.</div></div>
|
||||||
@@ -356,7 +356,7 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
|
<tr><th style="width:70px">Method</th><th>Path</th><th>What it does</th></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByPlate</code></td><td>Plate-based policy lookup (THIRD_PARTY). Body: <code>nationalCode</code>, <code>plk1–plk4</code>. Response is mapped to the old Tejarat format before being stored.</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByPlate</code></td><td>Plate-based policy lookup (THIRD_PARTY). Body: <code>nationalCode</code>, <code>plk1–plk4</code>. Response is mapped to the old Tejarat format before being stored.</td></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/policyByChassis</code></td><td>VIN/chassis-based alternative to plate inquiry. Called by <code>run-inquiries-vin</code> endpoints. Uses ESG chassis lookup (not the SandHub path). Body: <code>nationalCode</code>, <code>chassis</code>.</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/carByChassis</code></td><td>Two-factor VIN/chassis alternative to the plate inquiry. Called by <code>run-inquiries-vin</code> endpoints. Body: <code>nationalCode</code>, <code>chassisNo</code>. The one-factor <code>policyByChassis</code> route is not used because it rejects <code>nationalCode</code>.</td></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/person</code></td><td>Personal identity check. Body: <code>nationalCode</code>, <code>birthDate</code> (Jalali, NOT Gregorian).</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/person</code></td><td>Personal identity check. Body: <code>nationalCode</code>, <code>birthDate</code> (Jalali, NOT Gregorian).</td></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/sheba</code></td><td>Sheba / bank account validation.</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>/inquiry/sheba</code></td><td>Sheba / bank account validation.</td></tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -412,7 +412,7 @@
|
|||||||
<tr><td><code>yara-expert-lock</code></td><td>Expert locks a blame or claim file</td><td><code>token</code> = "تصادف"/"خسارت", <code>token2</code> = publicId, <code>token3</code> = expert surname</td></tr>
|
<tr><td><code>yara-expert-lock</code></td><td>Expert locks a blame or claim file</td><td><code>token</code> = "تصادف"/"خسارت", <code>token2</code> = publicId, <code>token3</code> = expert surname</td></tr>
|
||||||
<tr><td><code>yara-resend-documents</code></td><td>Expert requests document resend</td><td><code>token</code> = file kind, <code>token2</code> = publicId, <code>token3</code> = link</td></tr>
|
<tr><td><code>yara-resend-documents</code></td><td>Expert requests document resend</td><td><code>token</code> = file kind, <code>token2</code> = publicId, <code>token3</code> = link</td></tr>
|
||||||
<tr><td><code>yara-signature</code></td><td>Party notified to sign the expert's damage assessment</td><td><code>token</code> = file kind, <code>token2</code> = publicId, <code>token3</code> = expert surname, <code>token10</code> = link</td></tr>
|
<tr><td><code>yara-signature</code></td><td>Party notified to sign the expert's damage assessment</td><td><code>token</code> = file kind, <code>token2</code> = publicId, <code>token3</code> = expert surname, <code>token10</code> = link</td></tr>
|
||||||
<tr><td><code>yara-fanavaran-claim</code></td><td>Fanavaran submission confirmed — sent to claim owner with Fanavaran claim number and ID</td><td><code>token</code> = publicId, <code>token2</code> = Fanavaran claimId, <code>token3</code> = Fanavaran claimNo</td></tr>
|
<tr><td><code>yara-fanavaran-claim</code></td><td>Retained legacy template; automatic dispatch after the final Fanavaran stage is disabled</td><td><code>token</code> = publicId, <code>token2</code> = Fanavaran claimId, <code>token3</code> = Fanavaran claimNo</td></tr>
|
||||||
</table>
|
</table>
|
||||||
<p class="note" style="margin-top:8px;">
|
<p class="note" style="margin-top:8px;">
|
||||||
All SMS calls are fire-and-forget — they never throw. Failures are logged but do not block the main flow.
|
All SMS calls are fire-and-forget — they never throw. Failures are logged but do not block the main flow.
|
||||||
|
|||||||
@@ -308,7 +308,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`.
|
||||||
|
|||||||
@@ -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)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ sequenceDiagram
|
|||||||
YARA->>FV: POST .../expertise (GEN.08)
|
YARA->>FV: POST .../expertise (GEN.08)
|
||||||
FV-->>YARA: ExpertiseId
|
FV-->>YARA: ExpertiseId
|
||||||
YARA->>DB: expertiseId, history SUCCESS
|
YARA->>DB: expertiseId, history SUCCESS
|
||||||
YARA->>YARA: SMS owner (deduped)
|
Note over YARA: Post-expertise owner SMS is disabled
|
||||||
```
|
```
|
||||||
|
|
||||||
## Flow diagram (stages + soft-ensure)
|
## Flow diagram (stages + soft-ensure)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ source: fanavaran-module-docs
|
|||||||
| Tenant config | boot + System Settings | `fanavaran-client-config.service.ts`, `system-settings.*` |
|
| Tenant config | boot + System Settings | `fanavaran-client-config.service.ts`, `system-settings.*` |
|
||||||
| Orchestration | `ClaimRequestManagementModule` | `claim-request-management.service.ts` |
|
| Orchestration | `ClaimRequestManagementModule` | `claim-request-management.service.ts` |
|
||||||
| Policy select | same | `fanavaran-policy-selection.ts` |
|
| Policy select | same | `fanavaran-policy-selection.ts` |
|
||||||
| SMS after expertise | `SmsOrchestrationModule` | `sms-orchestration.service.ts` |
|
| SMS after expertise | Disabled by product decision | No message is dispatched |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ source: fanavaran-module-docs
|
|||||||
| Send | GEN.03 JSON + business headers |
|
| Send | GEN.03 JSON + business headers |
|
||||||
| Receive | `Id`, `ClaimNo` |
|
| Receive | `Id`, `ClaimNo` |
|
||||||
| Store | `claimId`, `claimNo`, `fanavaranSync.baseClaim.*`, history |
|
| Store | `claimId`, `claimNo`, `fanavaranSync.baseClaim.*`, history |
|
||||||
| Consumers | Damage/expertise soft-ensure; UI claim detail; SMS |
|
| Consumers | Damage/expertise soft-ensure; UI claim detail |
|
||||||
|
|
||||||
### C. Damage case
|
### C. Damage case
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -462,7 +462,7 @@
|
|||||||
<tr><td><span class="method post">POST</span></td><td><code>run-inquiries/:id</code> / <code>run-inquiries-vin/:id</code></td><td>Run plate or VIN inquiry. First call = guilty (+ auto-creates claim). Second call = damaged (THIRD_PARTY only).</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>run-inquiries/:id</code> / <code>run-inquiries-vin/:id</code></td><td>Run plate or VIN inquiry. First call = guilty (+ auto-creates claim). Second call = damaged (THIRD_PARTY only).</td></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>add-detail-location/:id</code> / <code>add-detail-description/:id</code> / <code>upload-voice/:id</code></td><td>Add location, description, and voice for current party (partyRole param selects FIRST/SECOND).</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>add-detail-location/:id</code> / <code>add-detail-description/:id</code> / <code>upload-voice/:id</code></td><td>Add location, description, and voice for current party (partyRole param selects FIRST/SECOND).</td></tr>
|
||||||
<tr><td><span class="method put">PUT</span></td><td><code>sign/:id</code></td><td>Upload party signature (partyRole=FIRST then SECOND). After second signature, file is sealed.</td></tr>
|
<tr><td><span class="method put">PUT</span></td><td><code>sign/:id</code></td><td>Upload party signature (partyRole=FIRST then SECOND). After second signature, file is sealed.</td></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Upload licences / car cards against the auto-created claim.</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Upload licences / car cards and the optional <code>accident_sketch</code> (کروکی) against the auto-created claim. The sketch never gates completion.</td></tr>
|
||||||
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>Step-aware capture requirements (phases: pre-capture docs vs damaged parts + chassis/engine).</td></tr>
|
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>Step-aware capture requirements (phases: pre-capture docs vs damaged parts + chassis/engine).</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -498,7 +498,7 @@
|
|||||||
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>Get the auto-created claim ID (from FileMaker's guilty-party inquiry).</td></tr>
|
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>Get the auto-created claim ID (from FileMaker's guilty-party inquiry).</td></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:requestId</code></td><td>Step 1 (FileReviewer): save accident fields (accidentWay, accidentReason, accidentType).</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:requestId</code></td><td>Step 1 (FileReviewer): save accident fields (accidentWay, accidentReason, accidentType).</td></tr>
|
||||||
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>Step-aware capture requirements (pre-capture docs phase vs capture-parts phase).</td></tr>
|
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>Step-aware capture requirements (pre-capture docs phase vs capture-parts phase).</td></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Upload chassis / engine / metal-plate documents.</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>upload-document/:claimId</code></td><td>Upload chassis / engine / metal-plate documents; <code>accident_sketch</code> (کروکی) remains optional and does not affect completion.</td></tr>
|
||||||
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code></td><td>Select outer (body) damaged parts.</td></tr>
|
<tr><td><span class="method patch">PATCH</span></td><td><code>select-outer-parts/:claimId</code></td><td>Select outer (body) damaged parts.</td></tr>
|
||||||
<tr><td><span class="method patch">PATCH</span></td><td><code>select-other-parts/:claimId</code></td><td>Select other (non-body) damaged parts.</td></tr>
|
<tr><td><span class="method patch">PATCH</span></td><td><code>select-other-parts/:claimId</code></td><td>Select other (non-body) damaged parts.</td></tr>
|
||||||
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>Capture part photos + angles for each selected damaged part.</td></tr>
|
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>Capture part photos + angles for each selected damaged part.</td></tr>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
export enum ClaimRequiredDocumentType {
|
export enum ClaimRequiredDocumentType {
|
||||||
|
/** Optional police accident sketch (Persian: کروکی). Never gates claim progress. */
|
||||||
|
ACCIDENT_SKETCH = "accident_sketch",
|
||||||
|
|
||||||
// Car green card
|
// Car green card
|
||||||
CAR_GREEN_CARD = "car_green_card",
|
CAR_GREEN_CARD = "car_green_card",
|
||||||
CAR_CERTIFICATE = "car_certificate",
|
CAR_CERTIFICATE = "car_certificate",
|
||||||
@@ -34,4 +37,3 @@ export enum CarAngle {
|
|||||||
LEFT = "left",
|
LEFT = "left",
|
||||||
RIGHT = "right",
|
RIGHT = "right",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
45
src/claim-request-management/claim-optional-document.spec.ts
Normal file
45
src/claim-request-management/claim-optional-document.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
|
||||||
|
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||||
|
|
||||||
|
describe("optional accident sketch", () => {
|
||||||
|
const service = Object.create(
|
||||||
|
ClaimRequestManagementService.prototype,
|
||||||
|
) as ClaimRequestManagementService;
|
||||||
|
|
||||||
|
it("is excluded from legacy required-document completion", () => {
|
||||||
|
const requiredTypes = (service as any).getRequiredDocumentTypes({
|
||||||
|
blameFile: { type: "THIRD_PARTY" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requiredTypes).not.toContain(
|
||||||
|
ClaimRequiredDocumentType.ACCIDENT_SKETCH,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not gate V2 owner-document completion", () => {
|
||||||
|
const mandatoryKeys = (service as any).requiredDocumentKeysV2(false);
|
||||||
|
const requiredDocuments = Object.fromEntries(
|
||||||
|
[...mandatoryKeys, ClaimRequiredDocumentType.CAR_GREEN_CARD].map(
|
||||||
|
(key) => [key, { uploaded: true }],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(service as any).allV2OwnerDocumentsComplete(
|
||||||
|
{ requiredDocuments },
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
requiredDocuments[ClaimRequiredDocumentType.ACCIDENT_SKETCH] = {
|
||||||
|
uploaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(service as any).allV2OwnerDocumentsComplete(
|
||||||
|
{ requiredDocuments },
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -58,7 +58,6 @@ import { MediaPolicyModule } from "src/media-policy/media-policy.module";
|
|||||||
import { FanavaranAuditModule } from "src/fanavaran/fanavaran-audit.module";
|
import { FanavaranAuditModule } from "src/fanavaran/fanavaran-audit.module";
|
||||||
import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module";
|
import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module";
|
||||||
import { PlateNormalizerModule } from "src/utils/plate-normalizer/plate-normalizer.module";
|
import { PlateNormalizerModule } from "src/utils/plate-normalizer/plate-normalizer.module";
|
||||||
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -77,7 +76,6 @@ import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.
|
|||||||
SandHubModule,
|
SandHubModule,
|
||||||
ClientModule,
|
ClientModule,
|
||||||
MediaPolicyModule,
|
MediaPolicyModule,
|
||||||
SmsOrchestrationModule,
|
|
||||||
JwtModule.register({}),
|
JwtModule.register({}),
|
||||||
MongooseModule.forFeature([
|
MongooseModule.forFeature([
|
||||||
{ name: ClaimCase.name, schema: ClaimCaseSchema },
|
{ name: ClaimCase.name, schema: ClaimCaseSchema },
|
||||||
|
|||||||
@@ -51,8 +51,10 @@ import {
|
|||||||
} from "./fanavaran-claim-product";
|
} from "./fanavaran-claim-product";
|
||||||
import {
|
import {
|
||||||
actualPremiumFromHullPolicyRecord,
|
actualPremiumFromHullPolicyRecord,
|
||||||
|
authorityCulpritIdFromDetectionRows,
|
||||||
customerIdFromHullPolicyRecord,
|
customerIdFromHullPolicyRecord,
|
||||||
FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID,
|
FANAVARAN_DEFAULT_HULL_AUTHORITY_CULPRIT_ID,
|
||||||
|
FANAVARAN_DEFAULT_HULL_CUSTOMER_FAULT_PERCENT,
|
||||||
fanavaranHullNestedClaimId,
|
fanavaranHullNestedClaimId,
|
||||||
toFanavaranHullBaseClaimPayload,
|
toFanavaranHullBaseClaimPayload,
|
||||||
} from "./fanavaran-hull-base-claim";
|
} from "./fanavaran-hull-base-claim";
|
||||||
@@ -64,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";
|
||||||
@@ -247,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,
|
||||||
@@ -485,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,
|
||||||
@@ -1226,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;
|
||||||
@@ -1262,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;
|
||||||
@@ -1302,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;
|
||||||
@@ -1313,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
|
||||||
) {
|
) {
|
||||||
@@ -1327,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[];
|
||||||
const selectedSlugStrings = this.carDamagePartDtoToOuterPartSlugs(
|
if (dto.selectedPartIds?.length) {
|
||||||
dto.carPartDamage,
|
const liveCatalog = await this.getLiveFanavaranCatalogItems();
|
||||||
);
|
const byId = new Map<number, OuterPartCatalogItem>(
|
||||||
if (!selectedSlugStrings.length) {
|
liveCatalog.map((part) => [part.id, part]),
|
||||||
throw new BadRequestException(
|
);
|
||||||
"No outer damaged parts could be derived from carPartDamage. Check part selections.",
|
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(
|
||||||
|
dto.carPartDamage!,
|
||||||
|
);
|
||||||
|
if (!selectedSlugStrings.length) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"No outer damaged parts could be derived from carPartDamage. Check part selections.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
selectedPartObjs = normalizeDamageSelectedParts(
|
||||||
|
selectedSlugStrings,
|
||||||
|
working.vehicle?.carType as ClaimVehicleTypeV2 | undefined,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const selectedPartObjs = normalizeDamageSelectedParts(
|
|
||||||
selectedSlugStrings,
|
|
||||||
working.vehicle?.carType as ClaimVehicleTypeV2 | 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,
|
||||||
@@ -1594,7 +1639,9 @@ export class ClaimRequestManagementService {
|
|||||||
private getRequiredDocumentTypes(
|
private getRequiredDocumentTypes(
|
||||||
claimRequest: any,
|
claimRequest: any,
|
||||||
): ClaimRequiredDocumentType[] {
|
): ClaimRequiredDocumentType[] {
|
||||||
const allTypes = Object.values(ClaimRequiredDocumentType);
|
const allTypes = Object.values(ClaimRequiredDocumentType).filter(
|
||||||
|
(type) => type !== ClaimRequiredDocumentType.ACCIDENT_SKETCH,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if it's CAR_BODY type
|
// Check if it's CAR_BODY type
|
||||||
const isCarBody = claimRequest.blameFile?.type === "CAR_BODY";
|
const isCarBody = claimRequest.blameFile?.type === "CAR_BODY";
|
||||||
@@ -4259,6 +4306,7 @@ export class ClaimRequestManagementService {
|
|||||||
/** Blame `Party` row (FIRST party for CAR_BODY); person shape is read via pickPerson* helpers. */
|
/** Blame `Party` row (FIRST party for CAR_BODY); person shape is read via pickPerson* helpers. */
|
||||||
firstParty?: { person?: unknown };
|
firstParty?: { person?: unknown };
|
||||||
logPrefix: string;
|
logPrefix: string;
|
||||||
|
claimCaseId?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const policyId = parseFanavaranId(input.payload.PolicyId);
|
const policyId = parseFanavaranId(input.payload.PolicyId);
|
||||||
if (policyId == null) return;
|
if (policyId == null) return;
|
||||||
@@ -4290,6 +4338,13 @@ export class ClaimRequestManagementService {
|
|||||||
input.payload.ActualPremium = actualPremium;
|
input.payload.ActualPremium = actualPremium;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const vehicleValue = vehicleCurrentValueFromHullPolicyRecord(policyRecord);
|
||||||
|
if (vehicleValue != null && input.claimCaseId) {
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(input.claimCaseId, {
|
||||||
|
$set: { "vehicle.carPrice": vehicleValue },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const person = input.firstParty?.person as
|
const person = input.firstParty?.person as
|
||||||
| {
|
| {
|
||||||
driverIsInsurer?: boolean;
|
driverIsInsurer?: boolean;
|
||||||
@@ -4331,14 +4386,25 @@ export class ClaimRequestManagementService {
|
|||||||
input.payload.AccidentCulpritId = accidentCulpritId;
|
input.payload.AccidentCulpritId = accidentCulpritId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const culpritTypeId =
|
input.payload.CustomerFaultPercent =
|
||||||
parseFanavaranId(input.payload.CulpritTypeId) ??
|
FANAVARAN_DEFAULT_HULL_CUSTOMER_FAULT_PERCENT;
|
||||||
FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID;
|
|
||||||
if (
|
if (parseFanavaranId(input.payload.AuthorityCulpritId) == null) {
|
||||||
input.payload.CustomerFaultPercent == null &&
|
try {
|
||||||
culpritTypeId === 300
|
const authorityRows = await this.getFanavaranLookupRows(
|
||||||
) {
|
input.clientKey,
|
||||||
input.payload.CustomerFaultPercent = 100;
|
"detection-accident-culprits",
|
||||||
|
);
|
||||||
|
input.payload.AuthorityCulpritId =
|
||||||
|
authorityCulpritIdFromDetectionRows(authorityRows);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
`${input.logPrefix} detection-accident-culprits lookup failed; using default AuthorityCulpritId=${FANAVARAN_DEFAULT_HULL_AUTHORITY_CULPRIT_ID}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
input.payload.AuthorityCulpritId =
|
||||||
|
FANAVARAN_DEFAULT_HULL_AUTHORITY_CULPRIT_ID;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4416,7 +4482,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(
|
||||||
@@ -4430,13 +4496,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 =
|
||||||
@@ -4448,13 +4507,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);
|
||||||
@@ -4498,16 +4550,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),
|
||||||
@@ -4517,20 +4559,20 @@ export class ClaimRequestManagementService {
|
|||||||
this.parseFanavaranMoney(part?.salary),
|
this.parseFanavaranMoney(part?.salary),
|
||||||
dmgKindId,
|
dmgKindId,
|
||||||
dmgCostKindId,
|
dmgCostKindId,
|
||||||
vehicleHullAccessoryId,
|
vehicleHullAccessoryId: FANAVARAN_DEFAULT_HULL_VEHICLE_ACCESSORY_ID,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let vehicleCurrentValue =
|
let vehicleCurrentValue =
|
||||||
this.parseFanavaranMoney(input.priceDropCarPrice) || null;
|
this.parseFanavaranMoney(input.claimCase?.vehicle?.carPrice) ||
|
||||||
|
this.parseFanavaranMoney(input.claimCase?.vehicle?.price) ||
|
||||||
|
this.parseFanavaranMoney(input.priceDropCarPrice) ||
|
||||||
|
null;
|
||||||
if (vehicleCurrentValue == null) {
|
if (vehicleCurrentValue == null) {
|
||||||
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,
|
||||||
@@ -6132,7 +6174,9 @@ export class ClaimRequestManagementService {
|
|||||||
blameCase,
|
blameCase,
|
||||||
firstParty,
|
firstParty,
|
||||||
logPrefix,
|
logPrefix,
|
||||||
|
claimCaseId,
|
||||||
});
|
});
|
||||||
|
delete payload.IsLicenseReplacement;
|
||||||
const hull = toFanavaranHullBaseClaimPayload(payload);
|
const hull = toFanavaranHullBaseClaimPayload(payload);
|
||||||
for (const key of Object.keys(payload)) delete payload[key];
|
for (const key of Object.keys(payload)) delete payload[key];
|
||||||
Object.assign(payload, hull);
|
Object.assign(payload, hull);
|
||||||
@@ -7369,13 +7413,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,
|
||||||
@@ -7451,13 +7489,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) {
|
||||||
@@ -7495,12 +7527,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,
|
||||||
@@ -7846,7 +7873,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(",")}`,
|
||||||
);
|
);
|
||||||
await this.putFanavaranJson(url, hull, clientKey, undefined, claimCaseId);
|
try {
|
||||||
|
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(
|
||||||
@@ -8669,111 +8707,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,
|
||||||
@@ -9416,6 +9349,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 ?? {},
|
||||||
@@ -10398,7 +10334,8 @@ export class ClaimRequestManagementService {
|
|||||||
if (!catalogByKey.has(item.key)) catalogByKey.set(item.key, item);
|
if (!catalogByKey.has(item.key)) catalogByKey.set(item.key, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required documents: CAR_BODY needs 7 (damaged + green card); THIRD_PARTY needs 13 (add guilty_*)
|
// Required documents vary by claim type. The accident sketch is offered
|
||||||
|
// in every flow, but is excluded from every completion calculation.
|
||||||
const blameRequest = claimCase.blameRequestId
|
const blameRequest = claimCase.blameRequestId
|
||||||
? await this.blameRequestDbService.findById(
|
? await this.blameRequestDbService.findById(
|
||||||
claimCase.blameRequestId.toString(),
|
claimCase.blameRequestId.toString(),
|
||||||
@@ -10406,53 +10343,68 @@ export class ClaimRequestManagementService {
|
|||||||
: null;
|
: null;
|
||||||
const isCarBody = blameRequest?.type === BlameRequestType.CAR_BODY;
|
const isCarBody = blameRequest?.type === BlameRequestType.CAR_BODY;
|
||||||
const requiredDocsDefinition = [
|
const requiredDocsDefinition = [
|
||||||
|
{
|
||||||
|
key: ClaimRequiredDocumentType.ACCIDENT_SKETCH,
|
||||||
|
label_fa: "کروکی",
|
||||||
|
label_en: "Accident Sketch",
|
||||||
|
category: "general",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "damaged_driving_license_front",
|
key: "damaged_driving_license_front",
|
||||||
label_fa: "گواهینامه طرف آسیبدیده - جلو",
|
label_fa: "گواهینامه طرف آسیبدیده - جلو",
|
||||||
label_en: "Damaged Party License - Front",
|
label_en: "Damaged Party License - Front",
|
||||||
category: "damaged_party",
|
category: "damaged_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "damaged_driving_license_back",
|
key: "damaged_driving_license_back",
|
||||||
label_fa: "گواهینامه طرف آسیبدیده - پشت",
|
label_fa: "گواهینامه طرف آسیبدیده - پشت",
|
||||||
label_en: "Damaged Party License - Back",
|
label_en: "Damaged Party License - Back",
|
||||||
category: "damaged_party",
|
category: "damaged_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "damaged_chassis_number",
|
key: "damaged_chassis_number",
|
||||||
label_fa: "شماره شاسی",
|
label_fa: "شماره شاسی",
|
||||||
label_en: "Chassis Number",
|
label_en: "Chassis Number",
|
||||||
category: "damaged_party",
|
category: "damaged_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "damaged_engine_photo",
|
key: "damaged_engine_photo",
|
||||||
label_fa: "عکس موتور",
|
label_fa: "عکس موتور",
|
||||||
label_en: "Engine Photo",
|
label_en: "Engine Photo",
|
||||||
category: "damaged_party",
|
category: "damaged_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "damaged_car_card_front",
|
key: "damaged_car_card_front",
|
||||||
label_fa: "کارت خودرو آسیبدیده - جلو",
|
label_fa: "کارت خودرو آسیبدیده - جلو",
|
||||||
label_en: "Damaged Car Card - Front",
|
label_en: "Damaged Car Card - Front",
|
||||||
category: "damaged_party",
|
category: "damaged_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "damaged_car_card_back",
|
key: "damaged_car_card_back",
|
||||||
label_fa: "کارت خودرو آسیبدیده - پشت",
|
label_fa: "کارت خودرو آسیبدیده - پشت",
|
||||||
label_en: "Damaged Car Card - Back",
|
label_en: "Damaged Car Card - Back",
|
||||||
category: "damaged_party",
|
category: "damaged_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "damaged_metal_plate",
|
key: "damaged_metal_plate",
|
||||||
label_fa: "پلاک فلزی آسیبدیده",
|
label_fa: "پلاک فلزی آسیبدیده",
|
||||||
label_en: "Damaged Metal Plate",
|
label_en: "Damaged Metal Plate",
|
||||||
category: "damaged_party",
|
category: "damaged_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "car_green_card",
|
key: "car_green_card",
|
||||||
label_fa: "کارت سبز خودرو",
|
label_fa: "کارت سبز خودرو",
|
||||||
label_en: "Car Green Card",
|
label_en: "Car Green Card",
|
||||||
category: "damaged_party",
|
category: "damaged_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
...(isCarBody
|
...(isCarBody
|
||||||
? []
|
? []
|
||||||
@@ -10462,30 +10414,35 @@ export class ClaimRequestManagementService {
|
|||||||
label_fa: "گواهینامه طرف مقصر - جلو",
|
label_fa: "گواهینامه طرف مقصر - جلو",
|
||||||
label_en: "Guilty Party License - Front",
|
label_en: "Guilty Party License - Front",
|
||||||
category: "guilty_party",
|
category: "guilty_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "guilty_driving_license_back",
|
key: "guilty_driving_license_back",
|
||||||
label_fa: "گواهینامه طرف مقصر - پشت",
|
label_fa: "گواهینامه طرف مقصر - پشت",
|
||||||
label_en: "Guilty Party License - Back",
|
label_en: "Guilty Party License - Back",
|
||||||
category: "guilty_party",
|
category: "guilty_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "guilty_car_card_front",
|
key: "guilty_car_card_front",
|
||||||
label_fa: "کارت خودرو مقصر - جلو",
|
label_fa: "کارت خودرو مقصر - جلو",
|
||||||
label_en: "Guilty Car Card - Front",
|
label_en: "Guilty Car Card - Front",
|
||||||
category: "guilty_party",
|
category: "guilty_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "guilty_car_card_back",
|
key: "guilty_car_card_back",
|
||||||
label_fa: "کارت خودرو مقصر - پشت",
|
label_fa: "کارت خودرو مقصر - پشت",
|
||||||
label_en: "Guilty Car Card - Back",
|
label_en: "Guilty Car Card - Back",
|
||||||
category: "guilty_party",
|
category: "guilty_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "guilty_metal_plate",
|
key: "guilty_metal_plate",
|
||||||
label_fa: "پلاک فلزی مقصر",
|
label_fa: "پلاک فلزی مقصر",
|
||||||
label_en: "Guilty Metal Plate",
|
label_en: "Guilty Metal Plate",
|
||||||
category: "guilty_party",
|
category: "guilty_party",
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
@@ -10497,6 +10454,7 @@ export class ClaimRequestManagementService {
|
|||||||
label_fa: doc.label_fa,
|
label_fa: doc.label_fa,
|
||||||
label_en: doc.label_en,
|
label_en: doc.label_en,
|
||||||
category: doc.category,
|
category: doc.category,
|
||||||
|
required: doc.required,
|
||||||
uploaded: docData?.uploaded || false,
|
uploaded: docData?.uploaded || false,
|
||||||
preferUploadDuringCapture: isCapturePhaseDamagedPartyDocKey(doc.key),
|
preferUploadDuringCapture: isCapturePhaseDamagedPartyDocKey(doc.key),
|
||||||
};
|
};
|
||||||
@@ -10559,7 +10517,10 @@ export class ClaimRequestManagementService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Calculate progress
|
// Calculate progress
|
||||||
const documentsUploaded = requiredDocuments.filter(
|
const requiredDocumentItems = requiredDocuments.filter(
|
||||||
|
(d) => d.required,
|
||||||
|
);
|
||||||
|
const documentsUploaded = requiredDocumentItems.filter(
|
||||||
(d) => d.uploaded,
|
(d) => d.uploaded,
|
||||||
).length;
|
).length;
|
||||||
const anglesCaptured = carAngles.filter((a) => a.captured).length;
|
const anglesCaptured = carAngles.filter((a) => a.captured).length;
|
||||||
@@ -10577,7 +10538,7 @@ export class ClaimRequestManagementService {
|
|||||||
captureProgress.anglesTotal - captureProgress.anglesCaptured,
|
captureProgress.anglesTotal - captureProgress.anglesCaptured,
|
||||||
);
|
);
|
||||||
const postCaptureDocsRemaining = requiredDocuments.filter(
|
const postCaptureDocsRemaining = requiredDocuments.filter(
|
||||||
(d) => !d.preferUploadDuringCapture && !d.uploaded,
|
(d) => d.required && !d.preferUploadDuringCapture && !d.uploaded,
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
const totalRemaining =
|
const totalRemaining =
|
||||||
@@ -10597,7 +10558,7 @@ export class ClaimRequestManagementService {
|
|||||||
totalRemaining,
|
totalRemaining,
|
||||||
progress: {
|
progress: {
|
||||||
documentsUploaded,
|
documentsUploaded,
|
||||||
documentsTotal: requiredDocuments.length,
|
documentsTotal: requiredDocumentItems.length,
|
||||||
anglesCaptured,
|
anglesCaptured,
|
||||||
anglesTotal: carAngles.length,
|
anglesTotal: carAngles.length,
|
||||||
partsCaptured,
|
partsCaptured,
|
||||||
@@ -10760,6 +10721,8 @@ export class ClaimRequestManagementService {
|
|||||||
|
|
||||||
const step = claimCase.workflow?.currentStep;
|
const step = claimCase.workflow?.currentStep;
|
||||||
const isResendUpload = step === ClaimWorkflowStep.USER_EXPERT_RESEND;
|
const isResendUpload = step === ClaimWorkflowStep.USER_EXPERT_RESEND;
|
||||||
|
const isOptionalDocument =
|
||||||
|
body.documentKey === ClaimRequiredDocumentType.ACCIDENT_SKETCH;
|
||||||
const isCapturePhaseDocUpload =
|
const isCapturePhaseDocUpload =
|
||||||
step === ClaimWorkflowStep.CAPTURE_PART_DAMAGES &&
|
step === ClaimWorkflowStep.CAPTURE_PART_DAMAGES &&
|
||||||
(isCapturePhaseDamagedPartyDocKey(body.documentKey) ||
|
(isCapturePhaseDamagedPartyDocKey(body.documentKey) ||
|
||||||
@@ -10790,7 +10753,9 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
const allowedInitialUploadStep =
|
const allowedInitialUploadStep =
|
||||||
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS ||
|
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS ||
|
||||||
isCapturePhaseDocUpload;
|
isCapturePhaseDocUpload ||
|
||||||
|
(isOptionalDocument &&
|
||||||
|
step === ClaimWorkflowStep.USER_SUBMISSION_COMPLETE);
|
||||||
if (!allowedInitialUploadStep) {
|
if (!allowedInitialUploadStep) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`Invalid workflow step. Expected ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS}, ${ClaimWorkflowStep.CAPTURE_PART_DAMAGES} (capture-phase documents only), or ${ClaimWorkflowStep.USER_EXPERT_RESEND}, but current step is ${claimCase.workflow?.currentStep}`,
|
`Invalid workflow step. Expected ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS}, ${ClaimWorkflowStep.CAPTURE_PART_DAMAGES} (capture-phase documents only), or ${ClaimWorkflowStep.USER_EXPERT_RESEND}, but current step is ${claimCase.workflow?.currentStep}`,
|
||||||
@@ -10909,7 +10874,7 @@ export class ClaimRequestManagementService {
|
|||||||
let remaining = 0;
|
let remaining = 0;
|
||||||
let captureStepCompletedOnThisUpload = false;
|
let captureStepCompletedOnThisUpload = false;
|
||||||
|
|
||||||
if (!isResendUpload) {
|
if (!isResendUpload && !isOptionalDocument) {
|
||||||
if (isCapturePhaseDocUpload) {
|
if (isCapturePhaseDocUpload) {
|
||||||
const afterThis = (k: string) =>
|
const afterThis = (k: string) =>
|
||||||
k === body.documentKey ||
|
k === body.documentKey ||
|
||||||
@@ -11047,6 +11012,28 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isResendUpload && isOptionalDocument) {
|
||||||
|
allDocumentsUploaded = options?.v3InPersonFlow
|
||||||
|
? this.allV3PreCaptureDocumentsComplete(
|
||||||
|
claimCase,
|
||||||
|
isCarBodyUpload,
|
||||||
|
undefined,
|
||||||
|
options?.skipMetalPlate,
|
||||||
|
)
|
||||||
|
: this.allV2OwnerDocumentsComplete(claimCase, isCarBodyUpload);
|
||||||
|
remaining = options?.v3InPersonFlow
|
||||||
|
? this.countRemainingV3PreCaptureDocuments(
|
||||||
|
claimCase,
|
||||||
|
isCarBodyUpload,
|
||||||
|
undefined,
|
||||||
|
options?.skipMetalPlate,
|
||||||
|
)
|
||||||
|
: this.countRemainingV2OwnerDocuments(
|
||||||
|
claimCase,
|
||||||
|
isCarBodyUpload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Build the final MongoDB update — all operators explicit and clean
|
// Build the final MongoDB update — all operators explicit and clean
|
||||||
const updatePayload: Record<string, unknown> = { $set };
|
const updatePayload: Record<string, unknown> = { $set };
|
||||||
|
|
||||||
@@ -11151,7 +11138,12 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auto-submit Fanavaran attachments when all documents are uploaded
|
// Auto-submit Fanavaran attachments when all documents are uploaded
|
||||||
if (allDocumentsUploaded && !isResendUpload && !options?.v3InPersonFlow) {
|
if (
|
||||||
|
allDocumentsUploaded &&
|
||||||
|
!isOptionalDocument &&
|
||||||
|
!isResendUpload &&
|
||||||
|
!options?.v3InPersonFlow
|
||||||
|
) {
|
||||||
this.submitFanavaranAttachmentsV2(claimRequestId, resolveFanavaranClientKey()).catch(
|
this.submitFanavaranAttachmentsV2(claimRequestId, resolveFanavaranClientKey()).catch(
|
||||||
(err) => this.logger.warn(`Fanavaran attachments auto-submit failed: ${err?.message}`),
|
(err) => this.logger.warn(`Fanavaran attachments auto-submit failed: ${err?.message}`),
|
||||||
);
|
);
|
||||||
@@ -11191,7 +11183,9 @@ export class ClaimRequestManagementService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = isCapturePhaseDocUpload
|
const message = isOptionalDocument
|
||||||
|
? "Optional accident sketch uploaded successfully."
|
||||||
|
: isCapturePhaseDocUpload
|
||||||
? captureStepCompletedOnThisUpload
|
? captureStepCompletedOnThisUpload
|
||||||
? options?.v3InPersonFlow
|
? options?.v3InPersonFlow
|
||||||
? "Capture-phase documents and all damage captures are complete. Upload the walk-around video (car-capture) to finalise."
|
? "Capture-phase documents and all damage captures are complete. Upload the walk-around video (car-capture) to finalise."
|
||||||
@@ -11214,7 +11208,9 @@ export class ClaimRequestManagementService {
|
|||||||
documentKey: body.documentKey,
|
documentKey: body.documentKey,
|
||||||
fileUrl,
|
fileUrl,
|
||||||
allDocumentsUploaded,
|
allDocumentsUploaded,
|
||||||
currentStep: isCapturePhaseDocUpload
|
currentStep: isOptionalDocument
|
||||||
|
? step || ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
||||||
|
: isCapturePhaseDocUpload
|
||||||
? captureStepCompletedOnThisUpload
|
? captureStepCompletedOnThisUpload
|
||||||
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
||||||
: ClaimWorkflowStep.CAPTURE_PART_DAMAGES
|
: ClaimWorkflowStep.CAPTURE_PART_DAMAGES
|
||||||
@@ -12971,7 +12967,10 @@ export class ClaimRequestManagementService {
|
|||||||
const preCaptureDocs = base.requiredDocuments.filter(
|
const preCaptureDocs = base.requiredDocuments.filter(
|
||||||
(d) => !d.preferUploadDuringCapture,
|
(d) => !d.preferUploadDuringCapture,
|
||||||
);
|
);
|
||||||
const remaining = preCaptureDocs.filter((d) => !d.uploaded).length;
|
const requiredPreCaptureDocs = preCaptureDocs.filter((d) => d.required);
|
||||||
|
const remaining = requiredPreCaptureDocs.filter(
|
||||||
|
(d) => !d.uploaded,
|
||||||
|
).length;
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
requiredDocuments: preCaptureDocs,
|
requiredDocuments: preCaptureDocs,
|
||||||
@@ -12983,8 +12982,9 @@ export class ClaimRequestManagementService {
|
|||||||
totalRemaining: remaining,
|
totalRemaining: remaining,
|
||||||
progress: {
|
progress: {
|
||||||
...base.progress,
|
...base.progress,
|
||||||
documentsTotal: preCaptureDocs.length,
|
documentsTotal: requiredPreCaptureDocs.length,
|
||||||
documentsUploaded: preCaptureDocs.filter((d) => d.uploaded).length,
|
documentsUploaded: requiredPreCaptureDocs.filter((d) => d.uploaded)
|
||||||
|
.length,
|
||||||
partsCaptured: 0,
|
partsCaptured: 0,
|
||||||
partsTotal: 0,
|
partsTotal: 0,
|
||||||
anglesCaptured: 0,
|
anglesCaptured: 0,
|
||||||
@@ -13039,6 +13039,7 @@ export class ClaimRequestManagementService {
|
|||||||
label_fa: "تصویر نقطه آسیبدیده مقصر",
|
label_fa: "تصویر نقطه آسیبدیده مقصر",
|
||||||
label_en: "Guilty Car Damaged Area",
|
label_en: "Guilty Car Damaged Area",
|
||||||
category: "guilty_party",
|
category: "guilty_party",
|
||||||
|
required: true,
|
||||||
uploaded: guiltyAreaUploaded,
|
uploaded: guiltyAreaUploaded,
|
||||||
preferUploadDuringCapture: true,
|
preferUploadDuringCapture: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -801,6 +801,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim)
|
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim)
|
||||||
|
|
||||||
**Upload one of the required documents** (12 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements):
|
**Upload one of the required documents** (12 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements):
|
||||||
|
- accident_sketch (optional; never blocks workflow completion)
|
||||||
- damaged_driving_license_front/back
|
- damaged_driving_license_front/back
|
||||||
- damaged_chassis_number, damaged_engine_photo
|
- damaged_chassis_number, damaged_engine_photo
|
||||||
- damaged_car_card_front/back, damaged_metal_plate
|
- damaged_car_card_front/back, damaged_metal_plate
|
||||||
@@ -836,6 +837,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
"guilty_car_card_front",
|
"guilty_car_card_front",
|
||||||
"guilty_car_card_back",
|
"guilty_car_card_back",
|
||||||
"guilty_metal_plate",
|
"guilty_metal_plate",
|
||||||
|
"accident_sketch",
|
||||||
],
|
],
|
||||||
example: "damaged_driving_license_front",
|
example: "damaged_driving_license_front",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ export class RequiredDocumentItem {
|
|||||||
example: true,
|
example: true,
|
||||||
})
|
})
|
||||||
preferUploadDuringCapture?: boolean;
|
preferUploadDuringCapture?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description:
|
||||||
|
'Whether this document is mandatory for workflow completion. Optional documents are uploadable but never included in remaining/progress counts.',
|
||||||
|
example: true,
|
||||||
|
})
|
||||||
|
required: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ export class ClaimVehicleSnapshot {
|
|||||||
|
|
||||||
@Prop({ type: String })
|
@Prop({ type: String })
|
||||||
price?: string;
|
price?: string;
|
||||||
|
|
||||||
|
/** Vehicle value in Rial from hull policy VehicleValue or expert assessment. */
|
||||||
|
@Prop({ type: Number })
|
||||||
|
carPrice?: number;
|
||||||
}
|
}
|
||||||
export const ClaimVehicleSnapshotSchema =
|
export const ClaimVehicleSnapshotSchema =
|
||||||
SchemaFactory.createForClass(ClaimVehicleSnapshot);
|
SchemaFactory.createForClass(ClaimVehicleSnapshot);
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export class FanavaranSyncStage {
|
|||||||
@Prop({ type: Date })
|
@Prop({ type: Date })
|
||||||
lastPayloadBuiltAt?: Date;
|
lastPayloadBuiltAt?: Date;
|
||||||
|
|
||||||
/** When we SMS'd the claim owner about Fanavaran claimId/ClaimNo (after expertise). */
|
/** Legacy marker from the retired post-expertise Fanavaran SMS flow. */
|
||||||
@Prop({ type: Date })
|
@Prop({ type: Date })
|
||||||
smsNotifiedAt?: Date;
|
smsNotifiedAt?: Date;
|
||||||
|
|
||||||
@@ -239,6 +239,10 @@ export class ClaimCase {
|
|||||||
@Prop({ type: Types.ObjectId, index: true })
|
@Prop({ type: Types.ObjectId, index: true })
|
||||||
initiatedByFieldExpertId?: Types.ObjectId;
|
initiatedByFieldExpertId?: Types.ObjectId;
|
||||||
|
|
||||||
|
/** Branch snapshot inherited from the originating expert-created blame. */
|
||||||
|
@Prop({ type: Types.ObjectId, index: true })
|
||||||
|
branchId?: Types.ObjectId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The damaged party's userId, resolved from the blame at claim-creation time.
|
* The damaged party's userId, resolved from the blame at claim-creation time.
|
||||||
* Stored here so view/list access does not require an extra blame lookup.
|
* Stored here so view/list access does not require an extra blame lookup.
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
"guilty_car_card_front",
|
"guilty_car_card_front",
|
||||||
"guilty_car_card_back",
|
"guilty_car_card_back",
|
||||||
"guilty_metal_plate",
|
"guilty_metal_plate",
|
||||||
|
"accident_sketch",
|
||||||
],
|
],
|
||||||
example: "damaged_driving_license_front",
|
example: "damaged_driving_license_front",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ describe("fanavaran hull base claim", () => {
|
|||||||
expect(fanavaranHullNestedClaimId({ ClaimNo: 2268 })).toBeNull();
|
expect(fanavaranHullNestedClaimId({ ClaimNo: 2268 })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps IsLicenseReplaced null even when ثالث IsLicenseReplacement is 0", () => {
|
||||||
|
const hull = toFanavaranHullBaseClaimPayload({
|
||||||
|
PolicyId: 1,
|
||||||
|
IsLicenseReplacement: 0,
|
||||||
|
});
|
||||||
|
expect(hull.IsLicenseReplaced).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("strips ثالث-only fields from a live hull create echo", () => {
|
it("strips ثالث-only fields from a live hull create echo", () => {
|
||||||
const hull = toFanavaranHullBaseClaimPayload({
|
const hull = toFanavaranHullBaseClaimPayload({
|
||||||
AccidentCauseId: 6,
|
AccidentCauseId: 6,
|
||||||
@@ -68,7 +76,7 @@ describe("fanavaran hull base claim", () => {
|
|||||||
expect(hull.IsLicenseReplaced).toBeNull();
|
expect(hull.IsLicenseReplaced).toBeNull();
|
||||||
expect(hull.CostSeparationToDmgSections).toBe(0);
|
expect(hull.CostSeparationToDmgSections).toBe(0);
|
||||||
expect(hull.IsOwnerChanged).toBe(0);
|
expect(hull.IsOwnerChanged).toBe(0);
|
||||||
expect(hull.CulpritTypeId).toBe(301);
|
expect(hull.CulpritTypeId).toBe(300);
|
||||||
expect(hull.DamagedCount).toBeUndefined();
|
expect(hull.DamagedCount).toBeUndefined();
|
||||||
expect(hull.HasOtherCulprit).toBeUndefined();
|
expect(hull.HasOtherCulprit).toBeUndefined();
|
||||||
expect(hull.CompensationReferenceId).toBeUndefined();
|
expect(hull.CompensationReferenceId).toBeUndefined();
|
||||||
|
|||||||
@@ -51,7 +51,17 @@ export const FANAVARAN_DEFAULT_HULL_ACCIDENT_TYPE_ID = 2;
|
|||||||
export const FANAVARAN_HULL_ANS_NO = 0;
|
export const FANAVARAN_HULL_ANS_NO = 0;
|
||||||
|
|
||||||
/** GEN.03 hull culprit type when third-party is at fault. Lookup: vehicle-hull-accident-culprit-type */
|
/** GEN.03 hull culprit type when third-party is at fault. Lookup: vehicle-hull-accident-culprit-type */
|
||||||
export const FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID = 301;
|
export const FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID = 300;
|
||||||
|
|
||||||
|
/** GEN.03 required: درصد تقصير بيمه گذار. Proven Parsian submit uses 100. */
|
||||||
|
export const FANAVARAN_DEFAULT_HULL_CUSTOMER_FAULT_PERCENT = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GEN.03 required: مرجع تعيين مقصر.
|
||||||
|
* Lookup: GET /lookups/detection-accident-culprits. Fallback is بیمهگذار.
|
||||||
|
*/
|
||||||
|
export const FANAVARAN_DEFAULT_HULL_AUTHORITY_CULPRIT_ID = 1;
|
||||||
|
export const FANAVARAN_HULL_AUTHORITY_CULPRIT_CAPTION = "بيمه گذار";
|
||||||
|
|
||||||
/** GEN.03 ActualPremium from GET car/vehicle-hull-policies/{PolicyId} → TotalPremium. */
|
/** GEN.03 ActualPremium from GET car/vehicle-hull-policies/{PolicyId} → TotalPremium. */
|
||||||
export function actualPremiumFromHullPolicyRecord(
|
export function actualPremiumFromHullPolicyRecord(
|
||||||
@@ -63,6 +73,41 @@ export function actualPremiumFromHullPolicyRecord(
|
|||||||
return Number.isFinite(n) && n >= 0 ? n : null;
|
return Number.isFinite(n) && n >= 0 ? n : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeHullCaption(value: unknown): string {
|
||||||
|
return String(value ?? "")
|
||||||
|
.replace(/ي/g, "ی")
|
||||||
|
.replace(/ك/g, "ک")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** AuthorityCulpritId from GET /lookups/detection-accident-culprits. */
|
||||||
|
export function authorityCulpritIdFromDetectionRows(
|
||||||
|
rows: unknown,
|
||||||
|
): number {
|
||||||
|
const list = Array.isArray(rows) ? rows : [];
|
||||||
|
const wanted = normalizeHullCaption(
|
||||||
|
FANAVARAN_HULL_AUTHORITY_CULPRIT_CAPTION,
|
||||||
|
);
|
||||||
|
for (const row of list) {
|
||||||
|
if (!row || typeof row !== "object") continue;
|
||||||
|
const caption = normalizeHullCaption(
|
||||||
|
(row as { Caption?: unknown }).Caption,
|
||||||
|
);
|
||||||
|
if (!caption.includes(wanted)) continue;
|
||||||
|
const raw = (row as { Id?: unknown }).Id;
|
||||||
|
const n = typeof raw === "number" ? raw : Number(raw);
|
||||||
|
if (Number.isFinite(n) && n > 0) return n;
|
||||||
|
}
|
||||||
|
for (const row of list) {
|
||||||
|
if (!row || typeof row !== "object") continue;
|
||||||
|
const raw = (row as { Id?: unknown }).Id;
|
||||||
|
const n = typeof raw === "number" ? raw : Number(raw);
|
||||||
|
if (Number.isFinite(n) && n > 0) return n;
|
||||||
|
}
|
||||||
|
return FANAVARAN_DEFAULT_HULL_AUTHORITY_CULPRIT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
/** Fallback AccidentCulpritId when parties inquiry is unavailable. */
|
/** Fallback AccidentCulpritId when parties inquiry is unavailable. */
|
||||||
export function customerIdFromHullPolicyRecord(
|
export function customerIdFromHullPolicyRecord(
|
||||||
policy: Record<string, unknown> | null | undefined,
|
policy: Record<string, unknown> | null | undefined,
|
||||||
@@ -79,8 +124,8 @@ export function toFanavaranHullBaseClaimPayload(
|
|||||||
const next: Record<string, unknown> = {};
|
const next: Record<string, unknown> = {};
|
||||||
for (const key of FANAVARAN_HULL_BASE_CLAIM_KEYS) {
|
for (const key of FANAVARAN_HULL_BASE_CLAIM_KEYS) {
|
||||||
if (key === "IsLicenseReplaced") {
|
if (key === "IsLicenseReplaced") {
|
||||||
next[key] =
|
// GEN.03: must be empty/null. Do not map ثالث IsLicenseReplacement (default 0).
|
||||||
built.IsLicenseReplaced ?? built.IsLicenseReplacement ?? null;
|
next[key] = null;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (key === "AccidentTypeId") {
|
if (key === "AccidentTypeId") {
|
||||||
@@ -108,15 +153,14 @@ export function toFanavaranHullBaseClaimPayload(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (key === "CustomerFaultPercent") {
|
if (key === "CustomerFaultPercent") {
|
||||||
const culpritType =
|
next[key] =
|
||||||
built.CulpritTypeId ?? FANAVARAN_DEFAULT_HULL_CULPRIT_TYPE_ID;
|
built.CustomerFaultPercent ??
|
||||||
if (built.CustomerFaultPercent != null) {
|
FANAVARAN_DEFAULT_HULL_CUSTOMER_FAULT_PERCENT;
|
||||||
next[key] = built.CustomerFaultPercent;
|
continue;
|
||||||
} else if (culpritType === 300) {
|
}
|
||||||
next[key] = 100;
|
if (key === "AuthorityCulpritId") {
|
||||||
} else {
|
next[key] =
|
||||||
next[key] = null;
|
built.AuthorityCulpritId ?? FANAVARAN_DEFAULT_HULL_AUTHORITY_CULPRIT_ID;
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
next[key] = Object.prototype.hasOwnProperty.call(built, key)
|
next[key] = Object.prototype.hasOwnProperty.call(built, key)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export const FANAVARAN_HULL_DMG_COST_KIND_CAPTION = "مجموع لوازم";
|
|||||||
export const FANAVARAN_DEFAULT_HULL_DMG_KIND_ID = 5485;
|
export const FANAVARAN_DEFAULT_HULL_DMG_KIND_ID = 5485;
|
||||||
/** Fallback when lookup fetch fails (`vehicle-hull-dmg-cost-kinds` Id). */
|
/** Fallback when lookup fetch fails (`vehicle-hull-dmg-cost-kinds` Id). */
|
||||||
export const FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID = 1;
|
export const FANAVARAN_DEFAULT_HULL_DMG_COST_KIND_ID = 1;
|
||||||
|
/** GEN.06 factory (فابریک) accessory row. Used for every hull DmgSection. */
|
||||||
|
export const FANAVARAN_DEFAULT_HULL_VEHICLE_ACCESSORY_ID = 3043330;
|
||||||
|
|
||||||
export type FanavaranHullDmgAccessoryRow = {
|
export type FanavaranHullDmgAccessoryRow = {
|
||||||
Id?: unknown;
|
Id?: unknown;
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ export {
|
|||||||
FANAVARAN_HULL_DMG_KIND_CAPTION,
|
FANAVARAN_HULL_DMG_KIND_CAPTION,
|
||||||
} from "./fanavaran-hull-expertise-lookups";
|
} from "./fanavaran-hull-expertise-lookups";
|
||||||
|
|
||||||
|
/** GEN.06 RepairDuration in days for every hull expertise. */
|
||||||
|
export const FANAVARAN_DEFAULT_HULL_REPAIR_DURATION = 3;
|
||||||
|
|
||||||
/** GEN.06 VehicleCurrentValue from GET car/vehicle-hull-policies/{PolicyId} → VehicleValue. */
|
/** GEN.06 VehicleCurrentValue from GET car/vehicle-hull-policies/{PolicyId} → VehicleValue. */
|
||||||
export function vehicleCurrentValueFromHullPolicyRecord(
|
export function vehicleCurrentValueFromHullPolicyRecord(
|
||||||
policy: Record<string, unknown> | null | undefined,
|
policy: Record<string, unknown> | null | undefined,
|
||||||
@@ -117,7 +120,8 @@ export function toFanavaranHullExpertisePayload(input: {
|
|||||||
ComponentReplacementCost: input.componentReplacementCost,
|
ComponentReplacementCost: input.componentReplacementCost,
|
||||||
WasteValue: input.wasteValue,
|
WasteValue: input.wasteValue,
|
||||||
CarryAndRescueCost: null,
|
CarryAndRescueCost: null,
|
||||||
RepairDuration: input.repairDuration ?? null,
|
RepairDuration:
|
||||||
|
input.repairDuration ?? FANAVARAN_DEFAULT_HULL_REPAIR_DURATION,
|
||||||
VehicleCurrentValue: input.vehicleCurrentValue,
|
VehicleCurrentValue: input.vehicleCurrentValue,
|
||||||
WreckHighestValue: null,
|
WreckHighestValue: null,
|
||||||
InspectionDeduction: null,
|
InspectionDeduction: null,
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
"guilty_car_card_front",
|
"guilty_car_card_front",
|
||||||
"guilty_car_card_back",
|
"guilty_car_card_back",
|
||||||
"guilty_metal_plate",
|
"guilty_metal_plate",
|
||||||
|
"accident_sketch",
|
||||||
],
|
],
|
||||||
example: "damaged_driving_license_front",
|
example: "damaged_driving_license_front",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export class ExternalInquiryFlagsDto implements ExternalInquiryFlags {
|
|||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description:
|
description:
|
||||||
"ESG VIN/chassis-number inquiry (`/inquiry/policyByChassis`). Required for the VIN initial-form path.",
|
"ESG two-factor VIN/chassis inquiry (`/inquiry/carByChassis`). Required for the VIN initial-form path.",
|
||||||
example: false,
|
example: false,
|
||||||
})
|
})
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
|||||||
@@ -614,9 +614,24 @@ export class ExpertBlameService {
|
|||||||
)) as Record<string, unknown>[];
|
)) as Record<string, unknown>[];
|
||||||
|
|
||||||
// Scope to this reviewer's insurance company via blame party clientId
|
// Scope to this reviewer's insurance company via blame party clientId
|
||||||
const visibleCases = allSealed.filter((doc) =>
|
const tenantVisibleCases = allSealed.filter((doc) =>
|
||||||
blameCaseTouchesClient(doc, clientKey),
|
blameCaseTouchesClient(doc, clientKey),
|
||||||
);
|
);
|
||||||
|
const visibleCases = (
|
||||||
|
await Promise.all(
|
||||||
|
tenantVisibleCases.map(async (doc) => {
|
||||||
|
try {
|
||||||
|
await this.requestManagementService.assertFileReviewerBranchAccess(
|
||||||
|
doc,
|
||||||
|
actor.sub,
|
||||||
|
);
|
||||||
|
return doc;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).filter((doc): doc is Record<string, unknown> => doc != null);
|
||||||
|
|
||||||
const pagedResult = await this.paginateBlameListV2(visibleCases, query);
|
const pagedResult = await this.paginateBlameListV2(visibleCases, query);
|
||||||
return pagedResult;
|
return pagedResult;
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ describe("ExpertClaimService expert-reply pricing", () => {
|
|||||||
expect(findByIdAndUpdate).toHaveBeenCalledTimes(1);
|
expect(findByIdAndUpdate).toHaveBeenCalledTimes(1);
|
||||||
expect(findByIdAndUpdate.mock.calls[0][1]).toMatchObject({
|
expect(findByIdAndUpdate.mock.calls[0][1]).toMatchObject({
|
||||||
"vehicle.price": "1250000000",
|
"vehicle.price": "1250000000",
|
||||||
|
"vehicle.carPrice": 1250000000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -514,6 +515,9 @@ describe("ExpertClaimService FileReviewer assignment", () => {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
service.fanavaranLocationService = {
|
||||||
|
assertMakerReviewerBranchCompatible: jest.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
service.assertExpertActorOnClaim = jest.fn();
|
service.assertExpertActorOnClaim = jest.fn();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -530,3 +534,28 @@ describe("ExpertClaimService FileReviewer assignment", () => {
|
|||||||
expect(service.assertExpertActorOnClaim).not.toHaveBeenCalled();
|
expect(service.assertExpertActorOnClaim).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ExpertClaimService branch-scoped claim views", () => {
|
||||||
|
it("shows branch-scoped claims only to damage experts in that branch", async () => {
|
||||||
|
const service = createService() as any;
|
||||||
|
const branchId = new Types.ObjectId();
|
||||||
|
const otherBranchId = new Types.ObjectId();
|
||||||
|
service.damageExpertDbService = {
|
||||||
|
findById: jest.fn().mockResolvedValue({ branchId }),
|
||||||
|
};
|
||||||
|
service.blameRequestDbService = {
|
||||||
|
find: jest.fn().mockResolvedValue([]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const inBranch = { _id: "in-branch", branchId };
|
||||||
|
const otherBranch = { _id: "other-branch", branchId: otherBranchId };
|
||||||
|
const legacyUnscoped = { _id: "legacy-unscoped" };
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.filterDamageExpertClaimsByBranch(
|
||||||
|
[inBranch, otherBranch, legacyUnscoped],
|
||||||
|
{ sub: V2_EXPERT_ID, role: RoleEnum.DAMAGE_EXPERT },
|
||||||
|
),
|
||||||
|
).resolves.toEqual([inBranch, legacyUnscoped]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -418,14 +418,145 @@ export class ExpertClaimService {
|
|||||||
claim: any,
|
claim: any,
|
||||||
actor: any,
|
actor: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const blame = await this.loadBlameForClaim(claim);
|
||||||
// FILE_REVIEWER: by the time we reach this method the caller has already
|
// FILE_REVIEWER: by the time we reach this method the caller has already
|
||||||
// verified that actor.sub === blame.assignedFileReviewerId (Phase 2 of
|
// verified that actor.sub === blame.assignedFileReviewerId (Phase 2 of
|
||||||
// assignClaimForReviewV2). The generic tenant-scope check would incorrectly
|
// assignClaimForReviewV2). The generic tenant-scope check would incorrectly
|
||||||
// reject them because initiatedByFieldExpertId belongs to the FileMaker, not
|
// reject them because initiatedByFieldExpertId belongs to the FileMaker, not
|
||||||
// the reviewer. Skip it — the assignment check is the access proof.
|
// the reviewer. Skip it — the assignment check is the access proof.
|
||||||
if ((actor as any).role === RoleEnum.FILE_REVIEWER) return;
|
if ((actor as any).role === RoleEnum.FILE_REVIEWER) {
|
||||||
const blame = await this.loadBlameForClaim(claim);
|
await this.assertActorBranchAccess(claim, actor, blame);
|
||||||
|
return;
|
||||||
|
}
|
||||||
assertClaimCaseForExpertActor(claim, actor, blame);
|
assertClaimCaseForExpertActor(claim, actor, blame);
|
||||||
|
await this.assertActorBranchAccess(claim, actor, blame);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertActorBranchAccess(
|
||||||
|
claim: any,
|
||||||
|
actor: any,
|
||||||
|
blame?: any,
|
||||||
|
): Promise<void> {
|
||||||
|
const caseBranchId = claim?.branchId
|
||||||
|
? String(claim.branchId)
|
||||||
|
: blame?.branchId
|
||||||
|
? String(blame.branchId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (actor?.role === RoleEnum.FILE_REVIEWER) {
|
||||||
|
await this.fanavaranLocationService.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: blame?.initiatedByFieldExpertId
|
||||||
|
? String(blame.initiatedByFieldExpertId)
|
||||||
|
: null,
|
||||||
|
fileReviewerId: String(actor.sub),
|
||||||
|
caseBranchId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor?.role === RoleEnum.FILE_MAKER) {
|
||||||
|
const actorBranchId = await this.fanavaranLocationService.fileMakerBranchId(
|
||||||
|
String(actor.sub),
|
||||||
|
);
|
||||||
|
if (!actorBranchId || (caseBranchId && caseBranchId !== actorBranchId)) {
|
||||||
|
throw new ForbiddenException("This file belongs to another branch.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor?.role === RoleEnum.FIELD_EXPERT) return;
|
||||||
|
|
||||||
|
const damageExpert = await this.damageExpertDbService.findById(
|
||||||
|
String(actor.sub),
|
||||||
|
);
|
||||||
|
const actorBranchId = (damageExpert as any)?.branchId
|
||||||
|
? String((damageExpert as any).branchId)
|
||||||
|
: undefined;
|
||||||
|
if (!actorBranchId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"DamageExpert account is not assigned to a branch.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Ordinary user-created legacy claims have no branch snapshot. Preserve
|
||||||
|
// their existing tenant queue; branch-created claims must match exactly.
|
||||||
|
if (caseBranchId && caseBranchId !== actorBranchId) {
|
||||||
|
throw new ForbiddenException("This file belongs to another branch.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async filterDamageExpertClaimsByBranch(
|
||||||
|
claims: any[],
|
||||||
|
actor: any,
|
||||||
|
): Promise<any[]> {
|
||||||
|
const damageExpert = await this.damageExpertDbService.findById(
|
||||||
|
String(actor.sub),
|
||||||
|
);
|
||||||
|
const actorBranchId = (damageExpert as any)?.branchId
|
||||||
|
? String((damageExpert as any).branchId)
|
||||||
|
: undefined;
|
||||||
|
if (!actorBranchId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"DamageExpert account is not assigned to a branch.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingBranchBlameIds = [
|
||||||
|
...new Set(
|
||||||
|
claims
|
||||||
|
.filter((claim) => !claim?.branchId && claim?.blameRequestId)
|
||||||
|
.map((claim) => String(claim.blameRequestId)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
const blames = missingBranchBlameIds.length
|
||||||
|
? ((await this.blameRequestDbService.find(
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
$in: missingBranchBlameIds.map((id) => new Types.ObjectId(id)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lean: true,
|
||||||
|
select:
|
||||||
|
"_id branchId isMadeByFileMaker initiatedByFieldExpertId",
|
||||||
|
},
|
||||||
|
)) as any[])
|
||||||
|
: [];
|
||||||
|
const blameById = new Map(
|
||||||
|
blames.map((blame) => [String(blame._id), blame] as const),
|
||||||
|
);
|
||||||
|
const makerBranchById = new Map<string, string | undefined>();
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
...new Set(
|
||||||
|
blames
|
||||||
|
.filter(
|
||||||
|
(blame) =>
|
||||||
|
blame.isMadeByFileMaker &&
|
||||||
|
!blame.branchId &&
|
||||||
|
blame.initiatedByFieldExpertId,
|
||||||
|
)
|
||||||
|
.map((blame) => String(blame.initiatedByFieldExpertId)),
|
||||||
|
),
|
||||||
|
].map(async (makerId) => {
|
||||||
|
makerBranchById.set(
|
||||||
|
makerId,
|
||||||
|
await this.fanavaranLocationService.fileMakerBranchId(makerId),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return claims.filter((claim) => {
|
||||||
|
let caseBranchId = claim?.branchId ? String(claim.branchId) : undefined;
|
||||||
|
if (!caseBranchId && claim?.blameRequestId) {
|
||||||
|
const blame = blameById.get(String(claim.blameRequestId));
|
||||||
|
caseBranchId = blame?.branchId
|
||||||
|
? String(blame.branchId)
|
||||||
|
: blame?.isMadeByFileMaker && blame?.initiatedByFieldExpertId
|
||||||
|
? makerBranchById.get(String(blame.initiatedByFieldExpertId))
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
return !caseBranchId || caseBranchId === actorBranchId;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fieldExpertOwnsClaim(
|
private async fieldExpertOwnsClaim(
|
||||||
@@ -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;
|
||||||
@@ -3341,9 +3490,10 @@ export class ExpertClaimService {
|
|||||||
|
|
||||||
const carPriceWasProvided = reply.carPrice != null;
|
const carPriceWasProvided = reply.carPrice != null;
|
||||||
let normalizedCurrentCarPrice: string | undefined;
|
let normalizedCurrentCarPrice: string | undefined;
|
||||||
|
let parsedCurrentCarPrice: number | undefined;
|
||||||
|
|
||||||
if (blame.type === BlameRequestType.CAR_BODY) {
|
if (blame.type === BlameRequestType.CAR_BODY) {
|
||||||
const parsedCurrentCarPrice = parseMoneyAmountToman(reply.carPrice);
|
parsedCurrentCarPrice = parseMoneyAmountToman(reply.carPrice) ?? undefined;
|
||||||
if (
|
if (
|
||||||
parsedCurrentCarPrice == null ||
|
parsedCurrentCarPrice == null ||
|
||||||
!Number.isSafeInteger(parsedCurrentCarPrice) ||
|
!Number.isSafeInteger(parsedCurrentCarPrice) ||
|
||||||
@@ -3600,8 +3750,11 @@ export class ExpertClaimService {
|
|||||||
"evaluation.ownerInsurerApproval": "",
|
"evaluation.ownerInsurerApproval": "",
|
||||||
"evaluation.ownerPricedPartsApproval": "",
|
"evaluation.ownerPricedPartsApproval": "",
|
||||||
},
|
},
|
||||||
...(normalizedCurrentCarPrice
|
...(normalizedCurrentCarPrice && parsedCurrentCarPrice != null
|
||||||
? { "vehicle.price": normalizedCurrentCarPrice }
|
? {
|
||||||
|
"vehicle.price": normalizedCurrentCarPrice,
|
||||||
|
"vehicle.carPrice": parsedCurrentCarPrice,
|
||||||
|
}
|
||||||
: {}),
|
: {}),
|
||||||
"workflow.currentStep": currentStep,
|
"workflow.currentStep": currentStep,
|
||||||
"workflow.nextStep": nextWorkflowStep,
|
"workflow.nextStep": nextWorkflowStep,
|
||||||
@@ -4117,9 +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(
|
||||||
@@ -4385,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[];
|
||||||
|
|
||||||
@@ -4394,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);
|
||||||
@@ -4474,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);
|
||||||
@@ -4916,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 =
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ExpertInsurerService } from "./expert-insurer.service";
|
import { ExpertInsurerService } from "./expert-insurer.service";
|
||||||
|
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||||
|
|
||||||
describe("insurer expert file summaries", () => {
|
describe("insurer expert file summaries", () => {
|
||||||
const service = Object.create(
|
const service = Object.create(
|
||||||
@@ -49,4 +50,25 @@ describe("insurer expert file summaries", () => {
|
|||||||
|
|
||||||
expect(summary.reviewDurationMinutes).toBe(0);
|
expect(summary.reviewDurationMinutes).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("exposes the four Fanavaran claim references in insurer claim detail", async () => {
|
||||||
|
const detail = await (service as any).buildInsurerClaimDetail(
|
||||||
|
{
|
||||||
|
_id: "6aa50ce70545fc1906433c86",
|
||||||
|
status: ClaimCaseStatus.COMPLETED,
|
||||||
|
claimId: 101,
|
||||||
|
claimNo: 102,
|
||||||
|
dmgCaseId: 103,
|
||||||
|
expertiseId: 104,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(detail.fanavaran).toEqual({
|
||||||
|
claimId: 101,
|
||||||
|
claimNo: 102,
|
||||||
|
dmgCaseId: 103,
|
||||||
|
expertiseId: 104,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import {
|
|||||||
} from "./helper/timeline-fa-labels";
|
} from "./helper/timeline-fa-labels";
|
||||||
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
|
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
|
||||||
import { serializeDamagedPartSelectionHistory } from "src/helpers/claim-damaged-part-audit";
|
import { serializeDamagedPartSelectionHistory } from "src/helpers/claim-damaged-part-audit";
|
||||||
|
import { fanavaranClaimReferences } from "src/claim-request-management/fanavaran-claim-references";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExpertInsurerService {
|
export class ExpertInsurerService {
|
||||||
@@ -786,6 +787,7 @@ export class ExpertInsurerService {
|
|||||||
publicId: claim.publicId,
|
publicId: claim.publicId,
|
||||||
status: claim.status,
|
status: claim.status,
|
||||||
claimStatus: claim.claimStatus,
|
claimStatus: claim.claimStatus,
|
||||||
|
fanavaran: fanavaranClaimReferences(claim),
|
||||||
blameDocumentResendPending: claim.blameDocumentResendPending,
|
blameDocumentResendPending: claim.blameDocumentResendPending,
|
||||||
workflow: claim.workflow,
|
workflow: claim.workflow,
|
||||||
owner: ownerOut,
|
owner: ownerOut,
|
||||||
|
|||||||
55
src/fanavaran/fanavaran-location.service.spec.ts
Normal file
55
src/fanavaran/fanavaran-location.service.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { ForbiddenException } from "@nestjs/common";
|
||||||
|
import { FanavaranLocationService } from "./fanavaran-location.service";
|
||||||
|
|
||||||
|
describe("FanavaranLocationService branch scope", () => {
|
||||||
|
const makerId = "maker-1";
|
||||||
|
const reviewerId = "reviewer-1";
|
||||||
|
|
||||||
|
const createService = (makerBranch?: string, reviewerBranch?: string) =>
|
||||||
|
new FanavaranLocationService(
|
||||||
|
{
|
||||||
|
findById: jest.fn().mockResolvedValue(
|
||||||
|
makerBranch ? { branchId: makerBranch } : {},
|
||||||
|
),
|
||||||
|
} as any,
|
||||||
|
{
|
||||||
|
findById: jest.fn().mockResolvedValue(
|
||||||
|
reviewerBranch ? { branchId: reviewerBranch } : {},
|
||||||
|
),
|
||||||
|
} as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
it("allows a reviewer to view a file from the same branch", async () => {
|
||||||
|
const service = createService("branch-1", "branch-1");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: makerId,
|
||||||
|
fileReviewerId: reviewerId,
|
||||||
|
}),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a reviewer from another branch", async () => {
|
||||||
|
const service = createService("branch-1", "branch-2");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: makerId,
|
||||||
|
fileReviewerId: reviewerId,
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(ForbiddenException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers the immutable case branch snapshot", async () => {
|
||||||
|
const service = createService("old-branch", "branch-2");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.assertMakerReviewerBranchCompatible({
|
||||||
|
fileMakerId: makerId,
|
||||||
|
fileReviewerId: reviewerId,
|
||||||
|
caseBranchId: "branch-2",
|
||||||
|
}),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -113,6 +113,54 @@ export class FanavaranLocationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fileMakerBranchId(
|
||||||
|
fileMakerId: string | null | undefined,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (!fileMakerId) return undefined;
|
||||||
|
const doc = await this.fileMakerDbService.findById(String(fileMakerId));
|
||||||
|
const branchId = (doc as any)?.branchId;
|
||||||
|
return branchId ? String(branchId) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fileReviewerBranchId(
|
||||||
|
fileReviewerId: string | null | undefined,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (!fileReviewerId) return undefined;
|
||||||
|
const doc = await this.fileReviewerDbService.findById(
|
||||||
|
String(fileReviewerId),
|
||||||
|
);
|
||||||
|
const branchId = (doc as any)?.branchId;
|
||||||
|
return branchId ? String(branchId) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V4/V5 case visibility is branch-scoped by the FileMaker who created it.
|
||||||
|
* Missing branch assignments are denied instead of widening visibility.
|
||||||
|
*/
|
||||||
|
async assertMakerReviewerBranchCompatible(input: {
|
||||||
|
fileMakerId?: string | null;
|
||||||
|
fileReviewerId?: string | null;
|
||||||
|
caseBranchId?: string | null;
|
||||||
|
}): Promise<void> {
|
||||||
|
const reviewerBranchId = await this.fileReviewerBranchId(
|
||||||
|
input.fileReviewerId,
|
||||||
|
);
|
||||||
|
const caseBranchId =
|
||||||
|
(input.caseBranchId ? String(input.caseBranchId) : undefined) ??
|
||||||
|
(await this.fileMakerBranchId(input.fileMakerId));
|
||||||
|
|
||||||
|
if (!reviewerBranchId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"FileReviewer account is not assigned to a branch.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!caseBranchId || caseBranchId !== reviewerBranchId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"This file belongs to another branch.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async loadPrimaryLocationId(
|
private async loadPrimaryLocationId(
|
||||||
userId: string | null | undefined,
|
userId: string | null | undefined,
|
||||||
kind: "maker" | "reviewer",
|
kind: "maker" | "reviewer",
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [
|
|||||||
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/vehicle-hull-accident-culprit-type`,
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/vehicle-hull-accident-culprit-type`,
|
||||||
cacheFile: "vehicle-hull-accident-culprit-type.json",
|
cacheFile: "vehicle-hull-accident-culprit-type.json",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "detection-accident-culprits",
|
||||||
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/detection-accident-culprits`,
|
||||||
|
cacheFile: "detection-accident-culprits.json",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "vehicle-hull-dmg-kind",
|
name: "vehicle-hull-dmg-kind",
|
||||||
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/vehicle-hull-dmg-kind`,
|
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/vehicle-hull-dmg-kind`,
|
||||||
|
|||||||
@@ -18,9 +18,22 @@ describe("parseIranLocalDateTime", () => {
|
|||||||
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
|
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("applies a separately supplied time to an explicit-offset date", () => {
|
||||||
|
const instant = parseIranLocalDateTime(
|
||||||
|
"2025-05-16T00:00:00.000Z",
|
||||||
|
"18:28",
|
||||||
|
);
|
||||||
|
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
|
||||||
|
});
|
||||||
|
|
||||||
it("combines Date calendar day in Iran with accidentTime", () => {
|
it("combines Date calendar day in Iran with accidentTime", () => {
|
||||||
const dateOnly = new Date("2025-05-16T00:00:00.000Z");
|
const dateOnly = new Date("2025-05-16T00:00:00.000Z");
|
||||||
const instant = parseIranLocalDateTime(dateOnly, "18:28");
|
const instant = parseIranLocalDateTime(dateOnly, "18:28");
|
||||||
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
|
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects out-of-range hours and minutes", () => {
|
||||||
|
expect(parseIranLocalDateTime("2025-05-16", "24:00")).toBeNull();
|
||||||
|
expect(parseIranLocalDateTime("2025-05-16", "12:60")).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ function normalizeTimeToHms(time: string): string | null {
|
|||||||
const hh = m[1].padStart(2, "0");
|
const hh = m[1].padStart(2, "0");
|
||||||
const mm = m[2];
|
const mm = m[2];
|
||||||
const ss = m[3] ?? "00";
|
const ss = m[3] ?? "00";
|
||||||
|
if (Number(hh) > 23 || Number(mm) > 59 || Number(ss) > 59) return null;
|
||||||
return `${hh}:${mm}:${ss}`;
|
return `${hh}:${mm}:${ss}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +57,12 @@ export function parseIranLocalDateTime(
|
|||||||
|
|
||||||
if (/[zZ]|[+-]\d{2}:?\d{2}$/.test(raw)) {
|
if (/[zZ]|[+-]\d{2}:?\d{2}$/.test(raw)) {
|
||||||
const d = new Date(raw);
|
const d = new Date(raw);
|
||||||
return Number.isNaN(d.getTime()) ? null : d;
|
if (Number.isNaN(d.getTime())) return null;
|
||||||
|
const cleanTime = (time ?? "").trim();
|
||||||
|
if (!cleanTime) return d;
|
||||||
|
const timeHms = normalizeTimeToHms(cleanTime);
|
||||||
|
if (!timeHms) return null;
|
||||||
|
return parseIranLocalIso(gregorianDateInIran(d), timeHms);
|
||||||
}
|
}
|
||||||
|
|
||||||
const naive = raw.match(NAIVE_ISO_DATETIME_RE);
|
const naive = raw.match(NAIVE_ISO_DATETIME_RE);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
|||||||
import { buildCoefficientsFromPartSeverities } from "./claim-price-drop";
|
import { buildCoefficientsFromPartSeverities } from "./claim-price-drop";
|
||||||
import {
|
import {
|
||||||
catalogPartIdFromSelectedPart,
|
catalogPartIdFromSelectedPart,
|
||||||
|
normalizeDamageSelectedParts,
|
||||||
resolveCatalogPartByPartId,
|
resolveCatalogPartByPartId,
|
||||||
resolvePartForExpertReply,
|
resolvePartForExpertReply,
|
||||||
resolveSelectedPartByPartId,
|
resolveSelectedPartByPartId,
|
||||||
@@ -86,6 +87,31 @@ describe("resolveSelectedPartByPartId", () => {
|
|||||||
expect(catalogPartIdFromSelectedPart(row)).toBeNull();
|
expect(catalogPartIdFromSelectedPart(row)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves a live Fanavaran id that is newer than the static snapshot", () => {
|
||||||
|
const [row] = normalizeDamageSelectedParts(
|
||||||
|
[{
|
||||||
|
id: 96,
|
||||||
|
name: "96",
|
||||||
|
side: "",
|
||||||
|
label_fa: "تیوتر",
|
||||||
|
catalogKey: "96",
|
||||||
|
}],
|
||||||
|
ClaimVehicleTypeV2.SEDAN,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(row).toEqual({
|
||||||
|
id: 96,
|
||||||
|
name: "96",
|
||||||
|
side: "",
|
||||||
|
label_fa: "تیوتر",
|
||||||
|
catalogKey: "96",
|
||||||
|
});
|
||||||
|
expect(catalogPartIdFromSelectedPart(row)).toBe(96);
|
||||||
|
expect(resolvePartForExpertReply(96, [row], ClaimVehicleTypeV2.SEDAN)).toBe(
|
||||||
|
row,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("recovers outer part wrongly stored as internal with Persian label", () => {
|
it("recovers outer part wrongly stored as internal with Persian label", () => {
|
||||||
const fixed = sanitizeDamageSelectedPartV2(
|
const fixed = sanitizeDamageSelectedPartV2(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -466,6 +466,28 @@ export function sanitizeDamageSelectedPartV2(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fanavaran serves this catalog live and can add ids that do not exist in
|
||||||
|
// our fallback snapshot yet. Live rows are persisted with their numeric id
|
||||||
|
// repeated as `name`/`catalogKey` and an empty side. Preserve that identity
|
||||||
|
// instead of downgrading the row to an internal free-text part. Otherwise a
|
||||||
|
// newly-added Fanavaran part (for example id 96) disappears from the expert
|
||||||
|
// reply payload and later fails with PART_NOT_ON_CLAIM.
|
||||||
|
const numericCatalogIdentity =
|
||||||
|
id != null &&
|
||||||
|
Number.isFinite(id) &&
|
||||||
|
Number(id) > 0 &&
|
||||||
|
sideTrim === "" &&
|
||||||
|
(nameTrim === String(id) || String(catalogKey ?? "").trim() === String(id));
|
||||||
|
if (numericCatalogIdentity) {
|
||||||
|
return {
|
||||||
|
id: Number(id),
|
||||||
|
name: String(id),
|
||||||
|
side: "",
|
||||||
|
label_fa: labelTrim || nameTrim || String(id),
|
||||||
|
catalogKey: String(id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (isInternalDamageSide(side)) {
|
if (isInternalDamageSide(side)) {
|
||||||
return {
|
return {
|
||||||
id: null,
|
id: null,
|
||||||
|
|||||||
@@ -64,6 +64,20 @@ export class LookupsController {
|
|||||||
return await this.lookupsService.getVehicleHullAccidentCulpritType();
|
return await this.lookupsService.getVehicleHullAccidentCulpritType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("detection-accident-culprits")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Fanavaran GEN.03 hull authority that determined the culprit",
|
||||||
|
description:
|
||||||
|
"Returns values for base-claim field AuthorityCulpritId from car/base-info/detection-accident-culprits.",
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Returns Fanavaran detection-accident-culprits rows",
|
||||||
|
schema: { type: "array", items: { type: "object" } },
|
||||||
|
})
|
||||||
|
async getDetectionAccidentCulprits() {
|
||||||
|
return await this.lookupsService.getDetectionAccidentCulprits();
|
||||||
|
}
|
||||||
|
|
||||||
@Get("vehicle-hull-dmg-kind")
|
@Get("vehicle-hull-dmg-kind")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Fanavaran GEN.06 hull damage kind lookup",
|
summary: "Fanavaran GEN.06 hull damage kind lookup",
|
||||||
|
|||||||
@@ -114,6 +114,10 @@ export class LookupsService {
|
|||||||
return await this.getClientRemoteLookup("vehicle-hull-accident-culprit-type");
|
return await this.getClientRemoteLookup("vehicle-hull-accident-culprit-type");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDetectionAccidentCulprits(): Promise<any> {
|
||||||
|
return await this.getClientRemoteLookup("detection-accident-culprits");
|
||||||
|
}
|
||||||
|
|
||||||
async getVehicleHullDmgKind(): Promise<any> {
|
async getVehicleHullDmgKind(): Promise<any> {
|
||||||
return await this.getClientRemoteLookup("vehicle-hull-dmg-kind");
|
return await this.getClientRemoteLookup("vehicle-hull-dmg-kind");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class CallCenterBlameV6Controller {
|
|||||||
description:
|
description:
|
||||||
"VIN alternative to `run-inquiry`. " +
|
"VIN alternative to `run-inquiry`. " +
|
||||||
"The agent supplies the chassis number and personal data collected from the caller. " +
|
"The agent supplies the chassis number and personal data collected from the caller. " +
|
||||||
"ESG chassis lookup (`policyByChassis`) is executed and the result is stored on the " +
|
"ESG two-factor chassis lookup (`carByChassis`) is executed and the result is stored on the " +
|
||||||
"blame document under `vehicle.vin` (plateId is left empty). " +
|
"blame document under `vehicle.vin` (plateId is left empty). " +
|
||||||
"Identical eligibility guards and insurer-company validation as the plate variant. " +
|
"Identical eligibility guards and insurer-company validation as the plate variant. " +
|
||||||
"After this call, proceed to `send-link` exactly as in the plate flow.",
|
"After this call, proceed to `send-link` exactly as in the plate flow.",
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import {
|
||||||
|
ArrayMinSize,
|
||||||
|
ArrayUnique,
|
||||||
|
IsArray,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
} from "class-validator";
|
||||||
import { CarDamagePartDto } from "src/claim-request-management/dto/car-part.dto";
|
import { CarDamagePartDto } from "src/claim-request-management/dto/car-part.dto";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6,6 +13,19 @@ import { CarDamagePartDto } from "src/claim-request-management/dto/car-part.dto"
|
|||||||
* All fields optional so expert can submit in one or two steps (e.g. car parts first, then sheba/other).
|
* All fields optional so expert can submit in one or two steps (e.g. car parts first, then sheba/other).
|
||||||
*/
|
*/
|
||||||
export class ExpertCompleteClaimDataDto {
|
export class ExpertCompleteClaimDataDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Selected damaged-part IDs from the live Fanavaran car-components catalog.",
|
||||||
|
example: [9, 10, 30],
|
||||||
|
type: [Number],
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray({ message: "selectedPartIds must be an array" })
|
||||||
|
@ArrayMinSize(1, { message: "At least one part ID must be selected" })
|
||||||
|
@ArrayUnique({ message: "Duplicate part IDs are not allowed" })
|
||||||
|
@IsInt({ each: true, message: "Each selected part ID must be an integer" })
|
||||||
|
selectedPartIds?: number[];
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: "Car part damage selection (same as selectCarPartDamage). Required for first claim data step.",
|
description: "Car part damage selection (same as selectCarPartDamage). Required for first claim data step.",
|
||||||
type: CarDamagePartDto,
|
type: CarDamagePartDto,
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ export class BlameRequest {
|
|||||||
@Prop({ type: Types.ObjectId })
|
@Prop({ type: Types.ObjectId })
|
||||||
initiatedByFieldExpertId?: Types.ObjectId;
|
initiatedByFieldExpertId?: Types.ObjectId;
|
||||||
|
|
||||||
|
/** Branch snapshot of the expert/FileMaker who created this file. */
|
||||||
|
@Prop({ type: Types.ObjectId, index: true })
|
||||||
|
branchId?: Types.ObjectId;
|
||||||
|
|
||||||
/** True when this blame was created by a registrar. */
|
/** True when this blame was created by a registrar. */
|
||||||
@Prop({ default: false })
|
@Prop({ default: false })
|
||||||
registrarInitiated?: boolean;
|
registrarInitiated?: boolean;
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ describe("RequestManagementService FileReviewer inbox", () => {
|
|||||||
(service as any).claimCaseDbService = {
|
(service as any).claimCaseDbService = {
|
||||||
find: jest.fn().mockResolvedValue([]),
|
find: jest.fn().mockResolvedValue([]),
|
||||||
};
|
};
|
||||||
|
(service as any).fanavaranLocationService = {
|
||||||
|
assertMakerReviewerBranchCompatible: jest.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
return { service, blameRequestDbService };
|
return { service, blameRequestDbService };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +92,22 @@ describe("RequestManagementService FileReviewer inbox", () => {
|
|||||||
expect(result.list).toEqual([]);
|
expect(result.list).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not list an open file from another branch", async () => {
|
||||||
|
const { service } = createService([sealedFile]);
|
||||||
|
(service as any).fanavaranLocationService
|
||||||
|
.assertMakerReviewerBranchCompatible.mockRejectedValue(
|
||||||
|
new Error("This file belongs to another branch."),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await service.getMyFileReviewerFiles({
|
||||||
|
sub: String(reviewerId),
|
||||||
|
role: RoleEnum.FILE_REVIEWER,
|
||||||
|
clientKey: String(clientId),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.list).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("sorts and paginates the reviewer inbox with the shared list contract", async () => {
|
it("sorts and paginates the reviewer inbox with the shared list contract", async () => {
|
||||||
const olderFile = {
|
const olderFile = {
|
||||||
...sealedFile,
|
...sealedFile,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
|
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||||
import { RequestManagementService } from "./request-management.service";
|
import { RequestManagementService } from "./request-management.service";
|
||||||
|
|
||||||
describe("RequestManagementService policyholder inquiry routing", () => {
|
describe("RequestManagementService policyholder inquiry routing", () => {
|
||||||
@@ -138,7 +139,7 @@ describe("RequestManagementService policyholder inquiry routing", () => {
|
|||||||
it("queries VIN with the third-party policyholder", async () => {
|
it("queries VIN with the third-party policyholder", async () => {
|
||||||
const service = Object.create(RequestManagementService.prototype) as any;
|
const service = Object.create(RequestManagementService.prototype) as any;
|
||||||
service.sandHubService = {
|
service.sandHubService = {
|
||||||
getPolicyByChassisInquiry: jest.fn().mockResolvedValue({
|
getCarByChassisInquiry: jest.fn().mockResolvedValue({
|
||||||
raw: {},
|
raw: {},
|
||||||
mapped: { CompanyName: "پارسیان" },
|
mapped: { CompanyName: "پارسیان" },
|
||||||
}),
|
}),
|
||||||
@@ -151,16 +152,74 @@ describe("RequestManagementService policyholder inquiry routing", () => {
|
|||||||
await service.getThirdPartyVinInquiry(submission);
|
await service.getThirdPartyVinInquiry(submission);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
service.sandHubService.getPolicyByChassisInquiry,
|
service.sandHubService.getCarByChassisInquiry,
|
||||||
).toHaveBeenCalledTimes(1);
|
).toHaveBeenCalledTimes(1);
|
||||||
expect(
|
expect(
|
||||||
service.sandHubService.getPolicyByChassisInquiry,
|
service.sandHubService.getCarByChassisInquiry,
|
||||||
).toHaveBeenCalledWith(
|
).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
nationalCode: "0022222222",
|
nationalCode: "0022222222",
|
||||||
chassis: vehicle.vin,
|
chassisNo: vehicle.vin,
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("runs both third-party and car-body inquiries for a car-body VIN", async () => {
|
||||||
|
const service = Object.create(RequestManagementService.prototype) as any;
|
||||||
|
service.getThirdPartyVinInquiry = jest.fn().mockResolvedValue({
|
||||||
|
raw: { success: true },
|
||||||
|
mapped: {
|
||||||
|
CompanyCode: "8",
|
||||||
|
CompanyName: "بیمه پارسیان",
|
||||||
|
PrntPlcyCmpDocNo: "TP-1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
service.sandHubService = {
|
||||||
|
getCarBodyInquiry: jest.fn().mockResolvedValue({
|
||||||
|
source: "ESG_CAR_BODY_VIN_INQUIRY",
|
||||||
|
raw: { policyId: 123 },
|
||||||
|
mapped: {
|
||||||
|
policyNumber: "BODY-1",
|
||||||
|
companyId: "8",
|
||||||
|
CompanyName: "بیمه پارسیان",
|
||||||
|
VinNumberField: vehicle.vin,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
service.clientService = {
|
||||||
|
findOrCreateClientByCompanyCode: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ _id: "client-third-party" }),
|
||||||
|
};
|
||||||
|
service.resolveCarBodyPolicyClientId = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue("client-car-body");
|
||||||
|
service.runParticipantPersonalInquiries = jest.fn().mockResolvedValue(undefined);
|
||||||
|
service.runVehicleOwnershipInquiry = jest.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const req: any = {
|
||||||
|
_id: "car-body-vin-request",
|
||||||
|
type: BlameRequestType.CAR_BODY,
|
||||||
|
inquiries: {},
|
||||||
|
};
|
||||||
|
const party: any = { person: {}, vehicle: {}, insurance: {} };
|
||||||
|
|
||||||
|
await service.runPartyInquiriesVinV3Internal(
|
||||||
|
req,
|
||||||
|
participantInput(BlameRequestType.CAR_BODY),
|
||||||
|
PartyRole.FIRST,
|
||||||
|
party,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(service.getThirdPartyVinInquiry).toHaveBeenCalledTimes(1);
|
||||||
|
expect(service.sandHubService.getCarBodyInquiry).toHaveBeenCalledWith({
|
||||||
|
nationalCodeOfInsurer: "0033333333",
|
||||||
|
plate: vehicle.vin,
|
||||||
|
});
|
||||||
|
expect(req.inquiries.thirdParty.has).toBe(true);
|
||||||
|
expect(req.inquiries.carBody.has).toBe(true);
|
||||||
|
expect(party.insurance.policyNumber).toBe("TP-1");
|
||||||
|
expect(party.insurance.carBodyInsurance.policyNumber).toBe("BODY-1");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.
|
|||||||
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
|
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
|
||||||
import { isOtpExpiryActive } from "src/helpers/user-otp-expiry";
|
import { isOtpExpiryActive } from "src/helpers/user-otp-expiry";
|
||||||
import { parseIranLocalDateTime } from "src/helpers/iran-datetime";
|
import { parseIranLocalDateTime } from "src/helpers/iran-datetime";
|
||||||
|
import { jalaliToGregorianDate } from "src/helpers/date-jalali";
|
||||||
import {
|
import {
|
||||||
applyListQueryV2,
|
applyListQueryV2,
|
||||||
isInListDateRange,
|
isInListDateRange,
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -189,9 +189,9 @@ describe("SandHubService inquiry mocks", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.getPolicyByChassisInquiry({
|
const result = await service.getCarByChassisInquiry({
|
||||||
nationalCode: "0012345678",
|
nationalCode: "0012345678",
|
||||||
chassis: "NAAR03HFFRDE07024",
|
chassisNo: "NAAR03HFFRDE07024",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.mapped.Error.Message).toBe(
|
expect(result.mapped.Error.Message).toBe(
|
||||||
@@ -239,10 +239,10 @@ describe("SandHubService inquiry mocks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.getPolicyByChassisInquiry(
|
service.getCarByChassisInquiry(
|
||||||
{
|
{
|
||||||
nationalCode: "1234567890",
|
nationalCode: "1234567890",
|
||||||
chassis: "NAAR03HFFRDE07024",
|
chassisNo: "NAAR03HFFRDE07024",
|
||||||
},
|
},
|
||||||
{ enforceDeploymentClientMatch: true },
|
{ enforceDeploymentClientMatch: true },
|
||||||
),
|
),
|
||||||
@@ -256,16 +256,16 @@ describe("SandHubService inquiry mocks", () => {
|
|||||||
data: { CmpCod: "8", CmpNam: "بیمه پارسیان" },
|
data: { CmpCod: "8", CmpNam: "بیمه پارسیان" },
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.getPolicyByChassisInquiry({
|
await service.getCarByChassisInquiry({
|
||||||
nationalCode: "0012345678",
|
nationalCode: "0012345678",
|
||||||
chassis: "NAAR03HFFRDE07024",
|
chassisNo: "NAAR03HFFRDE07024",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(esg).toHaveBeenCalledWith(
|
expect(esg).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("/inquiry/policyByChassis"),
|
expect.stringContaining("/inquiry/carByChassis"),
|
||||||
{
|
{
|
||||||
nationalCode: "0012345678",
|
nationalCode: "0012345678",
|
||||||
chassis: "NAAR03HFFRDE07024",
|
chassisNo: "NAAR03HFFRDE07024",
|
||||||
},
|
},
|
||||||
"vinChassis",
|
"vinChassis",
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
@@ -1187,7 +1187,7 @@ export class SandHubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ESG VIN/chassis-number inquiry (`/inquiry/policyByChassis`).
|
* ESG two-factor VIN/chassis inquiry (`/inquiry/carByChassis`).
|
||||||
*
|
*
|
||||||
* When `vinChassis` inquiry is disabled (mock mode) the response shape mirrors
|
* When `vinChassis` inquiry is disabled (mock mode) the response shape mirrors
|
||||||
* `buildMockPlateInquiryRaw` so the downstream mapper (`mapEsgPolicyByPlateToOldFormat`)
|
* `buildMockPlateInquiryRaw` so the downstream mapper (`mapEsgPolicyByPlateToOldFormat`)
|
||||||
@@ -1196,15 +1196,15 @@ export class SandHubService {
|
|||||||
* @param identity - policyholder national code and 17-character VIN/chassis
|
* @param identity - policyholder national code and 17-character VIN/chassis
|
||||||
* @param options - optional per-tenant client scope
|
* @param options - optional per-tenant client scope
|
||||||
*/
|
*/
|
||||||
async getPolicyByChassisInquiry(
|
async getCarByChassisInquiry(
|
||||||
identity: { nationalCode: string; chassis: string },
|
identity: { nationalCode: string; chassisNo: string },
|
||||||
options?: SandHubInquiryOptions,
|
options?: SandHubInquiryOptions,
|
||||||
): Promise<{ raw: any; mapped: any }> {
|
): Promise<{ raw: any; mapped: any }> {
|
||||||
const baseUrl = process.env.ESG_URL ?? "http://192.168.20.22:8085";
|
const baseUrl = process.env.ESG_URL ?? "http://192.168.20.22:8085";
|
||||||
const requestUrl = `${baseUrl}/inquiry/policyByChassis`;
|
const requestUrl = `${baseUrl}/inquiry/carByChassis`;
|
||||||
const requestPayload = {
|
const requestPayload = {
|
||||||
nationalCode: String(identity.nationalCode),
|
nationalCode: String(identity.nationalCode),
|
||||||
chassis: String(identity.chassis),
|
chassisNo: String(identity.chassisNo),
|
||||||
};
|
};
|
||||||
|
|
||||||
const live = await this.isInquiryLive("vinChassis", options);
|
const live = await this.isInquiryLive("vinChassis", options);
|
||||||
@@ -1213,7 +1213,7 @@ export class SandHubService {
|
|||||||
const ctx = await this.mockCompanyContext(options);
|
const ctx = await this.mockCompanyContext(options);
|
||||||
const raw = this.buildMockPlateInquiryRaw(ctx);
|
const raw = this.buildMockPlateInquiryRaw(ctx);
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`[MOCK] getPolicyByChassisInquiry nationalCode=${identity.nationalCode} chassis=${identity.chassis}`,
|
`[MOCK] getCarByChassisInquiry nationalCode=${identity.nationalCode} chassisNo=${identity.chassisNo}`,
|
||||||
);
|
);
|
||||||
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw, "thirdPartyVin");
|
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw, "thirdPartyVin");
|
||||||
if (!mapped?.Error) {
|
if (!mapped?.Error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user