Merge pull request 'feat: complete in-person claims without final sign' (#282) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#282
This commit is contained in:
2026-09-01 15:04:51 +03:30
16 changed files with 249 additions and 162 deletions

View File

@@ -1364,14 +1364,15 @@
<div class="step-num teal">10</div> <div class="step-num teal">10</div>
<div class="step-body"> <div class="step-body">
<div class="step-label"> <div class="step-label">
Owner sign on expert pricing (FileReviewer acts on Claim completes after expert work (no final owner
behalf of user) signature)
</div> </div>
<div class="step-route"> <div class="step-route">
PUT /claim-sign/:claimId Expert submits to Fanavaran manually when ready
</div> </div>
<div class="step-note"> <div class="step-note">
Submits agree + branchId + signature image Mixed priced/factor claims still require priced-line
acceptance before factor uploads
</div> </div>
</div> </div>
</div> </div>
@@ -1395,11 +1396,13 @@
FileMaker steps are identical to V4 except FileMaker steps are identical to V4 except
<code>requiresFileMakerApproval=true</code> is set at creation. The <code>requiresFileMakerApproval=true</code> is set at creation. The
FileReviewer steps are also identical to V4. The only difference is the FileReviewer steps are also identical to V4. The only difference is the
tail: after the damage expert completes their review and the owner signs, tail: after the damage expert completes their review (and any required factors
are validated),
the claim moves to the claim moves to
<strong>WAITING_FOR_FILE_MAKER_APPROVAL</strong> instead of proceeding <strong>WAITING_FOR_FILE_MAKER_APPROVAL</strong> instead of proceeding
directly to fanavaran submission. The FileMaker then approves (→ fanavaran) directly to completion. The FileMaker then approves (→ COMPLETED; manual
or rejects (→ back to WAITING_FOR_DAMAGE_EXPERT, max 2 rejections). Fanavaran submission) or rejects (→ back to WAITING_FOR_DAMAGE_EXPERT,
max 2 rejections).
</p> </p>
<div class="card"> <div class="card">
<div class="roles-row"> <div class="roles-row">
@@ -1468,7 +1471,7 @@
Claim → WAITING_FOR_FILE_MAKER_APPROVAL Claim → WAITING_FOR_FILE_MAKER_APPROVAL
</div> </div>
<div class="step-note"> <div class="step-note">
(instead of direct fanavaran submit) (before final completion; no owner signature)
</div> </div>
</div> </div>
</div> </div>
@@ -1496,7 +1499,7 @@
<div class="step-num orange">✓</div> <div class="step-num orange">✓</div>
<div class="step-body"> <div class="step-body">
<div class="step-label"> <div class="step-label">
Fanavaran submission (on approve) Claim COMPLETED (on approve)
</div> </div>
</div> </div>
</div> </div>
@@ -1845,8 +1848,8 @@
<td>DAMAGE_EXPERT</td> <td>DAMAGE_EXPERT</td>
<td>DAMAGE_EXPERT</td> <td>DAMAGE_EXPERT</td>
<td>DAMAGE_EXPERT</td> <td>DAMAGE_EXPERT</td>
<td>DAMAGE_EXPERT → owner sign (FileReviewer)</td> <td>DAMAGE_EXPERT → COMPLETED (manual Fanavaran submit)</td>
<td>DAMAGE_EXPERT → owner sign → FileMaker approval</td> <td>DAMAGE_EXPERT → FileMaker approval → COMPLETED (manual Fanavaran submit)</td>
<td>DAMAGE_EXPERT (standard V2)</td> <td>DAMAGE_EXPERT (standard V2)</td>
</tr> </tr>
<tr> <tr>

View File

@@ -220,7 +220,7 @@
<td><code>file_maker</code></td> <td><code>file_maker</code></td>
</tr> </tr>
<tr> <tr>
<td>ارزیابی خسارت V4/V5 (فیلدهای تصادف، قطعات، عکس‌ها، امضای مالک)</td> <td>ارزیابی خسارت V4/V5 (فیلدهای تصادف، قطعات و عکس‌ها؛ تأیید خطوط قیمت‌گذاری‌شده در جریان ترکیبیِ فاکتور در صورت نیاز)</td>
<td>فایل‌های تخصیص‌یافته</td> <td>فایل‌های تخصیص‌یافته</td>
<td>پنل بازبین فایل</td> <td>پنل بازبین فایل</td>
<td><code>file_reviewer</code></td> <td><code>file_reviewer</code></td>
@@ -451,7 +451,8 @@
OTPها، استعلام‌ها، موقعیت/توضیحات/صدا و امضاها برای هر دو طرف. OTPها، استعلام‌ها، موقعیت/توضیحات/صدا و امضاها برای هر دو طرف.
همچنین اسناد اولیه خسارت (گواهینامه‌ها، کارت‌های خودرو) را بارگذاری می‌کند. پس از همچنین اسناد اولیه خسارت (گواهینامه‌ها، کارت‌های خودرو) را بارگذاری می‌کند. پس از
امضای دوم، فایل برای تحویل به بازبین فایل «مهرومومه» می‌شود. در V5، فایل‌ساز امضای دوم، فایل برای تحویل به بازبین فایل «مهرومومه» می‌شود. در V5، فایل‌ساز
در انتها بازمی‌گردد تا خسارت تکمیل‌شده را قبل از ارسال به فناوران تأیید یا رد کند. در انتها بازمی‌گردد تا خسارت تکمیل‌شده را تأیید یا رد کند. پس از تأیید،
کارشناس پرونده را به‌صورت دستی به فناوران ارسال می‌کند.
</p> </p>
<div class="card card-purple"> <div class="card card-purple">
@@ -475,10 +476,10 @@
<div class="card card-purple"> <div class="card card-purple">
<h3>تأیید خسارت V5 — <code>v5/file-maker/claim-approval/</code></h3> <h3>تأیید خسارت V5 — <code>v5/file-maker/claim-approval/</code></h3>
<p class="note">فقط در V5 استفاده می‌شود. پس از بررسی کارشناس خسارت و امضای مالک، خسارت وارد <code>WAITING_FOR_FILE_MAKER_APPROVAL</code> می‌شود.</p> <p class="note">فقط در V5 استفاده می‌شود. پس از بررسی کارشناس خسارت و اعتبارسنجی فاکتورهای لازم، خسارت وارد <code>WAITING_FOR_FILE_MAKER_APPROVAL</code> می‌شود؛ امضای نهایی مالک لازم نیست.</p>
<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>approve/:claimId</code></td><td>تأیید خسارت تکمیل‌شده ← ارسال SMS امضای مالک را فعال می‌کند ← پس از امضای مالک به فناوران ارسال می‌شود.</td></tr> <tr><td><span class="method post">POST</span></td><td><code>approve/:claimId</code></td><td>تأیید خسارت تکمیل‌شده ← وضعیت خسارت <code>COMPLETED</code> می‌شود. کارشناس در زمان مناسب آن را به‌صورت دستی به فناوران ارسال می‌کند.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>reject/:claimId</code></td><td>رد به بازبین فایل ← خسارت به WAITING_FOR_DAMAGE_EXPERT برمی‌گردد. محدودیت: حداکثر ۲ رد در هر خسارت؛ تلاش سوم ۴۲۲ با کد <code>FILE_MAKER_REJECTION_LIMIT_EXCEEDED</code> برمی‌گرداند.</td></tr> <tr><td><span class="method post">POST</span></td><td><code>reject/:claimId</code></td><td>رد به بازبین فایل ← خسارت به WAITING_FOR_DAMAGE_EXPERT برمی‌گردد. محدودیت: حداکثر ۲ رد در هر خسارت؛ تلاش سوم ۴۲۲ با کد <code>FILE_MAKER_REJECTION_LIMIT_EXCEEDED</code> برمی‌گرداند.</td></tr>
</table> </table>
</div> </div>
@@ -489,7 +490,7 @@
دومین اکتور در تقسیم V4/V5. بازبین فایل فایل‌های مهرومومه‌شده (پس از اتمام کار دومین اکتور در تقسیم V4/V5. بازبین فایل فایل‌های مهرومومه‌شده (پس از اتمام کار
فایل‌ساز) را تحویل می‌گیرد و مرحله کامل ارزیابی خسارت را انجام می‌دهد: فیلدهای فایل‌ساز) را تحویل می‌گیرد و مرحله کامل ارزیابی خسارت را انجام می‌دهد: فیلدهای
تصادف، دریافت الزامات عکس‌برداری، بارگذاری اسناد (شاسی/موتور)، انتخاب قطعات، تصادف، دریافت الزامات عکس‌برداری، بارگذاری اسناد (شاسی/موتور)، انتخاب قطعات،
عکس‌های قطعات، ویدیوی دور زدن خودرو، امضای مالک. تقصیر با car-capture به عکس‌های قطعات و ویدیوی دور زدن خودرو. تقصیر با car-capture به
COMPLETED علامت‌گذاری می‌شود. بازبین فایل همچنین دسترسی خواندن به پنل expert-claim COMPLETED علامت‌گذاری می‌شود. بازبین فایل همچنین دسترسی خواندن به پنل expert-claim
برای خسارت‌هایی که بررسی می‌کند دارد. برای خسارت‌هایی که بررسی می‌کند دارد.
</p> </p>
@@ -499,8 +500,8 @@
<p class="note">اندپوینت‌های V4 و V5 یکسان هستند — فقط پیشوند تغییر می‌کند.</p> <p class="note">اندپوینت‌های V4 و V5 یکسان هستند — فقط پیشوند تغییر می‌کند.</p>
<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 get">GET</span></td><td><code>my-files</code></td><td>فهرست تمام فایل‌های تخصیص‌یافته به این بازبین فایل.</td></tr> <tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>فهرست فایل‌های مهروموم‌شدهٔ فایل‌ساز که در شرکت بیمهٔ این بازبین برای دریافت آماده‌اند، به‌علاوهٔ فایل‌های تخصیص‌یافته به خود او.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files/:requestId</code></td><td>جزئیات کامل یک فایل (طرفین، گردش کار، فیلدهای کارشناس، شناسه خسارت مرتبط).</td></tr> <tr><td><span class="method get">GET</span></td><td><code>my-files/:requestId</code></td><td>جزئیات کامل یک فایل آماده برای دریافت یا تخصیص‌یافته، در شرکت بیمهٔ همین بازبین (طرفین، گردش کار، فیلدهای کارشناس، شناسه خسارت مرتبط).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>دریافت شناسه خسارت ایجادشده به‌صورت خودکار (از استعلام طرف مقصر فایل‌ساز).</td></tr> <tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>دریافت شناسه خسارت ایجادشده به‌صورت خودکار (از استعلام طرف مقصر فایل‌ساز).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>accident-fields/:requestId</code></td><td>مرحله ۱ (بازبین): ذخیره فیلدهای تصادف (accidentWay، accidentReason، accidentType).</td></tr> <tr><td><span class="method post">POST</span></td><td><code>accident-fields/:requestId</code></td><td>مرحله ۱ (بازبین): ذخیره فیلدهای تصادف (accidentWay، accidentReason، accidentType).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>الزامات عکس‌برداری آگاه از مرحله (فاز اسناد پیش از عکس‌برداری در مقابل فاز عکس‌برداری قطعات).</td></tr> <tr><td><span class="method get">GET</span></td><td><code>capture-requirements/:claimId</code></td><td>الزامات عکس‌برداری آگاه از مرحله (فاز اسناد پیش از عکس‌برداری در مقابل فاز عکس‌برداری قطعات).</td></tr>
@@ -509,7 +510,7 @@
<tr><td><span class="method patch">PATCH</span></td><td><code>select-other-parts/:claimId</code></td><td>انتخاب سایر قطعات آسیب‌دیده (غیر بدنه).</td></tr> <tr><td><span class="method patch">PATCH</span></td><td><code>select-other-parts/:claimId</code></td><td>انتخاب سایر قطعات آسیب‌دیده (غیر بدنه).</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>عکس‌برداری از قطعات + زوایا برای هر قطعه آسیب‌دیده انتخاب‌شده.</td></tr> <tr><td><span class="method post">POST</span></td><td><code>capture-part/:claimId</code></td><td>عکس‌برداری از قطعات + زوایا برای هر قطعه آسیب‌دیده انتخاب‌شده.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>ویدیوی دور زدن خودرو (آخرین مرحله عکس‌برداری بازبین). خسارت ← <code>WAITING_FOR_DAMAGE_EXPERT</code>، تقصیر ← <code>COMPLETED</code>.</td></tr> <tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>ویدیوی دور زدن خودرو (آخرین مرحله عکس‌برداری بازبین). خسارت ← <code>WAITING_FOR_DAMAGE_EXPERT</code>، تقصیر ← <code>COMPLETED</code>.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>claim-sign/:claimId</code></td><td>ثبت امضای مالک روی قیمت‌گذاری کارشناس به نمایندگی از طرف زیان‌دیده (موافقت + branchId + تصویر امضا).</td></tr> <tr><td><span class="method put">PUT</span></td><td><code>claim-sign/:claimId</code></td><td>فقط برای خسارت‌های ترکیبیِ قیمت/فاکتور: ثبت موافقت با خطوط قیمت‌گذاری‌شده پیش از بارگذاری فاکتور. امضای نهایی مالک دیگر لازم نیست.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:requestId</code></td><td class="dep">در V4/V5 بی‌اثر است — تقصیر از قبل توسط car-capture COMPLETED شده. موفقیت idempotent برمی‌گرداند.</td></tr> <tr><td><span class="method post">POST</span></td><td><code>upload-video/:requestId</code></td><td class="dep">در V4/V5 بی‌اثر است — تقصیر از قبل توسط car-capture COMPLETED شده. موفقیت idempotent برمی‌گرداند.</td></tr>
</table> </table>
</div> </div>

View File

@@ -217,7 +217,7 @@
<td><code>file_reviewer</code></td> <td><code>file_reviewer</code></td>
<td>FileReviewer panel</td> <td>FileReviewer panel</td>
<td>Assigned files</td> <td>Assigned files</td>
<td>V4/V5 damage assessment (accident fields, parts, captures, owner sign)</td> <td>V4/V5 damage assessment (accident fields, parts, captures; mixed-factor priced-line acceptance when needed)</td>
</tr> </tr>
<tr> <tr>
<td><code>registrar</code></td> <td><code>registrar</code></td>
@@ -367,7 +367,7 @@
<tr><td><span class="method post">POST</span></td><td><code>v2/expert-claim/assign/:claimRequestId</code></td><td>Lock claim to this expert. Returns <code>assigned</code>, <code>already_assigned_to_you</code>, or 409.</td></tr> <tr><td><span class="method post">POST</span></td><td><code>v2/expert-claim/assign/:claimRequestId</code></td><td>Lock claim to this expert. Returns <code>assigned</code>, <code>already_assigned_to_you</code>, or 409.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/request/:claimRequestId/price-drop</code></td><td>Price-drop context: severity labels, coefficient catalog, damaged parts + mapping, suggested car year from blame inquiry.</td></tr> <tr><td><span class="method get">GET</span></td><td><code>v2/expert-claim/request/:claimRequestId/price-drop</code></td><td>Price-drop context: severity labels, coefficient catalog, damaged parts + mapping, suggested car year from blame inquiry.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/request/:claimRequestId/price-drop</code></td><td>Calculate and persist price-drop: carPrice × yearCoeff × sumOfCoeffs ÷ 400.</td></tr> <tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/request/:claimRequestId/price-drop</code></td><td>Calculate and persist price-drop: carPrice × yearCoeff × sumOfCoeffs ÷ 400.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/reply/submit/:claimRequestId</code></td><td>Submit damage assessment reply (priced parts list, daghi, branchId). Cap: total ≤ 53 000 000 Toman. Depending on factorNeeded flags moves claim to owner-sign, mixed-factors-pending, or factor-validation queue.</td></tr> <tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/reply/submit/:claimRequestId</code></td><td>Submit damage assessment reply (priced parts list, daghi, branchId). Cap: total ≤ 53 000 000 Toman. A priced-only claim completes immediately; factor claims continue through factor collection/validation. No final owner signature or automatic Fanavaran submission.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/reply/resend/:claimRequestId</code></td><td>Request user to resend documents/photos. One resend per claim lifecycle; returns 422 if already fulfilled.</td></tr> <tr><td><span class="method put">PUT</span></td><td><code>v2/expert-claim/reply/resend/:claimRequestId</code></td><td>Request user to resend documents/photos. One resend per claim lifecycle; returns 422 if already fulfilled.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/:claimRequestId/visit</code></td><td>Ask user to come in person. Unlocks claim, sets claimStatus to NEEDS_REVISION.</td></tr> <tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/:claimRequestId/visit</code></td><td>Ask user to come in person. Unlocks claim, sets claimStatus to NEEDS_REVISION.</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/validate-factors/:claimRequestId</code></td><td>Validate uploaded repair factor invoices. Approve or reject each factor line with totalPayment. Cap applies across all lines (≤ 53 000 000 Toman). Auto-completes when all lines are decided.</td></tr> <tr><td><span class="method patch">PATCH</span></td><td><code>v2/expert-claim/validate-factors/:claimRequestId</code></td><td>Validate uploaded repair factor invoices. Approve or reject each factor line with totalPayment. Cap applies across all lines (≤ 53 000 000 Toman). Auto-completes when all lines are decided.</td></tr>
@@ -444,7 +444,8 @@
OTPs, inquiries, location/description/voice, and signatures for both parties. OTPs, inquiries, location/description/voice, and signatures for both parties.
They also upload the initial claim documents (licences, car cards). After the second They also upload the initial claim documents (licences, car cards). After the second
signature the file is "sealed" for FileReviewer pickup. In V5, FileMaker comes signature the file is "sealed" for FileReviewer pickup. In V5, FileMaker comes
back at the end to approve or reject the completed claim before fanavaran submission. back at the end to approve or reject the completed claim. After approval, an
expert submits the case to Fanavaran manually.
</p> </p>
<div class="card card-purple"> <div class="card card-purple">
@@ -468,10 +469,10 @@
<div class="card card-purple"> <div class="card card-purple">
<h3>V5 claim approval — <code>v5/file-maker/claim-approval/</code></h3> <h3>V5 claim approval — <code>v5/file-maker/claim-approval/</code></h3>
<p class="note">Used only in V5. After damage expert review and owner sign, claim enters <code>WAITING_FOR_FILE_MAKER_APPROVAL</code>.</p> <p class="note">Used only in V5. After damage expert review and any required factor validation, claim enters <code>WAITING_FOR_FILE_MAKER_APPROVAL</code>; no final owner signature is needed.</p>
<table> <table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr> <tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method post">POST</span></td><td><code>approve/:claimId</code></td><td>Approve the completed claim → triggers owner sign SMS → fanavaran submission after owner signs.</td></tr> <tr><td><span class="method post">POST</span></td><td><code>approve/:claimId</code></td><td>Approve the completed claim → claim becomes <code>COMPLETED</code>. An expert submits to Fanavaran manually when ready.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>reject/:claimId</code></td><td>Reject back to FileReviewer → claim returns to WAITING_FOR_DAMAGE_EXPERT. Limit: max 2 rejections per claim; 3rd attempt returns 422 <code>FILE_MAKER_REJECTION_LIMIT_EXCEEDED</code>.</td></tr> <tr><td><span class="method post">POST</span></td><td><code>reject/:claimId</code></td><td>Reject back to FileReviewer → claim returns to WAITING_FOR_DAMAGE_EXPERT. Limit: max 2 rejections per claim; 3rd attempt returns 422 <code>FILE_MAKER_REJECTION_LIMIT_EXCEEDED</code>.</td></tr>
</table> </table>
</div> </div>
@@ -482,7 +483,7 @@
The second actor in the V4/V5 split. FileReviewer picks up sealed files (after The second actor in the V4/V5 split. FileReviewer picks up sealed files (after
FileMaker is done) and performs the full damage assessment pass: accident fields, FileMaker is done) and performs the full damage assessment pass: accident fields,
capture requirements lookup, document upload (chassis/engine), part selection, capture requirements lookup, document upload (chassis/engine), part selection,
part photos, walk-around video, owner signature. The blame is marked COMPLETED part photos and walk-around video. The blame is marked COMPLETED
by car-capture. FileReviewer also has read access to the expert-claim panel for by car-capture. FileReviewer also has read access to the expert-claim panel for
claims they are reviewing. claims they are reviewing.
</p> </p>
@@ -492,8 +493,8 @@
<p class="note">V4 and V5 endpoints are identical — only the prefix changes.</p> <p class="note">V4 and V5 endpoints are identical — only the prefix changes.</p>
<table> <table>
<tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr> <tr><th style="width:70px">Method</th><th>Route</th><th>What it does</th></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>List all files assigned to this FileReviewer.</td></tr> <tr><td><span class="method get">GET</span></td><td><code>my-files</code></td><td>List FileMaker-sealed files available to claim in this reviewer’s insurer, plus files already assigned to this FileReviewer.</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>my-files/:requestId</code></td><td>Full detail for one file (parties, workflow, expert fields, linked claim ID).</td></tr> <tr><td><span class="method get">GET</span></td><td><code>my-files/:requestId</code></td><td>Full detail for one available or assigned file in this reviewer’s insurer (parties, workflow, expert fields, linked claim ID).</td></tr>
<tr><td><span class="method get">GET</span></td><td><code>claim-id/:requestId</code></td><td>Get the auto-created claim ID (from FileMaker's guilty-party inquiry).</td></tr> <tr><td><span class="method 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>
@@ -502,7 +503,7 @@
<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>
<tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>Walk-around video (final FileReviewer capture step). Claim → <code>WAITING_FOR_DAMAGE_EXPERT</code>, blame → <code>COMPLETED</code>.</td></tr> <tr><td><span class="method patch">PATCH</span></td><td><code>car-capture/:claimId</code></td><td>Walk-around video (final FileReviewer capture step). Claim → <code>WAITING_FOR_DAMAGE_EXPERT</code>, blame → <code>COMPLETED</code>.</td></tr>
<tr><td><span class="method put">PUT</span></td><td><code>claim-sign/:claimId</code></td><td>Submit owner signature on expert pricing on behalf of the damaged party (agree + branchId + signature image).</td></tr> <tr><td><span class="method put">PUT</span></td><td><code>claim-sign/:claimId</code></td><td>For mixed priced/factor claims only: record acceptance of priced lines before factor uploads. A final owner signature is no longer required.</td></tr>
<tr><td><span class="method post">POST</span></td><td><code>upload-video/:requestId</code></td><td class="dep">No-op in V4/V5 — blame already COMPLETED by car-capture. Returns idempotent success.</td></tr> <tr><td><span class="method post">POST</span></td><td><code>upload-video/:requestId</code></td><td class="dep">No-op in V4/V5 — blame already COMPLETED by car-capture. Returns idempotent success.</td></tr>
</table> </table>
</div> </div>

View File

@@ -322,7 +322,8 @@ export class ClaimRequestManagementV2Controller {
} }
/** /**
* V2: Owner signature — priced-line gate (mixed factors) or final accept/reject. * V2–V5: owner signature used only as the priced-line gate for mixed-factor claims.
* The final accept/reject phase is retained for legacy rows only.
*/ */
@Put("request/:claimRequestId/owner-insurer-approval/sign") @Put("request/:claimRequestId/owner-insurer-approval/sign")
@ApiParam({ @ApiParam({
@@ -332,12 +333,12 @@ export class ClaimRequestManagementV2Controller {
}) })
@ApiConsumes("multipart/form-data") @ApiConsumes("multipart/form-data")
@ApiOperation({ @ApiOperation({
summary: "Sign priced lines or final claim pricing (owner)", summary: "Sign priced lines before factor uploads (owner; final phase is legacy only)",
description: description:
"Multipart: `sign`, `agree`, `branchId`. Requires `ClaimCaseStatus` **`INSURER_REVIEW_AWAITING_OWNER_SIGN`**, **`INSURER_REVIEW_MIXED_FACTORS_PENDING`**, or legacy **`WAITING_FOR_INSURER_APPROVAL`**, and `workflow.currentStep=INSURER_REVIEW` (not during owner factor upload or `EXPERT_COST_EVALUATION`).\n\n" + "Multipart: `sign`, `agree`, `branchId`. Requires `ClaimCaseStatus` **`INSURER_REVIEW_AWAITING_OWNER_SIGN`**, **`INSURER_REVIEW_MIXED_FACTORS_PENDING`**, or legacy **`WAITING_FOR_INSURER_APPROVAL`**, and `workflow.currentStep=INSURER_REVIEW` (not during owner factor upload or `EXPERT_COST_EVALUATION`).\n\n" +
"**Phase A — Mixed reply, priced lines only:** `claimStatus=NEEDS_REVISION`, no `evaluation.ownerPricedPartsApproval` yet. `agree=true` records that signature and moves to `OWNER_UPLOAD_FACTOR_DOCUMENTS` for factor uploads; `agree=false` rejects the whole case (`REJECTED`).\n\n" + "**Phase A — Mixed reply, priced lines only:** `claimStatus=NEEDS_REVISION`, no `evaluation.ownerPricedPartsApproval` yet. `agree=true` records that signature and moves to `OWNER_UPLOAD_FACTOR_DOCUMENTS` for factor uploads; `agree=false` rejects the whole case (`REJECTED`).\n\n" +
"**Phase B — Final:** `claimStatus=APPROVED`, no `evaluation.ownerInsurerApproval` yet. `agree=true` → `COMPLETED`; `agree=false` → `REJECTED`.\n\n" + "**Phase B — Legacy final phase only:** pre-existing rows with `claimStatus=APPROVED` may still be accepted or rejected through this endpoint. New V2–V5 claims complete after expert work (and V5 FileMaker approval) without a final owner signature.\n\n" +
"Response may include `phase`: `PRICED_PARTS_FOR_FACTORS` or `FINAL_APPROVAL` for UI state.", "Response may include `phase`: `PRICED_PARTS_FOR_FACTORS` or legacy `FINAL_APPROVAL` for UI state.",
}) })
@ApiBody({ @ApiBody({
description: "Signature file, agreement, and branch", description: "Signature file, agreement, and branch",

View File

@@ -102,7 +102,7 @@ export class ClaimDetailsV2ResponseDto {
@ApiProperty({ @ApiProperty({
description: description:
"ClaimCaseStatus; see also `ownerGuidance` for UX. Post-expert: INSURER_REVIEW_AWAITING_OWNER_SIGN | INSURER_REVIEW_MIXED_FACTORS_PENDING | OWNER_REPAIR_FACTOR_UPLOAD_PENDING | EXPERT_VALIDATING_REPAIR_FACTORS; legacy WAITING_FOR_INSURER_APPROVAL may still appear.", "ClaimCaseStatus; see also `ownerGuidance` for UX. New V2–V5 priced-only claims are COMPLETED after expert work; factor claims use INSURER_REVIEW_MIXED_FACTORS_PENDING | OWNER_REPAIR_FACTOR_UPLOAD_PENDING | EXPERT_VALIDATING_REPAIR_FACTORS. V5 then uses WAITING_FOR_FILE_MAKER_APPROVAL. Legacy WAITING_FOR_INSURER_APPROVAL or INSURER_REVIEW_AWAITING_OWNER_SIGN may still appear.",
example: "OWNER_REPAIR_FACTOR_UPLOAD_PENDING", example: "OWNER_REPAIR_FACTOR_UPLOAD_PENDING",
}) })
status: string; status: string;

View File

@@ -12,8 +12,8 @@ export class ClaimListItemV2Dto {
@ApiProperty({ @ApiProperty({
description: description:
"ClaimCaseStatus. Post-expert owner phase includes: INSURER_REVIEW_AWAITING_OWNER_SIGN (priced lines only → final owner sign); INSURER_REVIEW_MIXED_FACTORS_PENDING (priced + factor lines); OWNER_REPAIR_FACTOR_UPLOAD_PENDING (all lines factor-needed); EXPERT_VALIDATING_REPAIR_FACTORS (all factors uploaded, expert validating). Legacy DB rows may still use WAITING_FOR_INSURER_APPROVAL for those flows.", "ClaimCaseStatus. New V2–V5 priced-only claims become COMPLETED after expert work. Factor claims use INSURER_REVIEW_MIXED_FACTORS_PENDING (priced-line acceptance before factor uploads), OWNER_REPAIR_FACTOR_UPLOAD_PENDING, and EXPERT_VALIDATING_REPAIR_FACTORS. V5 then waits at WAITING_FOR_FILE_MAKER_APPROVAL. Legacy DB rows may still use WAITING_FOR_INSURER_APPROVAL or INSURER_REVIEW_AWAITING_OWNER_SIGN.",
example: "INSURER_REVIEW_AWAITING_OWNER_SIGN", example: "COMPLETED",
}) })
status: string; status: string;

View File

@@ -527,7 +527,7 @@ Returns status of each item (uploaded/captured or not).
); );
} }
// ─── Owner signature on expert pricing ─────────────────────────────────────── // ─── Mixed-factor priced-line signature ──────────────────────────────────────
@Put("claim-sign/:claimRequestId") @Put("claim-sign/:claimRequestId")
@ApiParam({ name: "claimRequestId" }) @ApiParam({ name: "claimRequestId" })
@@ -553,11 +553,12 @@ Returns status of each item (uploaded/captured or not).
}) })
@ApiOperation({ @ApiOperation({
summary: summary:
"Owner signature on expert pricing (Flow 3 — expert acts on behalf of user)", "Priced-line acceptance before factor uploads (Flow 3 — expert acts on behalf of user)",
description: description:
"Field expert submits the damaged party's signature during the final approval stage. " + "For mixed priced/factor claims only, the field expert records the damaged party's " +
"Delegates to the same service method as the user sign endpoint; the expert's " + "acceptance of priced lines before factor uploads. V2–V5 no longer require a final " +
"identity is resolved to the claim owner via `resolveClaimEffectiveUserId`.", "owner signature: they complete after expert work. The expert's identity is resolved " +
"to the claim owner via `resolveClaimEffectiveUserId`.",
}) })
@UseInterceptors( @UseInterceptors(
FileInterceptor("sign", { FileInterceptor("sign", {

View File

@@ -2223,10 +2223,16 @@ export class ExpertClaimService {
actorType: string; actorType: string;
}, },
metadata: Record<string, unknown>, metadata: Record<string, unknown>,
): Promise<void> { ): Promise<ClaimCaseStatus> {
// V5 retains the FileMaker approval gate, but no longer waits for a
// damaged-party final signature after that approval.
const completionStatus = (claimForTenant as any).requiresFileMakerApproval
? ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL
: ClaimCaseStatus.COMPLETED;
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: { $set: {
status: ClaimCaseStatus.COMPLETED, status: completionStatus,
claimStatus: ClaimStatus.APPROVED, claimStatus: ClaimStatus.APPROVED,
"workflow.currentStep": ClaimWorkflowStep.CLAIM_COMPLETED, "workflow.currentStep": ClaimWorkflowStep.CLAIM_COMPLETED,
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED, "workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
@@ -2248,6 +2254,8 @@ export class ExpertClaimService {
eventType: ExpertFileActivityType.HANDLED, eventType: ExpertFileActivityType.HANDLED,
idempotencyKey: `claim:${claimRequestId}:factor_validation:${historyType}:${actor.sub}`, idempotencyKey: `claim:${claimRequestId}:factor_validation:${historyType}:${actor.sub}`,
}); });
return completionStatus;
} }
/** /**
@@ -2420,7 +2428,7 @@ export class ExpertClaimService {
}; };
if (anyRejected) { if (anyRejected) {
await this.completeClaimCaseAfterFactorValidationV2( const completionStatus = await this.completeClaimCaseAfterFactorValidationV2(
claimRequestId, claimRequestId,
claim, claim,
actor, actor,
@@ -2428,28 +2436,19 @@ export class ExpertClaimService {
historyActor, historyActor,
{ replyField }, { replyField },
); );
const fanavaran =
await this.claimRequestManagementService.autoSubmitToFanavaranV2OnClaimCompleted(
claimRequestId,
);
// Expertise (stage 4) is intentionally NOT submitted here.
// It is only triggered after the owner's final approval signature
// (submitOwnerInsurerApprovalSignV2), because until that point the
// expert review is not considered final — the user can still object.
return { return {
message: this.appendFanavaranAutoSubmitToMessage( message:
"Factors were reviewed with expert repricing on rejected lines. The claim is completed without an owner signature (temporary policy; may require owner acceptance later).", completionStatus === ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL
fanavaran, ? "Factors were reviewed with expert repricing. The claim is awaiting FileMaker approval."
), : "Factors were reviewed with expert repricing. The claim is completed without a final owner signature.",
claimRequestId, claimRequestId,
claimStatus: ClaimStatus.APPROVED, claimStatus: ClaimStatus.APPROVED,
caseStatus: ClaimCaseStatus.COMPLETED, caseStatus: completionStatus,
outcome: "REJECTED_REPRICED_AUTO_COMPLETED", outcome: "REJECTED_REPRICED_AUTO_COMPLETED",
fanavaran,
}; };
} }
await this.completeClaimCaseAfterFactorValidationV2( const completionStatus = await this.completeClaimCaseAfterFactorValidationV2(
claimRequestId, claimRequestId,
claim, claim,
actor, actor,
@@ -2458,25 +2457,15 @@ export class ExpertClaimService {
{ replyField }, { replyField },
); );
const fanavaran =
await this.claimRequestManagementService.autoSubmitToFanavaranV2OnClaimCompleted(
claimRequestId,
);
// Expertise (stage 4) is intentionally NOT submitted here.
// It is only triggered after the owner's final approval signature
// (submitOwnerInsurerApprovalSignV2), because until that point the
// expert review is not considered final — the user can still object.
return { return {
message: this.appendFanavaranAutoSubmitToMessage( message:
"All factors were approved by the expert. The claim is completed without an additional owner signature.", completionStatus === ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL
fanavaran, ? "All factors were approved. The claim is awaiting FileMaker approval."
), : "All factors were approved by the expert. The claim is completed without a final owner signature.",
claimRequestId, claimRequestId,
claimStatus: ClaimStatus.APPROVED, claimStatus: ClaimStatus.APPROVED,
caseStatus: ClaimCaseStatus.COMPLETED, caseStatus: completionStatus,
outcome: "ALL_APPROVED_AUTO_COMPLETED", outcome: "ALL_APPROVED_AUTO_COMPLETED",
fanavaran,
}; };
} }
@@ -3230,9 +3219,9 @@ export class ExpertClaimService {
* On success: * On success:
* - Stores reply; clears owner signature fields (`ownerInsurerApproval`, `ownerPricedPartsApproval`) * - Stores reply; clears owner signature fields (`ownerInsurerApproval`, `ownerPricedPartsApproval`)
* - Unlocks the workflow * - Unlocks the workflow
* - Pricing-only (`factorNeeded=false` everywhere): INSURER_REVIEW_AWAITING_OWNER_SIGN, APPROVED, INSURER_REVIEW → owner final sign/reject. * - Pricing-only (`factorNeeded=false` everywhere): COMPLETED, APPROVED, CLAIM_COMPLETED — no final owner signature.
* - All lines `factorNeeded`: NEEDS_REVISION, OWNER_UPLOAD_FACTOR_DOCUMENTS → uploads → UNDER_REVIEW, EXPERT_COST_EVALUATION (validate factors). * - All lines `factorNeeded`: NEEDS_REVISION, OWNER_UPLOAD_FACTOR_DOCUMENTS → uploads → UNDER_REVIEW, EXPERT_COST_EVALUATION (validate factors).
* - Mixed priced + factor: NEEDS_REVISION, INSURER_REVIEW with next OWNER_UPLOAD_FACTOR_DOCUMENTS → owner signs priced lines first → upload factors → expert validates → final sign. * - Mixed priced + factor: NEEDS_REVISION, INSURER_REVIEW with next OWNER_UPLOAD_FACTOR_DOCUMENTS → owner signs priced lines first → upload factors → expert validates → complete.
*/ */
async submitExpertReplyV2( async submitExpertReplyV2(
claimRequestId: string, claimRequestId: string,
@@ -3398,14 +3387,23 @@ export class ExpertClaimService {
let currentStep = ClaimWorkflowStep.INSURER_REVIEW; let currentStep = ClaimWorkflowStep.INSURER_REVIEW;
let nextWorkflowStep = ClaimWorkflowStep.CLAIM_COMPLETED; let nextWorkflowStep = ClaimWorkflowStep.CLAIM_COMPLETED;
const nextCaseStatus = claimCaseStatusAfterExpertReplyV2(processedParts); const blameForFlow = claim.blameRequestId
? await this.blameRequestDbService.findById(String(claim.blameRequestId))
: undefined;
const skipsFinalOwnerSignature =
!!(blameForFlow as any)?.expertInitiated &&
(blameForFlow as any)?.creationMethod === "IN_PERSON";
const nextCaseStatus = claimCaseStatusAfterExpertReplyV2(
processedParts,
skipsFinalOwnerSignature,
);
let nextClaimStatus = ClaimStatus.APPROVED; let nextClaimStatus = ClaimStatus.APPROVED;
// V5 flow: FileMaker must approve before the owner is asked to sign. // V5 retains a FileMaker approval gate after expert work. Expert-initiated
// When no factor upload is needed, skip the owner-sign step entirely here // in-person V2–V5 flows do not request a final damaged-party signature.
// and park the claim at WAITING_FOR_FILE_MAKER_APPROVAL. The owner SMS
// and INSURER_REVIEW status are emitted by fileMakerApproveV5 instead.
const isV5Claim = !!(claim as any).requiresFileMakerApproval; const isV5Claim = !!(claim as any).requiresFileMakerApproval;
const completesWithoutFactors =
!needsFactorUpload && !isV5Claim && skipsFinalOwnerSignature;
if (needsFactorUpload) { if (needsFactorUpload) {
nextClaimStatus = ClaimStatus.NEEDS_REVISION; nextClaimStatus = ClaimStatus.NEEDS_REVISION;
@@ -3418,7 +3416,10 @@ export class ExpertClaimService {
} }
} else if (isV5Claim) { } else if (isV5Claim) {
// V5, no factors: hold at WAITING_FOR_FILE_MAKER_APPROVAL // V5, no factors: hold at WAITING_FOR_FILE_MAKER_APPROVAL
currentStep = ClaimWorkflowStep.INSURER_REVIEW; currentStep = ClaimWorkflowStep.CLAIM_COMPLETED;
nextWorkflowStep = ClaimWorkflowStep.CLAIM_COMPLETED;
} else if (completesWithoutFactors) {
currentStep = ClaimWorkflowStep.CLAIM_COMPLETED;
nextWorkflowStep = ClaimWorkflowStep.CLAIM_COMPLETED; nextWorkflowStep = ClaimWorkflowStep.CLAIM_COMPLETED;
} }
@@ -3436,10 +3437,11 @@ export class ExpertClaimService {
} }
} }
const updatePayload: Record<string, unknown> = { const persistedCaseStatus = isV5Claim && !needsFactorUpload
status: isV5Claim && !needsFactorUpload
? ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL ? ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL
: nextCaseStatus, : nextCaseStatus;
const updatePayload: Record<string, unknown> = {
status: persistedCaseStatus,
claimStatus: nextClaimStatus, claimStatus: nextClaimStatus,
...(expertAddedParts.length > 0 ...(expertAddedParts.length > 0
? { "damage.selectedParts": mergedSelectedParts } ? { "damage.selectedParts": mergedSelectedParts }
@@ -3454,9 +3456,7 @@ export class ExpertClaimService {
"evaluation.ownerPricedPartsApproval": "", "evaluation.ownerPricedPartsApproval": "",
}, },
"workflow.currentStep": currentStep, "workflow.currentStep": currentStep,
"workflow.nextStep": needsFactorUpload "workflow.nextStep": nextWorkflowStep,
? nextWorkflowStep
: ClaimWorkflowStep.CLAIM_COMPLETED,
[`evaluation.${replyField}`]: replyPayload, [`evaluation.${replyField}`]: replyPayload,
$push: { $push: {
"workflow.completedSteps": completedStep, "workflow.completedSteps": completedStep,
@@ -3495,9 +3495,12 @@ export class ExpertClaimService {
idempotencyKey: `claim:${claimRequestId}:handled:${actor.sub}`, idempotencyKey: `claim:${claimRequestId}:handled:${actor.sub}`,
}); });
// V5: owner SMS is sent by fileMakerApproveV5 after FileMaker approval. // V1 retains its existing final owner-signature notification. V2–V5 do not
// send this notification because their completion policy is manual Fanavaran
// submission after expert work (and FileMaker approval for V5).
if (!needsFactorUpload && !isV5Claim && !skipsFinalOwnerSignature) {
const ownerPhoneNotify = await this.resolveClaimOwnerPhone(claim); const ownerPhoneNotify = await this.resolveClaimOwnerPhone(claim);
if (ownerPhoneNotify && !needsFactorUpload && !isV5Claim) { if (ownerPhoneNotify) {
const expertLastName = const expertLastName =
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس"; actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
await this.smsOrchestrationService.sendSignatureReviewNotice({ await this.smsOrchestrationService.sendSignatureReviewNotice({
@@ -3505,20 +3508,23 @@ export class ExpertClaimService {
fileKind: "claim", fileKind: "claim",
publicId: claim.publicId, publicId: claim.publicId,
expertLastName, expertLastName,
link: this.smsOrchestrationService.buildClaimLink(String(claim._id), "v2"), link: this.smsOrchestrationService.buildClaimLink(
String(claim._id),
"v2",
),
}); });
} }
}
// Fanavaran expertise is now triggered on owner final sign, not here. // V2–V5 completion and FileMaker approval do not trigger a Fanavaran
// submission; the expert submits manually when ready.
return { return {
claimRequestId, claimRequestId,
status: nextCaseStatus, status: persistedCaseStatus,
claimStatus: nextClaimStatus, claimStatus: nextClaimStatus,
currentStep, currentStep,
workflowNextStep: needsFactorUpload workflowNextStep: nextWorkflowStep,
? nextWorkflowStep
: ClaimWorkflowStep.CLAIM_COMPLETED,
factorNeeded: needsFactorUpload, factorNeeded: needsFactorUpload,
mixedPricingAndFactors: mixedFactorAndPrice, mixedPricingAndFactors: mixedFactorAndPrice,
allPartsFactorNeeded: !!needsFactorUpload && !!allFactorLines, allPartsFactorNeeded: !!needsFactorUpload && !!allFactorLines,

View File

@@ -270,7 +270,7 @@ export class ExpertClaimV2Controller {
"**Frontend routing by `ClaimCaseStatus` (`status`):**\n" + "**Frontend routing by `ClaimCaseStatus` (`status`):**\n" +
"- **All parts `factorNeeded`:** `OWNER_REPAIR_FACTOR_UPLOAD_PENDING`, `claimStatus=NEEDS_REVISION`, `workflow.currentStep=OWNER_UPLOAD_FACTOR_DOCUMENTS`, `workflow.nextStep=EXPERT_COST_EVALUATION` → owner uploads all factors; then `status` becomes **`EXPERT_VALIDATING_REPAIR_FACTORS`**, `claimStatus=UNDER_REVIEW`, `currentStep=EXPERT_COST_EVALUATION` for expert **validate-factors**.\n" + "- **All parts `factorNeeded`:** `OWNER_REPAIR_FACTOR_UPLOAD_PENDING`, `claimStatus=NEEDS_REVISION`, `workflow.currentStep=OWNER_UPLOAD_FACTOR_DOCUMENTS`, `workflow.nextStep=EXPERT_COST_EVALUATION` → owner uploads all factors; then `status` becomes **`EXPERT_VALIDATING_REPAIR_FACTORS`**, `claimStatus=UNDER_REVIEW`, `currentStep=EXPERT_COST_EVALUATION` for expert **validate-factors**.\n" +
"- **Mixed (some priced, some factorNeeded):** `INSURER_REVIEW_MIXED_FACTORS_PENDING`, `claimStatus=NEEDS_REVISION`, `currentStep=INSURER_REVIEW`, `nextStep=OWNER_UPLOAD_FACTOR_DOCUMENTS` → owner must call **owner-insurer-approval/sign** first (priced-line acceptance); `currentStep` then moves to `OWNER_UPLOAD_FACTOR_DOCUMENTS` (same case `status` until factors are done).\n" + "- **Mixed (some priced, some factorNeeded):** `INSURER_REVIEW_MIXED_FACTORS_PENDING`, `claimStatus=NEEDS_REVISION`, `currentStep=INSURER_REVIEW`, `nextStep=OWNER_UPLOAD_FACTOR_DOCUMENTS` → owner must call **owner-insurer-approval/sign** first (priced-line acceptance); `currentStep` then moves to `OWNER_UPLOAD_FACTOR_DOCUMENTS` (same case `status` until factors are done).\n" +
"- **No factors:** **`INSURER_REVIEW_AWAITING_OWNER_SIGN`**, `claimStatus=APPROVED`, `currentStep=INSURER_REVIEW`, `nextStep=CLAIM_COMPLETED` → owner final sign/reject only.\n\n" + "- **No factors (V2–V5):** **`COMPLETED`**, `claimStatus=APPROVED`, `workflow.currentStep=CLAIM_COMPLETED` → no final owner signature. The expert can submit to Fanavaran manually when ready. For V5, the case instead waits for FileMaker approval before becoming `COMPLETED`.\n\n" +
"**Legacy rows** may still use `WAITING_FOR_INSURER_APPROVAL` instead of the specific values above.\n\n" + "**Legacy rows** may still use `WAITING_FOR_INSURER_APPROVAL` instead of the specific values above.\n\n" +
"**After the owner fulfilled an expert resend** (`damageExpertResend.fulfilledAt`), this expert **cannot** initiate **another resend**—use **reply/submit**, in-person visit, or **validate-factors** as appropriate.", "**After the owner fulfilled an expert resend** (`damageExpertResend.fulfilledAt`), this expert **cannot** initiate **another resend**—use **reply/submit**, in-person visit, or **validate-factors** as appropriate.",
}) })
@@ -352,8 +352,8 @@ export class ExpertClaimV2Controller {
"**Decisions:** each factor line gets `APPROVED` or `REJECTED`. **Every** decided line must include expert-entered `totalPayment` **or** both `price` and `salary` (factor photos are not read for amounts).\n\n" + "**Decisions:** each factor line gets `APPROVED` or `REJECTED`. **Every** decided line must include expert-entered `totalPayment` **or** both `price` and `salary` (factor photos are not read for amounts).\n\n" +
"**Cap (when every factor line is decided):** sum of **all** reply lines (priced parts + factor lines) must be ≤ **53,000,000 Toman**; otherwise `PRICE_CAP_ERROR` with message that the maximum acceptable total was exceeded.\n\n" + "**Cap (when every factor line is decided):** sum of **all** reply lines (priced parts + factor lines) must be ≤ **53,000,000 Toman**; otherwise `PRICE_CAP_ERROR` with message that the maximum acceptable total was exceeded.\n\n" +
"**Outcomes:**\n" + "**Outcomes:**\n" +
"- **All approved:** `caseStatus=COMPLETED`, `claimStatus=APPROVED`, workflow `CLAIM_COMPLETED` — no owner signature.\n" + "- **All approved:** `caseStatus=COMPLETED`, `claimStatus=APPROVED`, workflow `CLAIM_COMPLETED` — no owner signature. V5 instead waits for FileMaker approval.\n" +
"- **Any rejected (repriced):** same auto-complete for now (owner acceptance may be added later).\n" + "- **Any rejected (repriced):** same completion behavior for now (V5 waits for FileMaker approval).\n" +
"- **Partial batch:** returns pending until every factor line has a non-pending decision (no cap error until the batch is complete).", "- **Partial batch:** returns pending until every factor line has a non-pending decision (no cap error until the batch is complete).",
}) })
@ApiParam({ name: "claimRequestId" }) @ApiParam({ name: "claimRequestId" })

View File

@@ -0,0 +1,37 @@
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
import { claimCaseStatusAfterExpertReplyV2 } from "./claim-v2-expert-reply-workflow";
describe("claimCaseStatusAfterExpertReplyV2", () => {
it("keeps V1 priced-only claims in their final owner-signature state", () => {
expect(
claimCaseStatusAfterExpertReplyV2([
{ factorNeeded: false },
{ factorNeeded: false },
]),
).toBe(ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN);
});
it("completes a V2–V5 priced-only claim without a final owner signature", () => {
expect(
claimCaseStatusAfterExpertReplyV2(
[{ factorNeeded: false }, { factorNeeded: false }],
true,
),
).toBe(ClaimCaseStatus.COMPLETED);
});
it("keeps factor-required claims in their factor collection workflow", () => {
expect(
claimCaseStatusAfterExpertReplyV2([{ factorNeeded: true }]),
).toBe(ClaimCaseStatus.OWNER_REPAIR_FACTOR_UPLOAD_PENDING);
});
it("keeps the priced-line acceptance gate for mixed factor claims", () => {
expect(
claimCaseStatusAfterExpertReplyV2([
{ factorNeeded: false },
{ factorNeeded: true },
]),
).toBe(ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING);
});
});

View File

@@ -95,11 +95,16 @@ export function claimIsAwaitingExpertFactorValidationV2(claim: {
); );
} }
export function claimCaseStatusAfterExpertReplyV2(parts: ClaimPricingPartLite[]): ClaimCaseStatus { export function claimCaseStatusAfterExpertReplyV2(
parts: ClaimPricingPartLite[],
completeWithoutFinalOwnerSignature = false,
): ClaimCaseStatus {
const { mixedFactorAndPrice } = classifyV2ExpertPricingParts(parts); const { mixedFactorAndPrice } = classifyV2ExpertPricingParts(parts);
const needsFactorUpload = parts.some((p) => p.factorNeeded === true); const needsFactorUpload = parts.some((p) => p.factorNeeded === true);
if (!needsFactorUpload) { if (!needsFactorUpload) {
return ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN; return completeWithoutFinalOwnerSignature
? ClaimCaseStatus.COMPLETED
: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN;
} }
if (mixedFactorAndPrice) { if (mixedFactorAndPrice) {
return ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING; return ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING;

View File

@@ -37,12 +37,13 @@ class FileMakerRejectDto {
* V5 FileMaker approval panel. * V5 FileMaker approval panel.
* *
* After the full claim flow completes (FileReviewer does damage assessment via * After the full claim flow completes (FileReviewer does damage assessment via
* expert-claim APIs, user signs), the claim lands in WAITING_FOR_FILE_MAKER_APPROVAL. * expert-claim APIs and any required factors are validated), the claim lands in
* WAITING_FOR_FILE_MAKER_APPROVAL.
* The FileMaker who created the blame file can then: * The FileMaker who created the blame file can then:
* *
* approve → triggers fanavaran submission (claim → COMPLETED) * approve → claim → COMPLETED; an expert submits to Fanavaran manually
* reject → sends claim back to WAITING_FOR_DAMAGE_EXPERT so the FileReviewer * reject → sends claim back to WAITING_FOR_DAMAGE_EXPERT so the FileReviewer
* can re-lock, adjust pricing, and redo the user interaction * can re-lock, adjust pricing, and redo the claim work
* *
* All endpoints operate on `claimRequestId` (the claim case ID, not the blame ID). * All endpoints operate on `claimRequestId` (the claim case ID, not the blame ID).
* Use `GET v5/file-maker/blame-request-management/claim-id/:requestId` to obtain * Use `GET v5/file-maker/blame-request-management/claim-id/:requestId` to obtain
@@ -65,9 +66,8 @@ export class FileMakerClaimApprovalV5Controller {
summary: "Approve the completed claim (FileMaker V5)", summary: "Approve the completed claim (FileMaker V5)",
description: description:
"Approves a claim that is in `WAITING_FOR_FILE_MAKER_APPROVAL` status. " + "Approves a claim that is in `WAITING_FOR_FILE_MAKER_APPROVAL` status. " +
"Moves the claim to `INSURER_REVIEW_AWAITING_OWNER_SIGN` and sends the owner an SMS " + "Moves the claim to `COMPLETED`. No final owner signature is requested and " +
"with a signature link. Fanavaran submission is triggered automatically after the " + "Fanavaran submission remains a manual expert action.",
"owner signs.",
}) })
async approve( async approve(
@Param("claimRequestId") claimRequestId: string, @Param("claimRequestId") claimRequestId: string,

View File

@@ -0,0 +1,56 @@
import { Types } from "mongoose";
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { RequestManagementService } from "./request-management.service";
describe("RequestManagementService V5 FileMaker approval", () => {
it("completes the claim without notifying the owner for a final signature", async () => {
const fileMakerId = new Types.ObjectId();
const claimId = new Types.ObjectId();
const claimCaseDbService = {
findById: jest.fn().mockResolvedValue({
_id: claimId,
publicId: "CLM-V5",
status: ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL,
requiresFileMakerApproval: true,
fileMakerApprovalActorId: fileMakerId,
}),
findByIdAndUpdate: jest.fn().mockResolvedValue({}),
};
const service = new (RequestManagementService as any)(
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
claimCaseDbService,
) as RequestManagementService;
const result = await service.fileMakerApproveV5(
{ sub: String(fileMakerId), role: RoleEnum.FILE_MAKER },
String(claimId),
);
expect(result.status).toBe(ClaimCaseStatus.COMPLETED);
expect(result.message).toContain("Submit it to Fanavaran manually");
expect(claimCaseDbService.findByIdAndUpdate).toHaveBeenCalledWith(
String(claimId),
expect.objectContaining({
$set: expect.objectContaining({
status: ClaimCaseStatus.COMPLETED,
requiresFileMakerApproval: false,
}),
}),
);
});
});

View File

@@ -344,7 +344,7 @@ export class FileReviewerBlameV4Controller {
); );
} }
// ─── Owner signature on expert pricing ─────────────────────────────────────── // ─── Mixed-factor priced-line signature ──────────────────────────────────────
@Put("claim-sign/:claimRequestId") @Put("claim-sign/:claimRequestId")
@ApiParam({ name: "claimRequestId" }) @ApiParam({ name: "claimRequestId" })
@@ -362,11 +362,12 @@ export class FileReviewerBlameV4Controller {
}, },
}) })
@ApiOperation({ @ApiOperation({
summary: "Owner signature on expert pricing (V4 — FileReviewer acts on behalf of user)", summary: "Priced-line acceptance before factor uploads (V4 — FileReviewer acts for user)",
description: description:
"FileReviewer submits the damaged party's signature during the final approval stage. " + "For mixed priced/factor claims only, FileReviewer records the damaged party's " +
"Delegates to the same service method as the user sign endpoint; the FileReviewer's " + "acceptance of priced lines before factor uploads. V2–V5 no longer require a final " +
"identity is resolved to the claim owner via `resolveClaimEffectiveUserId`.", "owner signature: they complete after expert work. The FileReviewer's identity is " +
"resolved to the claim owner via `resolveClaimEffectiveUserId`.",
}) })
@UseInterceptors( @UseInterceptors(
FileInterceptor("sign", { FileInterceptor("sign", {

View File

@@ -342,7 +342,7 @@ export class FileReviewerBlameV5Controller {
); );
} }
// ─── Owner signature on expert pricing ─────────────────────────────────────── // ─── Mixed-factor priced-line signature ──────────────────────────────────────
@Put("claim-sign/:claimRequestId") @Put("claim-sign/:claimRequestId")
@ApiParam({ name: "claimRequestId" }) @ApiParam({ name: "claimRequestId" })
@@ -360,11 +360,12 @@ export class FileReviewerBlameV5Controller {
}, },
}) })
@ApiOperation({ @ApiOperation({
summary: "Owner signature on expert pricing (V5 — FileReviewer acts on behalf of user)", summary: "Priced-line acceptance before factor uploads (V5 — FileReviewer acts for user)",
description: description:
"FileReviewer submits the damaged party's signature during the final approval stage. " + "For mixed priced/factor claims only, FileReviewer records the damaged party's " +
"Delegates to the same service method as the user sign endpoint; the FileReviewer's " + "acceptance of priced lines before factor uploads. V2–V5 no longer require a final " +
"identity is resolved to the claim owner via `resolveClaimEffectiveUserId`.", "owner signature: they complete after expert work. The FileReviewer's identity is " +
"resolved to the claim owner via `resolveClaimEffectiveUserId`.",
}) })
@UseInterceptors( @UseInterceptors(
FileInterceptor("sign", { FileInterceptor("sign", {

View File

@@ -10462,9 +10462,9 @@ export class RequestManagementService {
/** /**
* V5 variant of expertUploadBlameVideoV3. * V5 variant of expertUploadBlameVideoV3.
* Same flow as V3/V4 but additionally marks the linked claim with * Same flow as V3/V4 but additionally marks the linked claim with
* `requiresFileMakerApproval: true` so that after the owner signs, * `requiresFileMakerApproval: true` so that after expert work completes,
* the claim is held at WAITING_FOR_FILE_MAKER_APPROVAL rather than * the claim is held at WAITING_FOR_FILE_MAKER_APPROVAL. FileMaker approval
* being auto-submitted to fanavaran. * completes the claim; Fanavaran submission is manual.
*/ */
async expertUploadBlameVideoV5( async expertUploadBlameVideoV5(
expert: any, expert: any,
@@ -10612,7 +10612,7 @@ export class RequestManagementService {
videoId, videoId,
status: req.status, status: req.status,
message: file message: file
? "Blame accident video uploaded. File is now in expert review queue. After the full claim flow completes and the owner signs, the FileMaker must approve before fanavaran submission." ? "Blame accident video uploaded. File is now in expert review queue. After the full claim flow completes, the FileMaker must approve before manual Fanavaran submission."
: "File completed. Claim is now ready for damage expert review.", : "File completed. Claim is now ready for damage expert review.",
}; };
} }
@@ -10625,7 +10625,7 @@ export class RequestManagementService {
* - claim.status === WAITING_FOR_FILE_MAKER_APPROVAL * - claim.status === WAITING_FOR_FILE_MAKER_APPROVAL
* - actor is FILE_MAKER and is the original creator of the linked blame file * - actor is FILE_MAKER and is the original creator of the linked blame file
* *
* On approval: triggers fanavaran submission and moves claim to COMPLETED. * On approval: completes the claim. Fanavaran submission is manual.
*/ */
async fileMakerApproveV5( async fileMakerApproveV5(
fileMaker: any, fileMaker: any,
@@ -10669,16 +10669,14 @@ export class RequestManagementService {
metadata: {}, metadata: {},
}; };
// V5 approval: move claim to INSURER_REVIEW_AWAITING_OWNER_SIGN so the // V5 approval is the final V5 gate. The damaged-party final signature is
// owner can now sign. Clear requiresFileMakerApproval so that when the // no longer required, and Fanavaran submission is intentionally manual.
// owner later signs and autoSubmitToFanavaranV2OnClaimCompleted runs, it
// does not re-intercept the claim as a pending V5 gate.
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: { $set: {
status: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN, status: ClaimCaseStatus.COMPLETED,
claimStatus: ClaimStatus.APPROVED, claimStatus: ClaimStatus.APPROVED,
requiresFileMakerApproval: false, requiresFileMakerApproval: false,
"workflow.currentStep": ClaimWorkflowStep.INSURER_REVIEW, "workflow.currentStep": ClaimWorkflowStep.CLAIM_COMPLETED,
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED, "workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
}, },
$push: { $push: {
@@ -10686,36 +10684,12 @@ export class RequestManagementService {
}, },
}); });
// Notify the owner that the claim is ready for their signature.
const notifyUserId = (claim as any).damagedPartyUserId ?? (claim as any).owner?.userId;
if (notifyUserId && claim.blameRequestId) {
const blame = await this.blameRequestDbService.findById(
String(claim.blameRequestId),
);
const ownerParty = (blame?.parties || []).find(
(p: any) =>
p?.person?.userId && String(p.person.userId) === String(notifyUserId),
);
const ownerPhone = ownerParty?.person?.phoneNumber
?? (await this.userDbService.findOne({ _id: new Types.ObjectId(String(notifyUserId)) }))?.mobile;
if (ownerPhone && typeof ownerPhone === "string") {
await this.smsOrchestrationService.sendSignatureReviewNotice({
receptor: ownerPhone,
fileKind: "claim",
publicId: claim.publicId,
expertLastName: actorName.split(/\s+/).pop() || "کارشناس",
link: this.smsOrchestrationService.buildClaimLink(claimRequestId, "v2"),
});
}
}
return { return {
claimRequestId, claimRequestId,
publicId: claim.publicId, publicId: claim.publicId,
status: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN, status: ClaimCaseStatus.COMPLETED,
message: message:
"Claim approved by FileMaker. Owner has been notified to sign. " + "Claim approved by FileMaker and completed. Submit it to Fanavaran manually when ready.",
"Once the owner signs, the claim will be completed and submitted to Fanavaran.",
}; };
} }