Compare commits

..

7 Commits

Author SHA1 Message Date
fc1c117b93 Map Fanavaran people and licences by party role; allow unlisted base-info lookups.
Resolve driver, owner, and policyholder from participants when they differ, prefer real licence / driverLicenseDate over dummies, and fall back unknown /lookups/fanavaran/{name} to car/base-info/{name} (including vehicle-groups).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-26 16:57:30 +03:30
4f8fe04380 merge upstream 2026-09-26 15:24:01 +03:30
73ed37852b Merge pull request 'feat(claims): support optional accident sketch' (#337) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#337
2026-09-22 11:39:07 +03:30
SepehrYahyaee
8ac185c861 feat(claims): support optional accident sketch 2026-09-22 11:37:21 +03:30
f07e510c5f Merge pull request 'Fixed unstable parts fetching from fanavaran' (#336) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#336
2026-09-21 10:44:24 +03:30
SepehrYahyaee
8b65e43c60 Fixed unstable parts fetching from fanavaran 2026-09-21 10:43:30 +03:30
985c28ed6b docs 2026-09-21 10:05:49 +03:30
29 changed files with 1857 additions and 68 deletions

View File

@@ -461,6 +461,16 @@
</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 &amp; Claim</h2>
<p class="section-intro">

View File

@@ -1,6 +1,6 @@
---
last_updated: 2026-08-08
tags: [fanavaran, api, write, gen03, gen07, gen08, gen12]
tags: [fanavaran, api, write, gen03, gen07, gen08, gen12, gen44]
source: fanavaran-module-docs
---
@@ -115,7 +115,8 @@ Full shape: see skill reference sample and `buildFanavaranSubmitPayload` / `appl
### Validation (YARA)
- Only `THIRD_PARTY` claims (not `CAR_BODY`)
- `THIRD_PARTY` → `third-party-car-financial-claims`; `CAR_BODY` → `vehicle-hull-claims`
- Hull skips `dmg-cases` (resource does not exist)
- `PolicyId` required on submit (`requirePolicyId: true`)
- Skip if local `claimId` / `claimNo` already set
- `EstimateAmount` normalized to positive
@@ -215,6 +216,26 @@ After local `SELECT_OUTER_PARTS` — before image upload.
Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/damage-case/submit`.
If `DriverId` is still missing, submit registers the damaged party with GEN.44 first (see below). Preview only warns.
---
## 4.1 GEN.44 — Other people (unknown person)
Doc: `CMN.GENR.APIH.GEN.44`
| Item | Value |
|------|-------|
| نام عملیات | ثبت سایر اشخاص |
| Endpoint | `POST /Api/BimeApi/v2.0/common/other-people` |
| Method | `POST` |
Also: `GET/PUT .../common/other-people/{Id}`. Local GET: `GET /lookups/other-people/:personId`.
Used when parties inquiry has no row for the damaged party. Iranian create needs national code + Jalali birth date; Fanavaran fills name/gender from ثبت احوال. Lookup ids: `CityId`, `GenderId`, `IsIranian` (`ans`), `PersonKindId`, `NationalityId`, `MaritalStatus`.
YARA: `registerFanavaranOtherPerson` during damage-case submit. Store `Id` as `driverId` / `otherPersonId`. Best-effort; do not create on preview; do not block the local claim flow.
---
## 5. GEN.07 — Attachments

View File

@@ -1,5 +1,5 @@
---
last_updated: 2026-08-09
last_updated: 2026-09-14
tags: [fanavaran, api, read, lookups]
source: fanavaran-module-docs
---
@@ -66,6 +66,10 @@ Used when building GEN.12 to resolve `DriverId`.
Prefer cache before live call.
If inquiry returns no person, damage-case **submit** registers them with GEN.44 (`POST /common/other-people`). See [01-write-apis.md](./01-write-apis.md) and the skill reference `other-people.md`. Preview only warns; it does not create.
Local GET: `GET /lookups/inquiry-by-unique-identifier` and `GET /lookups/other-people/:personId`.
---
## 3. Remote lookup catalogue
@@ -80,6 +84,7 @@ Base: `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0`
| dmg-pay-method | `/car/code-list/dmg-pay-method` | `GET /lookups/dmg-pay-method` | Pay method |
| driving-licence-types | `/car/base-info/driving-licence-types` | `GET /lookups/driving-licence-types` | Licence type |
| accident-culprit-type | `/car/code-list/accident-culprit-type` | `GET /lookups/accident-culprit-type` | Culprit type |
| detection-accident-culprits | `/car/base-info/detection-accident-culprits` | `GET /lookups/detection-accident-culprits` | GEN.03 `AuthorityCulpritId` |
| inspection-place | `/car/code-list/inspection-place` | `GET /lookups/inspection-place` | GEN.08 |
| drop-amount-status | `/car/code-list/drop-amount-status` | `GET /lookups/drop-amount-status` | GEN.08 |
| car-components | `/car/base-info/car-components` | `GET /lookups/car-components` | `DmgSectionId` |
@@ -95,6 +100,12 @@ Base: `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0`
| dmg-history-status | `/car/code-list/dmg-case-history-status` | `GET /lookups/dmg-history-status` | GEN.12 |
| used-place | `/car/code-list/used-place` | `GET /lookups/used-place` | Used place |
| dmg-business-line | `/car/code-list/dmg-business-line` | `GET /lookups/dmg-business-line` | Business line |
| gender | `/common/code-list/gender` | `GET /lookups/gender` | GEN.44 `GenderId` |
| marital-status | `/common/code-list/marital-status` | `GET /lookups/marital-status` | GEN.44 `MaritalStatus` |
| ans | `/common/code-list/ans` | `GET /lookups/ans` | GEN.44 `IsIranian` / yes-no |
| countries | `/common/base-info/countries` | `GET /lookups/countries` | GEN.44 `NationalityId` |
| person-kind | `/common/code-list/person-kind` | `GET /lookups/person-kind` | GEN.44 `PersonKindId` |
| cii-validation-status | `/common/code-list/cii-validation-status` | `GET /lookups/cii-validation-status` | GEN.44 CII status |
### Generic accessor
@@ -121,6 +132,7 @@ Auth: Bearer + `AuthGuard`. Active tenant from `FANAVARAN_CLIENT` / `CLIENT_ID`.
| `GET /lookups/dmg-pay-method` | `/car/code-list/dmg-pay-method` |
| `GET /lookups/driving-licence-types` | `/car/base-info/driving-licence-types` |
| `GET /lookups/accident-culprit-type` | `/car/code-list/accident-culprit-type` |
| `GET /lookups/detection-accident-culprits` | `/car/base-info/detection-accident-culprits` |
| `GET /lookups/inspection-place` | `/car/code-list/inspection-place` |
| `GET /lookups/drop-amount-status` | `/car/code-list/drop-amount-status` |
| `GET /lookups/car-components` | `/car/base-info/car-components` |
@@ -135,6 +147,12 @@ Auth: Bearer + `AuthGuard`. Active tenant from `FANAVARAN_CLIENT` / `CLIENT_ID`.
| `GET /lookups/dmg-history-status` | `/car/code-list/dmg-case-history-status` |
| `GET /lookups/used-place` | `/car/code-list/used-place` |
| `GET /lookups/dmg-business-line` | `/car/code-list/dmg-business-line` |
| `GET /lookups/gender` | `/common/code-list/gender` |
| `GET /lookups/marital-status` | `/common/code-list/marital-status` |
| `GET /lookups/ans` | `/common/code-list/ans` |
| `GET /lookups/countries` | `/common/base-info/countries` |
| `GET /lookups/person-kind` | `/common/code-list/person-kind` |
| `GET /lookups/cii-validation-status` | `/common/code-list/cii-validation-status` |
| `GET /lookups/fanavaran` | catalogue metadata (names + URLs) |
| `GET /lookups/fanavaran/{lookupName}` | any configured name (incl. `insurance-corp`) |
@@ -143,7 +161,11 @@ Auth: Bearer + `AuthGuard`. Active tenant from `FANAVARAN_CLIENT` / `CLIENT_ID`.
| Nest route | Fanavaran path / notes |
|------------|------------------------|
| `GET /lookups/inquiry-by-vin?vin=` | `/car/vehicles/inquiry-by-vin` |
| `GET /lookups/inquiry-by-unique-identifier?nationalCode=&birthday=` | `/common/parties/inquiry-by-unique-identifier` |
| `GET /lookups/other-people/:personId` | `/common/other-people/{Id}` (GEN.44) |
| `GET /lookups/my-policies?nationalCode=&insuranceLineId=` | `/common/Policies/inquiry-my-policies` |
| `GET /lookups/processed-third-party-policy?nationalCode=&vin=&plaque…` | Last matching ثالث policy for that car + vehicle |
| `GET /lookups/processed-body-policy?nationalCode=&vin=&plaque…` | Last matching بدنه policy for that car + vehicle |
| `GET /lookups/third-party-policy/:policyId` | `/car/third-party-car-policies/{id}` |
| `GET /lookups/body-policy/:policyId` | `/car/vehicle-hull-policies/{id}` |

View File

@@ -87,8 +87,8 @@ File: `claim-request-management.service.ts`
| Name | Relevant values | Fanavaran use |
|------|-----------------|---------------|
| Claim type | `THIRD_PARTY` only | Fanavaran path enabled |
| | `CAR_BODY` | **Not** submitted to Fanavaran |
| Claim type | `THIRD_PARTY` | `third-party-car-financial-claims` (base → dmg-cases → files → expertise) |
| | `CAR_BODY` | `vehicle-hull-claims` (base → files → expertise; **no dmg-cases**) |
| `ClaimCaseStatus` | local workflow | Triggers stages indirectly |
| History event types | `FANAVARAN_*_SUCCEEDED/FAILED` | Audit trail on claim |

View File

@@ -60,6 +60,12 @@ sequenceDiagram
User->>YARA: SELECT_OUTER_PARTS
YARA->>YARA: autoSubmitFanavaranDamageCase...
Note over YARA: soft-ensure base if missing
alt DriverId missing
YARA->>FV: GET parties/inquiry-by-unique-identifier
FV-->>YARA: empty
YARA->>FV: POST common/other-people (GEN.44)
FV-->>YARA: person Id
end
YARA->>FV: POST .../dmg-cases (GEN.12)
FV-->>YARA: DmgCaseId
YARA->>DB: dmgCaseId

View File

@@ -133,8 +133,9 @@ Expert reply path calls expertise auto-submit (~10576).
| `claimCases.dmgCaseId` | GEN.12 |
| `claimCases.expertiseId` | GEN.08 |
| `claimCases.fanavaranSync.*` | All |
| `claimCases.fanavaranSync.damageCase.otherPersonId` | GEN.44 |
| `claimCases.history[]` | Success/fail events |
| `blameCases.parties[].person.fanavaranDriverId` | GEN.12 driver resolve |
| `blameCases.parties[].person.fanavaranDriverId` | GEN.12 driver resolve / GEN.44 |
| `fanavaranAuditLogs` | Every real HTTP step |
| `fanavaranAuthTokens` | Auth cache |
| `fanavaranClientConfigs` | Tenant profiles |

View File

@@ -28,7 +28,7 @@ source: fanavaran-module-docs
| YARA | Fanavaran |
|------|-----------|
| `damage.selectedParts` labels | `Desc` (`/` joined) |
| Party `nationalCodeOfDriver` + birthday (+ insurer flag) → inquiry | `DriverId` |
| Party `nationalCodeOfDriver` + birthday (+ insurer flag) → inquiry, else GEN.44 create | `DriverId` |
| `claimCase.vehicle.carType` → vehicle-kinds | `VehicleKindId` |
| `INSURANCE_CORP_ID` caption → insurance-corp | `InsuranceCorpId` |
| Inquiry `ShsNum` / Chassis* | `ChassisNo` |
@@ -43,6 +43,20 @@ source: fanavaran-module-docs
| Tenant defaults | `DmgCaseTypeId`, `DmgHistoryStatus`, `PlaqueKindId`, `PlaqueSampleId`, `FaultPercent`, `AccidentVehicleUsedId`, `LicenceTypeId` |
| Provisional | `EstimateAmount` = 1000 |
## GEN.44 — Other people
| YARA | Fanavaran |
|------|-----------|
| Party national code | `NationalCode` |
| Party birthday (Jalali) | `BirthYear` / `BirthMonth` / `BirthDay` |
| `fullName` split | `Name` / `LastName` (registry may overwrite) |
| Party phone / user mobile | `Mobile` |
| User address / city / gender | `Address`, `CityId`, `GenderId` |
| `ans` lookup بله | `IsIranian` |
| `person-kind` حقیقی | `PersonKindId` |
Create only on GEN.12 submit when inquiry missed. Cache `otherPersonId` + `driverId`.
## GEN.07 — Attachments
| YARA | Fanavaran |
@@ -83,7 +97,7 @@ source: fanavaran-module-docs
| YARA | Note |
|------|------|
| `CAR_BODY` claims | No Fanavaran submit |
| `CAR_BODY` dmg-cases | Hull has no GEN.12 resource; skip that stage |
| Videos | Not uploaded via GEN.07 in current code |
| Full police report fields | Often null unless later enriched |

View File

@@ -0,0 +1,442 @@
---
last_updated: 2026-09-16
tags: [fanavaran, car-body, hull, curl, vhud, parsian]
source: fanavaran-apis skill + VHUD GEN docs
---
# Car-body (بدنه) Fanavaran — manual curl flow
Direct Fanavaran BimeApi calls for **vehicle-hull-claims** (VHUD GEN.03 / GEN.05 / GEN.06).
Use this when debugging production without YARA, or to compare with YARA preview payloads.
Credentials below match **`PARSIAN_FANAVARAN_CONFIG`** in `claim-request-management.service.ts` and `scripts/fanavaran-auth.sh parsian`. Tokens (`APP_TOKEN`, `AUTH_TOKEN`) change daily — run auth curls first or `./scripts/fanavaran-auth.sh parsian` then `source files/fanavaran-auth/parsian/tokens.env`.
## Base URLs
| Purpose | URL |
|---------|-----|
| Auth | `https://apimanager.iraneit.com/BimeApiManager/api/EITAuthentication/...` |
| Business (BimeApi v2 car) | `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/...` |
## Variables (set after each step)
```bash
# Parsian (ready values)
export FANAVARAN_APPNAME='ParsianService'
export FANAVARAN_SECRET='P@r30@n$erv!ce'
export FANAVARAN_USERNAME='ParsianServiceUser'
export FANAVARAN_PASSWORD='P@r30@n123'
export FANAVARAN_CORP_ID='543'
export FANAVARAN_CONTRACT_ID='28' # confirm hull ContractId with Fanavaran if files fail
export FANAVARAN_LOCATION='210050'
export APP_TOKEN='' # from GetAppToken response header apptoken
export AUTH_TOKEN='' # from Login response header/body
export HULL_CLAIM_ID='5023617' # GEN.03 response Id — کد رایانه (NOT ClaimNo); replace after POST
export HULL_CLAIM_NO='2268' # display only
export EXPERTISE_ID='' # GEN.06 response Id
```
### One-shot auth (Parsian) — copy/paste
```bash
curl -sS -D /tmp/fanavaran-apptoken.headers -o /tmp/fanavaran-apptoken.body \
-X POST 'https://apimanager.iraneit.com/BimeApiManager/api/EITAuthentication/GetAppToken' \
-H 'appname: ParsianService' \
-H 'secret: P@r30@n$erv!ce' \
-H 'Content-Length: 0'
export APP_TOKEN="$(awk -F': ' 'tolower($1)=="apptoken"{gsub(/\r/,"",$2);print $2;exit}' /tmp/fanavaran-apptoken.headers)"
curl -sS -D /tmp/fanavaran-login.headers -o /tmp/fanavaran-login.body \
-X POST 'https://apimanager.iraneit.com/BimeApiManager/api/EITAuthentication/Login' \
-H "appToken: ${APP_TOKEN}" \
-H 'userName: ParsianServiceUser' \
-H 'password: P@r30@n123' \
-H 'Content-Length: 0'
export AUTH_TOKEN="$(awk -F': ' 'tolower($1)=="authenticationtoken"{gsub(/\r/,"",$2);print $2;exit}' /tmp/fanavaran-login.headers)"
echo "APP_TOKEN=${APP_TOKEN}"
echo "AUTH_TOKEN=${AUTH_TOKEN}"
```
Or from repo root: `./scripts/fanavaran-auth.sh parsian && source files/fanavaran-auth/parsian/tokens.env` (uses `AUTHENTICATION_TOKEN` — alias `export AUTH_TOKEN="$AUTHENTICATION_TOKEN"`).
### Id vs ClaimNo (critical)
| Field | Persian | Use in URL? |
|-------|---------|-------------|
| **`Id`** | کد رایانه پرونده | **Yes** — `/vehicle-hull-claims/{Id}/files` and `/expertise` |
| **`ClaimNo`** | شماره پرونده | **No** — do not put `2268` in the path |
Example: `Id=5023617`, `ClaimNo=2268` → use **`5023617`** everywhere below.
---
## Auth — GetAppToken
**Doc:** API.GENR (EIT Authentication)
```bash
curl -sS -D /tmp/fanavaran-apptoken.headers -o /tmp/fanavaran-apptoken.body \
-X POST 'https://apimanager.iraneit.com/BimeApiManager/api/EITAuthentication/GetAppToken' \
-H 'appname: ParsianService' \
-H 'secret: P@r30@n$erv!ce' \
-H 'Content-Length: 0'
```
Read **`apptoken`** (or `appToken`) from response **headers**:
```bash
grep -i apptoken /tmp/fanavaran-apptoken.headers
export APP_TOKEN='<paste apptoken header value>'
```
Body is usually empty. No JSON body on this call.
---
## Auth — Login
```bash
curl -sS -D /tmp/fanavaran-login.headers -o /tmp/fanavaran-login.body \
-X POST 'https://apimanager.iraneit.com/BimeApiManager/api/EITAuthentication/Login' \
-H "appToken: ${APP_TOKEN}" \
-H 'userName: ParsianServiceUser' \
-H 'password: P@r30@n123' \
-H 'Content-Length: 0'
```
Read **`authenticationToken`** from response header or JSON body:
```bash
grep -i authenticationtoken /tmp/fanavaran-login.headers
# or
cat /tmp/fanavaran-login.body
export AUTH_TOKEN='<paste token>'
```
Token is typically valid until **Asia/Tehran midnight** (same as YARA `FanavaranAuthService`).
---
## Business headers (all stages below)
Every BimeApi call needs:
```text
authenticationToken: <AUTH_TOKEN>
CorpId: <FANAVARAN_CORP_ID>
ContractId: <FANAVARAN_CONTRACT_ID>
Location: <FANAVARAN_LOCATION>
Content-Type: application/json # except multipart files
```
---
## YARA preview (copy JSON bodies from production)
Before curling Fanavaran directly, get the exact payload from Parsian production YARA (same tenant auth as above):
| Stage | YARA preview (Parsian) |
|-------|-------------------------|
| 1 Base claim | `GET /v2/fanavaran/parsian/claim-cases/{claimCaseId}/base-claim/preview` |
| 2 Damage case | **N/A for CAR_BODY** — skipped |
| 3 Attachments | `GET /v2/fanavaran/parsian/claim-cases/{claimCaseId}/attachments/preview` |
| 4 Expertise | `GET /v2/fanavaran/parsian/claim-cases/{claimCaseId}/expertise/preview` |
Expert UI BFF (if used): `POST /car-damage/expert/api/fanavaran` with `{"claimCaseId":"..."}` runs remaining stages via the app.
Save preview `payload` into a file, e.g. `stage1-body.json`, then use `@stage1-body.json` in curl.
Example hull-only GEN.03 body (proven Parsian case): [`stage1-hull-body.json`](./stage1-hull-body.json).
**Enrichment before POST (YARA does this automatically):**
| Field | Source |
|-------|--------|
| `ActualPremium` | `GET /lookups/body-policy/{PolicyId}` → `TotalPremium` |
| `AccidentCulpritId` | `GET /lookups/inquiry-by-unique-identifier?nationalCode=…&birthday=…` → `driverId` (fallback: policy `CustomerId`) |
| `CustomerFaultPercent` | **100** (required; درصد تقصير بيمه گذار) |
| `AuthorityCulpritId` | `GET /lookups/detection-accident-culprits` → بیمه‌گذار (required; مرجع تعيين مقصر) |
Success example: `Id=5026065`, `ClaimNo=1747` — use **`5026065`** for files/expertise.
---
## Stage 1 — GEN.03 Create hull base claim
**Doc:** CAR.VHUD.APIH.GEN.03
**Method:** `POST`
**URL:** `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims`
```bash
curl -sS -X POST \
'https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims' \
-H "authenticationToken: ${AUTH_TOKEN}" \
-H "CorpId: ${FANAVARAN_CORP_ID}" \
-H "ContractId: ${FANAVARAN_CONTRACT_ID}" \
-H "Location: ${FANAVARAN_LOCATION}" \
-H 'Content-Type: application/json' \
-d @stage1-body.json
```
**Body:** paste from YARA **base-claim preview** `payload`. Must be **hull GEN.03 shape** (include `AccidentTypeId`, `IsLicenseReplaced`, `CostSeparationToDmgSections`, `IsOwnerChanged`; do **not** send ثالث-only fields like `DamagedCount`, `HasOtherCulprit`, `CompensationReferenceId`, plaque block on base).
**GEN.03 required lookups (Parsian live):**
| Field | Lookup | Typical collision value |
|-------|--------|-------------------------|
| `AccidentTypeId` | `GET /lookups/vehicle-hull-accident-types` | **2** = تصادف(حادثه) |
| `CostSeparationToDmgSections` | `GET /lookups/ans` | **0** = خیر |
| `IsOwnerChanged` | `GET /lookups/ans` | **0** = خیر |
| `IsLicenseReplaced` | — | **`null`** (must be empty; do not send `0` or ثالث `IsLicenseReplacement`) |
| `CulpritTypeId` | `GET /lookups/vehicle-hull-accident-culprit-type` | **301** = حادثه مقصر شناخته شده دارد (not ثالث `337`) |
| `CustomerFaultPercent` | — | **100** |
| `AuthorityCulpritId` | `GET /lookups/detection-accident-culprits` | بیمه‌گذار (`Id` from lookup, fallback **1**) |
Fanavaran direct: `car/base-info/vehicle-hull-accident-types`, `common/code-list/ans`, `car/code-list/vehicle-hull-accident-culprit-type`.
**Success:** JSON includes at least:
```json
{ "Id": 5023617, "ClaimNo": 2268, ... }
```
```bash
export HULL_CLAIM_ID='5023617' # use Id
export HULL_CLAIM_NO='2268' # do not use in nested URLs
```
### Optional — GET base claim (verify Id exists)
```bash
curl -sS -X GET \
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}" \
-H "authenticationToken: ${AUTH_TOKEN}" \
-H "CorpId: ${FANAVARAN_CORP_ID}" \
-H "ContractId: ${FANAVARAN_CONTRACT_ID}" \
-H "Location: ${FANAVARAN_LOCATION}"
```
### Optional — PUT hull shape (if files/expertise say پرونده خسارت بدنه یافت نشد)
If stage 1 was created with a wrong (ثالث-shaped) body, **PUT** the same hull GEN.03 field set onto the existing `Id` (body = preview payload after hull mapping):
```bash
curl -sS -X PUT \
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}" \
-H "authenticationToken: ${AUTH_TOKEN}" \
-H "CorpId: ${FANAVARAN_CORP_ID}" \
-H "ContractId: ${FANAVARAN_CONTRACT_ID}" \
-H "Location: ${FANAVARAN_LOCATION}" \
-H 'Content-Type: application/json' \
-d @stage1-body-hull.json
```
YARA does this automatically before attachments/expertise when product is CAR_BODY.
---
## Stage 2 — Damage case (ثالث GEN.12) — **does not exist for hull**
**Do not call:**
```text
POST .../vehicle-hull-claims/{id}/dmg-cases ❌ Feature:url(...) not found
```
Third-party only:
```text
POST .../third-party-car-financial-claims/{id}/dmg-cases
```
For car-body YARA: damage-case preview/submit **returns skipped**. Flow is **1 → 3 → 4**.
---
## Stage 3 — GEN.05 Attachments (one file per request)
**Doc:** CAR.VHUD.APIH.GEN.05
**Method:** `POST`
**URL:** `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/{HULL_CLAIM_ID}/files`
**Parsian `FileTypeId`:** `63` (سایر مدارک خسارت) — proven on ثالث; verify in `file-types` lookup for hull if rejected.
### JSON metadata (multipart part)
YARA sends this as multipart field **`Param`** (`application/json`):
```json
{
"FileName": "photo_example.jpg",
"FileTypeId": 63,
"ClaimId": 5023617
}
```
`FileName` must **exactly** match the uploaded file name. `ClaimId` = same as URL `{HULL_CLAIM_ID}` (`Id`, not `ClaimNo`).
### curl (matches YARA `postFanavaranMultipart`: Param + Param1)
```bash
export FILE_PATH='/path/to/photo_example.jpg'
export FILE_NAME='photo_example.jpg'
curl -sS -X POST \
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}/files" \
-H "authenticationToken: ${AUTH_TOKEN}" \
-H "CorpId: ${FANAVARAN_CORP_ID}" \
-H "ContractId: ${FANAVARAN_CONTRACT_ID}" \
-H "Location: ${FANAVARAN_LOCATION}" \
-F "Param={\"FileName\":\"${FILE_NAME}\",\"FileTypeId\":63,\"ClaimId\":${HULL_CLAIM_ID}};type=application/json" \
-F "Param1=@${FILE_PATH};filename=${FILE_NAME}"
```
Repeat once per image (YARA does not batch hull uploads).
**Success example:** `{ "Id": 2008737, "Status": 2003, ... }`
### Optional — list files (GET)
```bash
curl -sS -X GET \
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}/files" \
-H "authenticationToken: ${AUTH_TOKEN}" \
-H "CorpId: ${FANAVARAN_CORP_ID}" \
-H "ContractId: ${FANAVARAN_CONTRACT_ID}" \
-H "Location: ${FANAVARAN_LOCATION}"
```
---
## Stage 4 — GEN.06 Expertise
**Doc:** CAR.VHUD.APIH.GEN.06
**Method:** `POST`
**URL:** `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/{HULL_CLAIM_ID}/expertise`
**Body:** paste from YARA **expertise preview** `payload` (GEN.06 — **not** ثالث GEN.08).
| Field | Source (YARA) |
|-------|----------------|
| `VehicleCurrentValue` | `evaluation.priceDrop.carPrice` if set; else **`GET /lookups/body-policy/{PolicyId}` → `VehicleValue`** |
| `DmgSections[].DmgKindId` | **`5485`** (تخریب) from **`GET /lookups/vehicle-hull-dmg-kind`** |
| `DmgSectionCosts[].DmgCostKindId` | **`1`** (Id for «مجموع لوازم») from **`GET /lookups/vehicle-hull-dmg-cost-kinds`** — not `DmgCostCategoryId` |
Hull expects fields such as:
- `ClaimExpertId` (Parsian assessor: **29** = `ExpertiseClaimExpertId`)
- `Wage`, `ComponentReplacementCost`, `WasteValue`, `DropAmount`, `VehicleCurrentValue`
- `MotorNo`, `ChassisNo`, `VIN`, `PlaqueNo`, `PlaqueSerial`, `BuiltYear`
- `DmgSections[]` with `Count`, `Desc`, `AccessoryKindId`, `DmgSectionCosts[]`
- **No** `DmgCaseId`, `InspectionPlaceId`, `DropAmountStatus`, `RepairWage`
```bash
curl -sS -X POST \
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}/expertise" \
-H "authenticationToken: ${AUTH_TOKEN}" \
-H "CorpId: ${FANAVARAN_CORP_ID}" \
-H "ContractId: ${FANAVARAN_CONTRACT_ID}" \
-H "Location: ${FANAVARAN_LOCATION}" \
-H 'Content-Type: application/json' \
-d @stage4-expertise-body.json
```
**Success:** `{ "Id": 71927, "ClaimId": 78776, ... }` → store as `expertiseId`.
---
## Stage map (YARA vs Fanavaran)
| YARA / user stage | Fanavaran doc | HTTP | Exists for بدنه? |
|-------------------|---------------|------|------------------|
| Auth | GetAppToken + Login | POST | Yes |
| 1 Base claim | GEN.03 | POST `/vehicle-hull-claims` | Yes |
| 1b Sync hull body | GEN.03 | PUT `/vehicle-hull-claims/{Id}` | Yes (fix row) |
| 2 Damage case | GEN.12 (ثالث) | POST `.../dmg-cases` | **No — skip** |
| 3 Attachments | GEN.05 | POST `.../{Id}/files` | Yes |
| 4 Expertise | GEN.06 | POST `.../{Id}/expertise` | Yes |
## Not used for normal collision car-body
| Doc | URL |
|-----|-----|
| GEN.01 bulk import | POST `/vehicle-hull-import-claims` |
| GEN.02 import files | POST `/vehicle-hull-import-claims/{PrvId}/files` |
| GEN.04 culprits | POST `.../{Id}/culprits` |
## Related docs
- Skill summary: `.agents/skills/fanavaran-apis/references/car-body-cases.md`
- VHUD extracts: `.agents/skills/fanavaran-apis/references/vehicle-hull/`
- Local auth script: `scripts/fanavaran-auth.sh parsian`
---
## Parsian — full flow script (after auth)
Run the **One-shot auth** block above first. Save YARA preview JSON to `stage1-body.json` and `stage4-expertise-body.json`. Set `FILE_PATH` / `FILE_NAME` for stage 3.
```bash
# Stage 1 — POST hull base claim
curl -sS -X POST \
'https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims' \
-H "authenticationToken: ${AUTH_TOKEN}" \
-H 'CorpId: 543' \
-H 'ContractId: 28' \
-H 'Location: 210050' \
-H 'Content-Type: application/json' \
-d @stage1-body.json
# Stage 1 optional — GET
curl -sS -X GET \
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}" \
-H "authenticationToken: ${AUTH_TOKEN}" \
-H 'CorpId: 543' \
-H 'ContractId: 28' \
-H 'Location: 210050'
# Stage 1 optional — PUT hull sync
curl -sS -X PUT \
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}" \
-H "authenticationToken: ${AUTH_TOKEN}" \
-H 'CorpId: 543' \
-H 'ContractId: 28' \
-H 'Location: 210050' \
-H 'Content-Type: application/json' \
-d @stage1-body-hull.json
# Stage 2 — (skip — no dmg-cases for hull)
# Stage 3 — POST one file
FILE_PATH='/path/to/photo.jpg'
FILE_NAME='photo.jpg'
curl -sS -X POST \
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}/files" \
-H "authenticationToken: ${AUTH_TOKEN}" \
-H 'CorpId: 543' \
-H 'ContractId: 28' \
-H 'Location: 210050' \
-F "Param={\"FileName\":\"${FILE_NAME}\",\"FileTypeId\":63,\"ClaimId\":${HULL_CLAIM_ID}};type=application/json" \
-F "Param1=@${FILE_PATH};filename=${FILE_NAME}"
# Stage 3 optional — GET files list
curl -sS -X GET \
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}/files" \
-H "authenticationToken: ${AUTH_TOKEN}" \
-H 'CorpId: 543' \
-H 'ContractId: 28' \
-H 'Location: 210050'
# Stage 4 — POST expertise
curl -sS -X POST \
"https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/car/vehicle-hull-claims/${HULL_CLAIM_ID}/expertise" \
-H "authenticationToken: ${AUTH_TOKEN}" \
-H 'CorpId: 543' \
-H 'ContractId: 28' \
-H 'Location: 210050' \
-H 'Content-Type: application/json' \
-d @stage4-expertise-body.json
```

View File

@@ -0,0 +1,154 @@
# Car-body (بدنه) — Fanavaran stage bodies (current YARA)
Snapshot of **what hull submit actually sends** after `toFanavaranHullBaseClaimPayload` / `toFanavaranHullExpertisePayload`.
Staged flow (VHUD):
1. Base: `POST /car/vehicle-hull-claims` (GEN.03)
2. **No dmg-cases** (`vehicle-hull-claims/{id}/dmg-cases` does not exist)
3. Attachments: `POST …/{Id}/files` (GEN.05)
4. Expertise: `POST …/{Id}/expertise` (GEN.06)
Use hull `ContractId` (`hullContractId`, else tenant `contractId`). Nested URLs use GEN.03 **`Id`**, never `ClaimNo`.
A PUT to `/vehicle-hull-claims/{Id}` may run before files/expertise; Parsian often has **no PUT route**. That PUT is best-effort and must not block GEN.05/06.
**Send rule:** send the **hull whitelist only**. Do not send ثالث-only keys (`DamagedCount`, `HasOtherCulprit`, `CompensationReferenceId`, `DocReceivedDate`, `IsLicenseReplacement`, plaque block on base, …).
**Source tags:** same as `third-party-stage-fields.md` (`system` / `lookup` / `inquiry` / `user` / `staff` / `derived`).
---
## Stage 1 — Hull base claim (GEN.03)
Built by `buildFanavaranSubmitPayload` then `enrichCarBodyHullBaseClaimPayload` then `toFanavaranHullBaseClaimPayload`.
**Fanavaran has rejected as required (must not be null):**
`CustomerFaultPercent`, `AuthorityCulpritId`, `AccidentTypeId`, `CostSeparationToDmgSections`, `IsOwnerChanged`, `PolicyId`, `ClaimExpertId`, dates/time, `CulpritTypeId`.
**Must be null:** `IsLicenseReplaced` (error if `0` / mapped from ثالث `IsLicenseReplacement`).
| Field | Persian | Type | Null? | Current value / source |
|-------|---------|------|-------|------------------------|
| PolicyId | شناسه بیمه‌نامه بدنه | number | **no** | **inquiry** first-party hull policy (`processed-body-policy` / `body-policy/{id}` / stored inquiry `policyId`) |
| ClaimExpertId | کارشناس مسئول پرونده | number | **no** | **staff** FileMaker `CarBodyClaimExpertId` on V4/V5, else tenant |
| AccidentDate | تاریخ حادثه | string Jalali | **no** | **user** first-party `statement.accidentDate`, else createdAt |
| AccidentTime | ساعت حادثه | string `HH:mm` | **no** | **user** `statement.accidentTime` if set |
| AnnouncementDate | تاریخ اعلام | string Jalali | **no** | **derived** = AccidentDate |
| AccidentLocationAddress | نشانی محل حادثه | string | **no** | **system** `"استان تهران شهر تهران"` |
| AccidentCauseId | علت حادثه | number | **no** | **system** `6` · lookup `accident-causes` |
| AccidentCityId | شهر حادثه | number | **no** | **system** `701` · lookup `cities` |
| AccidentReportTypeId | نحوه اعلام | number | **no** | **system** `155` · lookup `accident-report-type` |
| AccidentTypeId | نوع حادثه بدنه | number | **no** | **system** `2` تصادف(حادثه) · **lookup** `GET /lookups/vehicle-hull-accident-types` |
| CulpritTypeId | نوع مقصر بدنه | number | **no** | **system** `300` (ثالث `337` remapped) · **lookup** `GET /lookups/vehicle-hull-accident-culprit-type` |
| AuthorityCulpritId | مرجع تعیین مقصر | number | **no** | **lookup** `GET /lookups/detection-accident-culprits` (caption بیمه‌گذار, fallback `1`) |
| AccidentCulpritId | شناسه مقصر فناوران | number | preferred | **inquiry** parties `inquiry-by-unique-identifier`, fallback hull policy `CustomerId` |
| CustomerFaultPercent | درصد تقصیر بیمه‌گذار | number | **no** | **system** `100` |
| CostSeparationToDmgSections | تفکیک هزینه به نقاط | number (ans) | **no** | **system** `0` خیر · lookup `ans` |
| IsOwnerChanged | مالک تغییر یافته؟ | number (ans) | **no** | **system** `0` · lookup `ans` |
| IsLicenseReplaced | گواهینامه تعویضی است؟ | number | **must be null** | never copy ثالث `IsLicenseReplacement` |
| ActualPremium | حق بیمه واقعی | number | preferred | **inquiry** hull policy `TotalPremium` via `GET /lookups/body-policy/{PolicyId}` |
| EstimateAmount | برآورد | number | **no** | **user** expert prices / provisional |
| CulpritLicenceNo | شماره گواهینامه | string | **no** | dummy / **user** |
| CulpritLicenceIssuDate | تاریخ صدور گواهینامه | string Jalali | **no** | **system** `"1394/10/13"` |
| CulpritLicenceTypeId | نوع گواهینامه | number | sent | tenant `2` · lookup `driving-licence-types` |
| AccidentVehicleUsedId | کاربری خودرو | number | **no** | **inquiry** VIN/policy, else tenant |
| PoliceOfficerId | افسر | number | sent | **system** `1` |
| IsAccidentOutOfBorder | خارج از مرز | number | sent | **system** `0` |
| ArchiveNo | بایگانی | string | yes | null |
| EntryDate | تاریخ ورود | string | yes | null |
| CulpritLicenceForeignCityName | شهر خارجی گواهینامه | string | yes | null |
| PoliceReportSeri / Serial / Desc | کروکی | string | yes | null until **user** |
| CulpritLevelTwoLicenceIssuDate | صدور گواهینامه پایه ۲ | string | yes | null |
| TrackingCode | کد پیگیری | string | yes | null |
| CourtArchiveNo | بایگانی دادگاه | string | yes | null |
| PlaqueReplacementDate | تاریخ تعویض پلاک | string | yes | null |
| StatusChangeDate | تغییر وضعیت | string | yes | null |
| ClaimCompletionDate | تکمیل پرونده | string | yes | null |
| DmgAssessorFirstCreationTime | زمان ارزیاب | string | yes | null |
| IsSurplusArticleEighthLaw | مازاد ماده ۸ | number | yes | null |
| GlassBreakReasonId | علت شکست شیشه | number | yes | null |
| CulpritLicenceCountryId / CityId | محل صدور گواهینامه | number | yes | null |
Also persisted locally (not a GEN.03 field): `claimCase.vehicle.carPrice` from hull policy **`VehicleValue`**.
**Inquiries at this stage**
- Hull policy: `GET /lookups/body-policy/{PolicyId}` → `TotalPremium`, `CustomerId`, `VehicleValue`, `VehicleId`
- Parties: `GET /lookups/inquiry-by-unique-identifier`
- Optional VIN / vehicle used-id
---
## Stage 2 — Damage case
**Skipped.** YARA returns `fanavaranSync.damageCase.status = skipped`. Do not POST `dmg-cases`.
---
## Stage 3 — Attachments (GEN.05)
Multipart `Param` + `Param1`. Proven `FileTypeId=63`.
| Field | Persian | Type | Null? | Source |
|-------|---------|------|-------|--------|
| FileName | نام فایل | string | **no** | **user** (exact match to binary) |
| FileTypeId | نوع مدرک | number | **no** | **lookup** `file-types` / tenant `ClaimFileTypeId` |
| ClaimId | کد رایانه پرونده بدنه | number | **no** | stage 1 `Id` |
---
## Stage 4 — Expertise (GEN.06)
Not ثالث GEN.08: no `DmgCaseId`, `InspectionPlaceId`, `RepairWage` on the root. Uses `Wage` + vehicle identity.
`ClaimExpertId` = FileReviewer `CarBodyExpertiseClaim` on V4/V5, else tenant `ExpertiseClaimExpertId`.
| Field | Persian | Type | Null? | Current source |
|-------|---------|------|-------|----------------|
| ClaimExpertId | کارشناس ارزیاب بدنه | number | **no** | **staff** / tenant |
| DmgAssessmentDate | تاریخ بازدید | string Jalali | **no** | **derived** expert `submittedAt` |
| InspectionTime | ساعت بازدید | string | **no** | **derived** |
| MotorNo | شماره موتور | string | preferred | **inquiry** hull vehicle / party mapped `MtrNum` |
| ChassisNo | شماره شاسی | string | preferred | **inquiry** `ShsNum` |
| BuiltYear | سال ساخت | number | preferred | **inquiry** `BuiltYear` / `PrdDte` |
| VIN | VIN | string | preferred | **inquiry** |
| PlaqueNo | پلاک رشته‌ای | string | preferred | **inquiry** plk1+letter+plk3 |
| PlaqueSerial | سریال پلاک (ایران) | string | preferred | **inquiry** |
| ColorId | رنگ | number | preferred | **inquiry** `GET` vehicle by policy `VehicleId` |
| Wage | اجرت کل | number | **no** | **user** sum `salary` |
| ComponentReplacementCost | قطعه کل | number | **no** | **user** sum `price` |
| WasteValue | داغی کل | number | **no** | **user** daghi |
| RepairDuration | مدت تعمیر (روز) | number | **no** | **system** `3` |
| VehicleCurrentValue | ارزش روز خودرو | number | preferred | **user** `vehicle.carPrice` / `vehicle.price`, else **inquiry** policy `VehicleValue` |
| DropAmount | افت قیمت | number | **no** | **user** price-drop total or `0` |
| CarryAndRescueCost | حمل و نجات | number | yes | null |
| WreckHighestValue | بالاترین ارزش لاشه | number | yes | null |
| InspectionDeduction | کسر بازدید | number | yes | null |
| DmgAndWasteDesc | شرح خسارت و داغی | string | yes | null |
| WentDistanceByExpert | مسافت | number | yes | null |
| IsDestruction | اسقاط؟ | number | yes | null |
| PlaqueDesignId | طرح پلاک | number | yes | null |
| PlaqueCityId | شهر پلاک | number | yes | null |
| AccidentPercent | درصد حادثه | number | yes | null |
| TheftCases | موارد سرقت | array | empty `[]` | system |
| DmgSections[].Count | تعداد | number | **no** | **system** `1` |
| DmgSections[].Desc | شرح قطعه | string | **no** | **user** part label |
| DmgSections[].WasteValue | داغی ردیف | number | **no** | **user** |
| DmgSections[].AccessoryKindId | نوع قطعه کاتالوگ | number | preferred | **user** `partId` · lookup `car-components` |
| DmgSections[].DmgKindId | نوع آسیب بدنه | number | **no** | **lookup** `vehicle-hull-dmg-kind` caption تخریب, fallback `5485` |
| DmgSections[].VehicleHullAccessoryId | شناسه لوازم بدنه | number | **no** | **system** `3043330` (فابریک) for **every** row |
| DmgSections[].DmgSectionCosts[].Caption | عنوان هزینه | string | **no** | **user** label |
| DmgSections[].DmgSectionCosts[].Amount | مبلغ (قطعه+اجرت) | number | **no** | **user** |
| DmgSections[].DmgSectionCosts[].DmgCostKindId | نوع هزینه | number | **no** | **lookup** `vehicle-hull-dmg-cost-kinds` caption مجموع لوازم, fallback `1` |
**Lookups used:** `vehicle-hull-dmg-kind`, `vehicle-hull-dmg-cost-kinds`, `detection-accident-culprits`, `vehicle-hull-accident-types`, `vehicle-hull-accident-culprit-type`, `ans`, optional `vehicle-hull-policies/{id}/dmg-accessories` (no longer used for AccessoryId).
**Inquiries:** `body-policy/{PolicyId}`, `vehicleById` (color + identity), parties inquiry, VIN/plate inquiry on blame.
---
## YARA preview / submit
Same routes as ثالث (`/base-claim`, `/attachments`, `/expertise`).
`/damage-case` preview/submit **skips** for `CAR_BODY`.

View File

@@ -0,0 +1,201 @@
# Fanavaran intake design — user vs inquiry (proposal)
This file is a **proposed product design**, not a description of every current default. Use it to decide which YARA screens should collect data and which calls should stay automatic.
Two intake channels only:
1. **Inquiries & lookups** (Fanavaran / ESG / SandHub — no extra typing when they succeed)
2. **User input** (owner, expert, file-maker, file-reviewer)
Persian titles match Fanavaran field captions where known. Types are what we send on the wire.
Related: [third-party-stage-fields.md](./third-party-stage-fields.md), [car-body-stage-fields.md](./car-body-stage-fields.md).
---
## Shared identity (both products)
Collect once on the blame file. Inquiries reuse these keys.
| Persian | Field / storage | Type | Channel | Why |
|---------|-----------------|------|---------|-----|
| کد ملی بیمه‌گذار | `nationalCodeOfInsurer` | string(10) | **user** | Policy inquiry key |
| تاریخ تولد (جلالی) | `birthday` | string `yyyy/mm/dd` or `yyyyMMdd` | **user** | Parties inquiry + GEN.44 |
| کد ملی راننده | `nationalCodeOfDriver` | string(10) | **user** if `driverIsInsurer=false` | Parties inquiry |
| راننده همان بیمه‌گذار است؟ | `driverIsInsurer` | boolean | **user** | Chooses which national code / GEN.44 |
| شماره موبایل | phone | string | **user** | OTP / SMS, not Fanavaran |
| VIN / شماره شاسی | vin, chassis | string | **inquiry first**, **user** if miss | VIN inquiry, vehicle GET |
| شماره موتور | motorNo | string | **inquiry**, else **user** | GEN.06 / GEN.12 |
| پلاک (۲ رقم، حرف، ۳ رقم، ایران) | plate parts | string/number | **user** (photo/OCR later) then **inquiry** confirm | Plaque + policy match |
| سال ساخت | builtYear | number | **inquiry**, else **user** | GEN.12 / GEN.06 |
| نوع بدنه خودرو (سواری/شاسی‌بلند/…) | `vehicle.carType` | enum string | **user** | Fallback `VehicleKindId` lookup |
**Auto after identity**
| Persian | Fanavaran field | Type | Call |
|---------|-----------------|------|------|
| شناسه بیمه‌نامه ثالث | `PolicyId` (ثالث) | number | `my-policies` / `processed-third-party-policy` |
| شناسه بیمه‌نامه بدنه | `PolicyId` (بدنه) | number | `processed-body-policy` / `body-policy/{id}` |
| شناسه شخص فناوران | `DriverId` / `AccidentCulpritId` | number | `inquiry-by-unique-identifier` → else GEN.44 |
| شناسه مشتری بیمه‌نامه | `CustomerId` | number | hull policy GET |
| حق بیمه | `ActualPremium` | number | hull `TotalPremium` |
| ارزش روز خودرو (بیمه‌نامه) | `VehicleCurrentValue` seed | number | hull `VehicleValue` → `vehicle.carPrice` |
| رنگ | `ColorId` | number | vehicle GET |
| نوع کاربری | `AccidentVehicleUsedId` | number | vehicle `UsedId` + `vehicle-use-types` |
| نوع خودرو فناوران | `VehicleKindId` | number | VIN `VehicleKindId` or `vehicle-kinds` |
| مشخصات پلاک فناوران | PlaqueKindId / SampleId / … | number/string | VIN / vehicle GET |
---
## Accident facts (both) — mostly user
| Persian | Field | Type | Channel |
|---------|-------|------|---------|
| تاریخ حادثه | AccidentDate | Jalali date | **user** |
| ساعت حادثه | AccidentTime | `HH:mm` | **user** |
| نشانی محل حادثه | AccidentLocationAddress | string | **user** (today hardcoded تهران) |
| شهر محل حادثه | AccidentCityId | number | **user** pick from `cities` (today `701`) |
| علت حادثه | AccidentCauseId | number | **user** pick from `accident-causes` (today `6`) |
| نحوه اعلام حادثه | AccidentReportTypeId | number | **user** or default from `accident-report-type` |
| کروکی — سری / سریال / شرح | PoliceReport* | string | **user** optional; null ok |
| خارج از مرز؟ | IsAccidentOutOfBorder | ans number | **user** default خیر |
---
## Third-party only — proposed screens
### File create (مقصر + زیان‌دیده)
| Persian | Field | Type | Channel |
|---------|-------|------|---------|
| نقش طرفین | FIRST / SECOND | enum | **user** |
| مقصر کیست | guiltyPartyId | id | **user** expert decision |
| نوع مقصر | CulpritTypeId | number | **user** or default `accident-culprit-type` |
| شماره گواهینامه مقصر | CulpritLicenceNo | string | **user** (stop dummy when captured) |
| نوع گواهینامه مقصر | CulpritLicenceTypeId | number | **user** from `driving-licence-types` |
| تاریخ صدور گواهینامه | CulpritLicenceIssuDate | Jalali date | **user** |
| گواهینامه تعویضی؟ | IsLicenseReplacement | ans | **user** default خیر |
| قطعات آسیب‌دیده زیان‌دیده | selectedParts | catalog ids | **user** · `car-components` |
| نوع خودرو زیان‌دیده | carType | enum | **user** |
### Expert pricing (ثالث)
| Persian | Field | Type | Channel |
|---------|-------|------|---------|
| قیمت قطعه | parts[].price | money | **user** |
| اجرت | parts[].salary | money | **user** |
| داغی | parts[].daghi | money/object | **user** |
| شدت آسیب | AccidentLevel / typeOfDamage | number | **user** · `accident-level` |
| افت قیمت — دارد/ندارد + مبلغ | DropAmount* | number | **user** · `drop-amount-status` |
| محل بازدید | InspectionPlaceId | number | **user** or branch default · `inspection-place` |
| قیمت روز خودرو (اختیاری ثالث) | DamagedVehicleCurrentPrice | money | **user** |
### Staff (V4/V5)
| Persian | Field | Type | Channel |
|---------|-------|------|---------|
| کد کارشناس پرونده ثالث | FileMaker.ThirdPartyClaimExpertId | string/number | **staff admin** |
| کد ارزیاب ثالث | FileReviewer.ThirdPartyExpertiseClaim | string/number | **staff admin** |
Do not ask the owner for these.
---
## Car-body only — proposed screens
Hull is **first party**. One vehicle, one policy.
### File create
| Persian | Field | Type | Channel |
|---------|-------|------|---------|
| نوع حادثه بدنه | AccidentTypeId | number | **user** from `vehicle-hull-accident-types` (default تصادف = 2) |
| نوع مقصر بدنه | CulpritTypeId | number | **user** from `vehicle-hull-accident-culprit-type` (default بیمه‌گذار = 300) |
| مرجع تعیین مقصر | AuthorityCulpritId | number | **user** from `detection-accident-culprits` (default بیمه‌گذار) |
| درصد تقصیر بیمه‌گذار | CustomerFaultPercent | number 0–100 | **user** if not 100%; today always 100 |
| تفکیک هزینه به نقاط | CostSeparationToDmgSections | ans | **user** default خیر |
| مالک تغییر یافته؟ | IsOwnerChanged | ans | **user** default خیر |
| گواهینامه تعویضی؟ | IsLicenseReplaced | ans or empty | **must stay empty/null** unless Fanavaran later requires a value |
| قطعات آسیب‌دیده | selectedParts | catalog ids | **user** |
### Expert pricing (بدنه)
| Persian | Field | Type | Channel |
|---------|-------|------|---------|
| قیمت قطعه / اجرت / داغی | price, salary, daghi | money | **user** |
| قیمت روز خودرو | vehicle.carPrice | money | **user** required; seed from policy `VehicleValue` |
| افت قیمت | DropAmount / priceDrop.total | money | **user** |
| مدت تعمیر (روز) | RepairDuration | number | **system** `3` unless product wants **user** |
| نوع آسیب ردیف | DmgKindId | number | default تخریب; optional **user** · `vehicle-hull-dmg-kind` |
| نوع هزینه ردیف | DmgCostKindId | number | default مجموع لوازم · `vehicle-hull-dmg-cost-kinds` |
| لوازم فابریک | VehicleHullAccessoryId | number | **system** `3043330` (do not ask user) |
### Staff (V4/V5)
| Persian | Field | Type | Channel |
|---------|-------|------|---------|
| کد کارشناس پرونده بدنه | FileMaker.CarBodyClaimExpertId | string/number | **staff admin** |
| کد ارزیاب بدنه | FileReviewer.CarBodyExpertiseClaim | string/number | **staff admin** |
If the matching code is missing, YARA must not create/review that product line.
---
## Documents (both) — user files
| Persian | Typical key | Channel |
|---------|-------------|---------|
| کارت ملی / گواهینامه / کارت خودرو / پلاک فلزی | requiredDocuments | **user** photos |
| عکس زاویه خودرو / قطعه | damage + car-angle captures | **user** |
| فاکتور تعمیر | factor images | **user** when `factorNeeded` |
`FileTypeId` is **lookup** (`file-types`), not typed by the owner.
---
## Lookups catalogue (for UI dropdowns)
Expose these as select lists; store the numeric `Id`.
| Persian UI | Lookup route | Used on |
|------------|--------------|---------|
| علت حادثه | `/lookups/accident-causes` | GEN.03 |
| نوع اعلام حادثه | `/lookups/accident-report-type` | GEN.03 |
| شهر | `/lookups/cities` | GEN.03 city |
| بله/خیر | `/lookups/ans` | hull flags |
| نوع مقصر ثالث | `/lookups/accident-culprit-type` | ثالث GEN.03 |
| نوع مقصر بدنه | `/lookups/vehicle-hull-accident-culprit-type` | بدنه GEN.03 |
| نوع حادثه بدنه | `/lookups/vehicle-hull-accident-types` | بدنه GEN.03 |
| مرجع تعیین مقصر | `/lookups/detection-accident-culprits` | بدنه GEN.03 |
| نوع گواهینامه | `/lookups/driving-licence-types` | GEN.03 / GEN.12 |
| کاربری خودرو | `/lookups/vehicle-use-types` | GEN.03 / GEN.12 |
| نوع خودرو | `/lookups/vehicle-kinds` | GEN.12 |
| قطعات | `/lookups/car-components` | parts / DmgSectionId |
| شدت آسیب | `/lookups/accident-level` | ثالث expertise |
| محل بازدید | `/lookups/inspection-place` | ثالث expertise |
| وضعیت افت | `/lookups/drop-amount-status` | ثالث expertise |
| نوع آسیب بدنه | `/lookups/vehicle-hull-dmg-kind` | بدنه expertise |
| نوع هزینه بدنه | `/lookups/vehicle-hull-dmg-cost-kinds` | بدنه expertise |
| نوع فایل | `/lookups/file-types` | attachments |
| جنسیت / تأهل / تابعیت / نوع شخص | gender, marital-status, countries, person-kind, ans | GEN.44 only if inquiry misses |
Inquiry helpers (not dropdowns): `inquiry-by-vin`, `inquiry-by-unique-identifier`, `my-policies`, `processed-third-party-policy`, `processed-body-policy`, `third-party-policy/:id`, `body-policy/:id`.
---
## Suggested flow (what to collect, in order)
```text
1. User: phone + national code + birthday + plate (and VIN if known)
2. Inquiry: policy + vehicle + person Id
3. If person missing → GEN.44 using user national code + birthday (submit time)
4. User: accident date/time/address + (بدنه) accident type / culprit authority
5. User: damaged parts
6. Expert: prices, daghi, carPrice (بدنه)
7. System: ClaimExpertId / expertise ClaimExpertId from staff profiles
8. System: POST stages in order (ثالث 1→2→3→4, بدنه 1→3→4)
```
**Do not ask the user for:** Fanavaran `PolicyId`, `DriverId`, `CustomerId`, `ColorId`, `VehicleKindId`, `VehicleHullAccessoryId` (فابریک), `ActualPremium`, `FileTypeId`, expert numeric ids.
**Stop using dummies when the screen exists:** `CulpritLicenceNo`, `CulpritLicenceIssuDate`, `AccidentLocationAddress`, `AccidentCityId`, `AccidentCauseId`. Until then, current defaults keep Fanavaran from rejecting the create.

View File

@@ -0,0 +1,199 @@
# Third-party (ثالث) — Fanavaran stage bodies (current YARA)
Snapshot of **what `ClaimRequestManagementService` actually sends** for `THIRD_PARTY` as of this version.
- Base: `POST /car/third-party-car-financial-claims` (GEN.03)
- Damage case: `POST …/{claimId}/dmg-cases` (GEN.12)
- Attachments: `POST …/{claimId}/files` (GEN.07, multipart)
- Expertise: `POST …/{claimId}/expertise` (GEN.08)
**Send rule:** keep keys in the JSON even when the value is `null`. Fanavaran treats omitted keys differently from explicit `null`.
**Source columns**
| Tag | Meaning |
|-----|---------|
| **system** | Hardcoded YARA default / tenant `fanavaranClientConfigs` |
| **lookup** | Fanavaran codebook (`GET /lookups/…`) |
| **inquiry** | Live Fanavaran inquiry (policies, VIN, parties, vehicle GET) |
| **user** | Collected in YARA UI (owner, expert, file-maker) |
| **staff** | FileMaker `ThirdPartyClaimExpertId` / FileReviewer `ThirdPartyExpertiseClaim` on V4/V5; else tenant defaults |
| **derived** | Computed from other fields (dates, sums) |
Parsian tenant defaults used below: `ClaimExpertId=154`, `ExpertiseClaimExpertId=29`, `ClaimFileTypeId=63`, `AccidentCityId=701`, `AccidentReportTypeId=155`, `CulpritTypeId=337`, `CulpritLicenceTypeId=2`, `CompensationReferenceId=167`.
---
## Stage 1 — Base claim (GEN.03)
**Must send (non-null):** `PolicyId`, `ClaimExpertId`, `AccidentDate`, `AnnouncementDate`, `AccidentTime`, `AccidentCauseId`, `AccidentCityId`, `AccidentReportTypeId`, `CulpritTypeId`, `CulpritLicenceTypeId`, `CulpritLicenceNo`, `CulpritLicenceIssuDate`, `DamagedCount`, `EstimateAmount` (> 0), `AccidentLocationAddress`, `HasOtherCulprit`, `IsFatalAccident`, `IsAccidentOutOfBorder`, `IsPlaqueChanged`, `IsLicenseMatchWithVehicleKind`, `IsLicenseReplacement`, `PoliceOfficerId`, `AccidentVehicleUsedId`.
**May be null** (and currently are, unless noted): everything else in the table.
| Field | Persian | Type | Null? | Current source | Notes |
|-------|---------|------|-------|----------------|-------|
| PolicyId | شناسه بیمه‌نامه مقصر | number | **no** | **inquiry** `GET /lookups/my-policies` (guilty national code, line ثالث) | Cached on `fanavaranSync.baseClaim.policyId` |
| ClaimExpertId | کارشناس مسئول پرونده | number | **no** | **staff** FileMaker `ThirdPartyClaimExpertId` on V4/V5, else tenant | GEN.03 role, not expertise |
| AccidentDate | تاریخ حادثه | string (Jalali `yyyy/mm/dd`) | **no** | **user** accident datetime, else blame `createdAt` | |
| AnnouncementDate | تاریخ اعلام | string Jalali | **no** | **derived** = AccidentDate | |
| DocReceivedDate | تاریخ وصول مدارک | string Jalali | **no** | **derived** = AccidentDate | Third-party only (not on hull whitelist) |
| AccidentTime | ساعت حادثه | string `HH:mm` | **no** | **user** / **derived** from createdAt | |
| AccidentLocationAddress | نشانی محل حادثه | string | **no** | **system** `"استان تهران شهر تهران"` | Should later be **user** |
| AccidentCauseId | علت حادثه | number | **no** | **system** `6` | Lookup: `GET /lookups/accident-causes`. Expert `accidentReason.fanavaran` exists but is not yet wired as the sent id |
| AccidentCityId | شهر محل حادثه | number | **no** | **system** `701` تهران | Lookup: `GET /lookups/cities` |
| AccidentReportTypeId | نحوه اعلام حادثه | number | **no** | **system** `155` | Lookup: `GET /lookups/accident-report-type` |
| AccidentVehicleUsedId | نوع کاربری خودرو | number | **no** | **inquiry** VIN / policy vehicle `UsedId`, else tenant `1` | Lookup: `GET /lookups/vehicle-use-types` |
| CulpritTypeId | نوع مقصر | number | **no** | **system** `337` | Lookup: `GET /lookups/accident-culprit-type` (ثالث, not hull) |
| CulpritLicenceTypeId | نوع گواهینامه مقصر | number | **no** | **system** `2` | Lookup: `GET /lookups/driving-licence-types` |
| CulpritLicenceNo | شماره گواهینامه مقصر | string | **no** | **user** party licence if stored, else dummy `9705463515` | Must never be empty |
| CulpritLicenceIssuDate | تاریخ صدور گواهینامه | string Jalali | **no** | **system** `"1394/10/13"` | Should later be **user** |
| EstimateAmount | مبلغ برآورد | number | **no** | **user** expert part prices, else provisional `1000` | Must be > 0 |
| CompensationReferenceId | مرجع جبران | number | **no** | **system** `167` | Tenant default |
| DamagedCount | تعداد زیان‌دیدگان | number | **no** | **system** `1` | |
| HasOtherCulprit | مقصر دیگر دارد؟ | number (ans) | **no** | **system** `0` | Lookup `GET /lookups/ans` |
| IsFatalAccident | فوت دارد؟ | number | **no** | **system** `0` | |
| IsAccidentOutOfBorder | خارج از مرز؟ | number | **no** | **system** `0` | |
| IsPlaqueChanged | پلاک تعویضی؟ | number | **no** | **system** `0` | |
| IsLicenseMatchWithVehicleKind | گواهینامه متناسب؟ | number | **no** | **system** `1` | |
| IsLicenseReplacement | گواهینامه تعویضی؟ | number | **no** | **system** `0` | Third-party uses this name (hull uses `IsLicenseReplaced` and must be null) |
| PoliceOfficerId | افسر پلیس | number | **no** | **system** `1` | |
| ActualPremium | حق بیمه واقعی | number | yes | unused on ثالث create | |
| ArchiveNo | شماره بایگانی | string | yes | — | |
| AuthorityCulpritId | مرجع تعیین مقصر | number | yes | **null** on ثالث | Hull requires this |
| AccidentCulpritId | شناسه مقصر در فناوران | number | yes | **null** on ثالث | Hull fills from parties inquiry |
| ClaimCompletionDate | تاریخ تکمیل | string | yes | — | |
| CostSeparationToDmgSections | تفکیک هزینه به نقاط | number | yes | **null** on ثالث | Hull sends `0` |
| CouponNo | شماره کوپن | string | yes | — | |
| CourtArchiveNo | شماره بایگانی دادگاه | string | yes | — | |
| CustomerFaultPercent | درصد تقصیر بیمه‌گذار | number | yes | **null** on ثالث | Hull sends `100` |
| CulpritLicenceCityId | شهر صدور گواهینامه | number | yes | Lookup `cities` | |
| CulpritLicenceCountryId | کشور صدور گواهینامه | number | yes | Lookup `countries` | |
| CulpritLicenceForeignCityName | شهر خارجی صدور | string | yes | — | |
| DmgAssessorFirstCreationTime | زمان ایجاد ارزیاب | string | yes | — | |
| EntryDate | تاریخ ورود | string | yes | — | |
| GlassBreakReasonId | علت شکست شیشه | number | yes | — | |
| IsOwnerChanged | مالک تغییر کرده؟ | number | yes | **null** | Hull sends `0` |
| IsSurplusArticleEighthLaw | مازاد ماده ۸ | number | yes | — | |
| PlaqueCityId … PlaqueSerial | پلاک (بلوک پایه) | mixed | yes | **null** on GEN.03 ثالث | Plaque belongs on GEN.12 |
| PoliceReportDesc / Seri / Serial | مشخصات کروکی | string | yes | **user** later | |
| PreviousPolicyEndDate | پایان بیمه‌نامه قبلی | string | empty `""` | **system** | |
| StatusChangeDate | تاریخ تغییر وضعیت | string | yes | — | |
| TrackingCode | کد پیگیری | string | yes | — | |
| UnknownCulpritCauseId | علت مقصر ناشناس | number | yes | — | |
**Inquiries used at this stage**
- `GET /lookups/my-policies?nationalCode=&insuranceLineId=` (ثالث)
- Optional VIN → `AccidentVehicleUsedId`
**User input already needed (even if some are still defaulted)**
- Guilty party national code (to find PolicyId)
- Accident date/time
- Expert priced parts (EstimateAmount) when available
---
## Stage 2 — Damage case (GEN.12)
Nested under Fanavaran `claimId` (`Id` from stage 1, never `ClaimNo`).
**Must send (non-null in proven Parsian):** `DriverId`, `VehicleKindId`, `AccidentVehicleUsedId`, `DmgCaseTypeId`, `DmgHistoryStatus`, `FaultPercent`, `InsuranceCorpId`, `LicenceNo`, `LicenceTypeId`, `LicenceIssuDate`, `EstimateAmount`, `Desc`, plaque block (`PlaqueKindId`, `PlaqueSampleId`, left/middle/right/serial), `BeginDate`/`EndDate` when inquiry has them.
If `DriverId` is missing, **submit** (not preview) registers GEN.44 `POST /common/other-people` and stores `otherPersonId`.
| Field | Persian | Type | Null? | Current source |
|-------|---------|------|-------|----------------|
| DriverId | شناسه راننده زیان‌دیده در فناوران | number | **no** on submit | **inquiry** `GET /lookups/inquiry-by-unique-identifier` (national code + Jalali birthday). Fallback **GEN.44** create |
| OwnerId | شناسه مالک | number | yes | currently `null` |
| VehicleKindId | نوع خودرو | number | **no** | **inquiry** VIN vehicle `VehicleKindId`, else **lookup** `GET /lookups/vehicle-kinds` matched to `claim.vehicle.carType` |
| AccidentVehicleUsedId | کاربری | number | **no** | **inquiry** vehicle `UsedId` / use-types, else tenant |
| VIN | شماره شاسی ۱۷ رقمی | string | preferred | **inquiry** plate/VIN inquiry |
| ChassisNo | شماره شاسی | string | preferred | **inquiry** |
| MotorNo | شماره موتور | string | preferred | **inquiry** |
| BuiltYear | سال ساخت | number | preferred | **inquiry** VIN / mapped `PrdDte` |
| PlaqueKindId | نوع پلاک | number | **no** | **inquiry** vehicle GET, else national-plate default |
| PlaqueSampleId | نمونه پلاک | number | **no** | same |
| PlaqueLeftNo | دو رقم سمت چپ | string | **no** | **user** plate / **inquiry** |
| PlaqueMiddleCodeId | حرف پلاک (کد فناوران) | number | **no** | **derived** from plate letter |
| PlaqueRightNo | سه رقم | string | **no** | **user** / **inquiry** |
| PlaqueSerial | ایران | string | **no** | **user** / **inquiry** |
| PlaqueNo | پلاک رشته‌ای | string | preferred | **derived** |
| PlaqueCityId | شهر پلاک | number | yes | **inquiry** vehicle |
| PolicyNo | شماره چاپی بیمه‌نامه زیان‌دیده | string | preferred | **inquiry** ESG/Tejarat mapped |
| PolicyCINumber | کد یکتای بیمه‌نامه | string | preferred | **inquiry** `PlcyUnqCod` |
| BeginDate | شروع پوشش | string Jalali | preferred | **inquiry** |
| EndDate | پایان پوشش | string Jalali | preferred | **inquiry** |
| PreviousPolicyEndDate | پایان بیمه‌نامه قبلی | string | empty ok | **derived** from EndDate |
| InsuranceCorpId | شرکت بیمه زیان‌دیده | number | **no** | **lookup** insurance-corp for tenant |
| DmgCaseTypeId | نوع مورد خسارت | number | **no** | **system** `175` · lookup `dmg-case-type` |
| DmgHistoryStatus | وضعیت سابقه خسارت | number | **no** | **system** `5214` · lookup `dmg-history-status` |
| FaultPercent | درصد تقصیر | number | **no** | **system** `100` |
| DriverIsOwner | راننده همان مالک؟ | number | **no** | **user** `driverIsInsurer` → 1/0 |
| LicenceNo | شماره گواهینامه زیان‌دیده | string | **no** | **user**, else dummy |
| LicenceTypeId | نوع گواهینامه | number | **no** | **system** same as CulpritLicenceTypeId |
| LicenceIssuDate | تاریخ صدور | string Jalali | **no** | **user** birthday used as stand-in, else dummy date |
| LicenceCityId / CountryId / ForeignCityName | محل صدور گواهینامه | mixed | yes | currently null |
| EstimateAmount | برآورد مورد خسارت | number | **no** | **system** provisional constant |
| Desc | شرح قطعات آسیب‌دیده | string | **no** | **user** selected outer parts labels joined with `/` |
| DmgCaseId | (response) | number | — | store as local `dmgCaseId` |
**Lookups:** `vehicle-kinds`, `vehicle-use-types`, `dmg-case-type`, `dmg-history-status`, `insurance-corp`, `file-types` (later), `driving-licence-types`, `ans`.
**Inquiries:** parties by national code + birthday; VIN inquiry; optional vehicle GET; plate inquiry on blame.
---
## Stage 3 — Attachments (GEN.07)
Multipart: field `Param` = JSON, field `Param1` = file bytes. One file per request. Parsian `FileTypeId=63` (سایر مدارک خسارت).
| Field | Persian | Type | Null? | Current source |
|-------|---------|------|-------|----------------|
| FileName | نام فایل | string | **no** | **user** upload name (must match binary name) |
| FileTypeId | نوع مدرک | number | **no** | **lookup** `GET /lookups/file-types` via tenant `ClaimFileTypeId` |
| Files[].FileName / FileTypeId | تکرار متادیتا | object | **no** | same |
| ClaimId | (hull only on Param) | number | hull yes | third-party URL already has claimId |
Files themselves are **user** (documents, damage photos, factors).
---
## Stage 4 — Expertise (GEN.08)
Requires local `dmgCaseId`. `ClaimExpertId` here is the **assessor** role (Parsian `29`), from FileReviewer `ThirdPartyExpertiseClaim` on V4/V5.
| Field | Persian | Type | Null? | Current source |
|-------|---------|------|-------|----------------|
| DmgCaseId | شناسه مورد خسارت | number | **no** | stage 2 response |
| ClaimExpertId | کارشناس ارزیاب | number | **no** | **staff** / tenant `ExpertiseClaimExpertId` |
| DmgAssessmentDate | تاریخ بازدید | string Jalali | **no** | **derived** expert reply `submittedAt` |
| InspectionTime | ساعت بازدید | string `HH:mm` | **no** | **derived** |
| InspectionPlaceId | محل بازدید | number | **no** | **system** `282` · lookup `GET /lookups/inspection-place` |
| RepairWage | اجرت کل | number | **no** | **user** sum of part `salary` |
| ComponentReplacementCost | قطعه کل | number | **no** | **user** sum of part `price` |
| WasteValue | داغی کل | number | **no** | **user** daghi prices |
| DropAmountStatus | وضعیت افت | number | **no** | **system** `5458` · lookup `drop-amount-status` |
| DropAmountAdditionsDeductions | مبلغ افت | number | **no** | **user** `evaluation.priceDrop.total` or `0` |
| ComponentReplacementTaxAndToll | مالیات قطعه | number | **no** | **system** `0` |
| DamagedVehicleCurrentPrice | قیمت روز خودرو | number | yes | **user** price-drop `carPrice` |
| WentDistanceByExpert | مسافت کارشناس | number | yes | currently `null` |
| DmgSections[].DmgSectionId | شناسه نقطه آسیب | number | **no** | **user** catalog `partId` · lookup `car-components` |
| DmgSections[].AccidentLevel | شدت آسیب | number | **no** | **user** `typeOfDamage` or default `5456` · lookup `accident-level` |
| DmgSections[].Desc | شرح | string | **no** | **user** |
| DmgSections[].ComponentReplacementCost | قیمت قطعه | number | **no** | **user** |
| DmgSections[].RepairWage | اجرت | number | **no** | **user** |
| DmgSections[].WasteValue | داغی | number | **no** | **user** |
---
## YARA preview / submit
| Stage | Preview | Submit |
|-------|---------|--------|
| 1 | `GET /v2/fanavaran/{client}/claim-cases/{id}/base-claim/preview` | `POST …/base-claim/submit` |
| 2 | `GET …/damage-case/preview` | `POST …/damage-case/submit` |
| 3 | `GET …/attachments/preview` | `POST …/attachments/submit` |
| 4 | `GET …/expertise/preview` | `POST …/expertise/submit` |
Nested Fanavaran URLs always use stored `claimId` (کد رایانه), never `claimNo`.

View File

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

View File

@@ -1,4 +1,7 @@
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_CERTIFICATE = "car_certificate",
@@ -34,4 +37,3 @@ export enum CarAngle {
LEFT = "left",
RIGHT = "right",
}

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

View File

@@ -262,7 +262,16 @@ import {
pickPersonBirthday,
pickPersonNationalCode,
selectFanavaranDriverId,
selectFanavaranOwnerId,
} from "./fanavaran-driver-inquiry";
import {
resolveDriverFromParty,
resolveDriverIsOwner,
resolveFanavaranLicenceIssuDate,
resolveFanavaranLicenceNo,
resolveOwnerFromParty,
resolvePolicyholderFromParty,
} from "./fanavaran-party-roles";
import {
buildFanavaranOtherPeoplePayload,
pickFanavaranRecordId,
@@ -1639,7 +1648,9 @@ export class ClaimRequestManagementService {
private getRequiredDocumentTypes(
claimRequest: any,
): ClaimRequiredDocumentType[] {
const allTypes = Object.values(ClaimRequiredDocumentType);
const allTypes = Object.values(ClaimRequiredDocumentType).filter(
(type) => type !== ClaimRequiredDocumentType.ACCIDENT_SKETCH,
);
// Check if it's CAR_BODY type
const isCarBody = claimRequest.blameFile?.type === "CAR_BODY";
@@ -3674,23 +3685,23 @@ export class ClaimRequestManagementService {
/**
* Resolve licence number to send to Fanavaran.
* Preference order:
* - use the first non-empty value from DB (driverLicense/insurerLicense)
* - else use the configured 10-digit dummy
* - driver participant / person.driverLicense (and insurerLicense as legacy)
* - else the configured 10-digit dummy
*/
private resolveFanavaranLicenceNoFromParty(input: {
driverIsInsurer?: boolean;
driverLicense?: unknown;
insurerLicense?: unknown;
/** Optional already-resolved digits from participants. */
resolvedLicenseNo?: string | null;
}): string {
const { driverIsInsurer, driverLicense, insurerLicense } = input;
const fromResolved = this.extractNonEmptyLicenceDigits(
input.resolvedLicenseNo,
);
if (fromResolved) return fromResolved;
// When driverIsInsurer === true, UI usually mirrors insurer/driver values,
// but DB can still miss one of the two fields — prefer both.
const candidates = driverIsInsurer
? [driverLicense, insurerLicense]
: [driverLicense, insurerLicense];
for (const c of candidates) {
const { driverLicense, insurerLicense } = input;
for (const c of [driverLicense, insurerLicense]) {
const digits = this.extractNonEmptyLicenceDigits(c);
if (digits) return digits;
}
@@ -4296,6 +4307,36 @@ export class ClaimRequestManagementService {
}
}
/** Best-effort OwnerId from parties inquiry (no GEN.44 create). */
private async resolveOwnerFanavaranId(
clientKey: FanavaranClientKey,
nationalCode: string | null | undefined,
birthday: string | number | null | undefined,
): Promise<number | null> {
if (!nationalCode) return null;
const parsed = this.parseJalaliBirthday(birthday);
if (!parsed) return null;
try {
const response = await this.fanavaranLookupService.inquiryByUniqueIdentifier(
clientKey,
{
nationalCode,
birthYear: parsed.year,
birthMonth: parsed.month,
birthDay: parsed.day,
},
);
return selectFanavaranOwnerId(response);
} catch (error) {
this.logger.warn(
`[resolveOwnerFanavaranId] inquiry failed for nationalCode=${nationalCode}: ${
error instanceof Error ? error.message : error
}`,
);
return null;
}
}
/** GEN.03 hull: TotalPremium + parties inquiry (AccidentCulpritId) before hull field whitelist. */
private async enrichCarBodyHullBaseClaimPayload(input: {
clientKey: FanavaranClientKey;
@@ -4355,13 +4396,14 @@ export class ClaimRequestManagementService {
}
| undefined;
const driverIdentity = resolveDriverFromParty(input.firstParty as any);
const nationalCode =
driverIdentity.nationalCode ??
pickPersonNationalCode(person) ??
pickCarBodyPolicyNationalCode(
input.blameCase as { parties?: unknown[] },
);
const birthday = pickPersonBirthday(person);
const driverIsInsurer = person?.driverIsInsurer ?? true;
const birthday = driverIdentity.birthday ?? pickPersonBirthday(person);
let accidentCulpritId: number | null = null;
if (nationalCode && birthday) {
@@ -4369,7 +4411,7 @@ export class ClaimRequestManagementService {
input.clientKey,
nationalCode,
birthday,
driverIsInsurer,
false,
);
}
if (accidentCulpritId == null) {
@@ -4384,6 +4426,13 @@ export class ClaimRequestManagementService {
input.payload.AccidentCulpritId = accidentCulpritId;
}
input.payload.CulpritLicenceNo = resolveFanavaranLicenceNo(
driverIdentity,
FANAVARAN_DUMMY_LICENCE_NO,
);
input.payload.CulpritLicenceIssuDate =
resolveFanavaranLicenceIssuDate(driverIdentity);
input.payload.CustomerFaultPercent =
FANAVARAN_DEFAULT_HULL_CUSTOMER_FAULT_PERCENT;
@@ -4617,6 +4666,9 @@ export class ClaimRequestManagementService {
const person = damagedParty?.person ?? {};
const vehicle = damagedParty?.vehicle ?? {};
const insurance = damagedParty?.insurance ?? {};
const driverIdentity = resolveDriverFromParty(damagedParty);
const ownerIdentity = resolveOwnerFromParty(damagedParty);
const driverIsOwner = resolveDriverIsOwner(damagedParty);
const product = fanavaranClaimProductFromBlameType(input.blameCase?.type);
const { mapped: inquiryMapped, raw: inquiryRawPayload } =
fanavaranPartyInquirySources(product, damagedParty);
@@ -4628,12 +4680,14 @@ export class ClaimRequestManagementService {
this.logger.log(
`[buildFanavaranDamageCasePayload] damagedParty resolved: userId=${damagedParty?.person?.userId ?? "NONE"}, ` +
`nationalCode=${pickPersonNationalCode(person) ?? "MISSING"}, ` +
`driverNationalCode=${driverIdentity.nationalCode ?? "MISSING"}, ` +
`ownerNationalCode=${ownerIdentity.nationalCode ?? "MISSING"}, ` +
`nationalCodeOfDriver=${person.nationalCodeOfDriver ?? "MISSING"}, ` +
`nationalCodeOfInsurer=${person.nationalCodeOfInsurer ?? "MISSING"}, ` +
`driverBirthday=${pickPersonBirthday(person) ?? "MISSING"}, ` +
`driverIsInsurer=${person.driverIsInsurer ?? "MISSING"}, ` +
`driverLicense=${person.driverLicense ?? "MISSING"}`,
`driverBirthday=${driverIdentity.birthday ?? "MISSING"}, ` +
`driverIsOwner=${driverIsOwner}, ` +
`driverLicense=${driverIdentity.licenseNo ?? person.driverLicense ?? "MISSING"}, ` +
`driverLicenseDate=${driverIdentity.licenseIssuDate ?? "MISSING"}`,
);
if (!damagedParty) {
this.logger.warn(
@@ -4759,12 +4813,23 @@ export class ClaimRequestManagementService {
if (vehicleKindId != null) vehicleKindSource = "car-type-lookup";
}
let driverFanavaranId = await this.resolveDriverFanavaranIdFromBlameCase(
let driverFanavaranId: number | null = null;
if (driverIdentity.nationalCode && driverIdentity.birthday) {
driverFanavaranId = await this.resolveDriverFanavaranId(
input.clientKey,
driverIdentity.nationalCode,
driverIdentity.birthday,
false,
);
}
if (driverFanavaranId == null) {
driverFanavaranId = await this.resolveDriverFanavaranIdFromBlameCase(
input.clientKey,
input.blameCase?.parties ?? [],
person,
input.claimCase,
);
}
let otherPersonId = this.firstPositiveFanavaranId(cachedDamage.otherPersonId);
if (driverFanavaranId) {
@@ -4779,7 +4844,13 @@ export class ClaimRequestManagementService {
} else if (input.registerMissingPerson) {
const registered = await this.registerFanavaranOtherPerson({
clientKey: input.clientKey,
person,
person: {
...person,
nationalCodeOfDriver:
driverIdentity.nationalCode ?? person.nationalCodeOfDriver,
driverBirthday: driverIdentity.birthday ?? person.driverBirthday,
driverIsInsurer: false,
},
claimCaseId: input.claimCase?._id
? String(input.claimCase._id)
: undefined,
@@ -4790,6 +4861,21 @@ export class ClaimRequestManagementService {
}
}
let ownerFanavaranId: number | null = null;
if (
ownerIdentity.nationalCode &&
ownerIdentity.nationalCode === driverIdentity.nationalCode &&
driverFanavaranId != null
) {
ownerFanavaranId = driverFanavaranId;
} else if (ownerIdentity.nationalCode && ownerIdentity.birthday) {
ownerFanavaranId = await this.resolveOwnerFanavaranId(
input.clientKey,
ownerIdentity.nationalCode,
ownerIdentity.birthday,
);
}
if (driverFanavaranId && input.blameCase?._id && damagedParty) {
const partyIndex = (input.blameCase.parties ?? []).findIndex(
(p: any) => p === damagedParty ||
@@ -4842,7 +4928,7 @@ export class ClaimRequestManagementService {
}
this.logger.log(
`[buildFanavaranDamageCasePayload] Final DriverId=${driverFanavaranId ?? "NULL"} for nationalCode=${pickPersonNationalCode(person) ?? "MISSING"}, ` +
`[buildFanavaranDamageCasePayload] Final DriverId=${driverFanavaranId ?? "NULL"} OwnerId=${ownerFanavaranId ?? "NULL"} for driverNationalCode=${driverIdentity.nationalCode ?? "MISSING"}, ` +
`PolicyNo=${policyNo ?? "NULL"}, PolicyCINumber=${policyCINumber ?? "NULL"}`,
);
@@ -4864,16 +4950,17 @@ export class ClaimRequestManagementService {
EstimateAmount: FANAVARAN_PROVISIONAL_ESTIMATE_AMOUNT,
FaultPercent: input.defaults.FaultPercent,
InsuranceCorpId: insuranceCorpId,
DriverIsOwner: person.driverIsInsurer ? 1 : input.defaults.DriverIsOwner,
DriverIsOwner: driverIsOwner ? 1 : 0,
LicenceCityId: null,
LicenceCountryId: null,
LicenceForeignCityName: null,
LicenceIssuDate: person.driverBirthday ?? "1394/10/13",
LicenceIssuDate: resolveFanavaranLicenceIssuDate(driverIdentity),
// Must never be empty/null; prefer stored DB value, else use dummy.
LicenceNo: this.resolveFanavaranLicenceNoFromParty({
driverIsInsurer: person.driverIsInsurer,
driverLicense: person.driverLicense,
insurerLicense: person.insurerLicense,
resolvedLicenseNo: driverIdentity.licenseNo,
}),
LicenceTypeId: input.defaults.CulpritLicenceTypeId,
MotorNo:
@@ -4882,7 +4969,7 @@ export class ClaimRequestManagementService {
"EngineNumberField",
"mtrnum",
]) ?? (typeof apiVehicle?.MotorNo === "string" ? apiVehicle.MotorNo : null),
OwnerId: null,
OwnerId: ownerFanavaranId,
PlaqueCityId: apiPlaque.cityId,
PlaqueKindId: plaqueKindId,
PlaqueLeftNo:
@@ -5817,6 +5904,8 @@ export class ClaimRequestManagementService {
parties: Array<{
role?: PartyRole;
person?: { userId?: Types.ObjectId; nationalCodeOfInsurer?: string };
participants?: Array<Record<string, any>>;
participantRoles?: Record<string, string | undefined>;
statement?: { admitsGuilt?: boolean };
}>,
guiltyPartyId?: Types.ObjectId | string | null,
@@ -5828,8 +5917,13 @@ export class ClaimRequestManagementService {
const guiltyParty = parties.find(
(party) => party.person?.userId?.toString() === guiltyPartyId.toString(),
);
if (!guiltyParty) return null;
return guiltyParty?.person?.nationalCodeOfInsurer ?? null;
return (
resolvePolicyholderFromParty(guiltyParty, "third-party").nationalCode ??
guiltyParty.person?.nationalCodeOfInsurer ??
null
);
}
private resolveGuiltyPartyIdV2(
@@ -6108,6 +6202,31 @@ export class ClaimRequestManagementService {
},
});
// GEN.03 CulpritLicence*: guilty-party DRIVER for THIRD_PARTY, FIRST-party DRIVER for CAR_BODY.
{
const culpritParty =
blameCase.type === BlameRequestType.CAR_BODY
? firstParty
: (() => {
const guiltyPartyId = this.resolveGuiltyPartyIdV2(
blameCase.parties ?? [],
expertDecision?.guiltyPartyId,
);
return (blameCase.parties ?? []).find(
(party: any) =>
String(party?.person?.userId ?? "") ===
String(guiltyPartyId ?? ""),
);
})();
const culpritDriver = resolveDriverFromParty(culpritParty as any);
payload.CulpritLicenceNo = resolveFanavaranLicenceNo(
culpritDriver,
FANAVARAN_DUMMY_LICENCE_NO,
);
payload.CulpritLicenceIssuDate =
resolveFanavaranLicenceIssuDate(culpritDriver);
}
const carBodyAccidentTime = (
firstParty as { statement?: { accidentTime?: string } } | undefined
)?.statement?.accidentTime;
@@ -10332,7 +10451,8 @@ export class ClaimRequestManagementService {
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
? await this.blameRequestDbService.findById(
claimCase.blameRequestId.toString(),
@@ -10340,53 +10460,68 @@ export class ClaimRequestManagementService {
: null;
const isCarBody = blameRequest?.type === BlameRequestType.CAR_BODY;
const requiredDocsDefinition = [
{
key: ClaimRequiredDocumentType.ACCIDENT_SKETCH,
label_fa: "کروکی",
label_en: "Accident Sketch",
category: "general",
required: false,
},
{
key: "damaged_driving_license_front",
label_fa: "گواهینامه طرف آسیب‌دیده - جلو",
label_en: "Damaged Party License - Front",
category: "damaged_party",
required: true,
},
{
key: "damaged_driving_license_back",
label_fa: "گواهینامه طرف آسیب‌دیده - پشت",
label_en: "Damaged Party License - Back",
category: "damaged_party",
required: true,
},
{
key: "damaged_chassis_number",
label_fa: "شماره شاسی",
label_en: "Chassis Number",
category: "damaged_party",
required: true,
},
{
key: "damaged_engine_photo",
label_fa: "عکس موتور",
label_en: "Engine Photo",
category: "damaged_party",
required: true,
},
{
key: "damaged_car_card_front",
label_fa: "کارت خودرو آسیب‌دیده - جلو",
label_en: "Damaged Car Card - Front",
category: "damaged_party",
required: true,
},
{
key: "damaged_car_card_back",
label_fa: "کارت خودرو آسیب‌دیده - پشت",
label_en: "Damaged Car Card - Back",
category: "damaged_party",
required: true,
},
{
key: "damaged_metal_plate",
label_fa: "پلاک فلزی آسیب‌دیده",
label_en: "Damaged Metal Plate",
category: "damaged_party",
required: true,
},
{
key: "car_green_card",
label_fa: "کارت سبز خودرو",
label_en: "Car Green Card",
category: "damaged_party",
required: true,
},
...(isCarBody
? []
@@ -10396,30 +10531,35 @@ export class ClaimRequestManagementService {
label_fa: "گواهینامه طرف مقصر - جلو",
label_en: "Guilty Party License - Front",
category: "guilty_party",
required: true,
},
{
key: "guilty_driving_license_back",
label_fa: "گواهینامه طرف مقصر - پشت",
label_en: "Guilty Party License - Back",
category: "guilty_party",
required: true,
},
{
key: "guilty_car_card_front",
label_fa: "کارت خودرو مقصر - جلو",
label_en: "Guilty Car Card - Front",
category: "guilty_party",
required: true,
},
{
key: "guilty_car_card_back",
label_fa: "کارت خودرو مقصر - پشت",
label_en: "Guilty Car Card - Back",
category: "guilty_party",
required: true,
},
{
key: "guilty_metal_plate",
label_fa: "پلاک فلزی مقصر",
label_en: "Guilty Metal Plate",
category: "guilty_party",
required: true,
},
]),
];
@@ -10431,6 +10571,7 @@ export class ClaimRequestManagementService {
label_fa: doc.label_fa,
label_en: doc.label_en,
category: doc.category,
required: doc.required,
uploaded: docData?.uploaded || false,
preferUploadDuringCapture: isCapturePhaseDamagedPartyDocKey(doc.key),
};
@@ -10493,7 +10634,10 @@ export class ClaimRequestManagementService {
});
// Calculate progress
const documentsUploaded = requiredDocuments.filter(
const requiredDocumentItems = requiredDocuments.filter(
(d) => d.required,
);
const documentsUploaded = requiredDocumentItems.filter(
(d) => d.uploaded,
).length;
const anglesCaptured = carAngles.filter((a) => a.captured).length;
@@ -10511,7 +10655,7 @@ export class ClaimRequestManagementService {
captureProgress.anglesTotal - captureProgress.anglesCaptured,
);
const postCaptureDocsRemaining = requiredDocuments.filter(
(d) => !d.preferUploadDuringCapture && !d.uploaded,
(d) => d.required && !d.preferUploadDuringCapture && !d.uploaded,
).length;
const totalRemaining =
@@ -10531,7 +10675,7 @@ export class ClaimRequestManagementService {
totalRemaining,
progress: {
documentsUploaded,
documentsTotal: requiredDocuments.length,
documentsTotal: requiredDocumentItems.length,
anglesCaptured,
anglesTotal: carAngles.length,
partsCaptured,
@@ -10694,6 +10838,8 @@ export class ClaimRequestManagementService {
const step = claimCase.workflow?.currentStep;
const isResendUpload = step === ClaimWorkflowStep.USER_EXPERT_RESEND;
const isOptionalDocument =
body.documentKey === ClaimRequiredDocumentType.ACCIDENT_SKETCH;
const isCapturePhaseDocUpload =
step === ClaimWorkflowStep.CAPTURE_PART_DAMAGES &&
(isCapturePhaseDamagedPartyDocKey(body.documentKey) ||
@@ -10724,7 +10870,9 @@ export class ClaimRequestManagementService {
}
const allowedInitialUploadStep =
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS ||
isCapturePhaseDocUpload;
isCapturePhaseDocUpload ||
(isOptionalDocument &&
step === ClaimWorkflowStep.USER_SUBMISSION_COMPLETE);
if (!allowedInitialUploadStep) {
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}`,
@@ -10843,7 +10991,7 @@ export class ClaimRequestManagementService {
let remaining = 0;
let captureStepCompletedOnThisUpload = false;
if (!isResendUpload) {
if (!isResendUpload && !isOptionalDocument) {
if (isCapturePhaseDocUpload) {
const afterThis = (k: string) =>
k === body.documentKey ||
@@ -10981,6 +11129,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
const updatePayload: Record<string, unknown> = { $set };
@@ -11085,7 +11255,12 @@ export class ClaimRequestManagementService {
}
// 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(
(err) => this.logger.warn(`Fanavaran attachments auto-submit failed: ${err?.message}`),
);
@@ -11125,7 +11300,9 @@ export class ClaimRequestManagementService {
};
}
const message = isCapturePhaseDocUpload
const message = isOptionalDocument
? "Optional accident sketch uploaded successfully."
: isCapturePhaseDocUpload
? captureStepCompletedOnThisUpload
? options?.v3InPersonFlow
? "Capture-phase documents and all damage captures are complete. Upload the walk-around video (car-capture) to finalise."
@@ -11148,7 +11325,9 @@ export class ClaimRequestManagementService {
documentKey: body.documentKey,
fileUrl,
allDocumentsUploaded,
currentStep: isCapturePhaseDocUpload
currentStep: isOptionalDocument
? step || ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
: isCapturePhaseDocUpload
? captureStepCompletedOnThisUpload
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
: ClaimWorkflowStep.CAPTURE_PART_DAMAGES
@@ -12905,7 +13084,10 @@ export class ClaimRequestManagementService {
const preCaptureDocs = base.requiredDocuments.filter(
(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 {
...base,
requiredDocuments: preCaptureDocs,
@@ -12917,8 +13099,9 @@ export class ClaimRequestManagementService {
totalRemaining: remaining,
progress: {
...base.progress,
documentsTotal: preCaptureDocs.length,
documentsUploaded: preCaptureDocs.filter((d) => d.uploaded).length,
documentsTotal: requiredPreCaptureDocs.length,
documentsUploaded: requiredPreCaptureDocs.filter((d) => d.uploaded)
.length,
partsCaptured: 0,
partsTotal: 0,
anglesCaptured: 0,
@@ -12973,6 +13156,7 @@ export class ClaimRequestManagementService {
label_fa: "تصویر نقطه آسیب‌دیده مقصر",
label_en: "Guilty Car Damaged Area",
category: "guilty_party",
required: true,
uploaded: guiltyAreaUploaded,
preferUploadDuringCapture: true,
};

View File

@@ -801,6 +801,7 @@ Returns status of each item (uploaded/captured or not).
**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):
- accident_sketch (optional; never blocks workflow completion)
- damaged_driving_license_front/back
- damaged_chassis_number, damaged_engine_photo
- 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_back",
"guilty_metal_plate",
"accident_sketch",
],
example: "damaged_driving_license_front",
},

View File

@@ -42,6 +42,13 @@ export class RequiredDocumentItem {
example: true,
})
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;
}
/**

View File

@@ -416,6 +416,7 @@ Returns status of each item (uploaded/captured or not).
"guilty_car_card_front",
"guilty_car_card_back",
"guilty_metal_plate",
"accident_sketch",
],
example: "damaged_driving_license_front",
},

View File

@@ -107,6 +107,29 @@ describe("fanavaran claim product", () => {
expect(nationalCode).toBe("0012345678");
});
it("prefers CAR_BODY_POLICYHOLDER participant over person.nationalCodeOfInsurer", () => {
const nationalCode = pickCarBodyPolicyNationalCode({
parties: [
{
role: PartyRole.FIRST,
person: { nationalCodeOfInsurer: "0012345678" },
participants: [
{
participantId: "CAR_BODY_POLICYHOLDER",
nationalCode: "0499370899",
birthday: "1368/05/20",
},
],
participantRoles: {
carBodyPolicyholder: "CAR_BODY_POLICYHOLDER",
},
},
],
});
expect(nationalCode).toBe("0499370899");
});
it("flattens car-body inquiry fields onto the damage-case aliases", () => {
const { mapped } = fanavaranPartyInquirySources("car-body", {
insurance: {

View File

@@ -7,6 +7,7 @@ import {
parseFanavaranId,
type FanavaranCarPolicyProduct,
} from "src/lookups/fanavaran-last-car-policy";
import { resolvePolicyholderFromParty } from "./fanavaran-party-roles";
export type FanavaranClaimProduct = FanavaranCarPolicyProduct;
@@ -101,6 +102,8 @@ export function fanavaranInsuranceLineIdForProduct(
type BlamePartyForCarBodyPolicy = {
role?: string;
person?: { nationalCodeOfInsurer?: unknown };
participants?: Array<Record<string, any>>;
participantRoles?: Record<string, string | undefined>;
insurance?: {
carBodyInsurance?: {
policyId?: unknown;
@@ -168,6 +171,7 @@ export function pickCarBodyPolicyNationalCode(
): string | null {
const party = firstCarBodyParty(blame);
return (
resolvePolicyholderFromParty(party, "car-body").nationalCode ??
nonEmptyText(party?.person?.nationalCodeOfInsurer) ??
nonEmptyText(party?.insurance?.carBodyInsurance?.ownerNationalCode) ??
nonEmptyText(party?.insurance?.carBodyInsurance?.insurerNationalCode)

View File

@@ -34,6 +34,21 @@ export function asPartyInquiryRows(value: unknown): FanavaranPartyInquiryRow[] {
return [];
}
function selectFanavaranIdByRoles(
rows: unknown,
preferredRoles: number[],
): number | null {
const parties = asPartyInquiryRows(rows);
if (parties.length === 0) return null;
for (const roleId of preferredRoles) {
const match = parties.find((row) => asPositiveId(row.RoleId) === roleId);
if (match) return asPositiveId(match.Id);
}
return asPositiveId(parties[0].Id);
}
/**
* DriverId for GEN.12 زیان‌دیده. Prefer insurer (161) or driver (166)
* based on driverIsInsurer, then any returned party id.
@@ -42,19 +57,27 @@ export function selectFanavaranDriverId(
rows: unknown,
driverIsInsurer: boolean | undefined,
): number | null {
const parties = asPartyInquiryRows(rows);
if (parties.length === 0) return null;
const preferredRoles = driverIsInsurer
? [FANAVARAN_INSURER_ROLE_ID, FANAVARAN_DRIVER_ROLE_ID, FANAVARAN_PLAQUE_OWNER_ROLE_ID]
: [FANAVARAN_DRIVER_ROLE_ID, FANAVARAN_INSURER_ROLE_ID, FANAVARAN_PLAQUE_OWNER_ROLE_ID];
? [
FANAVARAN_INSURER_ROLE_ID,
FANAVARAN_DRIVER_ROLE_ID,
FANAVARAN_PLAQUE_OWNER_ROLE_ID,
]
: [
FANAVARAN_DRIVER_ROLE_ID,
FANAVARAN_INSURER_ROLE_ID,
FANAVARAN_PLAQUE_OWNER_ROLE_ID,
];
return selectFanavaranIdByRoles(rows, preferredRoles);
}
for (const roleId of preferredRoles) {
const match = parties.find((row) => asPositiveId(row.RoleId) === roleId);
if (match) return asPositiveId(match.Id);
}
return asPositiveId(parties[0].Id);
/** OwnerId: prefer plaque-owner role (163), then insurer / driver. */
export function selectFanavaranOwnerId(rows: unknown): number | null {
return selectFanavaranIdByRoles(rows, [
FANAVARAN_PLAQUE_OWNER_ROLE_ID,
FANAVARAN_INSURER_ROLE_ID,
FANAVARAN_DRIVER_ROLE_ID,
]);
}
export function normalizeNationalCode(value: unknown): string | null {

View File

@@ -0,0 +1,125 @@
import { InquiryParticipantRole } from "src/common/dto/inquiry-participants.dto";
import {
FANAVARAN_DUMMY_LICENCE_ISSU_DATE,
formatFanavaranLicenceIssuDate,
resolveDriverFromParty,
resolveDriverIsOwner,
resolveFanavaranLicenceIssuDate,
resolveFanavaranLicenceNo,
resolveOwnerFromParty,
resolvePolicyholderFromParty,
} from "./fanavaran-party-roles";
describe("fanavaran-party-roles", () => {
const partyWithDistinctRoles = {
participants: [
{
participantId: "DRIVER",
nationalCode: "0012345678",
birthday: "1370/01/15",
licenseNumber: "1234567890",
licenseType: "1",
driverLicenseDate: "1398/06/01",
},
{
participantId: "VEHICLE_OWNER",
nationalCode: "0499370899",
birthday: "1368/05/20",
},
{
participantId: "THIRD_PARTY_POLICYHOLDER",
nationalCode: "0080086519",
birthday: "1364/06/28",
},
],
participantRoles: {
driver: "DRIVER",
vehicleOwner: "VEHICLE_OWNER",
thirdPartyPolicyholder: "THIRD_PARTY_POLICYHOLDER",
},
person: {
nationalCodeOfDriver: "0012345678",
nationalCodeOfInsurer: "0080086519",
driverLicense: "1234567890",
driverIsInsurer: false,
},
};
it("resolves driver licence and driverLicenseDate from participants", () => {
const driver = resolveDriverFromParty(partyWithDistinctRoles);
expect(driver.nationalCode).toBe("0012345678");
expect(driver.licenseNo).toBe("1234567890");
expect(driver.licenseIssuDate).toBe("1398/06/01");
expect(resolveFanavaranLicenceNo(driver, "9705463515")).toBe("1234567890");
expect(resolveFanavaranLicenceIssuDate(driver)).toBe("1398/06/01");
});
it("falls back to dummy licence issue date when driverLicenseDate is missing", () => {
const driver = resolveDriverFromParty({
participants: [
{
participantId: InquiryParticipantRole.DRIVER,
nationalCode: "0012345678",
birthday: "1370/01/15",
licenseNumber: "999",
},
],
participantRoles: { driver: InquiryParticipantRole.DRIVER },
});
expect(driver.licenseIssuDate).toBeNull();
expect(resolveFanavaranLicenceIssuDate(driver)).toBe(
FANAVARAN_DUMMY_LICENCE_ISSU_DATE,
);
});
it("marks DriverIsOwner false when driver and owner differ", () => {
expect(resolveDriverIsOwner(partyWithDistinctRoles)).toBe(false);
});
it("marks DriverIsOwner true when driver and owner share a national code", () => {
expect(
resolveDriverIsOwner({
participants: [
{
participantId: "SAME",
nationalCode: "0012345678",
birthday: "1370/01/15",
},
],
participantRoles: {
driver: "SAME",
vehicleOwner: "SAME",
thirdPartyPolicyholder: "SAME",
},
}),
).toBe(true);
});
it("resolves owner and policyholder independently of the driver", () => {
expect(resolveOwnerFromParty(partyWithDistinctRoles).nationalCode).toBe(
"0499370899",
);
expect(
resolvePolicyholderFromParty(partyWithDistinctRoles, "third-party")
.nationalCode,
).toBe("0080086519");
});
it("formats compact Jalali driverLicenseDate", () => {
expect(formatFanavaranLicenceIssuDate(13980601)).toBe("1398/06/01");
});
it("falls back to legacy person.driverLicense when participants are absent", () => {
const driver = resolveDriverFromParty({
person: {
nationalCodeOfDriver: "0011111111",
driverBirthday: "13700101",
driverLicense: "5555555555",
driverLicenseDate: "1390/01/01",
},
});
expect(driver.nationalCode).toBe("0011111111");
expect(driver.licenseNo).toBe("5555555555");
expect(driver.licenseIssuDate).toBe("1390/01/01");
});
});

View File

@@ -0,0 +1,217 @@
import { InquiryParticipantRole } from "src/common/dto/inquiry-participants.dto";
import { participantForStoredPartyRole } from "src/request-management/inquiry-participant-resolver";
import {
normalizeNationalCode,
parseJalaliDateParts,
} from "./fanavaran-driver-inquiry";
import type { FanavaranClaimProduct } from "./fanavaran-claim-product";
/** Fanavaran dummy licence issue date when the user did not supply one. */
export const FANAVARAN_DUMMY_LICENCE_ISSU_DATE = "1394/10/13";
export type FanavaranRoleIdentity = {
nationalCode: string | null;
birthday: string | number | null;
/** Digits-only licence number when known. */
licenseNo: string | null;
licenseType: string | null;
/**
* Jalali `YYYY/MM/DD` from `driverLicenseDate` when present.
* Not a persisted model field — read opportunistically from party/person/participant.
*/
licenseIssuDate: string | null;
};
type PartyLike = {
participants?: Array<Record<string, any>>;
participantRoles?: Record<string, string | undefined>;
person?: Record<string, any> | null;
};
function nonEmptyText(value: unknown): string | null {
if (value == null) return null;
const text = String(value).trim();
if (!text || text.toLowerCase() === "null" || text.toLowerCase() === "undefined") {
return null;
}
return text;
}
/** Digits-only licence number, or null when empty / non-digit. */
export function extractLicenceDigits(value: unknown): string | null {
const text = nonEmptyText(value);
if (!text) return null;
const digits = text.replace(/[^\d]/g, "");
return digits || null;
}
/**
* Normalize a stored `driverLicenseDate` (or similar) to Fanavaran Jalali `YYYY/MM/DD`.
* Returns null when missing / unparseable so callers can fall back to the dummy.
*/
export function formatFanavaranLicenceIssuDate(value: unknown): string | null {
const parsed = parseJalaliDateParts(value);
if (!parsed) return null;
const month = String(parsed.month).padStart(2, "0");
const day = String(parsed.day).padStart(2, "0");
return `${parsed.year}/${month}/${day}`;
}
function readDriverLicenseDate(
...sources: Array<Record<string, any> | null | undefined>
): string | null {
for (const source of sources) {
if (!source) continue;
const formatted = formatFanavaranLicenceIssuDate(
source.driverLicenseDate ?? source.licenseDate ?? source.licenceIssuDate,
);
if (formatted) return formatted;
}
return null;
}
function fromParticipant(
participant: Record<string, any> | undefined,
): FanavaranRoleIdentity {
if (!participant) {
return {
nationalCode: null,
birthday: null,
licenseNo: null,
licenseType: null,
licenseIssuDate: null,
};
}
return {
nationalCode: normalizeNationalCode(participant.nationalCode),
birthday: nonEmptyText(participant.birthday) ?? participant.birthday ?? null,
licenseNo: extractLicenceDigits(participant.licenseNumber),
licenseType: nonEmptyText(participant.licenseType),
licenseIssuDate: readDriverLicenseDate(participant),
};
}
/**
* Damaged/guilty party DRIVER: participants first, then legacy person.* fields.
* Licence number prefers real user values; callers fall back to the Fanavaran dummy.
*/
export function resolveDriverFromParty(party: PartyLike | null | undefined): FanavaranRoleIdentity {
const fromRole = fromParticipant(
participantForStoredPartyRole(
party ?? {},
InquiryParticipantRole.DRIVER,
) as Record<string, any> | undefined,
);
const person = party?.person ?? undefined;
return {
nationalCode:
fromRole.nationalCode ??
normalizeNationalCode(person?.nationalCodeOfDriver) ??
normalizeNationalCode(person?.nationalCode),
birthday:
(fromRole.birthday != null && String(fromRole.birthday).trim() !== ""
? fromRole.birthday
: null) ??
nonEmptyText(person?.driverBirthday) ??
nonEmptyText(person?.birthday) ??
person?.driverBirthday ??
person?.birthday ??
null,
licenseNo:
fromRole.licenseNo ??
extractLicenceDigits(person?.driverLicense) ??
extractLicenceDigits(person?.insurerLicense),
licenseType: fromRole.licenseType ?? nonEmptyText(person?.licenseType),
licenseIssuDate:
fromRole.licenseIssuDate ?? readDriverLicenseDate(person),
};
}
/** Vehicle owner for OwnerId / DriverIsOwner / Sheba national-code routing. */
export function resolveOwnerFromParty(party: PartyLike | null | undefined): FanavaranRoleIdentity {
const fromRole = fromParticipant(
participantForStoredPartyRole(
party ?? {},
InquiryParticipantRole.VEHICLE_OWNER,
) as Record<string, any> | undefined,
);
if (fromRole.nationalCode || fromRole.birthday) {
return fromRole;
}
// Legacy parties had no dedicated owner fields.
return {
nationalCode: null,
birthday: null,
licenseNo: null,
licenseType: null,
licenseIssuDate: null,
};
}
/**
* Policyholder national code for GEN.03 PolicyId inquiry:
* THIRD_PARTY → third-party policyholder; CAR_BODY → car-body policyholder.
*/
export function resolvePolicyholderFromParty(
party: PartyLike | null | undefined,
product: FanavaranClaimProduct,
): FanavaranRoleIdentity {
const role =
product === "car-body"
? InquiryParticipantRole.CAR_BODY_POLICYHOLDER
: InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER;
const fromRole = fromParticipant(
participantForStoredPartyRole(party ?? {}, role) as
| Record<string, any>
| undefined,
);
const person = party?.person ?? undefined;
return {
nationalCode:
fromRole.nationalCode ??
normalizeNationalCode(person?.nationalCodeOfInsurer) ??
normalizeNationalCode(person?.nationalCode),
birthday:
(fromRole.birthday != null && String(fromRole.birthday).trim() !== ""
? fromRole.birthday
: null) ??
nonEmptyText(person?.insurerBirthday) ??
person?.insurerBirthday ??
null,
licenseNo:
fromRole.licenseNo ?? extractLicenceDigits(person?.insurerLicense),
licenseType: fromRole.licenseType ?? null,
licenseIssuDate: fromRole.licenseIssuDate,
};
}
/**
* GEN.12 `DriverIsOwner`: driver national code equals vehicle-owner national code.
* Falls back to legacy `person.driverIsInsurer` when owner role is absent.
*/
export function resolveDriverIsOwner(party: PartyLike | null | undefined): boolean {
const driver = resolveDriverFromParty(party);
const owner = resolveOwnerFromParty(party);
if (driver.nationalCode && owner.nationalCode) {
return driver.nationalCode === owner.nationalCode;
}
return party?.person?.driverIsInsurer === true;
}
/** Licence number for Fanavaran; never empty — uses `dummy` when user value is missing. */
export function resolveFanavaranLicenceNo(
identity: Pick<FanavaranRoleIdentity, "licenseNo">,
dummy: string,
): string {
return identity.licenseNo || dummy;
}
/** Licence issue date for Fanavaran; uses dummy when `driverLicenseDate` is absent. */
export function resolveFanavaranLicenceIssuDate(
identity: Pick<FanavaranRoleIdentity, "licenseIssuDate">,
dummy: string = FANAVARAN_DUMMY_LICENCE_ISSU_DATE,
): string {
return identity.licenseIssuDate || dummy;
}

View File

@@ -373,6 +373,7 @@ Returns status of each item (uploaded/captured or not).
"guilty_car_card_front",
"guilty_car_card_back",
"guilty_metal_plate",
"accident_sketch",
],
example: "damaged_driving_license_front",
},

View File

@@ -172,8 +172,41 @@ export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [
url: `${FANAVARAN_LOOKUP_BASE_URL}/common/code-list/cii-validation-status`,
cacheFile: "cii-validation-status.json",
},
{
name: "vehicle-groups",
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/vehicle-groups`,
cacheFile: "vehicle-groups.json",
},
];
/** Safe Fanavaran lookup slug: lowercase segments separated by hyphens. */
const FANAVARAN_LOOKUP_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
/**
* Resolve a remote lookup definition.
* Known names use the catalogue; unknown slugs fall back to
* `car/base-info/{lookupName}` so `/lookups/fanavaran/{name}` works without
* a dedicated route or config entry (e.g. vehicle-groups).
*/
export function resolveFanavaranRemoteLookup(
name: string,
): FanavaranRemoteLookupDefinition | null {
const configured = FANAVARAN_REMOTE_LOOKUPS.find((item) => item.name === name);
if (configured) {
return configured;
}
if (!FANAVARAN_LOOKUP_NAME_PATTERN.test(name)) {
return null;
}
return {
name,
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/${name}`,
cacheFile: `${name}.json`,
};
}
export const TEJARAT_STATIC_ACCIDENT_FILES = {
accidentReason: "ACCIDENT_REASON.json",
accidentWay: "ACCIDENT_WAY.json",

View File

@@ -2,6 +2,7 @@ import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
import { buildCoefficientsFromPartSeverities } from "./claim-price-drop";
import {
catalogPartIdFromSelectedPart,
normalizeDamageSelectedParts,
resolveCatalogPartByPartId,
resolvePartForExpertReply,
resolveSelectedPartByPartId,
@@ -86,6 +87,31 @@ describe("resolveSelectedPartByPartId", () => {
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", () => {
const fixed = sanitizeDamageSelectedPartV2(
{

View File

@@ -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)) {
return {
id: null,

View File

@@ -582,7 +582,8 @@ export class LookupsController {
@ApiOperation({
summary: "List configured Fanavaran remote lookups",
description:
"Returns the lookup names available through /lookups/fanavaran/{lookupName}.",
"Returns catalogue entries for /lookups/fanavaran/{lookupName}. " +
"Unlisted car/base-info slugs still work via the same route (fallback).",
})
async listFanavaranRemoteLookups() {
return this.lookupsService.listFanavaranRemoteLookups().map((lookup) => ({
@@ -596,12 +597,14 @@ export class LookupsController {
@ApiOperation({
summary: "Fetch a Fanavaran remote lookup by name",
description:
"Generic cached Fanavaran lookup fetcher for configured lookup names, including GEN.08 expertise lookups.",
"Generic cached Fanavaran lookup fetcher. Configured names use their catalogue URL; " +
"unlisted slugs (e.g. vehicle-groups) fall back to car/base-info/{lookupName}.",
})
@ApiParam({
name: "lookupName",
description:
"Configured Fanavaran lookup name, for example inspection-place, drop-amount-status, car-components, accident-level, expert-status",
"Fanavaran lookup slug, for example vehicle-groups, inspection-place, car-components. " +
"Unconfigured names are fetched from car/base-info/{lookupName}.",
})
@ApiOkResponse({
description: "Returns cached or live Fanavaran lookup data",

View File

@@ -10,6 +10,7 @@ import {
} from "src/core/config/fanavaran-client.config";
import {
FANAVARAN_REMOTE_LOOKUPS,
resolveFanavaranRemoteLookup,
type FanavaranRemoteLookupDefinition,
TEJARAT_STATIC_ACCIDENT_FILES,
} from "src/fanavaran/fanavaran-lookup.config";
@@ -66,7 +67,7 @@ export class LookupsService {
}
private findRemoteLookup(name: string) {
const lookup = FANAVARAN_REMOTE_LOOKUPS.find((item) => item.name === name);
const lookup = resolveFanavaranRemoteLookup(name);
if (!lookup) {
throw new NotFoundException(`Unknown Fanavaran remote lookup: ${name}`);
}