Compare commits

...

2 Commits

Author SHA1 Message Date
SepehrYahyaee
fee0bcb5be Fixed images being removed, fixed v4/v5 wrong status on uploadDocument 2026-09-16 12:50:12 +03:30
SepehrYahyaee
dc30518a7f Completed DOCS for participants 2026-09-16 10:24:22 +03:30
7 changed files with 339 additions and 46 deletions

View File

@@ -2,25 +2,6 @@
این سند قرارداد نهایی فرانت‌اند برای مرحله استعلام است. از این به بعد اطلاعات اشخاص و خودرو باید با ساختار نقش‌محور زیر ارسال شود. فیلدهای تخت قدیمی مانند `nationalCodeOfDriver` و `nationalCodeOfInsurer` دیگر ورودی معتبر نیستند.
## پاسخ کوتاه درباره `unknown`
`unknown` فقط برای یک حالت استثنایی لازم است: وقتی در مرحله طرف زیان‌دیده یک پرونده `THIRD_PARTY`، هویت بیمه‌گذار شخص ثالث واقعاً مشخص نیست.
```json
{
"thirdPartyPolicyholder": { "unknown": true }
}
```
قواعد آن:
- فقط برای طرف زیان‌دیده (`SECOND`) مجاز است؛ برای طرف مقصر (`FIRST`) خطا برمی‌گردد.
- فقط برای `thirdPartyPolicyholder` مجاز است؛ برای راننده، مالک یا بیمه‌گذار بدنه مجاز نیست.
- وقتی `unknown: true` ارسال می‌شود، هیچ فیلد هویتی دیگری در همان آبجکت نفرستید.
- در استعلام شخص ثالث با پلاک یا VIN، استعلام بیمه‌گذار عمداً skip می‌شود و سیستم نباید کد ملی راننده یا مالک را جایگزین کند.
- استعلام‌های راننده، مالکیت خودرو و اطلاعات افراد شناخته‌شده همچنان اجرا می‌شوند.
- اگر هویت بیمه‌گذار مشخص است، اصلاً از `unknown` استفاده نکنید و اطلاعات واقعی یا `sameAs` را بفرستید.
## ساختار کلی درخواست
برای پرونده `THIRD_PARTY`:
@@ -113,7 +94,6 @@
| `birthday` | تاریخ تولد جلالی | برای شخص جدید الزامی |
| `fullName` | نام نمایشی شخص | اختیاری |
| `sameAs` | اتصال این نقش به نقش دیگر | به‌جای اطلاعات شخص جدید |
| `unknown` | نامشخص بودن بیمه‌گذار ثالث | فقط `SECOND` در `THIRD_PARTY` |
| `hasDrivingLicense` | داشتن گواهینامه راننده | برای نقش راننده الزامی |
| `licenseNumber` | شماره گواهینامه | اگر `hasDrivingLicense=true` الزامی |
| `licenseType` | نوع گواهینامه | اگر `hasDrivingLicense=true` الزامی |
@@ -136,7 +116,8 @@
| `vehicle.registrationState` | وضعیت ثبت رسمی خودرو | `CURRENT` یا `RECENTLY_TRANSFERRED`؛ پیش‌فرض `CURRENT` |
| `vehicle.currentPlate` | پلاک رسمی فعلی و شناسه اصلی خودرو | الزامی |
| `vehicle.previousPlate` | پلاک قبلی در انتقال اخیر | فقط در `RECENTLY_TRANSFERRED` |
| `vehicle.vin` | شماره شاسی/VIN | در انتقال اخیر الزامی؛ حداکثر ۱۷ کاراکتر |
| `vehicle.previousPolicyholderNationalCode` | کد ملی بیمه‌گذار مربوط به پلاک قبلی | فقط در `RECENTLY_TRANSFERRED` و الزامی |
| `vehicle.vin` | شماره شاسی/VIN | در انتقال اخیر الزامی؛ در صورت ارسال دقیقاً ۱۷ کاراکتر |
| `vehicle.isNewCar` | نو بودن خودرو | اختیاری |
اجزای پلاک:
@@ -170,19 +151,20 @@
"centerDigits": "222",
"ir": "33"
},
"previousPolicyholderNationalCode": "0098765432",
"vin": "NAAM01E15HK123456"
}
}
```
سیستم ابتدا پلاک فعلی را استعلام می‌کند. اگر نتیجه ناموجود، منقضی یا فاقد بیمه‌نامه مرتبط باشد، پلاک قبلی را امتحان می‌کند. نتیجه پلاک قبلی فقط در صورت تطبیق VIN پذیرفته می‌شود؛ پلاک قبلی هرگز جایگزین پلاک فعلی نمی‌شود.
سیستم ابتدا پلاک فعلی را با کد ملی بیمه‌گذار فعلیِ مرتبط با نوع بیمه استعلام می‌کند. اگر نتیجه ناموجود، منقضی یا فاقد بیمه‌نامه مرتبط باشد، پلاک قبلی را با `previousPolicyholderNationalCode` امتحان می‌کند. نتیجه پلاک قبلی فقط در صورت تطبیق VIN پذیرفته می‌شود؛ پلاک قبلی هرگز جایگزین پلاک فعلی نمی‌شود.
حتی در route مربوط به VIN، آبجکت `vehicle` از قرارداد مشترک استفاده می‌کند و `currentPlate` در قرارداد فعلی الزامی است. مقدار VIN در `vehicle.vin` قرار می‌گیرد، نه در فیلد سطح بالای `vin`.
## ترتیب پیشنهادی نمایش فرم
1. پلاک فعلی و وضعیت انتقال خودرو را بگیرید.
2. اگر انتقال اخیر بود، پلاک قبلی و VIN را بگیرید.
2. اگر انتقال اخیر بود، پلاک قبلی، کد ملی بیمه‌گذار پلاک قبلی و VIN را بگیرید.
3. اطلاعات راننده و وضعیت گواهینامه را بگیرید.
4. بپرسید مالک خودرو همان راننده است یا شخص دیگری؛ در حالت یکسان از `sameAs` استفاده کنید.
5. بیمه‌گذار شخص ثالث را از بین راننده، مالک یا شخص دیگر انتخاب کنید.
@@ -199,7 +181,19 @@
| کارشناس/پرونده‌ساز V3 تا V5 | `.../run-inquiries/:requestId` | `.../run-inquiries-vin/:requestId` |
| مرکز تماس V6 | `/v6/call-center-blame/run-inquiry/:requestId` | `/v6/call-center-blame/run-inquiry-vin/:requestId` |
در جریان‌های V3 تا V5، فراخوان اول برای طرف مقصر (`FIRST`) و فراخوان دوم، فقط در `THIRD_PARTY`، برای طرف زیان‌دیده (`SECOND`) است. همین تفاوت تعیین می‌کند که `unknown` مجاز است یا نه.
در جریان‌های V3 تا V5، فراخوان اول برای طرف مقصر (`FIRST`) و فراخوان دوم، فقط در `THIRD_PARTY`، برای طرف زیان‌دیده (`SECOND`) است. اطلاعات بیمه‌گذار برای هر دو طرف الزامی است.
## تفاوت اطلاعات مقصر و زیان‌دیده
ساختار نقش‌محور اشخاص و خودرو برای هر دو طرف یکسان است، اما ترتیب و قواعد کسب‌وکار آن‌ها تفاوت دارد:
| پرونده و طرف | نقش‌ها و رفتار |
| --- | --- |
| `THIRD_PARTY / FIRST` (مقصر) | راننده، مالک و بیمه‌گذار شخص ثالثِ خودروی مقصر ارسال می‌شوند. شبا در این فراخوان لازم نیست. بیمه‌نامه مقصر باید متعلق به شرکت بیمه همین سامانه باشد. |
| `THIRD_PARTY / SECOND` (زیان‌دیده) | راننده، مالک و بیمه‌گذار شخص ثالثِ خودروی زیان‌دیده ارسال می‌شوند. این مرحله فقط بعد از امضای مقصر و احراز OTP زیان‌دیده اجرا می‌شود. `sheba` الزامی است و با کد ملی `vehicleOwner` اعتبارسنجی می‌شود. بیمه‌گذار زیان‌دیده نیز همیشه باید مشخص باشد. |
| `CAR_BODY / FIRST` (بیمه‌گذار/زیان‌دیده بدنه) | هر چهار نقش راننده، مالک، بیمه‌گذار شخص ثالث و بیمه‌گذار بدنه ارسال می‌شوند. `sheba` در همین فراخوان الزامی است و با کد ملی `vehicleOwner` اعتبارسنجی می‌شود. استعلام بدنه با کد ملی `carBodyPolicyholder` انجام می‌شود. |
در V2 و V6 که شبا در مرحله جداگانه از کاربر دریافت می‌شود، شبا داخل درخواست استعلام مقصر ارسال نمی‌شود؛ بک‌اند هنگام مرحله بانکی آن را با کد ملی مالک ذخیره‌شده تطبیق می‌دهد.
## فیلدهایی که نباید ارسال شوند
@@ -220,6 +214,7 @@ plateId
vin // در سطح بالا؛ مقدار صحیح داخل vehicle.vin است
isNewCar // در سطح بالا؛ مقدار صحیح داخل vehicle.isNewCar است
phoneNumber // در participantها
unknown // حذف شده؛ بیمه‌گذار همیشه باید مشخص باشد
```
## خطاهای رایج فرانت‌اند
@@ -227,9 +222,10 @@ phoneNumber // در participantها
- ارسال `vehicleOwner` به‌صورت خالی؛ باید شخص جدید یا `sameAs` باشد.
- استفاده از `sameAs` همراه با `nationalCode` یا `birthday`.
- ارسال `carBodyPolicyholder` برای `THIRD_PARTY`.
- ارسال `unknown` برای طرف مقصر یا برای نقشی غیر از بیمه‌گذار ثالث.
- ارسال `unknown` برای هر نقش؛ این فیلد دیگر پذیرفته نمی‌شود.
- فرستادن `previousPlate` بدون `registrationState=RECENTLY_TRANSFERRED`.
- فرستادن `RECENTLY_TRANSFERRED` بدون `previousPlate` یا `vin`.
- فرستادن `RECENTLY_TRANSFERRED` بدون `previousPlate`، `previousPolicyholderNationalCode` یا `vin`.
- فرستادن `previousPolicyholderNationalCode` برای خودروی دارای وضعیت `CURRENT`.
- قرار دادن VIN یا پلاک در سطح بالای body.
- ارسال شماره تلفن در آبجکت شخص.
- تکرار کد ملی راننده یا بیمه‌گذار در مرحله شبا؛ تطبیق شبا همیشه با مالک خودرو انجام می‌شود.

View File

@@ -8,7 +8,7 @@
فیلدهای نقش‌ها و آبجکت الزامی `vehicle` که در ادامه آمده‌اند، مستقیماً در body تمام درخواست‌های استعلام فعلی پذیرفته می‌شوند. این تغییر شامل فرم اولیه کاربر و mirror کارشناس/ثبت‌کننده در V2، جریان کارشناس V3، جریان‌های پرونده‌ساز V4/V5، مرکز تماس V6 و مسیرهای تک‌درخواستی حضوری است. routeهای پلاک و VIN از قوانین مشترک اشخاص استفاده می‌کنند. فیلدهای تخت راننده/بیمه‌گذار و شماره تلفن، ورودی استعلام نیستند.
پاسخ‌ها و جزئیات پرونده برای نقش‌های عملیاتی، در صورت وجود داده، فیلدهای نرمال‌شده `participants`، `participantRoles`، `vehicle.registrationState` و `vehicle.previousPlateId` را نمایش می‌دهند.
پاسخ‌ها و جزئیات پرونده برای نقش‌های عملیاتی، در صورت وجود داده، فیلدهای نرمال‌شده `participants`، `participantRoles`، `vehicle.registrationState`، `vehicle.previousPlateId` و `vehicle.previousPolicyholderNationalCode` را نمایش می‌دهند.
## مسئله
@@ -49,7 +49,7 @@
فیلد `carBodyPolicyholder` برای `THIRD_PARTY` مجاز نیست و برای `CAR_BODY` الزامی است. هر نقش باید فقط یکی از دو حالت «اطلاعات شخص» یا `sameAs` را داشته باشد. بک‌اند این ورودی را به فهرست اشخاص یکتا و اتصال نقش‌ها به آن‌ها تبدیل می‌کند.
فقط برای طرف زیان‌دیده در پرونده `THIRD_PARTY` می‌توان نامشخص بودن بیمه‌گذار را به‌صورت صریح با `"thirdPartyPolicyholder": { "unknown": true }` ارسال کرد. این حالت برای طرف مقصر پذیرفته نمی‌شود. استعلام بیمه شخص ثالث با پلاک یا VIN ــ چون هر دو به کد ملی بیمه‌گذار نیاز دارند ــ به‌شکل قابل‌ممیزی «عمداً اجرا نشد» ثبت می‌شود و بک‌اند نباید کد ملی راننده یا مالک را جایگزین کند. اعتبارسنجی راننده و مالک و استعلام‌های هویت، مالکیت و گواهینامه همچنان انجام می‌شوند.
هر بیمه‌گذار باید با اطلاعات هویتی یا `sameAs` به یک شخص مشخص متصل شود. گزینه حذف‌شده `unknown` برای هیچ نقشی پذیرفته نمی‌شود؛ بنابراین استعلام بیمه به‌دلیل نامشخص بودن هویت بیمه‌گذار رد یا عمداً اجرا‌نشده ثبت نمی‌شود.
برای راننده، `hasDrivingLicense` الزامی است. اگر مقدار آن `true` باشد، هر دو فیلد `licenseNumber` و `licenseType` نیز الزامی هستند؛ اگر مقدار آن `false` باشد، استعلام گواهینامه عمداً اجرا نمی‌شود.
@@ -73,18 +73,19 @@
"centerDigits": "222",
"ir": "33"
},
"previousPolicyholderNationalCode": "0098765432",
"vin": "NAAM01E15HK123456"
}
}
```
مقدار پیش‌فرض `registrationState` برابر `CURRENT` است و برای این مسیر استثنایی مقدار `RECENTLY_TRANSFERRED` استفاده می‌شود. `previousPlate` فقط در حالت انتقال اخیر الزامی است. پلاک فعلی همچنان شناسه اصلی خودرو است. هماهنگ‌کننده استعلام باید ابتدا پلاک فعلی را بررسی کند و اگر نتیجه ناموجود، قدیمی یا فاقد بیمه‌نامه مرتبط بود، پلاک قبلی را به‌صورت خودکار استعلام کند.
مقدار پیش‌فرض `registrationState` برابر `CURRENT` است و برای این مسیر استثنایی مقدار `RECENTLY_TRANSFERRED` استفاده می‌شود. در انتقال اخیر، `previousPlate`، `previousPolicyholderNationalCode` و `vin` الزامی‌اند؛ فیلدهای مربوط به پلاک قبلی در حالت عادی `CURRENT` نباید ارسال شوند. پلاک فعلی همچنان شناسه اصلی خودرو است. هماهنگ‌کننده استعلام ابتدا پلاک فعلی را با کد ملی بیمه‌گذار فعلی بررسی می‌کند و اگر نتیجه ناموجود، قدیمی یا فاقد بیمه‌نامه مرتبط بود، پلاک قبلی را با `previousPolicyholderNationalCode` استعلام می‌کند.
پیش از پذیرش نتیجه پلاک قبلی، بک‌اند باید یکسان بودن VIN/شماره شاسی را بررسی کند. در صورت مغایرت، انتخاب خودکار متوقف و اصلاح اطلاعات یا بررسی دستی الزامی شود. هر دو پلاک و تمام تلاش‌های استعلام برای ممیزی نگه‌داری شوند، اما پلاک قبلی هیچ‌گاه نباید روی پلاک فعلی نوشته شود.
## ترتیب پیشنهادی فرم
1. پلاک فعلی دریافت و درباره انتقال مالکیت اخیر پرسیده شود. در صورت انتقال اخیر، پلاک قبلی و VIN/شماره شاسی نیز دریافت شوند.
1. پلاک فعلی دریافت و درباره انتقال مالکیت اخیر پرسیده شود. در صورت انتقال اخیر، پلاک قبلی، کد ملی بیمه‌گذار مربوط به پلاک قبلی و VIN/شماره شاسی نیز دریافت شوند.
2. اطلاعات هویتی و گواهینامه راننده دریافت شود.
3. پرسیده شود آیا مالک خودرو همان راننده است؛ فقط در صورت تفاوت، اطلاعات مالک دریافت شود.
4. برای بیمه‌گذار شخص ثالث یکی از «راننده»، «مالک» یا «شخص دیگر» انتخاب شود؛ فقط برای شخص دیگر فرم جدید نمایش داده شود.
@@ -100,6 +101,7 @@
- نقش‌های لازم را بر اساس نوع پرونده اعتبارسنجی و ارجاع‌های نامعتبر یا حلقوی `sameAs` را رد کند؛
- شخص نهایی هر نقش را برگرداند؛
- هویت درست را به استعلام مرتبط بدهد: گواهینامه ← راننده، مالکیت و تطبیق شبا ← مالک خودرو، بیمه شخص ثالث با پلاک/VIN ← بیمه‌گذار شخص ثالث، بیمه بدنه با پلاک/VIN ← بیمه‌گذار بدنه؛
- شبا را در استعلام شخص مطالبه‌کننده خسارت (`SECOND` زیان‌دیده در `THIRD_PARTY` و طرف اول در `CAR_BODY`) الزامی کند و با کد ملی مالک خودرو اعتبارسنجی کند؛ در استعلام `FIRST` مقصر پرونده ثالث شبا دریافت نمی‌شود؛
- بر اساس یک قاعده مشخص، پلاک فعلی یا قبلی را انتخاب و نتیجه پلاک قبلی را با VIN/شماره شاسی تطبیق دهد؛
- استعلام هویت را برای هر شخص یکتا فقط یک بار اجرا کند؛
- اشخاص نرمال‌شده و نقش‌های آن‌ها را در `Party` مربوط ذخیره کند.

View File

@@ -8,7 +8,7 @@ Persian version: [inquiry-participants-proposal.fa.md](./inquiry-participants-pr
The role fields and required `vehicle` object shown below are accepted directly in every existing inquiry request body. This covers V2 user and expert/registrar mirror initial forms, V3 expert flow, V4/V5 FileMaker flows, V6 call-center flow, and the one-shot in-person completion paths. Plate and VIN routes share the same participant rules. Flat driver/insurer fields and phone numbers are not inquiry inputs.
Responses and file-detail views for operational actors expose normalized `participants`, `participantRoles`, `vehicle.registrationState`, and `vehicle.previousPlateId` where available.
Responses and file-detail views for operational actors expose normalized `participants`, `participantRoles`, `vehicle.registrationState`, `vehicle.previousPlateId`, and `vehicle.previousPolicyholderNationalCode` where available.
## Problem
@@ -49,7 +49,7 @@ Use explicit role references instead:
`carBodyPolicyholder` is forbidden for `THIRD_PARTY` and required for `CAR_BODY`. A role is either a new person's identity or a `sameAs` reference, never both. The backend should normalize this input into unique participants plus role assignments.
For the damaged party of a `THIRD_PARTY` case only, the policyholder can be explicitly unresolved with `"thirdPartyPolicyholder": { "unknown": true }`. This is not accepted for the guilty party. Third-party-policy inquiry by either plate or VIN is recorded as intentionally skipped because both routes require the policyholder national code; the backend must not substitute the driver or owner. The driver, owner, personal, ownership, and licence rules remain enforced.
Every policyholder must resolve to a known participant through identity fields or `sameAs`. The removed `unknown` option is rejected for every role, so policy inquiries are never skipped because a policyholder identity is missing.
For Driver, `hasDrivingLicense` is required. When it is `true`, both `licenseNumber` and `licenseType` are required; when it is `false`, the licence inquiry is intentionally skipped.
@@ -73,18 +73,19 @@ Participant roles and vehicle identifiers are separate concerns. When a vehicle
"centerDigits": "222",
"ir": "33"
},
"previousPolicyholderNationalCode": "0098765432",
"vin": "NAAM01E15HK123456"
}
}
```
`registrationState` is `CURRENT` by default or `RECENTLY_TRANSFERRED` for this exceptional path. `previousPlate` is required only when `registrationState=RECENTLY_TRANSFERRED`. The current plate remains the vehicle's primary identifier. The inquiry orchestrator should query the current plate first and automatically try the previous plate when the current result is missing, stale, or does not find the relevant policy.
`registrationState` is `CURRENT` by default or `RECENTLY_TRANSFERRED` for this exceptional path. `previousPlate`, `previousPolicyholderNationalCode`, and `vin` are required when `registrationState=RECENTLY_TRANSFERRED`; the previous-plate fields are forbidden for the normal `CURRENT` path. The current plate remains the vehicle's primary identifier. The inquiry orchestrator queries the current plate with the current policyholder's national code first, then automatically tries the previous plate with `previousPolicyholderNationalCode` when the current result is missing, stale, or does not find the relevant policy.
Before accepting a previous-plate result, the backend must correlate it to the same VIN/chassis. A mismatch must stop automatic selection and require correction or manual review. Both identifiers and every attempted inquiry should be retained for audit, but a previous plate must never overwrite the current plate.
## Suggested UI sequence
1. Collect the current plate and ask whether the vehicle was recently transferred. If yes, collect the previous plate and VIN/chassis.
1. Collect the current plate and ask whether the vehicle was recently transferred. If yes, collect the previous plate, its policyholder's national code, and VIN/chassis.
2. Collect driver identity and licence details.
3. Ask whether the vehicle owner is the driver; collect owner identity only when different.
4. Ask whether the third-party policyholder is the driver, the owner, or another person; collect identity only for “another person”.
@@ -100,6 +101,7 @@ Create one shared participant resolver used by every inquiry route. Its interfac
- validate required roles by case type and reject circular/invalid `sameAs` references;
- return the resolved person for each role;
- route the correct identity to each inquiry: driver licence → Driver, ownership and Sheba validation → Vehicle Owner, third-party policy by plate/VIN → Third-party Policyholder, car-body policy by plate/VIN → Car-body Policyholder;
- require Sheba in the claimant inquiry (`THIRD_PARTY` damaged/SECOND party and `CAR_BODY` first party) and validate it against the resolved Vehicle Owner; the `THIRD_PARTY` guilty/FIRST inquiry does not collect Sheba;
- choose the current or previous plate deterministically and verify previous-plate results against VIN/chassis;
- run personal identity inquiry once per distinct person;
- persist normalized participants and role assignments on the relevant `Party`.

View File

@@ -0,0 +1,113 @@
/**
* Regression: concurrent capturePart writes must not clobber sibling slots.
*
* capturePartV2 used to `$set` the entire `media.damagedParts` array from a
* stale read. Parallel uploads (common while Fanavaran attachment submit keeps
* the HTTP request open) made the last writer win — Fanavaran still saw each
* file on disk and could return errors, while Mongo was missing captures.
*
* Required strategy: per-index `$set` (`media.damagedParts.N`), matching
* `media.carAngles.<key>`.
*/
describe("capture-part media.damagedParts write strategies", () => {
type Row = { path?: string; fileName?: string; name?: string };
type Claim = { media: { damagedParts: Row[] } };
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
/** Legacy (buggy) strategy: replace the entire array from a stale read. */
async function writeWholeArray(
store: { claim: Claim },
index: number,
capture: Row,
readDelayMs: number,
) {
const snapshot = structuredClone(store.claim);
await sleep(readDelayMs);
const next = snapshot.media.damagedParts.map((row) => ({ ...row }));
while (next.length <= index) next.push({});
next[index] = { ...next[index], ...capture };
store.claim = {
...store.claim,
media: { ...store.claim.media, damagedParts: next },
};
}
/** Required strategy: set only the target index (Mongo $set media.damagedParts.N). */
async function writeSingleIndex(
store: { claim: Claim },
index: number,
capture: Row,
readDelayMs: number,
) {
await sleep(readDelayMs);
const next = store.claim.media.damagedParts.map((row) => ({ ...row }));
while (next.length <= index) next.push({});
next[index] = { ...next[index], ...capture };
store.claim.media.damagedParts[index] = next[index];
}
it("documents that whole-array replace loses a concurrent capture", async () => {
const store: { claim: Claim } = {
claim: {
media: {
damagedParts: [{ name: "hood" }, { name: "front_bumper" }],
},
},
};
await Promise.all([
writeWholeArray(
store,
0,
{ path: "files/claim-captures/hood.jpg", fileName: "hood.jpg" },
30,
),
writeWholeArray(
store,
1,
{
path: "files/claim-captures/bumper.jpg",
fileName: "bumper.jpg",
},
10,
),
]);
expect(store.claim.media.damagedParts.map((r) => r.path).filter(Boolean))
.toHaveLength(1);
});
it("keeps both concurrent captures with per-index writes", async () => {
const store: { claim: Claim } = {
claim: {
media: {
damagedParts: [{ name: "hood" }, { name: "front_bumper" }],
},
},
};
await Promise.all([
writeSingleIndex(
store,
0,
{ path: "files/claim-captures/hood.jpg", fileName: "hood.jpg" },
30,
),
writeSingleIndex(
store,
1,
{
path: "files/claim-captures/bumper.jpg",
fileName: "bumper.jpg",
},
10,
),
]);
expect(store.claim.media.damagedParts.map((r) => r.path)).toEqual([
"files/claim-captures/hood.jpg",
"files/claim-captures/bumper.jpg",
]);
});
});

View File

@@ -6452,7 +6452,9 @@ export class ClaimRequestManagementService {
);
}
// Schedule retry for attachments
// Schedule retry for attachments — never let retry bookkeeping fail the
// caller: local media/docs are already persisted before this submit.
try {
await this.scheduleFanavaranRetry(
claimCaseId,
"attachments",
@@ -6460,6 +6462,12 @@ export class ClaimRequestManagementService {
logPrefix,
{ error, clientKey },
);
} catch (retryScheduleError) {
this.logger.error(
`${logPrefix} Failed to schedule Fanavaran attachment retry`,
retryScheduleError,
);
}
return {
attempted: true,
@@ -10911,7 +10919,14 @@ export class ClaimRequestManagementService {
...nextMedia[idx],
...captureData,
};
// Per-index $set avoids lost updates when clients upload multiple parts
// in parallel (whole-array replace raced with Fanavaran-awaited requests).
// Legacy object maps still need a full write to migrate to array shape.
if (!Array.isArray(claimCase.media?.damagedParts)) {
updateData["media.damagedParts"] = nextMedia;
} else {
updateData[`media.damagedParts.${idx}`] = nextMedia[idx];
}
if (
isResendCapture &&
nextSelected.length !== selectedBeforeNorm.length
@@ -10920,10 +10935,14 @@ export class ClaimRequestManagementService {
}
}
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
await this.claimCaseDbService.findByIdAndUpdate(
claimRequestId,
updateData,
);
// Re-read so capture-progress sees sibling concurrent part/angle writes.
const updatedClaim =
(await this.claimCaseDbService.findById(claimRequestId)) ??
claimCase;
if (isResendCapture) {
await this.tryFinalizeExpertResendAfterUserAction(

View File

@@ -0,0 +1,112 @@
import { Types } from "mongoose";
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
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("FileMaker V4/V5 status resume bridge", () => {
const makerId = new Types.ObjectId();
const blameId = new Types.ObjectId();
const blameFile = {
_id: blameId,
publicId: "BL-FM-001",
requestNo: "R-1",
type: "THIRD_PARTY",
status: CaseStatus.OPEN,
blameStatus: "IN_PROGRESS",
isMadeByFileMaker: true,
initiatedByFieldExpertId: makerId,
requiresFileMakerApproval: false,
parties: [],
workflow: {
currentStep: "SECOND_COMPLETED",
nextStep: "WAITING_FOR_GUILT_DECISION",
completedSteps: ["SECOND_COMPLETED"],
},
createdAt: new Date(),
updatedAt: new Date(),
};
it("overlays claim UPLOADING_REQUIRED_DOCUMENTS onto detail status", async () => {
const service =
new (RequestManagementService as any)() as RequestManagementService;
(service as any).blameRequestDbService = {
findById: jest.fn().mockResolvedValue(blameFile),
};
(service as any).claimCaseDbService = {
findOne: jest.fn().mockResolvedValue({
_id: new Types.ObjectId(),
blameRequestId: blameId,
status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
workflow: {
currentStep: "UPLOAD_REQUIRED_DOCUMENTS",
nextStep: "SELECT_OUTER_PARTS",
},
}),
};
const detail = await service.getMyFileMakerFileDetail(
{ sub: String(makerId), role: RoleEnum.FILE_MAKER },
String(blameId),
);
expect(detail.status).toBe(ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS);
expect(detail.blameCaseStatus).toBe(CaseStatus.OPEN);
expect(detail.claimStatus).toBe(
ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
);
});
it("does not overlay status when claim docs phase is finished", async () => {
const service =
new (RequestManagementService as any)() as RequestManagementService;
(service as any).blameRequestDbService = {
findById: jest.fn().mockResolvedValue({
...blameFile,
status: CaseStatus.WAITING_FOR_FILE_REVIEWER,
}),
};
(service as any).claimCaseDbService = {
findOne: jest.fn().mockResolvedValue({
_id: new Types.ObjectId(),
blameRequestId: blameId,
status: ClaimCaseStatus.WAITING_FOR_FILE_REVIEWER,
workflow: { currentStep: "SELECT_OUTER_PARTS" },
}),
};
const detail = await service.getMyFileMakerFileDetail(
{ sub: String(makerId), role: RoleEnum.FILE_MAKER },
String(blameId),
);
expect(detail.status).toBe(CaseStatus.WAITING_FOR_FILE_REVIEWER);
expect(detail.blameCaseStatus).toBeUndefined();
});
it("overlays the same bridge on my-files list rows", async () => {
const service =
new (RequestManagementService as any)() as RequestManagementService;
(service as any).blameRequestDbService = {
find: jest.fn().mockResolvedValue([blameFile]),
};
(service as any).claimCaseDbService = {
find: jest.fn().mockResolvedValue([
{
blameRequestId: blameId,
status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
},
]),
};
const rows = await service.getMyFileMakerFiles({
sub: String(makerId),
role: RoleEnum.FILE_MAKER,
});
expect(rows).toHaveLength(1);
expect(rows[0].status).toBe(ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS);
expect(rows[0].blameCaseStatus).toBe(CaseStatus.OPEN);
});
});

View File

@@ -12733,6 +12733,26 @@ export class RequestManagementService {
return { ...workflow, completedSteps };
}
/**
* V4/V5 dirty bridge: FileMaker FE resumes from blame `status`, but pre-capture
* document upload lives on the claim (`UPLOADING_REQUIRED_DOCUMENTS`) while blame
* is still at FIRST/SECOND_COMPLETED. Mirror claim status into `status` only for
* that phase so leave/re-enter can continue; keep real blame status as
* `blameCaseStatus`. Remove once FE keys off `claimStatus` / a unified resume pointer.
*/
private fileMakerStatusForResume(
blameStatus: unknown,
claimStatus: unknown,
): { status: unknown; blameCaseStatus?: unknown } {
if (claimStatus === ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS) {
return {
status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
blameCaseStatus: blameStatus,
};
}
return { status: blameStatus };
}
async getMyFileMakerFiles(fileMaker: any): Promise<any[]> {
if (fileMaker?.role !== RoleEnum.FILE_MAKER) {
throw new ForbiddenException("Only FileMakers can use this endpoint.");
@@ -12742,14 +12762,35 @@ export class RequestManagementService {
isMadeByFileMaker: true,
initiatedByFieldExpertId: makerId,
});
const blameIds = (files || []).map((f: any) => f._id).filter(Boolean);
const claims =
blameIds.length > 0
? await this.claimCaseDbService.find(
{ blameRequestId: { $in: blameIds } },
{ lean: true, select: "blameRequestId status" },
)
: [];
const claimStatusByBlameId = new Map<string, unknown>();
for (const c of claims as any[]) {
const blameId = c?.blameRequestId != null ? String(c.blameRequestId) : "";
if (blameId) claimStatusByBlameId.set(blameId, c.status);
}
return (files || []).map((f: any) => {
const workflow = this.fileMakerWorkflowProjection(f);
const resume = this.fileMakerStatusForResume(
f.status,
claimStatusByBlameId.get(String(f._id)),
);
return {
_id: f._id,
publicId: f.publicId,
requestNo: f.requestNo,
type: f.type,
status: f.status,
status: resume.status,
...(resume.blameCaseStatus !== undefined
? { blameCaseStatus: resume.blameCaseStatus }
: {}),
blameStatus: f.blameStatus,
workflow: {
currentStep: workflow.currentStep,
@@ -12794,12 +12835,20 @@ export class RequestManagementService {
: claim
? { ...(claim as any) }
: null;
const resume = this.fileMakerStatusForResume(
plain.status,
claimPlain?.status,
);
return {
_id: plain._id,
publicId: plain.publicId,
requestNo: plain.requestNo,
type: plain.type,
status: plain.status,
status: resume.status,
...(resume.blameCaseStatus !== undefined
? { blameCaseStatus: resume.blameCaseStatus }
: {}),
blameStatus: plain.blameStatus,
workflow: this.fileMakerWorkflowProjection(plain),
requiresFileMakerApproval: plain.requiresFileMakerApproval,