Compare commits

..

7 Commits

Author SHA1 Message Date
26da533a52 Merge pull request 'main' (#256) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#256
2026-08-08 15:59:50 +03:30
a03d557718 Update the ReadMe.md file, Documents of the apps should be LINKED here.
Suiggestion : 
for example we currently have [Fanavaran integration](docs/fanavaran/README.md) linked to the main files, I suggest for the flows and etc we should LINK the documented file.
2026-08-08 15:59:17 +03:30
5a4a511e84 fanavaran sales integration docs 2026-08-08 15:53:35 +03:30
91028f999c Merge pull request 'sms logging in mongo part was added' (#255) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#255
2026-08-08 12:15:33 +03:30
07d6cdbe7c sms logging in mongo part was added 2026-08-08 12:14:31 +03:30
e6038c73c5 Merge pull request 'licence number dummy data fills and never empty' (#254) from s.hajizadeh/yara724api:main into main
Reviewed-on: Yara724/api#254
2026-08-05 14:30:26 +03:30
d4487776fb licence number dummy data fills and never empty 2026-08-05 14:26:13 +03:30
27 changed files with 2051 additions and 23 deletions

View File

@@ -1,15 +1,3 @@
# api
## Documentation
Current JWT payload:
```json
{
"username": "saman_insurer@gmail.com",
"sub": "6a144979799f3c63aa63f67c",
"fullName": "بیمه گر سامان",
"role": "company",
"userType": "UserType",
"clientKey": "67f0fd0e53868dc1ff8a2738",
"iat": 1781339791,
"exp": 1781343391
}
```
- [Fanavaran integration](docs/fanavaran/README.md)

View File

@@ -0,0 +1,27 @@
---
last_updated: 2026-08-08
tags: [fanavaran, documentation]
source: fanavaran-module-docs
---
# Fanavaran documentation brief (moved)
The implementation reference for Fanavaran (Parsian template, multi-tenant) now lives under:
**[docs/fanavaran/README.md](./fanavaran/README.md)**
That set covers the original goals:
1. Write APIs (auth + GEN.03/07/08/12)
2. Read/lookup APIs
3. Constants
4. Tenant vs shared matrix
5. Full claim flow + diagrams
6. YARA integration points
7. Data mapping
8. System dependencies
9. Error handling
10. Testing
11. New-client onboarding checklist
Related: [external-api-curls.md](./external-api-curls.md), `.agents/skills/fanavaran-apis/references/third-party-cases.md`.

View File

@@ -0,0 +1,345 @@
---
last_updated: 2026-08-08
tags: [fanavaran, api, write, gen03, gen07, gen08, gen12]
source: fanavaran-module-docs
---
# 01 — Write APIs (Claim Registration)
Base host:
```text
https://apimanager.iraneit.com/BimeApiManager/api
BimeApi v2: .../api/BimeApi/v2.0
```
Common business headers (after Login):
| Header | Source |
|--------|--------|
| `authenticationToken` | Login response |
| `CorpId` | Tenant auth |
| `ContractId` | Tenant auth |
| `Location` | Tenant auth |
| `Content-Type` | `application/json` (except GEN.07 multipart) |
Template values below are **Parsian-proven** unless noted.
---
## 1. GetAppToken
| Item | Value |
|------|-------|
| نام عملیات | دریافت App Token |
| Endpoint | `POST /api/EITAuthentication/GetAppToken` |
| Method | `POST` |
| Body | Empty (`Content-Length: 0`; no JSON) |
| Authentication | Headers `appname`, `secret` (tenant) |
**Headers**
| Header | Required | Notes |
|--------|----------|-------|
| `appname` | ✅ | Tenant `auth.appName` |
| `secret` | ✅ | Tenant `auth.secret` |
**Success:** token in response header `appToken` / `apptoken`.
**Error:** invalid app credentials → Fanavaran error body/message.
**YARA:** `FanavaranAuthService` → `getAppTokenUrl`.
---
## 2. Login
| Item | Value |
|------|-------|
| نام عملیات | ورود و دریافت authenticationToken |
| Endpoint | `POST /api/EITAuthentication/Login` |
| Method | `POST` |
| Body | Empty |
| Authentication | `appToken` + `userName` + `password` |
**Headers**
| Header | Required |
|--------|----------|
| `appToken` | ✅ fresh from GetAppToken |
| `userName` | ✅ |
| `password` | ✅ |
**Success:** `authenticationToken` in header or body.
**Cache (YARA):** until next **Asia/Tehran midnight** — memory + Mongo `fanavaranAuthTokens`.
**Error example:** `نام کاربر یا رمز عبور صحیح نیست` (wrong password or stale/wrong appToken).
**YARA:** `FanavaranAuthService.getAuthenticationToken(clientKey)`.
---
## 3. GEN.03 — Base claim create
Doc: `CAR.THID.APIH.GEN.03`
| Item | Value |
|------|-------|
| نام عملیات | ایجاد پرونده خسارت مالی ثالث |
| Endpoint | `POST /Api/BimeApi/v2.0/car/third-party-car-financial-claims` |
| Method | `POST` |
| Auth | Business headers |
Also available: `GET .../{claimid}`, `GET ...?{ODATA}`.
### Key request fields
| Field | Parsian template | Notes |
|-------|------------------|-------|
| `PolicyId` | from inquiry | **Required** for submit |
| `ClaimExpertId` | `154` | GEN.03 role = مسئول پرونده مالی (≠ GEN.08) |
| `AccidentCityId` | `701` | Shared default |
| `AccidentReportTypeId` | `155` | Shared |
| `AccidentVehicleUsedId` | `1` | Shared |
| `CompensationReferenceId` | `167` | Shared |
| `CulpritLicenceTypeId` | `2` | Shared |
| `CulpritTypeId` | `337` | Shared |
| `AccidentCauseId` | `6` | Shared code default |
| `AccidentDate` / `AnnouncementDate` / `DocReceivedDate` | Jalali from blame/claim time | |
| `AccidentTime` | `HH:mm` | |
| `AccidentLocationAddress` | `استان تهران شهر تهران` | Provisional constant |
| `EstimateAmount` | ≥ 1; provisional `1000` | Must be positive |
| `CulpritLicenceNo` | real or dummy | Never empty |
| `CulpritLicenceIssuDate` | party / default | |
| Many others | `null` | **Keep explicit nulls** — do not omit |
Full shape: see skill reference sample and `buildFanavaranSubmitPayload` / `applyFanavaranDefaultFields`.
### Validation (YARA)
- Only `THIRD_PARTY` claims (not `CAR_BODY`)
- `PolicyId` required on submit (`requirePolicyId: true`)
- Skip if local `claimId` / `claimNo` already set
- `EstimateAmount` normalized to positive
### Success response
| Field | Local store |
|-------|-------------|
| `Id` | `claimCases.claimId` |
| `ClaimNo` | `claimCases.claimNo` |
History: `FANAVARAN_EARLY_AUTO_SUBMIT_SUCCEEDED`.
SMS owner via `SmsOrchestrationService` (deduped `fanavaranSync.baseClaim.smsNotifiedAt`).
### Error handling
- Never throw out of normal user claim flow on auto-submit
- Persist `fanavaranSync.baseClaim.status=failed`, `lastError`, schedule retry
- Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/base-claim/submit`
### Sample (Parsian-shaped)
```json
{
"AccidentCityId": 701,
"AccidentReportTypeId": 155,
"AccidentVehicleUsedId": 1,
"ClaimExpertId": 154,
"CompensationReferenceId": 167,
"CulpritLicenceTypeId": 2,
"CulpritTypeId": 337,
"AccidentCauseId": 6,
"AccidentDate": "1405/04/05",
"AnnouncementDate": "1405/04/05",
"DocReceivedDate": "1405/04/05",
"AccidentTime": "08:03",
"AccidentLocationAddress": "استان تهران شهر تهران",
"EstimateAmount": 1000,
"PolicyId": 13764408,
"CulpritLicenceNo": "1124242",
"CulpritLicenceIssuDate": "1394/10/13",
"DamagedCount": 1,
"IsLicenseMatchWithVehicleKind": 1,
"HasOtherCulprit": 0,
"IsAccidentOutOfBorder": 0,
"IsFatalAccident": 0,
"IsPlaqueChanged": 0,
"PoliceOfficerId": 1,
"IsLicenseReplacement": 0,
"PreviousPolicyEndDate": "",
"ActualPremium": null,
"ArchiveNo": null
}
```
Proven Parsian: `claimId=4909952`, `claimNo=1632`, `policyId=13764408`.
---
## 4. GEN.12 — Damage case
Doc: `CAR.THID.APIH.GEN.12`
| Item | Value |
|------|-------|
| نام عملیات | ثبت مورد خسارت (خودرو/شخص زیان‌دیده) |
| Endpoint | `POST .../third-party-car-financial-claims/{claimId}/dmg-cases` |
| Method | `POST` |
Requires existing `claimId` (soft-ensures GEN.03 if missing).
### Key request fields
| Field | Source |
|-------|--------|
| `Desc` | Joined selected outer part labels (`سپر عقب/...`) |
| `DriverId` | Fanavaran person inquiry (cached) |
| `VehicleKindId` | Lookup match on `claimCase.vehicle.carType` |
| `InsuranceCorpId` | Resolve from `INSURANCE_CORP_ID` caption |
| `ChassisNo` / `MotorNo` / `VIN` / plate fields | Party vehicle inquiry |
| `PolicyNo` / `PolicyCINumber` / dates | Inquiry aliases |
| `LicenceNo` | Driver/insurer licence; never empty |
| `EstimateAmount` | Provisional `1000` early |
| `DmgCaseTypeId` / `DmgHistoryStatus` / plate kinds | Tenant defaults |
### Success
| Field | Local store |
|-------|-------------|
| `Id` | `claimCases.dmgCaseId` |
History: `FANAVARAN_DAMAGE_CASE_AUTO_SUBMIT_SUCCEEDED`.
Proven Parsian: `dmgCaseId=427594`, `DriverId=2426953`.
### Trigger
After local `SELECT_OUTER_PARTS` — before image upload.
Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/damage-case/submit`.
---
## 5. GEN.07 — Attachments
Doc: `CAR.THID.APIH.GEN.07`
| Item | Value |
|------|-------|
| نام عملیات | آپلود فایل پیوست پرونده |
| Endpoint | `POST .../third-party-car-financial-claims/{claimId}/files` |
| Method | `POST` |
| Content-Type | `multipart/form-data` (boundary by FormData) |
### Multipart shape
| Part name | Content |
|-----------|---------|
| `content` | JSON string (`application/json`) with `FileName`, `FileTypeId`, optional `Files[]` |
| `files` | Real file bytes (one request per local image) |
**Do not** base64-encode. Filenames in JSON must match multipart filenames.
### FileTypeId (critical — tenant-specific)
| Tenant | `ClaimFileTypeId` | Note |
|--------|-------------------|------|
| **parsian** | **63** | `ساير مدارک خسارت` — proven. **Do not use 70** |
| tejaratno | `23` | Default |
| moallem | shared `23` until confirmed | Verify in `file-types` lookup |
Fanavaran error if id missing from tenant lookup:
`مقدار فیلد نوع فايل با منبع لوکاپ مطابقت ندارد`.
### Success
File ids recorded under `fanavaranSync.attachments.files[]`.
Best-effort: failures audited + retried; local user flow continues.
No videos unless Fanavaran confirms support.
Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/attachments/submit`.
---
## 6. GEN.08 — Expertise
Doc: `CAR.THID.APIH.GEN.08`
| Item | Value |
|------|-------|
| نام عملیات | ثبت کارشناسی خسارت مالی ثالث |
| Endpoint | `POST .../third-party-car-financial-claims/{claimId}/expertise` |
| Method | `POST` |
Requires `claimId` + `dmgCaseId` (soft-ensures earlier stages).
### Key request fields
| Field | Mapping |
|-------|---------|
| `ClaimExpertId` | Tenant **`ExpertiseClaimExpertId`** (Parsian `29`) — assessor role |
| `DmgCaseId` | Local `dmgCaseId` |
| `RepairWage` | Sum of part `salary` |
| `ComponentReplacementCost` | Sum of part `price` |
| `WasteValue` | Sum of `daghi.price` |
| `DmgAssessmentDate` / `InspectionTime` | From expert reply submit time |
| `InspectionPlaceId` | Currently `282` (code constant; verify per tenant lookup) |
| `DropAmountStatus` | Currently `5458` |
| `DropAmountAdditionsDeductions` | `evaluation.priceDrop.total` |
| `DamagedVehicleCurrentPrice` | `evaluation.priceDrop.carPrice` |
| `DmgSections[]` | One row per priced part |
`DmgSections[]` row:
| Field | Source |
|-------|--------|
| `DmgSectionId` | Fanavaran car-components / part id |
| `AccidentLevel` | Part damage type / default `5456` |
| `Desc` | Damage type / label |
| `RepairWage` / `ComponentReplacementCost` / `WasteValue` | Line amounts |
### Validation
- Active expert reply with submit-ready parts
- Factor-needed lines wait until `totalPayment > 0`
- Missing `DmgCaseId` / section ids → `BadRequestException` with `warnings[]`
### Success
| Field | Local store |
|-------|-------------|
| `Id` | `claimCases.expertiseId` |
History: `FANAVARAN_EXPERTISE_AUTO_SUBMIT_SUCCEEDED`.
Proven Parsian: `expertiseId=403144`, `ClaimExpertId=29`.
Manual: `POST /v2/fanavaran/{client}/claim-cases/{id}/expertise/submit`.
---
## Follow-up APIs (documented upstream, not all wired)
| Doc | Purpose |
|-----|---------|
| GEN.09 | Damaged points |
| GEN.10 | `dmg-department-referral` |
| GEN.11 | Cancel expertise |
| GEN.13 | Drop amounts GET |
| GEN.14 | Culprit damaged points |
Current YARA production path focuses on GEN.03 → 12 → 07 → 08.
---
## YARA manual HTTP surface
Controller: `FanavaranController` (`src/fanavaran/fanavaran.controller.ts`)
Prefix: `/v2/fanavaran` — Bearer + `LocalActorAuthGuard`.
| Method | Path |
|--------|------|
| GET | `/clients` |
| GET/POST | `/:client/claim-cases/:id/base-claim/preview\|submit` |
| GET/POST | `/:client/claim-cases/:id/damage-case/preview\|submit` |
| GET/POST | `/:client/claim-cases/:id/attachments/preview\|submit` |
| GET/POST | `/:client/claim-cases/:id/expertise/preview\|submit` |
`:client` ∈ `parsian` \| `tejaratno` \| `moallem`.

View File

@@ -0,0 +1,139 @@
---
last_updated: 2026-08-08
tags: [fanavaran, api, read, lookups]
source: fanavaran-module-docs
---
# 02 — Read APIs & Lookups
All read calls use the same business headers as write APIs (`authenticationToken`, `CorpId`, `ContractId`, `Location`).
Lookup responses are **tenant-specific**. Cache directory:
```text
files/fanavaran-lookups/{clientKey}/
```
Config: `src/fanavaran/fanavaran-lookup.config.ts`
Service: `FanavaranLookupService`
Local HTTP: `LookupsController` / `LookupsService`
---
## 1. Policy inquiry (guilty party)
| Item | Value |
|------|-------|
| Endpoint | `GET /Api/BimeApi/v2.0/common/Policies/inquiry-my-policies` |
| Query | `InsuranceLineId=5` (third-party), `NationalCode={insurerNationalCode}` |
| Usage | Resolve `PolicyId` for GEN.03 |
| Selection | `selectLatestActiveFanavaranPolicy` — latest non-expired by `EndDate` |
| Cache | `fanavaranSync.baseClaim.policyId` (resolve-once) |
| Force refresh | Manual preview `forceRefreshPolicy=true` only |
**Errors (YARA messages):**
- No policies → contact admin
- Latest expired → cannot send
- Invalid PolicyId/EndDate → contact admin
**Do not** treat UI `resolvePolicy=true` as cache-bust (deprecated; ignored for re-inquiry).
---
## 2. Driver / person inquiry
Used when building GEN.12 to resolve `DriverId`.
| Item | Value |
|------|-------|
| Endpoint | `GET /Api/BimeApi/v2.0/common/parties/inquiry-by-unique-identifier` |
| Usage | `resolveDriverFanavaranId(clientKey, nationalCode, birthday, driverIsInsurer)` via `FanavaranLookupService` |
| Cache | `fanavaranSync.damageCase.driverId` and `blameCase.parties[].person.fanavaranDriverId` |
Prefer cache before live call.
---
## 3. Remote lookup catalogue
Base: `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0`
| Name | Fanavaran path | Local YARA route | Used for |
|------|----------------|------------------|----------|
| accident-causes | `/car/base-info/accident-causes` | `GET /lookups/accident-causes` | Accident cause options |
| accident-report-type | `/car/code-list/accident-report-type` | `GET /lookups/accident-report-type` | Defaults / UI |
| vehicle-use-types | `/car/base-info/vehicle-use-types` | `GET /lookups/vehicle-use-types` | Vehicle use |
| 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 |
| 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` |
| accident-level | `/car/code-list/accident-level` | `GET /lookups/accident-level` | Section severity |
| expert-status | `/car/code-list/expert-status` | `GET /lookups/expert-status` | Expertise status |
| vehicle-kinds | `/car/base-info/vehicle-kinds` | `GET /lookups/vehicle-kinds` | GEN.12 `VehicleKindId` |
| person-role | `/common/code-list/person-role` | `GET /lookups/person-role` | Roles |
| insurance-corp | `/common/code-list/insurance-corp` | via fanavaran + resolve | GEN.12 `InsuranceCorpId` |
| file-types | `/common/base-info/file-types` | `GET /lookups/file-types` | GEN.07 `FileTypeId` |
| cities | `/common/base-info/cities` | `GET /lookups/cities` | City ids |
| provinces | `/common/base-info/Provinces` | `GET /lookups/provinces` | Provinces |
| dmg-case-type | `/car/code-list/dmg-case-type` | `GET /lookups/dmg-case-type` | GEN.12 |
| dmg-history-status | `/car/code-list/dmg-case-history-status` | `GET /lookups/dmg-history-status` | GEN.12 |
| used-place | `/car/code-list/used-place` | `GET /lookups/used-place` | Used place |
| dmg-business-line | `/car/code-list/dmg-business-line` | `GET /lookups/dmg-business-line` | Business line |
### Generic accessor
```http
GET /lookups/fanavaran
GET /lookups/fanavaran/{lookupName}
```
Lists / fetches by name for the **active** `FANAVARAN_CLIENT`.
---
## 4. Other read helpers (LookupsController)
| Route | Purpose |
|-------|---------|
| `GET /lookups/inquiry-by-vin` | VIN inquiry via Fanavaran |
| `GET /lookups/my-policies` | Policies for national code |
| `GET /lookups/third-party-policy/:policyId` | Policy by id |
| `GET /lookups/body-policy/:policyId` | Body policy by id |
| `GET /lookups/accident-reason\|way\|type\|fields` | UI accident fields (Tejarat static + Fanavaran map) |
---
## 5. Insurance corp resolve
| Item | Value |
|------|-------|
| Env | `INSURANCE_CORP_ID` = Persian **Caption** in insurance-corp list |
| Method | `FanavaranLookupService.resolveInsuranceCorpId(clientKey)` |
| Behavior | Fetch list once, match caption → numeric `Id`, cache |
| Parsian proven | `InsuranceCorpId=329` (for that deployment caption) |
---
## 6. Vehicle kind resolve
| Item | Value |
|------|-------|
| Input | Local `claimCase.vehicle.carType` (sedan/suv/…) |
| Method | Match Fanavaran `vehicle-kinds` Caption keywords |
| Cache | `fanavaranSync.damageCase.vehicleKindId` |
| Parsian proven | `VehicleKindId=6704` on sample case |
---
## 7. How lookups are used in project
1. First call for a tenant fetches from Fanavaran (authenticated).
2. Writes JSON under `files/fanavaran-lookups/{client}/`.
3. Subsequent reads prefer cache file.
4. Claim payload builders call `getFanavaranLookupRows(clientKey, name)` when mapping ids.
5. Outer car parts catalogue can align with Fanavaran car-components (`FANAVARAN_CAR_PARTS_CATALOG` / helpers).
**Dependency:** Auth must succeed for cold cache. Transient “try again later” triggers tenant backoff (see [09-error-handling.md](./09-error-handling.md)).

View File

@@ -0,0 +1,111 @@
---
last_updated: 2026-08-08
tags: [fanavaran, constants, config]
source: fanavaran-module-docs
---
# 03 — Constants
Legend for **Varies by company?**
✅ = different per insurer tenant · ❌ = shared across tenants in current code
Secrets are **not** printed here — read from Mongo `fanavaranClientConfigs` or seed profiles.
---
## Auth (tenant)
| Name | Example (Parsian) | Where used | Why | Varies? |
|------|-------------------|------------|-----|---------|
| `appName` | `ParsianService` | GetAppToken | App identity | ✅ |
| `secret` | *(config)* | GetAppToken | App secret | ✅ |
| `username` | `ParsianServiceUser` | Login | User | ✅ |
| `password` | *(config)* | Login | Password | ✅ |
| `corpId` | `543` | All business calls | Corp scope | ✅ |
| `contractId` | `28` | All business calls | Contract | ✅ |
| `location` | `210050` | All business calls / OpBUId | Branch/location | ✅ |
Seed: `SEED_FANAVARAN_CLIENT_PROFILES` · Runtime: `getFanavaranClientProfile(key)`.
---
## Payload defaults (tenant profile.defaults)
| Name | Shared seed | Parsian override | Tejaratno | Moallem | Varies? |
|------|-------------|------------------|-----------|---------|---------|
| `AccidentCityId` | 701 | same | same | same | usually ❌ |
| `AccidentReportTypeId` | 155 | same | same | same | usually ❌ |
| `AccidentVehicleUsedId` | 1 | same | same | same | usually ❌ |
| `ClaimExpertId` (GEN.03) | 4543092 | **154** | 4543092 | shared TBD | ✅ |
| `ExpertiseClaimExpertId` (GEN.08) | 2709 | **29** | **2709** | shared TBD | ✅ |
| `CompensationReferenceId` | 167 | same | same | same | usually ❌ |
| `CulpritLicenceTypeId` | 2 | same | same | same | usually ❌ |
| `CulpritTypeId` | 337 | same | same | same | usually ❌ |
| `DmgCaseTypeId` | 175 | same | same | same | may ✅ |
| `DmgHistoryStatus` | 5214 | same | same | same | may ✅ |
| `PlaqueKindId` | 8 | same | same | same | may ✅ |
| `PlaqueSampleId` | 10 | same | same | same | may ✅ |
| `DriverIsOwner` | 0 | same | same | same | ❌ |
| `FaultPercent` | 100 | same | same | same | ❌ |
| `ClaimFileTypeId` (GEN.07) | 23 | **63** | 23 | 23 TBD | ✅ |
**Never swap** GEN.03 `ClaimExpertId` with GEN.08 `ExpertiseClaimExpertId` — different Fanavaran roles.
---
## Code-level shared constants
File: `claim-request-management.service.ts`
| Name | Value | Where | Why | Varies? |
|------|-------|-------|-----|---------|
| `FANAVARAN_SUBMIT_URL` | `.../third-party-car-financial-claims` | All stage posts | API root | ❌ (same host) |
| `FANAVARAN_ACCIDENT_LOCATION_ADDRESS` | `استان تهران شهر تهران` | GEN.03 | Provisional address | currently ❌ |
| `FANAVARAN_DEFAULT_ACCIDENT_CAUSE_ID` | `6` | GEN.03 | Default cause | may ✅ later |
| `FANAVARAN_DEFAULT_ACCIDENT_LEVEL` | `5456` | GEN.08 sections | Default severity | may ✅ |
| `FANAVARAN_PROVISIONAL_ESTIMATE_AMOUNT` | `1000` | GEN.03/12 early | Positive estimate | ❌ |
| `FANAVARAN_DUMMY_LICENCE_NO` | env or `9705463515` | Licence fields | Never empty | env override |
| `InspectionPlaceId` (expertise) | `282` | GEN.08 | Hardcoded today | should verify ✅ |
| `DropAmountStatus` (expertise) | `5458` | GEN.08 | Hardcoded today | should verify ✅ |
| Auth URLs | apimanager.iraneit.com | Auth service | Host | ❌ |
| Lookup base | same host `/BimeApi/v2.0` | Lookups | Host | ❌ |
---
## Env / activation
| Name | Example | Where | Why | Varies? |
|------|---------|-------|-----|---------|
| `FANAVARAN_CLIENT` | `parsian` | `resolveFanavaranClientKey` | Active tenant | ✅ per deploy |
| `CLIENT_ID` | `8` → parsian | Fallback hint | Legacy deploy map | ✅ |
| `INSURANCE_CORP_ID` | Persian caption | Damage `InsuranceCorpId` | Match lookup Caption | ✅ |
| `FANAVARAN_DUMMY_LICENCE_NO` | digits | Licence fallback | Test/prod dummy | optional |
---
## Enums / claim types (YARA)
| Name | Relevant values | Fanavaran use |
|------|-----------------|---------------|
| Claim type | `THIRD_PARTY` only | Fanavaran path enabled |
| | `CAR_BODY` | **Not** submitted to Fanavaran |
| `ClaimCaseStatus` | local workflow | Triggers stages indirectly |
| History event types | `FANAVARAN_*_SUCCEEDED/FAILED` | Audit trail on claim |
---
## Retry / backoff constants
| Name | Value | Where |
|------|-------|-------|
| `FANAVARAN_RETRY_DELAY_MS` | 5 min | Stage retry |
| `FANAVARAN_TRANSIENT_RETRY_DELAY_MS` | 10 min | “Try again later” stage retry |
| `FANAVARAN_MAX_RETRIES` | 2 | Per stage |
| `TRANSIENT_BACKOFF_MS` | 5 min | Tenant-wide auth backoff |
| Token TTL | Until Tehran midnight | Auth cache |
---
## Plate letter codes
`FANAVARAN_PLATE_LETTER_CODE` maps Persian plate letters → Fanavaran middle codes (shared mapping table in claim service).

View File

@@ -0,0 +1,63 @@
---
last_updated: 2026-08-08
tags: [fanavaran, tenant, matrix]
source: fanavaran-module-docs
---
# 04 — Tenant-specific vs shared
## Comparison table
| مورد | مشترک | وابسته به شرکت |
|------|:-----:|:--------------:|
| API host (`apimanager.iraneit.com`) | ✅ | ❌ |
| Endpoint paths (GEN.03/07/08/12, auth) | ✅ | ❌ |
| HTTP methods & payload **shape** | ✅ | ❌ |
| Auth flow (GetAppToken → Login) | ✅ | ❌ |
| Auth credentials (app/user/secret) | ❌ | ✅ |
| `CorpId` / `ContractId` / `Location` | ❌ | ✅ |
| `ClaimExpertId` (GEN.03) | ❌ | ✅ |
| `ExpertiseClaimExpertId` (GEN.08) | ❌ | ✅ |
| `ClaimFileTypeId` (GEN.07) | ❌ | ✅ |
| Lookup **Id values** (file-types, experts, …) | ❌ | ✅ |
| Lookup **endpoint URLs** | ✅ | ❌ |
| Policy inquiry algorithm | ✅ | ❌ |
| YARA orchestration / soft-ensure / retry | ✅ | ❌ |
| YARA internal routes `/v2/fanavaran/...` | ✅ | path `:client` only |
| `INSURANCE_CORP_ID` caption / resolved id | ❌ | ✅ |
| `FANAVARAN_CLIENT` env | ❌ | ✅ per deploy |
| Mapping YARA→Fanavaran field names | ✅ | ❌ |
| Validation: PolicyId required, EstimateAmount > 0 | ✅ | ❌ |
| Validation: FileTypeId ∈ tenant lookup | logic ✅ | allowed ids ✅ |
| Business: THIRD_PARTY only | ✅ | ❌ |
| Provisional address / dummy licence | ✅ today | may customize later |
| SMS template after base claim | shared orchestration | provider/templates may ✅ |
| Mongo collections / audit schema | ✅ | ❌ |
| Offline inquiry seeds | may exist per client | ✅ |
## Current tenants
| Key | Status | Notes |
|-----|--------|-------|
| `parsian` | **Template / proven E2E** | Use as reference for new clients |
| `tejaratno` | Working shape | Different experts + FileTypeId 23 |
| `moallem` | Auth seeded | Confirm expert + file-type ids via lookups before go-live |
## What is reusable when adding a client
1. Entire staged pipeline in `ClaimRequestManagementService`
2. `FanavaranAuthService`, audit, lookup cache machinery
3. Controllers + preview/submit surface
4. Policy selection helper
5. Soft-ensure + retry + history events
## What must be configured per client
1. Auth block + Corp/Contract/Location
2. GEN.03 / GEN.08 expert ids (from that tenant’s expert lists)
3. `ClaimFileTypeId` present in that tenant’s `file-types.json`
4. `INSURANCE_CORP_ID` caption for deploy
5. Warm lookups under `files/fanavaran-lookups/{key}/`
6. Optional: override city/cause/inspection ids if shared defaults fail validation
See [11-onboard-new-client.md](./11-onboard-new-client.md).

View File

@@ -0,0 +1,130 @@
---
last_updated: 2026-08-08
tags: [fanavaran, flow, sequence]
source: fanavaran-module-docs
---
# 05 — Full claim registration flow
Applies to **THIRD_PARTY** claims only. Template behavior = **Parsian**.
## High-level stages
```text
Start local claim
→ Validate THIRD_PARTY + data readiness
→ Auth (cached token)
→ Policy inquiry (resolve-once PolicyId)
→ GEN.03 base claim → store claimId / claimNo → SMS owner
→ SELECT_OUTER_PARTS
→ GEN.12 damage case → store dmgCaseId
→ Upload docs/images locally
→ GEN.07 attachments (per file, best-effort)
→ Expert pricing ready
→ GEN.08 expertise → store expertiseId
→ End (local completion independent of Fanavaran success)
```
## Sequence diagram
```mermaid
sequenceDiagram
autonumber
participant User as User / Expert
participant YARA as YARA ClaimRequestManagement
participant Auth as FanavaranAuthService
participant FV as Fanavaran API
participant DB as Mongo claimCases
User->>YARA: Create THIRD_PARTY claim
YARA->>DB: Persist claimCase
YARA->>YARA: autoSubmitToFanavaranV2OnClaimCreated
YARA->>Auth: getAuthenticationToken(client)
alt cache miss / past Tehran midnight
Auth->>FV: POST GetAppToken
Auth->>FV: POST Login
Auth->>DB: fanavaranAuthTokens
end
Auth-->>YARA: authenticationToken
alt no cached policyId
YARA->>FV: GET inquiry-my-policies
YARA->>DB: fanavaranSync.baseClaim.policyId
end
YARA->>FV: POST third-party-car-financial-claims (GEN.03)
FV-->>YARA: Id, ClaimNo
YARA->>DB: claimId, claimNo, history SUCCESS
YARA->>YARA: SMS owner (deduped)
User->>YARA: SELECT_OUTER_PARTS
YARA->>YARA: autoSubmitFanavaranDamageCase...
Note over YARA: soft-ensure base if missing
YARA->>FV: POST .../dmg-cases (GEN.12)
FV-->>YARA: DmgCaseId
YARA->>DB: dmgCaseId
User->>YARA: Upload images/docs
loop each image not yet uploaded
YARA->>FV: POST .../files multipart (GEN.07)
YARA->>DB: attachments.files[]
end
User->>YARA: Expert reply priced
YARA->>YARA: autoSubmitFanavaranExpertise...
Note over YARA: soft-ensure damage/base
YARA->>FV: POST .../expertise (GEN.08)
FV-->>YARA: ExpertiseId
YARA->>DB: expertiseId, history SUCCESS
```
## Flow diagram (stages + soft-ensure)
```mermaid
flowchart TD
A[Local claim created THIRD_PARTY] --> B{claimId exists?}
B -->|No| C[GEN.03 Base claim]
B -->|Yes| D[Skip base]
C --> E[Store claimId/claimNo]
E --> F[Outer parts selected]
D --> F
F --> G{dmgCaseId exists?}
G -->|No| H[Ensure base then GEN.12]
G -->|Yes| I[Skip damage]
H --> J[Store dmgCaseId]
J --> K[Local media upload]
I --> K
K --> L[GEN.07 per file]
L --> M[Expert pricing ready]
M --> N{expertiseId exists?}
N -->|No| O[Ensure damage then GEN.08]
N -->|Yes| P[Done]
O --> P
```
## Local triggers
| Stage | Auto trigger | Function |
|-------|--------------|----------|
| Base | Claim create (early) | `autoSubmitToFanavaranV2OnClaimCreated` |
| Base | Claim completed (legacy/fallback; skips if already submitted) | `autoSubmitToFanavaranV2OnClaimCompleted` |
| Damage | After outer parts selection | `autoSubmitFanavaranDamageCaseOnOuterPartsSelected` |
| Attachments | After successful local image/doc upload | `autoSubmitFanavaranAttachment` |
| Expertise | After expert reply when priced | `autoSubmitFanavaranExpertiseOnExpertReply` |
V5 note: when `requiresFileMakerApproval=true`, Fanavaran submit may wait for FileMaker approval per claim flow rules.
## Manual retry
Use `/v2/fanavaran/{client}/claim-cases/{claimCaseId}/.../submit` for any failed stage. Preview endpoints build payload without requiring submit success.
## Status persistence
Per stage under `claimCases.fanavaranSync.{baseClaim|damageCase|attachments|expertise}`:
- `status`: success / failed / pending / skipped
- `lastError`, `lastTriedAt`, `retryCount`, `nextRetryAt`
- Cached ids + `lastPayload`
Top-level: `claimId`, `claimNo`, `dmgCaseId`, `expertiseId`.

View File

@@ -0,0 +1,140 @@
---
last_updated: 2026-08-08
tags: [fanavaran, yara, integration]
source: fanavaran-module-docs
---
# 06 — YARA ↔ Fanavaran integration points
## Module map
| Concern | Module | Primary files |
|---------|--------|---------------|
| HTTP surface | `FanavaranModule` | `fanavaran.controller.ts` |
| Auth | `FanavaranModule` | `fanavaran-auth.service.ts` |
| Lookups | `FanavaranLookupModule` / `LookupsModule` | `fanavaran-lookup.service.ts`, `lookups.*` |
| Audit | `FanavaranAuditModule` | `fanavaran-audit.service.ts` |
| Tenant config | boot + System Settings | `fanavaran-client-config.service.ts`, `system-settings.*` |
| Orchestration | `ClaimRequestManagementModule` | `claim-request-management.service.ts` |
| Policy select | same | `fanavaran-policy-selection.ts` |
| SMS after base | `SmsOrchestrationModule` | `sms-orchestration.service.ts` |
---
## Interaction catalogue
### A. Authentication
| | |
|--|--|
| Module | `src/fanavaran` |
| Service | `FanavaranAuthService` |
| Controller | *(none external — used by claim/lookup)* |
| Functions | `getAuthenticationToken`, GetAppToken + Login internals |
| When | Before any Fanavaran business/lookup HTTP |
| Send | appname/secret → appToken → username/password |
| Receive | `authenticationToken` |
| Store | Memory cache + `fanavaranAuthTokens` |
| Consumers | Claim submit, lookups, policy inquiry |
### B. Base claim preview/submit
| | |
|--|--|
| Module | Fanavaran + ClaimRequestManagement |
| Controller | `FanavaranController.preview` / `submit` |
| Service | `ClaimRequestManagementService` |
| Functions | `previewFanavaranSubmitV2`, `submitFanavaranV2`, `executeFanavaranV2Submit`, `buildFanavaranSubmitPayload`, `getPolicyIdFromNationalCode` |
| When | Auto on claim create; manual preview/submit |
| Send | GEN.03 JSON + business headers |
| Receive | `Id`, `ClaimNo` |
| Store | `claimId`, `claimNo`, `fanavaranSync.baseClaim.*`, history |
| Consumers | Damage/expertise soft-ensure; UI claim detail; SMS |
### C. Damage case
| | |
|--|--|
| Controller | `previewDamageCase` / `submitDamageCase` |
| Functions | `previewFanavaranDamageCaseV2`, `submitFanavaranDamageCaseV2`, `buildFanavaranDamageCasePayload`, `autoSubmitFanavaranDamageCaseOnOuterPartsSelected`, `ensureFanavaranBaseClaim` |
| When | After outer parts selected; soft-ensure from later stages |
| Send | GEN.12 JSON |
| Receive | `Id` → `dmgCaseId` |
| Store | `dmgCaseId`, `fanavaranSync.damageCase.{driverId,vehicleKindId,insuranceCorpId,lastPayload}` |
| Consumers | Expertise payload (`DmgCaseId`) |
### D. Attachments
| | |
|--|--|
| Controller | `previewAttachments` / `submitAttachments` |
| Functions | `previewFanavaranAttachmentsV2`, `submitFanavaranAttachmentsV2`, `autoSubmitFanavaranAttachment` |
| When | After local required-doc / capture upload |
| Send | multipart GEN.07 |
| Receive | file metadata / ids |
| Store | `fanavaranSync.attachments.files[]` |
| Consumers | Manual retry of missing uploads |
### E. Expertise
| | |
|--|--|
| Controller | `previewExpertise` / `submitExpertise` |
| Functions | `previewFanavaranExpertiseV2`, `submitFanavaranExpertiseV2`, `buildFanavaranExpertisePayload`, `autoSubmitFanavaranExpertiseOnExpertReply`, `ensureFanavaranDamageCase` |
| When | Expert reply priced / factor validated |
| Send | GEN.08 JSON |
| Receive | `Id` → `expertiseId` |
| Store | `expertiseId`, `fanavaranSync.expertise.*` |
| Consumers | Claim completion reporting / history |
### F. Lookups (read)
| | |
|--|--|
| Controller | `LookupsController` |
| Service | `LookupsService` → `FanavaranLookupService` |
| When | UI dropdowns; cold cache; payload id resolution |
| Store | `files/fanavaran-lookups/{client}/*.json` |
### G. Tenant config admin
| | |
|--|--|
| Module | System Settings |
| Service | `SystemSettingsService` / `FanavaranClientConfigService` |
| When | Boot seed missing keys; admin update |
| Store | `fanavaranClientConfigs` → in-memory cache via `setFanavaranClientProfilesCache` |
### H. Claim create call sites (auto base)
`autoSubmitToFanavaranV2OnClaimCreated` is invoked from claim creation paths inside `ClaimRequestManagementService` (registrar / expert-initiated / v2 create flows — search call sites ~7717, 7839, 8141, 8243).
Outer parts selection invokes damage auto-submit (~8422).
Document/capture upload hooks call attachment auto-submit (~9496+).
Expert reply path calls expertise auto-submit (~10576).
---
## Controllers summary
| Controller | Fanavaran-related routes |
|------------|--------------------------|
| `FanavaranController` | All `/v2/fanavaran/*` preview/submit |
| `LookupsController` | `/lookups/*` Fanavaran-backed reads |
| Claim V2 / registrar / expert mirrors | Create/select/upload that **trigger** auto-submit (no direct Fanavaran URL) |
---
## Data written by Fanavaran stages
| Field path | Stage |
|------------|-------|
| `claimCases.claimId` / `claimNo` | GEN.03 |
| `claimCases.dmgCaseId` | GEN.12 |
| `claimCases.expertiseId` | GEN.08 |
| `claimCases.fanavaranSync.*` | All |
| `claimCases.history[]` | Success/fail events |
| `blameCases.parties[].person.fanavaranDriverId` | GEN.12 driver resolve |
| `fanavaranAuditLogs` | Every real HTTP step |
| `fanavaranAuthTokens` | Auth cache |
| `fanavaranClientConfigs` | Tenant profiles |

View File

@@ -0,0 +1,92 @@
---
last_updated: 2026-08-08
tags: [fanavaran, mapping]
source: fanavaran-module-docs
---
# 07 — Data mapping (YARA → Fanavaran)
## GEN.03 — Base claim
| YARA | Fanavaran |
|------|-----------|
| Guilty party insurer national code → policy inquiry | `PolicyId` |
| Tenant `defaults.ClaimExpertId` | `ClaimExpertId` |
| Tenant defaults city/report/vehicle-used/… | `AccidentCityId`, `AccidentReportTypeId`, … |
| Blame/claim `createdAt` (Jalali) | `AccidentDate`, `AnnouncementDate`, `DocReceivedDate` |
| Blame/claim time | `AccidentTime` |
| Constant address | `AccidentLocationAddress` |
| Sum damage parts estimate (or 1000) | `EstimateAmount` |
| Accident reason fanavaran id (else default 6) | `AccidentCauseId` |
| Culprit licence digits / dummy | `CulpritLicenceNo` |
| Culprit licence issue / birthday fallback | `CulpritLicenceIssuDate` |
| Tenant `CulpritLicenceTypeId` / `CulpritTypeId` | same |
| — | Explicit `null` placeholders for unused template fields |
## GEN.12 — Damage case
| YARA | Fanavaran |
|------|-----------|
| `damage.selectedParts` labels | `Desc` (`/` joined) |
| Party `nationalCodeOfDriver` + birthday (+ insurer flag) → inquiry | `DriverId` |
| `claimCase.vehicle.carType` → vehicle-kinds | `VehicleKindId` |
| `INSURANCE_CORP_ID` caption → insurance-corp | `InsuranceCorpId` |
| Inquiry `ShsNum` / Chassis* | `ChassisNo` |
| Inquiry `MtrNum` / Engine* | `MotorNo` |
| Inquiry `VIN` / vin | `VIN` |
| Inquiry / plate snapshot | `PlaqueLeftNo`, `PlaqueRightNo`, `PlaqueSerial`, `PlaqueMiddleCodeId`, `PlaqueNo` |
| Inquiry policy numbers / dates | `PolicyNo`, `PolicyCINumber`, `BeginDate`, `EndDate`, `PreviousPolicyEndDate` |
| Inquiry model year | `BuiltYear` |
| `person.driverLicense` / `insurerLicense` / dummy | `LicenceNo` |
| `person.driverBirthday` | `LicenceIssuDate` |
| `person.driverIsInsurer` | `DriverIsOwner` (1/0) |
| Tenant defaults | `DmgCaseTypeId`, `DmgHistoryStatus`, `PlaqueKindId`, `PlaqueSampleId`, `FaultPercent`, `AccidentVehicleUsedId`, `LicenceTypeId` |
| Provisional | `EstimateAmount` = 1000 |
## GEN.07 — Attachments
| YARA | Fanavaran |
|------|-----------|
| Local image filename | `FileName` (+ multipart `files`) |
| Tenant `ClaimFileTypeId` | `FileTypeId` |
| Local file bytes | multipart binary |
| `claimId` | URL path |
## GEN.08 — Expertise
| YARA | Fanavaran |
|------|-----------|
| Tenant `ExpertiseClaimExpertId` | `ClaimExpertId` |
| `claimCase.dmgCaseId` | `DmgCaseId` |
| Expert reply parts `salary` (sum) | `RepairWage` |
| Expert reply parts `price` (sum) | `ComponentReplacementCost` |
| Part `daghi.price` (sum) | `WasteValue` |
| Reply `submittedAt` | `DmgAssessmentDate`, `InspectionTime` |
| `evaluation.priceDrop.total` | `DropAmountAdditionsDeductions` |
| `evaluation.priceDrop.carPrice` | `DamagedVehicleCurrentPrice` |
| Part catalog / `partId` | `DmgSections[].DmgSectionId` |
| Part `typeOfDamage` / default level | `DmgSections[].AccidentLevel` |
| Part label / type | `DmgSections[].Desc` |
| Part line `salary` / `price` / daghi | section money fields |
| Code constants today | `InspectionPlaceId`, `DropAmountStatus` |
## Auth headers
| YARA profile.auth | Fanavaran header |
|-------------------|------------------|
| `corpId` | `CorpId` |
| `contractId` | `ContractId` |
| `location` | `Location` |
| token from Login | `authenticationToken` |
## Not mapped / out of scope
| YARA | Note |
|------|------|
| `CAR_BODY` claims | No Fanavaran submit |
| Videos | Not uploaded via GEN.07 in current code |
| Full police report fields | Often null unless later enriched |
## Alias notes (inquiry)
ESG / Tejarat inquiry payloads use multiple key names; builders try ordered aliases (e.g. `PrntCmpDocNo` / `printNumber` / `insuranceNumber` for policy number). See `pickPartyInquiryField` in claim service.

View File

@@ -0,0 +1,45 @@
---
last_updated: 2026-08-08
tags: [fanavaran, dependencies]
source: fanavaran-module-docs
---
# 08 — System dependencies
## Required
| Dependency | Role |
|------------|------|
| **MongoDB** | `claimCases`, `blameCases` / request-management, `fanavaranAuthTokens`, `fanavaranAuditLogs`, `fanavaranClientConfigs`, claim documents refs |
| **Outbound HTTPS** | Fanavaran API Manager (`apimanager.iraneit.com`) |
| **Filesystem** | `files/fanavaran-lookups/{client}/`, uploaded claim media for GEN.07 |
| **Env / config** | `FANAVARAN_CLIENT`, `INSURANCE_CORP_ID`, optional `FANAVARAN_DUMMY_LICENCE_NO`, SMS provider env |
| **Nest HttpModule** | Axios via `HttpService` |
| **Logging** | Nest `Logger` + audit collection |
## Optional / related
| Dependency | Role |
|------------|------|
| **SMS provider** | Notify owner after successful GEN.03 (`SmsOrchestrationService`; same as login SMS stack) |
| **Offline inquiry seeds** | Can supply driver/policy test data without live inquiry |
| **Redis / Queue** | **Not** used for Fanavaran stage orchestration today — retries are in-process `setTimeout` + Mongo `nextRetryAt` |
## Config sources (priority)
1. Mongo `fanavaranClientConfigs` (loaded to memory on boot)
2. Seed `SEED_FANAVARAN_CLIENT_PROFILES` if key missing (never overwrite existing DB edits on seed)
3. Env selects **which** client is active (`FANAVARAN_CLIENT`)
## Collections (Fanavaran-specific)
| Collection | Purpose |
|------------|---------|
| `fanavaranClientConfigs` | Per-tenant auth + defaults |
| `fanavaranAuthTokens` | Shared token until Tehran midnight |
| `fanavaranAuditLogs` | Request/response audit (secrets masked, bodies truncated ~80KB) |
| `claimCases.fanavaranSync` | Stage state machine |
## File storage
Local claim images/documents must be readable by the process for multipart upload. Failures on missing files are recorded on the attachments stage without blocking the user journey.

View File

@@ -0,0 +1,72 @@
---
last_updated: 2026-08-08
tags: [fanavaran, errors, retry]
source: fanavaran-module-docs
---
# 09 — Error handling
## Principles
1. Fanavaran failures must **not** abort the local user claim flow on auto-submit.
2. Persist warning + history + `fanavaranSync.*.lastError`.
3. Allow manual retry via `/v2/fanavaran/.../submit`.
4. Audit only real Fanavaran HTTP (warm cache hits are silent).
---
## Common errors
| دلیل | پیام نمونه | مدیریت | Retry | Log | اطلاع‌رسانی |
|------|------------|--------|-------|-----|-------------|
| Wrong login / appToken | `نام کاربر یا رمز عبور صحیح نیست` | Fix credentials; ensure fresh GetAppToken before Login | After fix | Auth audit | Ops |
| Transient overload | `لطفا پس از چند لحظه مجدد تلاش فرمایید` | Tenant-wide backoff 5 min; do **not** immediately re-auth storm | Delayed (~5 min) | Auth + stage | Ops via audit |
| Missing PolicyId | No/expired policies messages | Block submit; preview may leave null | Manual after data fix | Stage fail history | Admin |
| Duplicate create | Local `claimId`/`claimNo` exists | Skip create; use follow-ups | N/A | skipReason | — |
| Invalid FileTypeId | `نوع فايل با منبع لوکاپ مطابقت ندارد` | Set `ClaimFileTypeId` from tenant `file-types` (Parsian **63**) | After config fix | Attachment stage | Ops |
| Expertise not ready | `Fanavaran expertise payload is not ready` + warnings | Wait for priced parts / factor | Auto when ready | — | Expert UI |
| Network / 5xx | Axios / gateway errors | Stage failed + schedule retry | Up to `maxRetries` (2) | Audit | Ops |
| Max retries exhausted | Logged warn | Manual submit only | Stop auto | Stage status | Ops |
---
## Retry mechanics
Implemented in `scheduleFanavaranRetry`:
| Rule | Behavior |
|------|----------|
| Delay (normal) | 5 minutes |
| Delay (transient try-later) | 10 minutes + `registerFailure` tenant backoff (5 min) |
| Max | Default 2 per stage (`fanavaranSync.*.maxRetries`) |
| Dedupe | In-process timer map + existing future `nextRetryAt` |
| Lock | `withFanavaranStageLock` prevents concurrent stage runs |
---
## History event types (examples)
| Event | Meaning |
|-------|---------|
| `FANAVARAN_EARLY_AUTO_SUBMIT_SUCCEEDED` | GEN.03 ok |
| `FANAVARAN_EARLY_AUTO_SUBMIT_FAILED` | GEN.03 fail |
| `FANAVARAN_DAMAGE_CASE_AUTO_SUBMIT_SUCCEEDED` / `_FAILED` | GEN.12 |
| `FANAVARAN_EXPERTISE_AUTO_SUBMIT_SUCCEEDED` / `_FAILED` | GEN.08 |
| Attachment success/fail | Via sync status + audit (and related history where pushed) |
---
## Audit log (`fanavaranAuditLogs`)
Per HTTP step:
- `requestUrl`, `requestMethod`, `httpStatus`, `durationMs`
- Headers/bodies (secrets masked, truncated)
- `errorMessage` / `errorDetails` on failure
- Look for `fromCache: true` meta when PolicyId/auth reused without live call
---
## SMS
After successful base claim, owner SMS may be sent once (`smsNotifiedAt`). Failure to SMS should not roll back Fanavaran claim create; treat as separate notification concern.

View File

@@ -0,0 +1,90 @@
---
last_updated: 2026-08-08
tags: [fanavaran, testing]
source: fanavaran-module-docs
---
# 10 — Testing
## Prerequisites
1. `FANAVARAN_CLIENT=parsian` (or target tenant)
2. Valid tenant credentials in Mongo / seed
3. `INSURANCE_CORP_ID` matching that tenant’s insurance-corp Caption
4. Network access to `apimanager.iraneit.com`
5. Optional: warm lookups via first `GET /lookups/fanavaran/{name}` or auth script
## Auth smoke test
```bash
scripts/fanavaran-auth.sh parsian
source files/fanavaran-auth/parsian/tokens.env
# then curl a lookup — see docs/external-api-curls.md
```
## Unit / service specs
| Spec | Focus |
|------|-------|
| `src/fanavaran/fanavaran-auth.service.spec.ts` | Token cache / midnight / backoff |
| `src/claim-request-management/fanavaran-policy-selection.spec.ts` | Latest active policy selection |
Prefer mocks for Fanavaran HTTP in unit tests; use live calls only in controlled integration.
## Manual E2E (Parsian template)
1. Create THIRD_PARTY claim with guilty party national code that has an **active** Fanavaran policy.
2. Confirm history `FANAVARAN_EARLY_AUTO_SUBMIT_SUCCEEDED` and `claimId`/`claimNo`.
3. Select outer parts → `dmgCaseId` set.
4. Upload images → attachment file ids under `fanavaranSync.attachments`.
5. Submit expert pricing → `expertiseId`.
6. Cross-check `fanavaranAuditLogs` for each stage.
Preview first if debugging:
```http
GET /v2/fanavaran/parsian/claim-cases/{id}/base-claim/preview?debug=true
GET /v2/fanavaran/parsian/claim-cases/{id}/damage-case/preview
GET /v2/fanavaran/parsian/claim-cases/{id}/attachments/preview
GET /v2/fanavaran/parsian/claim-cases/{id}/expertise/preview
```
## Success scenarios
| Scenario | Expect |
|----------|--------|
| Happy path Parsian | All four stages succeed |
| Re-preview after PolicyId cached | No new policy inquiry |
| Soft-ensure | Damage submit creates base if missing |
| Skip duplicate base | Second auto-submit skipped when claimId exists |
## Error scenarios
| Scenario | Expect |
|----------|--------|
| Expired policy | Clear BadRequest; no create |
| Wrong FileTypeId (e.g. 70 on Parsian) | Attachment fail message about lookup |
| Transient try-later | Backoff; delayed retry; no login storm |
| Fanavaran down on auto-submit | Local claim continues; history FAILED; manual retry path in warning |
## Proven reference case (Parsian)
| Local | Fanavaran |
|-------|-----------|
| `CL68535` / `A00153` / `_id` `6a707a3a8f4c6fbd851cfa75` | — |
| Base | `claimId=4909952`, `claimNo=1632`, `policyId=13764408` |
| Damage | `dmgCaseId=427594`, `DriverId=2426953` |
| Attachments | FileTypeId **63**; file ids `4629737`… |
| Expertise | `expertiseId=403144`, assessor `29` |
Defaults used: GEN.03 expert `154`, GEN.08 `29`, Location `210050`, InsuranceCorpId `329`, VehicleKindId `6704`.
## Mocks
- Mock `HttpService` / axios for auth + submit in unit tests.
- Offline plate/driver seeds for local payload builds without live inquiry (used in proven Parsian offline+live mix).
- Do not commit live tokens.
## Curl catalogue
Operational curl sequences (auth, lookups, sample submits): [`docs/external-api-curls.md`](../external-api-curls.md).

View File

@@ -0,0 +1,76 @@
---
last_updated: 2026-08-08
tags: [fanavaran, onboarding, checklist]
source: fanavaran-module-docs
---
# 11 — Onboard a new insurance client
Use **Parsian** as the behavioral template. API shapes stay the same; only tenant config + lookup ids change.
## Checklist
### 1. Register tenant key
- [ ] Add key to `FanavaranClientKey` / `FANAVARAN_CLIENT_KEYS` in `fanavaran-client.config.ts`
- [ ] Add `SEED_FANAVARAN_CLIENT_PROFILES[key]` with auth + defaults
- [ ] Boot app once so `FanavaranClientConfigService` seeds Mongo if missing
### 2. Auth verification
- [ ] `scripts/fanavaran-auth.sh <key>` succeeds
- [ ] Login returns `authenticationToken`
- [ ] Business call with CorpId/ContractId/Location succeeds (any small lookup)
### 3. Warm lookups
- [ ] Fetch `file-types`, `vehicle-kinds`, `insurance-corp`, `car-components`, `inspection-place`, `accident-level`, expert-related lists
- [ ] Confirm cache under `files/fanavaran-lookups/<key>/`
### 4. Choose tenant-specific ids
| Field | How to pick |
|-------|-------------|
| `ClaimExpertId` (GEN.03) | Financial case owner role for that insurer |
| `ExpertiseClaimExpertId` (GEN.08) | Assessor role — **different** from GEN.03 |
| `ClaimFileTypeId` | Must exist in **that** tenant’s `file-types.json` |
| Shared codebook defaults | Start from `SHARED_FANAVARAN_DEFAULTS`; override if Fanavaran rejects |
### 5. Deploy env
```bash
FANAVARAN_CLIENT=<key>
INSURANCE_CORP_ID='<exact Caption from insurance-corp lookup>'
```
### 6. Dry-run on a test claim
- [ ] Preview base → PolicyId resolves
- [ ] Submit base → `claimId`/`claimNo`
- [ ] Outer parts → `dmgCaseId`
- [ ] Upload → attachments with correct FileTypeId
- [ ] Expertise → `expertiseId`
- [ ] Review `fanavaranAuditLogs` and history events
### 7. Document deltas
- [ ] Add a short section under [04-tenant-matrix.md](./04-tenant-matrix.md) for the new key
- [ ] Note any non-Parsian business rules (validation, required fields)
## Do / Don’t
| Do | Don’t |
|----|-------|
| Copy orchestration from existing code | Copy Parsian expert/file-type ids blindly |
| Verify FileTypeId in tenant lookup | Use template sample `70` without checking |
| Keep null fields in payloads | Strip nulls from GEN.03/12 templates |
| Soft-fail auto-submit | Block user UX on Fanavaran outage |
## Acceptance for “docs-only onboarding”
A backend engineer should be able to complete the checklist above using only:
1. This docs set (`docs/fanavaran/`)
2. `docs/external-api-curls.md`
3. Seed/config + System Settings for credentials
4. Parsian proven values as the reference baseline

99
docs/fanavaran/README.md Normal file
View File

@@ -0,0 +1,99 @@
---
last_updated: 2026-08-08
tags: [fanavaran, documentation, parsian, third-party-claim]
source: fanavaran-module-docs
---
# Fanavaran Integration — Technical Reference
مرجع فنی یکپارچه‌سازی یارا با سرویس‌های فناوران برای ثبت خسارت مالی ثالث خودرو.
**Template tenant:** `parsian` (proven end-to-end, 2026-08-03 — claim `CL68535` / publicId `A00153`)
**Also configured:** `tejaratno` (production shape), `moallem` (auth seeded; expert/file-type ids TBD)
## Goals
1. Document every Fanavaran API used in claim registration
2. Clarify per-insurer dependencies
3. Identify reusable pieces
4. Map YARA ↔ Fanavaran integration points
5. Enable onboarding a new insurer from this docs set alone
## Document index
| # | Document | Covers |
|---|----------|--------|
| 1 | [01-write-apis.md](./01-write-apis.md) | Auth + GEN.03 / GEN.12 / GEN.07 / GEN.08 write APIs |
| 2 | [02-read-apis-lookups.md](./02-read-apis-lookups.md) | Policy inquiry, driver inquiry, lookups |
| 3 | [03-constants.md](./03-constants.md) | Shared vs tenant constants |
| 4 | [04-tenant-matrix.md](./04-tenant-matrix.md) | Shared ✅ / tenant-specific ✅ matrix |
| 5 | [05-claim-flow.md](./05-claim-flow.md) | Full claim flow + sequence diagrams |
| 6 | [06-yara-integration.md](./06-yara-integration.md) | Module / service / controller / function map |
| 7 | [07-data-mapping.md](./07-data-mapping.md) | YARA field → Fanavaran field |
| 8 | [08-dependencies.md](./08-dependencies.md) | Mongo, files, env, SMS, audit |
| 9 | [09-error-handling.md](./09-error-handling.md) | Errors, retry, backoff, logging |
| 10 | [10-testing.md](./10-testing.md) | Test scenarios, scripts, proven case |
| 11 | [11-onboard-new-client.md](./11-onboard-new-client.md) | Checklist to add a new insurer |
## Related sources (code)
| Path | Role |
|------|------|
| `src/core/config/fanavaran-client.config.ts` | Tenant keys, seed auth + defaults |
| `src/fanavaran/` | Auth, lookup, audit, YARA HTTP surface |
| `src/claim-request-management/claim-request-management.service.ts` | Payload build + staged submit orchestration |
| `src/claim-request-management/fanavaran-policy-selection.ts` | Latest active policy selection |
| `src/lookups/` | Local lookup HTTP façade over Fanavaran |
| `.agents/skills/fanavaran-apis/references/third-party-cases.md` | Agent-oriented implementation rules |
| `docs/external-api-curls.md` | Ready-to-run curl sequences |
| `scripts/fanavaran-auth.sh` | Token helper |
## Architecture (one glance)
```text
YARA claim flow (THIRD_PARTY only)
│
├─ auto / manual stage triggers
│
▼
ClaimRequestManagementService
├─ FanavaranAuthService (GetAppToken → Login, cache until Tehran midnight)
├─ FanavaranLookupService (lookups + insurance-corp resolve)
├─ FanavaranAuditService (fanavaranAuditLogs)
└─ FanavaranClientConfigService (Mongo fanavaranClientConfigs)
│
▼
Fanavaran API Manager
https://apimanager.iraneit.com/BimeApiManager/api
```
## Stages (order)
| Stage | Fanavaran doc | Local ids stored |
|-------|---------------|------------------|
| Auth | EITAuthentication | token cache (`fanavaranAuthTokens`) |
| Policy inquiry | inquiry-my-policies | `fanavaranSync.baseClaim.policyId` |
| Base claim | GEN.03 | `claimId`, `claimNo` |
| Damage case | GEN.12 | `dmgCaseId`, `driverId`, … |
| Attachments | GEN.07 | `fanavaranSync.attachments.files[]` |
| Expertise | GEN.08 | `expertiseId` |
Soft-ensure: later stages create earlier ones if missing (damage → base; expertise → damage → base).
## Secrets policy
This documentation **does not** embed live secrets. Auth values (`appName`, `secret`, `username`, `password`) live in:
1. Mongo collection `fanavaranClientConfigs` (runtime source of truth after boot seed)
2. Seed fallback: `SEED_FANAVARAN_CLIENT_PROFILES` in `fanavaran-client.config.ts`
Use `scripts/fanavaran-auth.sh <client>` or System Settings admin APIs to inspect/update tenant config.
## Activate a tenant
```bash
FANAVARAN_CLIENT=parsian # or tejaratno | moallem
INSURANCE_CORP_ID='...' # Persian caption matching Fanavaran insurance-corp lookup
```
Optional: `CLIENT_ID=8` maps to `parsian` when `FANAVARAN_CLIENT` is unset.

View File

@@ -19,6 +19,7 @@ import {
readOtpExpireMinutesFromEnv,
} from "src/helpers/user-otp-expiry";
import { OtpGeneratorService } from "src/sms-orchestration/otp-generator.service";
import { SmsSendLogService } from "src/sms-orchestration/entities/db-service/sms-send-log.service";
import { UserDbService } from "src/users/entities/db-service/user.db.service";
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
import { HashService } from "src/utils/hash/hash.service";
@@ -38,6 +39,7 @@ export class UserAuthService {
private readonly hashService: HashService,
private readonly otpCreator: OtpGeneratorService,
private readonly smsOrchestrationService: SmsOrchestrationService,
private readonly smsSendLogService: SmsSendLogService,
private readonly userLinkAccessService: UserLinkAccessService,
) {}
@@ -175,6 +177,10 @@ export class UserAuthService {
this.logger.log(
`FAKE_OTP=true — skipped SMS for phone=${mobile} (use OTP ${FAKE_OTP_CODE})`,
);
await this.smsSendLogService.recordSkippedOtp({
receptor: mobile,
otp: FAKE_OTP_CODE,
});
return;
}
const ok = await this.smsOrchestrationService.sendAuthOtp(

View File

@@ -274,6 +274,12 @@ const FANAVARAN_ACCIDENT_LOCATION_ADDRESS = "استان تهران شهر تهر
const FANAVARAN_DEFAULT_ACCIDENT_CAUSE_ID = 6;
const FANAVARAN_DEFAULT_ACCIDENT_LEVEL = 5456;
const FANAVARAN_PROVISIONAL_ESTIMATE_AMOUNT = 1000;
/**
* Fanavaran sometimes rejects missing/empty/whitespace licence numbers.
* We always send either the user-entered value (from DB) or this 10-digit dummy.
*/
const FANAVARAN_DUMMY_LICENCE_NO =
process.env.FANAVARAN_DUMMY_LICENCE_NO?.trim() || "9705463515";
/** Local carType → Fanavaran vehicle-kind Caption keywords for matching. */
const VEHICLE_KIND_KEYWORDS: Record<ClaimVehicleTypeV2, string[]> = {
@@ -3522,6 +3528,58 @@ export class ClaimRequestManagementService {
};
}
/**
* Extract digits-only licence number from a possibly-null/empty value.
* Returns `null` when the value is "null", "undefined", "", '' or whitespace.
*/
private extractNonEmptyLicenceDigits(
value: unknown,
): string | null {
if (value === null || value === undefined) return null;
const asString =
typeof value === "number" ? String(value) : String(value);
const trimmed = asString.trim();
if (!trimmed) return null;
const lowered = trimmed.toLowerCase();
if (lowered === "null" || lowered === "undefined") return null;
const digits = trimmed.replace(/[^\d]/g, "");
return digits ? digits : null;
}
/**
* 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
*/
private resolveFanavaranLicenceNoFromParty(input: {
driverIsInsurer?: boolean;
driverLicense?: unknown;
insurerLicense?: unknown;
}): string {
const { driverIsInsurer, driverLicense, insurerLicense } = input;
// 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 digits = this.extractNonEmptyLicenceDigits(c);
if (digits) return digits;
}
return FANAVARAN_DUMMY_LICENCE_NO;
}
/** Last-resort sanitization for payload values coming from manual overrides. */
private ensureFanavaranNonEmptyLicenceNo(value: unknown): string {
return (
this.extractNonEmptyLicenceDigits(value) ?? FANAVARAN_DUMMY_LICENCE_NO
);
}
private applyFanavaranDefaultFields(result: Record<string, unknown>): void {
result.ActualPremium = null;
result.ArchiveNo = null;
@@ -3536,7 +3594,8 @@ export class ClaimRequestManagementService {
result.CulpritLicenceCountryId = null;
result.CulpritLicenceForeignCityName = null;
result.CulpritLicenceIssuDate = "1394/10/13";
result.CulpritLicenceNo = "1124242";
// Must never be empty/null.
result.CulpritLicenceNo = FANAVARAN_DUMMY_LICENCE_NO;
result.DamagedCount = 1;
result.DmgAssessorFirstCreationTime = null;
result.EntryDate = null;
@@ -4055,7 +4114,12 @@ export class ClaimRequestManagementService {
LicenceCountryId: null,
LicenceForeignCityName: null,
LicenceIssuDate: person.driverBirthday ?? "1394/10/13",
LicenceNo: person.driverLicense ?? "1124242",
// Must never be empty/null; prefer stored DB value, else use dummy.
LicenceNo: this.resolveFanavaranLicenceNoFromParty({
driverIsInsurer: person.driverIsInsurer,
driverLicense: person.driverLicense,
insurerLicense: person.insurerLicense,
}),
LicenceTypeId: input.defaults.CulpritLicenceTypeId,
MotorNo: this.pickPartyInquiryField(inquiryMapped, inquiryRaw, [
"MtrNum",
@@ -6324,6 +6388,10 @@ export class ClaimRequestManagementService {
clientKey,
defaults: profile.defaults,
});
// Guardrail: never send empty/null licence fields even if manual overrides arrive.
payload.LicenceNo = this.ensureFanavaranNonEmptyLicenceNo(payload.LicenceNo);
const url = `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/dmg-cases`;
this.logger.log(
`[executeFanavaranDamageCaseSubmit] claimCaseId=${claimCaseId} claimId=${claimCase.claimId} payload.DriverId=${payload.DriverId ?? "NULL"} payload keys=${Object.keys(payload).join(", ")}`,
@@ -7124,6 +7192,12 @@ export class ClaimRequestManagementService {
auditSession,
persistPayload: true,
}));
// Guardrail: never send empty/null licence fields even if manual overrides arrive.
fanavaranData.CulpritLicenceNo = this.ensureFanavaranNonEmptyLicenceNo(
fanavaranData.CulpritLicenceNo,
);
this.logger.log(
`${logPrefix} Mapped data prepared:`,
JSON.stringify(fanavaranData, null, 2),

View File

@@ -138,7 +138,9 @@ export const SEED_FANAVARAN_CLIENT_PROFILES: Record<
ClaimExpertId: 154,
// GEN.08 — ارزیاب خسارت ثالث مالی (رسول کرکی / شعبه غرب → 29)
ExpertiseClaimExpertId: 29,
ClaimFileTypeId: 70,
// GEN.07 — proven 2026-08-03 (CL68535): 63 = سایر مدارک خسارت in parsian file-types.
// Do not use 70 — not in Parsian lookup → "نوع فايل با منبع لوکاپ مطابقت ندارد".
ClaimFileTypeId: 63,
},
},
moallem: {

View File

@@ -0,0 +1,95 @@
import { SmsSendLogService } from "./sms-send-log.service";
import {
SmsSendLogKind,
SmsSendLogOperation,
SmsSendLogProvider,
SmsSendLogStatus,
} from "../schema/sms-send-log.schema";
describe("SmsSendLogService", () => {
const createMock = jest.fn();
let service: SmsSendLogService;
beforeEach(() => {
createMock.mockReset();
createMock.mockResolvedValue({});
service = new SmsSendLogService({
create: createMock,
} as never);
delete process.env.SMS;
delete process.env.SMS_PROVIDER;
process.env.AUTH_SMS_TEMPLATE = "auth-otp";
process.env.AUTH_SMS_MESSAGE =
"کاربر محترم رمز یکبار مصرف شما:\nCode: {token}";
});
it("records successful verifyLookup OTP with plain-text code and kavenegar sender", async () => {
await service.recordGatewayVerifyLookup({
data: {
receptor: "09120000000",
template: "auth-otp",
token: "54321",
isOtp: true,
},
startedAt: Date.now() - 12,
result: { return: { status: 200 } },
});
expect(createMock).toHaveBeenCalledTimes(1);
const doc = createMock.mock.calls[0][0];
expect(doc).toMatchObject({
receptor: "09120000000",
provider: SmsSendLogProvider.KAVENEGAR,
sender: "10008663",
operation: SmsSendLogOperation.VERIFY_LOOKUP,
status: SmsSendLogStatus.SUCCESS,
kind: SmsSendLogKind.OTP,
isOtp: true,
template: "auth-otp",
tokens: { token: "54321" },
messageText: "کاربر محترم رمز یکبار مصرف شما:\nCode: 54321",
});
expect(doc.providerResponse).toEqual({ return: { status: 200 } });
});
it("records skipped fake OTP as tejaratno provider with plain-text OTP", async () => {
await service.recordSkippedOtp({
receptor: "09121111111",
otp: "12345",
});
expect(createMock).toHaveBeenCalledWith(
expect.objectContaining({
receptor: "09121111111",
provider: SmsSendLogProvider.TEJARATNO,
operation: SmsSendLogOperation.SKIPPED,
status: SmsSendLogStatus.SKIPPED,
kind: SmsSendLogKind.SKIPPED,
isOtp: true,
tokens: { token: "12345" },
messageText: "کاربر محترم رمز یکبار مصرف شما:\nCode: 12345",
}),
);
});
it("records parsian plain send with message text and no sender shortcode", async () => {
process.env.SMS = "parsian";
await service.recordGatewaySend({
data: { receptor: "09123333333", message: "hello party" },
startedAt: Date.now(),
result: { ok: true },
});
expect(createMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: SmsSendLogProvider.PARSIAN,
sender: undefined,
operation: SmsSendLogOperation.SEND,
status: SmsSendLogStatus.SUCCESS,
kind: SmsSendLogKind.PLAIN,
messageText: "hello party",
}),
);
});
});

View File

@@ -0,0 +1,259 @@
import { Injectable, Logger } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { resolveFanavaranClientKey } from "src/core/config/fanavaran-client.config";
import { buildParsianTemplateMessage } from "../../provider/parsian-template-messages";
import {
describeSmsError,
SmsProviderException,
SmsTransportException,
} from "../../provider/sms-provider.exception";
import { getSmsProvider } from "../../provider/sms-provider.config";
import type {
SendMessage,
VerifyLookUpMessage,
} from "../../provider/sms-gateway.types";
import { KAVENEGAR_SMS_SENDER } from "../../provider/kavenegar-sender";
import {
SmsSendLog,
SmsSendLogDocument,
SmsSendLogKind,
SmsSendLogOperation,
SmsSendLogProvider,
SmsSendLogStatus,
} from "../schema/sms-send-log.schema";
export type RecordSmsSendLogInput = {
receptor: string;
provider: SmsSendLogProvider;
sender?: string;
operation: SmsSendLogOperation;
status: SmsSendLogStatus;
kind: SmsSendLogKind;
messageText?: string;
template?: string;
tokens?: {
token?: string;
token2?: string;
token3?: string;
token10?: string;
};
isOtp?: boolean;
tenant?: string;
providerResponse?: unknown;
errorMessage?: string;
errorDetails?: Record<string, unknown>;
durationMs?: number;
};
@Injectable()
export class SmsSendLogService {
private readonly logger = new Logger(SmsSendLogService.name);
constructor(
@InjectModel(SmsSendLog.name)
private readonly smsSendLogModel: Model<SmsSendLogDocument>,
) {}
/** Persist one SMS attempt; never throws (mirrors Fanavaran audit). */
async record(input: RecordSmsSendLogInput): Promise<void> {
try {
await this.smsSendLogModel.create({
receptor: input.receptor,
provider: input.provider,
sender: input.sender,
operation: input.operation,
status: input.status,
kind: input.kind,
messageText: input.messageText,
template: input.template,
tokens: input.tokens,
isOtp: input.isOtp,
tenant: input.tenant ?? resolveFanavaranClientKey(),
providerResponse: input.providerResponse,
errorMessage: input.errorMessage,
errorDetails: input.errorDetails,
durationMs: input.durationMs,
});
} catch (error) {
this.logger.error(
`Failed to persist SMS send log receptor=${input.receptor} status=${input.status}`,
error,
);
}
}
activeGatewayProvider(): SmsSendLogProvider {
return getSmsProvider() === "parsian"
? SmsSendLogProvider.PARSIAN
: SmsSendLogProvider.KAVENEGAR;
}
senderForProvider(provider: SmsSendLogProvider): string | undefined {
if (provider === SmsSendLogProvider.KAVENEGAR) {
return KAVENEGAR_SMS_SENDER;
}
return undefined;
}
isAuthOtp(data: Pick<VerifyLookUpMessage, "template" | "isOtp">): boolean {
if (data.isOtp === true) {
return true;
}
const auth = process.env.AUTH_SMS_TEMPLATE?.trim();
return Boolean(auth && data.template && data.template === auth);
}
resolveMessageTextForLookup(data: VerifyLookUpMessage): string {
return buildParsianTemplateMessage(data);
}
async recordGatewaySend(params: {
data: SendMessage;
startedAt: number;
result?: unknown;
error?: unknown;
}): Promise<void> {
const provider = this.activeGatewayProvider();
const durationMs = Date.now() - params.startedAt;
if (params.error == null) {
await this.record({
receptor: params.data.receptor,
provider,
sender: this.senderForProvider(provider),
operation: SmsSendLogOperation.SEND,
status: SmsSendLogStatus.SUCCESS,
kind: SmsSendLogKind.PLAIN,
messageText: params.data.message,
providerResponse: params.result,
durationMs,
});
return;
}
await this.record({
receptor: params.data.receptor,
provider,
sender: this.senderForProvider(provider),
operation: SmsSendLogOperation.SEND,
status: SmsSendLogStatus.FAILURE,
kind: SmsSendLogKind.PLAIN,
messageText: params.data.message,
...this.errorFields(params.error),
durationMs,
});
}
async recordGatewayVerifyLookup(params: {
data: VerifyLookUpMessage;
startedAt: number;
result?: unknown;
error?: unknown;
}): Promise<void> {
const provider = this.activeGatewayProvider();
const durationMs = Date.now() - params.startedAt;
const isOtp = this.isAuthOtp(params.data);
const tokens = {
token: params.data.token,
...(params.data.token2 != null ? { token2: params.data.token2 } : {}),
...(params.data.token3 != null ? { token3: params.data.token3 } : {}),
...(params.data.token10 != null ? { token10: params.data.token10 } : {}),
};
const messageText = this.resolveMessageTextForLookup(params.data);
const kind = isOtp ? SmsSendLogKind.OTP : SmsSendLogKind.TEMPLATE;
if (params.error == null) {
await this.record({
receptor: params.data.receptor,
provider,
sender: this.senderForProvider(provider),
operation: SmsSendLogOperation.VERIFY_LOOKUP,
status: SmsSendLogStatus.SUCCESS,
kind,
messageText,
template: params.data.template,
tokens,
isOtp,
providerResponse: params.result,
durationMs,
});
return;
}
await this.record({
receptor: params.data.receptor,
provider,
sender: this.senderForProvider(provider),
operation: SmsSendLogOperation.VERIFY_LOOKUP,
status: SmsSendLogStatus.FAILURE,
kind,
messageText,
template: params.data.template,
tokens,
isOtp,
...this.errorFields(params.error),
durationMs,
});
}
/** FAKE_OTP / local path — no external gateway call. */
async recordSkippedOtp(params: {
receptor: string;
otp: string;
reason?: string;
}): Promise<void> {
const authMessage =
process.env.AUTH_SMS_MESSAGE?.trim() ??
"کاربر محترم رمز یکبار مصرف شما:\nCode: {token}";
const messageText = authMessage.split("{token}").join(params.otp);
await this.record({
receptor: params.receptor,
provider: SmsSendLogProvider.TEJARATNO,
operation: SmsSendLogOperation.SKIPPED,
status: SmsSendLogStatus.SKIPPED,
kind: SmsSendLogKind.SKIPPED,
messageText,
template: process.env.AUTH_SMS_TEMPLATE?.trim() || undefined,
tokens: { token: params.otp },
isOtp: true,
errorMessage: params.reason ?? "FAKE_OTP enabled — SMS provider not called",
});
}
private errorFields(error: unknown): {
errorMessage: string;
errorDetails?: Record<string, unknown>;
providerResponse?: unknown;
} {
if (error instanceof SmsProviderException) {
return {
errorMessage: describeSmsError(error),
providerResponse: error.meta.providerBody,
errorDetails: {
operation: error.operation,
normalized: error.meta.normalized,
},
};
}
if (error instanceof SmsTransportException) {
return {
errorMessage: describeSmsError(error),
errorDetails: {
operation: error.operation,
cause:
error.causeUnknown instanceof Error
? {
name: error.causeUnknown.name,
message: error.causeUnknown.message,
}
: error.causeUnknown,
},
};
}
return {
errorMessage: describeSmsError(error),
};
}
}

View File

@@ -0,0 +1,119 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";
/** Who dispatched the SMS: external gateways, or tejaratno when sent/skipped locally. */
export enum SmsSendLogProvider {
KAVENEGAR = "kavenegar",
PARSIAN = "parsian",
TEJARATNO = "tejaratno",
}
export enum SmsSendLogOperation {
SEND = "send",
VERIFY_LOOKUP = "verifyLookup",
SKIPPED = "skipped",
}
export enum SmsSendLogStatus {
SUCCESS = "success",
FAILURE = "failure",
SKIPPED = "skipped",
}
export enum SmsSendLogKind {
OTP = "otp",
TEMPLATE = "template",
PLAIN = "plain",
SKIPPED = "skipped",
}
@Schema({ collection: "smsSendLogs", timestamps: true, versionKey: false })
export class SmsSendLog {
/** Destination mobile number. */
@Prop({ type: String, required: true, index: true })
receptor: string;
/** SMS dispatcher: kavenegar, parsian, or tejaratno (local/skipped). */
@Prop({
type: String,
required: true,
enum: SmsSendLogProvider,
index: true,
})
provider: SmsSendLogProvider;
/** Originating shortcode/number when the provider exposes one (e.g. Kavenegar). */
@Prop({ type: String, required: false })
sender?: string;
@Prop({
type: String,
required: true,
enum: SmsSendLogOperation,
index: true,
})
operation: SmsSendLogOperation;
@Prop({
type: String,
required: true,
enum: SmsSendLogStatus,
index: true,
})
status: SmsSendLogStatus;
@Prop({
type: String,
required: true,
enum: SmsSendLogKind,
index: true,
})
kind: SmsSendLogKind;
/** Full message body when known (OTP kept as plain text). */
@Prop({ type: String, required: false })
messageText?: string;
/** Kavenegar/Parsian template name for verifyLookup sends. */
@Prop({ type: String, required: false, index: true })
template?: string;
/**
* Template tokens as sent (OTP is `token` for auth — stored plain text on purpose).
*/
@Prop({ type: Object, required: false })
tokens?: {
token?: string;
token2?: string;
token3?: string;
token10?: string;
};
/** True when this send is an auth OTP (or a skipped fake OTP). */
@Prop({ type: Boolean, required: false, index: true })
isOtp?: boolean;
/** Deployment Fanavaran tenant when known (parsian / tejaratno / moallem). */
@Prop({ type: String, required: false, index: true })
tenant?: string;
/** Raw provider API response body (success or rejection). */
@Prop({ type: Object, required: false })
providerResponse?: unknown;
@Prop({ type: String, required: false })
errorMessage?: string;
@Prop({ type: Object, required: false })
errorDetails?: Record<string, unknown>;
@Prop({ type: Number, required: false })
durationMs?: number;
}
export type SmsSendLogDocument = HydratedDocument<SmsSendLog>;
export const SmsSendLogSchema = SchemaFactory.createForClass(SmsSendLog);
SmsSendLogSchema.index({ receptor: 1, createdAt: -1 });
SmsSendLogSchema.index({ provider: 1, createdAt: -1 });
SmsSendLogSchema.index({ isOtp: 1, createdAt: -1 });

View File

@@ -0,0 +1,2 @@
/** Kavenegar shortcode used on plain `send` (and recorded on SMS logs). */
export const KAVENEGAR_SMS_SENDER = "10008663";

View File

@@ -11,6 +11,7 @@ import {
SmsProviderException,
SmsTransportException,
} from "./sms-provider.exception";
import { KAVENEGAR_SMS_SENDER } from "./kavenegar-sender";
import { SendMessage, VerifyLookUpMessage } from "./sms-gateway.types";
@Injectable()
@@ -22,7 +23,7 @@ export class KavenegarSmsGateway {
async sendMessage(data: SendMessage) {
try {
const body = await this.sender.send({
sender: "10008663",
sender: KAVENEGAR_SMS_SENDER,
...data,
});
@@ -48,7 +49,8 @@ export class KavenegarSmsGateway {
async verifyLookUp(data: VerifyLookUpMessage) {
try {
const body = await this.sender.verifyLookup(data);
const { isOtp: _isOtp, ...providerPayload } = data;
const body = await this.sender.verifyLookup(providerPayload);
if (!isKavenegarSuccess(body)) {
const normalized = normalizeKavenegarBody(body);

View File

@@ -1,7 +1,13 @@
import { HttpModule } from "@nestjs/axios";
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { MongooseModule } from "@nestjs/mongoose";
import { createHttpModuleOptions } from "src/core/config/http-proxy.factory";
import { SmsSendLogService } from "../entities/db-service/sms-send-log.service";
import {
SmsSendLog,
SmsSendLogSchema,
} from "../entities/schema/sms-send-log.schema";
import { KavenegarService } from "./kavenegar.service";
import { KavenegarSmsGateway } from "./kavenegar-sms.gateway";
@@ -16,13 +22,17 @@ import { SmsGatewayService } from "./sms-gateway.service";
useFactory: createHttpModuleOptions,
}),
ConfigModule,
MongooseModule.forFeature([
{ name: SmsSendLog.name, schema: SmsSendLogSchema },
]),
],
providers: [
KavenegarService,
KavenegarSmsGateway,
ParsianSmsGateway,
SmsSendLogService,
SmsGatewayService,
],
exports: [SmsGatewayService],
exports: [SmsGatewayService, SmsSendLogService],
})
export class SmsGatewayModule {}

View File

@@ -1,4 +1,5 @@
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
import { SmsSendLogService } from "../entities/db-service/sms-send-log.service";
import { KavenegarSmsGateway } from "./kavenegar-sms.gateway";
import { ParsianSmsGateway } from "./parsian-sms.gateway";
import { getSmsProvider } from "./sms-provider.config";
@@ -13,6 +14,7 @@ export class SmsGatewayService implements OnModuleInit {
constructor(
private readonly kavenegar: KavenegarSmsGateway,
private readonly parsian: ParsianSmsGateway,
private readonly smsSendLogService: SmsSendLogService,
) {}
onModuleInit() {
@@ -20,11 +22,43 @@ export class SmsGatewayService implements OnModuleInit {
}
async sendMessage(data: SendMessage) {
return this.activeGateway().sendMessage(data);
const startedAt = Date.now();
try {
const result = await this.activeGateway().sendMessage(data);
await this.smsSendLogService.recordGatewaySend({
data,
startedAt,
result,
});
return result;
} catch (error) {
await this.smsSendLogService.recordGatewaySend({
data,
startedAt,
error,
});
throw error;
}
}
async verifyLookUp(data: VerifyLookUpMessage) {
return this.activeGateway().verifyLookUp(data);
const startedAt = Date.now();
try {
const result = await this.activeGateway().verifyLookUp(data);
await this.smsSendLogService.recordGatewayVerifyLookup({
data,
startedAt,
result,
});
return result;
} catch (error) {
await this.smsSendLogService.recordGatewayVerifyLookup({
data,
startedAt,
error,
});
throw error;
}
}
private activeGateway(): KavenegarSmsGateway | ParsianSmsGateway {

View File

@@ -10,4 +10,6 @@ export interface VerifyLookUpMessage {
token3?: string;
token10?: string;
receptor: string;
/** When true, SMS log records this as an OTP (plain-text token preserved). */
isOtp?: boolean;
}

View File

@@ -20,6 +20,10 @@ import { createHttpModuleOptions } from "src/core/config/http-proxy.factory";
MongooseModule.forFeature([{ name: SmsText.name, schema: SmsTextSchema }]),
],
providers: [SmsTextDbService, SmsOrchestrationService, OtpGeneratorService],
exports: [SmsOrchestrationService, OtpGeneratorService],
exports: [
SmsOrchestrationService,
OtpGeneratorService,
SmsGatewayModule,
],
})
export class SmsOrchestrationModule {}

View File

@@ -11,6 +11,7 @@ type TemplateArgs = {
token2?: string;
token3?: string;
token10?: string;
isOtp?: boolean;
};
@Injectable()
@@ -80,6 +81,7 @@ export class SmsOrchestrationService implements OnModuleInit {
template: template || process.env.AUTH_SMS_TEMPLATE || "",
receptor: mobile,
token: otp,
isOtp: true,
});
}