Merge pull request 'main' (#162) from s.hajizadeh/yara724api:main into main

Reviewed-on: Yara724/api#162
This commit is contained in:
2026-06-30 11:52:40 +03:30
5 changed files with 501 additions and 76 deletions

View File

@@ -1,7 +1,9 @@
# External API Curl Guide
This file documents outbound HTTP calls made by the app or maintenance scripts.
Use placeholder values for secrets, tokens, IDs, plate values, and payloads before running any curl.
Client credentials that are hardcoded in the codebase are filled in below. Keep
placeholders only for runtime data such as national codes, plate values, tokens
returned by login calls, and payload files.
## Common Notes
@@ -32,29 +34,73 @@ Used by:
2. Login with `appToken` to get `authenticationToken`.
3. Call lookup, policy inquiry, or submit endpoint with `authenticationToken`, `CorpId`, `ContractId`, and `Location`.
### Auth Script
Use this when you only need fresh Fanavaran tokens and do not want to copy the
curl commands manually:
```sh
scripts/fanavaran-auth.sh tejaratno
scripts/fanavaran-auth.sh parsian
```
The script stores raw responses and reusable variables under
`files/fanavaran-auth/<client>/`. For example:
```sh
source files/fanavaran-auth/tejaratno/tokens.env
curl -X GET "$FANAVARAN_BIME_URL/car/base-info/accident-causes" \
-H "authenticationToken: $AUTHENTICATION_TOKEN" \
-H "CorpId: $CORP_ID" \
-H "ContractId: $CONTRACT_ID" \
-H "Location: $LOCATION" \
-H "Content-Type: application/json"
```
### Tenant Credentials
The app supports these Fanavaran client profiles:
| Client | appname | secret | userName | password | CorpId | ContractId | Location |
| --- | --- | --- | --- | --- | --- | --- | --- |
| tejaratno | from `src/core/config/fanavaran-client.config.ts` | hardcoded in source | hardcoded in source | hardcoded in source | `3539` | `263` | `100` |
| parsian | from `src/core/config/fanavaran-client.config.ts` | hardcoded in source | hardcoded in source | hardcoded in source | `543` | `28` | `210050` |
| tejaratno | `fanhab` | `5Fa@N#A2B` | `fanhabUser` | `Fan#@2U$3er` | `3539` | `263` | `100` |
| parsian | `ParsianService` | `P@r30@n$erv!ce` | `ParsianServiceUser` | `P@r30@n123` | `543` | `28` | `210050` |
Set them as shell variables before running:
Set the client variables before running. These values come from
`src/core/config/fanavaran-client.config.ts` and match
`src/claim-request-management/claim-request-management.service.ts`.
Tejaratno:
```sh
APP_NAME="<appname>"
APP_SECRET="<secret>"
FANAVARAN_USERNAME="<userName>"
FANAVARAN_PASSWORD="<password>"
CORP_ID="<CorpId>"
CONTRACT_ID="<ContractId>"
LOCATION="<Location>"
FANAVARAN_CLIENT="tejaratno"
APP_NAME='fanhab'
APP_SECRET='5Fa@N#A2B'
FANAVARAN_USERNAME='fanhabUser'
FANAVARAN_PASSWORD='Fan#@2U$3er'
CORP_ID='3539'
CONTRACT_ID='263'
LOCATION='100'
```
Parsian:
```sh
FANAVARAN_CLIENT="parsian"
APP_NAME='ParsianService'
APP_SECRET='P@r30@n$erv!ce'
FANAVARAN_USERNAME='ParsianServiceUser'
FANAVARAN_PASSWORD='P@r30@n123'
CORP_ID='543'
CONTRACT_ID='28'
LOCATION='210050'
```
### 1. Get App Token
The app sends an empty string body, deletes `Content-Type`, and keeps
`Content-Length: 0`.
```sh
curl -i -X POST "$FANAVARAN_BASE_URL/EITAuthentication/GetAppToken" \
-H "appname: $APP_NAME" \
@@ -65,11 +111,21 @@ curl -i -X POST "$FANAVARAN_BASE_URL/EITAuthentication/GetAppToken" \
The token is returned in a response header named `appToken` or `apptoken`.
```sh
APP_TOKEN="<response appToken header>"
curl -sS -D /tmp/fanavaran-app-token.headers -o /tmp/fanavaran-app-token.body \
-X POST "$FANAVARAN_BASE_URL/EITAuthentication/GetAppToken" \
-H "appname: $APP_NAME" \
-H "secret: $APP_SECRET" \
-H "Content-Length: 0"
APP_TOKEN="$(awk -F': ' 'tolower($1)=="apptoken" {gsub(/\r/,"",$2); print $2}' /tmp/fanavaran-app-token.headers)"
printf 'APP_TOKEN=%s\n' "$APP_TOKEN"
```
### 2. Login
Use the `APP_TOKEN` returned by the immediately previous GetAppToken request.
Do not reuse an old app token pasted from logs or another machine; the app does
not do that.
```sh
curl -i -X POST "$FANAVARAN_BASE_URL/EITAuthentication/Login" \
-H "appToken: $APP_TOKEN" \
@@ -81,7 +137,95 @@ curl -i -X POST "$FANAVARAN_BASE_URL/EITAuthentication/Login" \
The token is returned in a response header or body field named `authenticationToken`, `authenticationtoken`, or `authentication_token`.
```sh
AUTHENTICATION_TOKEN="<response authenticationToken>"
curl -sS -D /tmp/fanavaran-login.headers -o /tmp/fanavaran-login.body \
-X POST "$FANAVARAN_BASE_URL/EITAuthentication/Login" \
-H "appToken: $APP_TOKEN" \
-H "userName: $FANAVARAN_USERNAME" \
-H "password: $FANAVARAN_PASSWORD" \
-H "Content-Length: 0"
AUTHENTICATION_TOKEN="$(awk -F': ' 'tolower($1)=="authenticationtoken" {gsub(/\r/,"",$2); print $2}' /tmp/fanavaran-login.headers)"
if [ -z "$AUTHENTICATION_TOKEN" ]; then
AUTHENTICATION_TOKEN="$(node -e 'const fs=require("fs"); const body=fs.readFileSync("/tmp/fanavaran-login.body","utf8"); try { const json=JSON.parse(body); console.log(json.authenticationToken || json.authenticationtoken || json.authentication_token || ""); } catch { console.log(""); }')"
fi
printf 'AUTHENTICATION_TOKEN=%s\n' "$AUTHENTICATION_TOKEN"
```
If login returns `نام کاربر یا رمز عبور صحیح نیست` for `tejaratno`, first check
that `APP_TOKEN` was generated with `appname: fanhab` and `secret: 5Fa@N#A2B` in
the same sequence. The runtime code calls `GetAppToken` first, then passes that
fresh header value to `Login`; it does not use a static value such as
`182197f6-7b41-47b4-9b18-5bfcbc027f2e`.
### Ready-To-Run Auth Sequences
Tejaratno:
```sh
FANAVARAN_BASE_URL="https://apimanager.iraneit.com/BimeApiManager/api"
FANAVARAN_BIME_URL="$FANAVARAN_BASE_URL/BimeApi/v2.0"
FANAVARAN_CLIENT="tejaratno"
APP_NAME='fanhab'
APP_SECRET='5Fa@N#A2B'
FANAVARAN_USERNAME='fanhabUser'
FANAVARAN_PASSWORD='Fan#@2U$3er'
CORP_ID='3539'
CONTRACT_ID='263'
LOCATION='100'
curl -sS -D /tmp/fanavaran-app-token.headers -o /tmp/fanavaran-app-token.body \
-X POST "$FANAVARAN_BASE_URL/EITAuthentication/GetAppToken" \
-H "appname: $APP_NAME" \
-H "secret: $APP_SECRET" \
-H "Content-Length: 0"
APP_TOKEN="$(awk -F': ' 'tolower($1)=="apptoken" {gsub(/\r/,"",$2); print $2}' /tmp/fanavaran-app-token.headers)"
curl -sS -D /tmp/fanavaran-login.headers -o /tmp/fanavaran-login.body \
-X POST "$FANAVARAN_BASE_URL/EITAuthentication/Login" \
-H "appToken: $APP_TOKEN" \
-H "userName: $FANAVARAN_USERNAME" \
-H "password: $FANAVARAN_PASSWORD" \
-H "Content-Length: 0"
AUTHENTICATION_TOKEN="$(awk -F': ' 'tolower($1)=="authenticationtoken" {gsub(/\r/,"",$2); print $2}' /tmp/fanavaran-login.headers)"
if [ -z "$AUTHENTICATION_TOKEN" ]; then
AUTHENTICATION_TOKEN="$(node -e 'const fs=require("fs"); const body=fs.readFileSync("/tmp/fanavaran-login.body","utf8"); try { const json=JSON.parse(body); console.log(json.authenticationToken || json.authenticationtoken || json.authentication_token || ""); } catch { console.log(""); }')"
fi
printf 'APP_TOKEN=%s\nAUTHENTICATION_TOKEN=%s\n' "$APP_TOKEN" "$AUTHENTICATION_TOKEN"
```
Parsian:
```sh
FANAVARAN_BASE_URL="https://apimanager.iraneit.com/BimeApiManager/api"
FANAVARAN_BIME_URL="$FANAVARAN_BASE_URL/BimeApi/v2.0"
FANAVARAN_CLIENT="parsian"
APP_NAME='ParsianService'
APP_SECRET='P@r30@n$erv!ce'
FANAVARAN_USERNAME='ParsianServiceUser'
FANAVARAN_PASSWORD='P@r30@n123'
CORP_ID='543'
CONTRACT_ID='28'
LOCATION='210050'
curl -sS -D /tmp/fanavaran-app-token.headers -o /tmp/fanavaran-app-token.body \
-X POST "$FANAVARAN_BASE_URL/EITAuthentication/GetAppToken" \
-H "appname: $APP_NAME" \
-H "secret: $APP_SECRET" \
-H "Content-Length: 0"
APP_TOKEN="$(awk -F': ' 'tolower($1)=="apptoken" {gsub(/\r/,"",$2); print $2}' /tmp/fanavaran-app-token.headers)"
curl -sS -D /tmp/fanavaran-login.headers -o /tmp/fanavaran-login.body \
-X POST "$FANAVARAN_BASE_URL/EITAuthentication/Login" \
-H "appToken: $APP_TOKEN" \
-H "userName: $FANAVARAN_USERNAME" \
-H "password: $FANAVARAN_PASSWORD" \
-H "Content-Length: 0"
AUTHENTICATION_TOKEN="$(awk -F': ' 'tolower($1)=="authenticationtoken" {gsub(/\r/,"",$2); print $2}' /tmp/fanavaran-login.headers)"
if [ -z "$AUTHENTICATION_TOKEN" ]; then
AUTHENTICATION_TOKEN="$(node -e 'const fs=require("fs"); const body=fs.readFileSync("/tmp/fanavaran-login.body","utf8"); try { const json=JSON.parse(body); console.log(json.authenticationToken || json.authenticationtoken || json.authentication_token || ""); } catch { console.log(""); }')"
fi
printf 'APP_TOKEN=%s\nAUTHENTICATION_TOKEN=%s\n' "$APP_TOKEN" "$AUTHENTICATION_TOKEN"
```
### 3A. Lookup Endpoints
@@ -167,6 +311,8 @@ Default base URL:
```sh
TEJARAT_INQUIRY_BASE_URL="${TEJARAT_INQUIRY_BASE_URL:-http://82.99.202.245:3027}"
TEJARAT_INQUIRY_EMAIL='xxx@example.com'
TEJARAT_INQUIRY_PASSWORD='123321'
```
Used by `src/sand-hub/sand-hub.service.ts` for third-party plate and car-body plate inquiries when ESG is not selected.
@@ -311,8 +457,10 @@ Used by `src/sand-hub/sand-hub.service.ts` for legacy inquiry flows.
Environment:
```sh
SANHUB_URL_LOGIN="<login URL>"
SANHUB_BASE_URL="<base URL>"
SANHUB_BASE_URL='http://82.99.202.245:3027'
SANHUB_URL_LOGIN='http://82.99.202.245:3027/user/login'
SANHUB_USERNAME='default@admin.com'
SANHUB_PASSWORD='123321'
```
### Sequence
@@ -415,7 +563,7 @@ curl -X POST "$SANHUB_BASE_URL/sheba/sheba-tejaratno" \
Used by `src/claim-request-management/claim-request-management.service.ts`.
```sh
MAP_IR_API_KEY="<x-api-key>"
MAP_IR_API_KEY='eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImI5ZDZjMThkNDRjZjc2OWI2Yjk1ODcyMGFjYmEzMmRiN2NhZjg0Zjk4OTRlMjZiZDg0Yzg3YjVlMzhlMTAyZDlkMWYxOGM5NjNmOTk4YjY2In0.eyJhdWQiOiIyMTcxOCIsImp0aSI6ImI5ZDZjMThkNDRjZjc2OWI2Yjk1ODcyMGFjYmEzMmRiN2NhZjg0Zjk4OTRlMjZiZDg0Yzg3YjVlMzhlMTAyZDlkMWYxOGM5NjNmOTk4YjY2IiwiaWF0IjoxNjgwNjA4NTkxLCJuYmYiOjE2ODA2MDg1OTEsImV4cCI6MTY4MzIwMDU5MSwic3ViIjoiIiwic2NvcGVzIjpbImJhc2ljIl19.rTviLd8b5yTHUDa3ODZyva593eMnL0d3XPg3sKkZxMOf_jNIH6lFQyIfbId-wsd1EAdsOdsL3CME_Y8t332PWJbxMNgnEq4Rf2IkClkvkSx6Sb5_4bmlhBM75zw2SmccvgbFUn4xkTOw0FT4vABC2Y3-MKctjMpmO8QOrVULSKt4psrmQhr7hBu7YRDnAAEc6muZ1VpRvdB1kqNKddoSIrfDaq6aDRJ-BNbGRAaFFvP_kH4cgSCKV4dU0TknL3mRKUiVy6_TDkjtzAN8fE2wsdvNo2pGTJPzKFsR2ipgGNTvB__g3bOnVpKsgFXPBH0e_Qa7ff1tZ3VGWy3jRNh9Lg'
LAT="35.6892"
LON="51.3890"
@@ -431,7 +579,7 @@ curl -X GET "https://map.ir/fast-reverse?lat=$LAT&lon=$LON" \
Used by `src/sms-orchestration/provider/kavenegar.service.ts`.
```sh
SMS_API_KEY="<kavenegar API key>"
SMS_API_KEY='75776C717969412B4B52306A5956462F4A714E6F6C65544D6A2B654B7566786E'
KAVENEGAR_BASE_URL="https://api.kavenegar.com/v1/$SMS_API_KEY"
```
@@ -462,9 +610,9 @@ Used by `src/sms-orchestration/provider/parsian-sms.gateway.ts`.
`PARSIAN_SMS_URL` is expected to already include the provider URL prefix and query key before receptor. The app appends `=<receptor>&Message=<encoded message>`.
```sh
PARSIAN_SMS_URL="<provider URL prefix>"
PARSIAN_API_KEY="<package API key>"
PARSIAN_BASIC_TOKEN="<basic token>"
PARSIAN_SMS_URL='https://apigateway.parsianinsurance.com/api/SendSMS?ReceiverNumbers'
PARSIAN_API_KEY='be988c9c-dbd6-494e-9c04-944ecc6426bf'
PARSIAN_BASIC_TOKEN='UGFyc2lhbkFQSTpQYXJzaWFuQHBpMjI='
RECEPTOR="09120000000"
MESSAGE="Hello"
@@ -494,7 +642,9 @@ curl -X GET "${CW_URL}price?hamrah"
3. `POST $AI_URL_V2/services/car-damage/detector?version=ai-v7`
```sh
AI_URL_V2="<AI service base URL>"
AI_URL_V2='https://ai-gw.ittalie.ir'
AI_USERNAME='yara@gmail.io'
AI_PASSWORD='123321'
curl -X POST "$AI_URL_V2/auth/login" \
-H "Content-Type: application/json" \

143
scripts/fanavaran-auth.sh Executable file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage:
scripts/fanavaran-auth.sh <tejaratno|parsian>
Calls Fanavaran GetAppToken, then Login with the returned appToken.
Outputs the appToken and authenticationToken, and writes raw responses to:
files/fanavaran-auth/<client>/
Optional:
FANAVARAN_BASE_URL can override the default API Manager base URL.
USAGE
}
client="${1:-}"
if [[ -z "$client" || "$client" == "-h" || "$client" == "--help" ]]; then
usage
exit 0
fi
case "$client" in
tejaratno)
app_name='fanhab'
app_secret='5Fa@N#A2B'
fanavaran_username='fanhabUser'
fanavaran_password='Fan#@2U$3er'
corp_id='3539'
contract_id='263'
location='100'
;;
parsian)
app_name='ParsianService'
app_secret='P@r30@n$erv!ce'
fanavaran_username='ParsianServiceUser'
fanavaran_password='P@r30@n123'
corp_id='543'
contract_id='28'
location='210050'
;;
*)
printf 'Unknown Fanavaran client: %s\n\n' "$client" >&2
usage >&2
exit 1
;;
esac
base_url="${FANAVARAN_BASE_URL:-https://apimanager.iraneit.com/BimeApiManager/api}"
auth_dir="files/fanavaran-auth/$client"
mkdir -p "$auth_dir"
app_token_headers="$auth_dir/get-app-token.headers"
app_token_body="$auth_dir/get-app-token.body.json"
login_headers="$auth_dir/login.headers"
login_body="$auth_dir/login.body.json"
tokens_file="$auth_dir/tokens.env"
extract_header() {
local header_name="$1"
local header_file="$2"
awk -F': ' -v wanted="$header_name" '
tolower($1) == tolower(wanted) {
gsub(/\r/, "", $2)
print $2
exit
}
' "$header_file"
}
extract_authentication_token_from_body() {
local body_file="$1"
node -e '
const fs = require("fs");
const path = process.argv[1];
const body = fs.existsSync(path) ? fs.readFileSync(path, "utf8") : "";
try {
const json = JSON.parse(body || "{}");
console.log(json.authenticationToken || json.authenticationtoken || json.authentication_token || "");
} catch {
console.log("");
}
' "$body_file"
}
printf 'Fanavaran client: %s\n' "$client"
printf 'Base URL: %s\n\n' "$base_url"
printf '1. Calling GetAppToken...\n'
curl -sS -D "$app_token_headers" -o "$app_token_body" \
-X POST "$base_url/EITAuthentication/GetAppToken" \
-H "appname: $app_name" \
-H "secret: $app_secret" \
-H "Content-Length: 0"
app_token="$(extract_header "apptoken" "$app_token_headers")"
if [[ -z "$app_token" ]]; then
printf 'Failed to extract appToken from %s\n' "$app_token_headers" >&2
printf 'Response body is saved at %s\n' "$app_token_body" >&2
exit 1
fi
printf '2. Calling Login...\n'
curl -sS -D "$login_headers" -o "$login_body" \
-X POST "$base_url/EITAuthentication/Login" \
-H "appToken: $app_token" \
-H "userName: $fanavaran_username" \
-H "password: $fanavaran_password" \
-H "Content-Length: 0"
authentication_token="$(extract_header "authenticationtoken" "$login_headers")"
if [[ -z "$authentication_token" ]]; then
authentication_token="$(extract_authentication_token_from_body "$login_body")"
fi
if [[ -z "$authentication_token" ]]; then
printf 'Failed to extract authenticationToken from login response.\n' >&2
printf 'Headers: %s\n' "$login_headers" >&2
printf 'Body: %s\n' "$login_body" >&2
exit 1
fi
cat > "$tokens_file" <<TOKENS
FANAVARAN_CLIENT='$client'
FANAVARAN_BASE_URL='$base_url'
FANAVARAN_BIME_URL='$base_url/BimeApi/v2.0'
APP_TOKEN='$app_token'
AUTHENTICATION_TOKEN='$authentication_token'
CORP_ID='$corp_id'
CONTRACT_ID='$contract_id'
LOCATION='$location'
TOKENS
printf '\nDone.\n'
printf 'APP_TOKEN=%s\n' "$app_token"
printf 'AUTHENTICATION_TOKEN=%s\n' "$authentication_token"
printf 'CORP_ID=%s\n' "$corp_id"
printf 'CONTRACT_ID=%s\n' "$contract_id"
printf 'LOCATION=%s\n' "$location"
printf '\nSaved token variables: %s\n' "$tokens_file"
printf 'Saved raw GetAppToken response: %s, %s\n' "$app_token_headers" "$app_token_body"
printf 'Saved raw Login response: %s, %s\n' "$login_headers" "$login_body"

View File

@@ -176,6 +176,7 @@ import {
FanavaranAuditStatus,
FanavaranAuditStep,
} from "src/fanavaran/schema/fanavaran-audit-log.schema";
import { selectLatestActiveFanavaranPolicy } from "./fanavaran-policy-selection";
export interface FanavaranAutoSubmitResult {
attempted: boolean;
@@ -3438,17 +3439,13 @@ export class ClaimRequestManagementService {
this.applyFanavaranDefaultFields(result);
try {
const policyId = await input.resolvePolicyId();
if (policyId !== undefined && policyId !== null) {
result.PolicyId = policyId;
}
} catch (error) {
this.logger.error(
`${input.logPrefix} Failed to get PolicyId from external API:`,
error,
if (policyId === undefined || policyId === null) {
throw new BadRequestException(
`${input.logPrefix} PolicyId is required for Fanavaran submit. Policy inquiry returned no valid policy; contact the administrator.`,
);
}
result.PolicyId = policyId;
return result;
}
@@ -3567,15 +3564,18 @@ export class ClaimRequestManagementService {
blameRequest,
claimRequestId,
);
if (policyId) {
result.PolicyId = policyId;
if (!policyId) {
throw new BadRequestException(
`[Fanavaran Submit] PolicyId is required for Fanavaran submit. Policy inquiry returned no valid policy; contact the administrator.`,
);
}
result.PolicyId = policyId;
} catch (error) {
this.logger.error(
`[Fanavaran Submit] Failed to get PolicyId from external API:`,
error,
);
// Continue without PolicyId if API call fails
throw error;
}
return result;
@@ -3866,11 +3866,7 @@ export class ClaimRequestManagementService {
}),
);
const policyCount = Array.isArray(response.data)
? response.data.length
: 0;
const lastPolicy =
policyCount > 0 ? response.data[policyCount - 1] : null;
const policyCount = Array.isArray(response.data) ? response.data.length : 0;
this.logger.log(
`${logPrefix} Policy inquiry response status=${response.status} count=${policyCount}`,
@@ -3882,20 +3878,11 @@ export class ClaimRequestManagementService {
2,
)}`,
);
if (lastPolicy && typeof lastPolicy === "object") {
const selectedPolicy = selectLatestActiveFanavaranPolicy(response.data);
this.logger.log(
`${logPrefix} Policy inquiry last policy keys: ${Object.keys(
lastPolicy,
).join(", ")}`,
`${logPrefix} Selected latest active policy PolicyId=${selectedPolicy.policyId} EndDate=${selectedPolicy.endDate}`,
);
}
if (Array.isArray(response.data) && response.data.length > 0) {
const policyId = lastPolicy?.PolicyId;
if (policyId !== undefined && policyId !== null) {
this.logger.log(
`${logPrefix} Successfully retrieved PolicyId from last policy entry: ${policyId}`,
);
if (auditSession) {
await this.fanavaranAuditService.recordStep({
session: auditSession,
@@ -3904,34 +3891,15 @@ export class ClaimRequestManagementService {
requestUrl: policyInquiryUrl,
httpStatus: response.status,
responseMeta: {
policyId,
policyCount: response.data.length,
policyId: selectedPolicy.policyId,
policyEndDate: selectedPolicy.endDate,
policyEndDateGregorian: selectedPolicy.endDateGregorian,
policyCount,
},
durationMs: Date.now() - startedAt,
});
}
return policyId;
}
}
this.logger.warn(`${logPrefix} No PolicyId found in API response`);
if (auditSession) {
await this.fanavaranAuditService.recordStep({
session: auditSession,
step: FanavaranAuditStep.POLICY_INQUIRY,
status: FanavaranAuditStatus.SUCCESS,
requestUrl: policyInquiryUrl,
httpStatus: response.status,
responseMeta: {
policyId: null,
policyCount: Array.isArray(response.data)
? response.data.length
: 0,
},
durationMs: Date.now() - startedAt,
});
}
return null;
return selectedPolicy.policyId;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "unknown policy inquiry error";
@@ -3948,8 +3916,11 @@ export class ClaimRequestManagementService {
});
}
this.logger.warn(
`${logPrefix} Policy inquiry failed; continuing with empty PolicyId: ${errorMessage}`,
`${logPrefix} Policy inquiry failed: ${errorMessage}`,
);
if (error instanceof BadRequestException) {
throw error;
}
return null;
}
}
@@ -4011,6 +3982,9 @@ export class ClaimRequestManagementService {
`[Fanavaran Submit] Error getting PolicyId from external API:`,
error,
);
if (error instanceof BadRequestException) {
throw error;
}
return null;
}
}

View File

@@ -0,0 +1,63 @@
import { BadRequestException } from "@nestjs/common";
import { selectLatestActiveFanavaranPolicy } from "./fanavaran-policy-selection";
describe("selectLatestActiveFanavaranPolicy", () => {
const today = "2026-06-30";
it("selects the policy with the latest EndDate when newest is first", () => {
const selected = selectLatestActiveFanavaranPolicy(
[
{ PolicyId: 4826286, EndDate: "1405/09/23" },
{ PolicyId: 3719458, EndDate: "1404/09/06" },
{ PolicyId: 2800731, EndDate: "1403/09/05" },
],
today,
);
expect(selected.policyId).toBe(4826286);
expect(selected.endDate).toBe("1405/09/23");
});
it("selects the policy with the latest EndDate when newest is last", () => {
const selected = selectLatestActiveFanavaranPolicy(
[
{ PolicyId: 2800731, EndDate: "1403/09/05" },
{ PolicyId: 3719458, EndDate: "1404/09/06" },
{ PolicyId: 4826286, EndDate: "1405/09/23" },
],
today,
);
expect(selected.policyId).toBe(4826286);
});
it("rejects when no policies are returned", () => {
expect(() => selectLatestActiveFanavaranPolicy([], today)).toThrow(
BadRequestException,
);
});
it("rejects when the latest policy is expired", () => {
expect(() =>
selectLatestActiveFanavaranPolicy(
[
{ PolicyId: 3719458, EndDate: "1404/09/06" },
{ PolicyId: 2800731, EndDate: "1403/09/05" },
],
today,
),
).toThrow(BadRequestException);
});
it("rejects when the latest policy has no valid PolicyId", () => {
expect(() =>
selectLatestActiveFanavaranPolicy(
[
{ PolicyId: 3719458, EndDate: "1404/09/06" },
{ PolicyId: null, EndDate: "1405/09/23" },
],
today,
),
).toThrow(BadRequestException);
});
});

View File

@@ -0,0 +1,95 @@
import { BadRequestException } from "@nestjs/common";
import { jalaliToGregorianDate } from "src/helpers/date-jalali";
import { gregorianDateInIran } from "src/helpers/iran-datetime";
export type FanavaranPolicyInquiryRow = {
PolicyId?: unknown;
EndDate?: unknown;
};
export type SelectedFanavaranPolicy = {
policy: FanavaranPolicyInquiryRow;
policyId: number;
endDate: string;
endDateGregorian: string;
};
type DatedFanavaranPolicy = {
policy: FanavaranPolicyInquiryRow;
endDate: string;
endDateGregorian: string;
};
const NO_POLICY_MESSAGE =
"No Fanavaran policies were found for the insurer national code. PolicyId is required; contact the administrator.";
const EXPIRED_POLICY_MESSAGE =
"The latest insurance policy is expired and cannot be sent to Fanavaran. Contact the administrator.";
const INVALID_POLICY_MESSAGE =
"Fanavaran policy inquiry returned policies without a valid PolicyId or EndDate. PolicyId is required; contact the administrator.";
function parsePolicyId(value: unknown): number | null {
if (value === null || value === undefined) return null;
if (typeof value === "string" && value.trim() === "") return null;
const id = Number(value);
return Number.isFinite(id) && id > 0 ? id : null;
}
function normalizeEndDate(value: unknown): {
endDate: string;
endDateGregorian: string;
} | null {
if (value === null || value === undefined) return null;
const endDate = String(value).trim();
if (!endDate) return null;
const endDateGregorian = jalaliToGregorianDate(endDate);
if (!endDateGregorian) return null;
return { endDate, endDateGregorian };
}
export function selectLatestActiveFanavaranPolicy(
policies: unknown,
todayGregorian: string = gregorianDateInIran(new Date()),
): SelectedFanavaranPolicy {
if (!Array.isArray(policies) || policies.length === 0) {
throw new BadRequestException(NO_POLICY_MESSAGE);
}
const candidates = policies
.map((policy) => {
if (!policy || typeof policy !== "object") return null;
const row = policy as FanavaranPolicyInquiryRow;
const endDate = normalizeEndDate(row.EndDate);
if (!endDate) return null;
return {
policy: row,
endDate: endDate.endDate,
endDateGregorian: endDate.endDateGregorian,
};
})
.filter((policy): policy is DatedFanavaranPolicy => policy !== null);
if (candidates.length === 0) {
throw new BadRequestException(INVALID_POLICY_MESSAGE);
}
const latest = candidates.reduce((currentLatest, candidate) =>
candidate.endDateGregorian > currentLatest.endDateGregorian
? candidate
: currentLatest,
);
if (latest.endDateGregorian < todayGregorian) {
throw new BadRequestException(EXPIRED_POLICY_MESSAGE);
}
const policyId = parsePolicyId(latest.policy.PolicyId);
if (policyId === null) {
throw new BadRequestException(INVALID_POLICY_MESSAGE);
}
return { ...latest, policyId };
}