From 0f9f702a00e9d28980eef8ef43950c8ff803cd73 Mon Sep 17 00:00:00 2001 From: "s.hajizadeh" Date: Mon, 17 Aug 2026 17:05:33 +0330 Subject: [PATCH] Add Fanavaran-only flow-test script and document how to run it. Track the tester and env template in git so a new tenant can be exercised without a YARA claim; keep filled client env files (secrets, national codes) ignored. Co-authored-by: Cursor --- .gitignore | 40 + docs/fanavaran/10-testing.md | 94 +- docs/fanavaran/README.md | 6 +- scripts/data/fanavaran-flow.env.example | 91 ++ scripts/fanavaran-flow-test.sh | 1111 +++++++++++++++++++++++ 5 files changed, 1338 insertions(+), 4 deletions(-) create mode 100644 scripts/data/fanavaran-flow.env.example create mode 100755 scripts/fanavaran-flow-test.sh diff --git a/.gitignore b/.gitignore index 2fb9a0b..5803d18 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ pids *.pid *.seed *.pid.lock +files/fanavaran-auth/ # Directory for instrumented libs generated by jscoverage/JSCover lib-cov @@ -80,6 +81,9 @@ web_modules/ .env.production.local .env.local .env.development.env +scripts/data/fanavaran-flow.*.env +!scripts/data/fanavaran-flow.env.example +files/fanavaran-flow/ # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache @@ -143,4 +147,40 @@ dist /docker .development.env *.env +!scripts/data/fanavaran-flow.env.example /files + +*.jpg +*.jpeg +*.png +*.gif +*.bmp +*.tiff +*.ico +*.webp +*.svg +*.heic +*.heif +*.heif-srgb +*.heif-srgb-alpha +*.heif-srgb-alpha-heic + +*.mp3 +*.mp4 +*.wav +*.ogg +*.flac +*.aac +*.m4a +*.m4v +*.m4b +*.m4p +*.m4r +*.m4w +*.m4x +*.txt +*.md +*.sh +!scripts/fanavaran-flow-test.sh +!scripts/fanavaran-auth.sh +*.json \ No newline at end of file diff --git a/docs/fanavaran/10-testing.md b/docs/fanavaran/10-testing.md index 545a223..39ae6c8 100644 --- a/docs/fanavaran/10-testing.md +++ b/docs/fanavaran/10-testing.md @@ -1,5 +1,5 @@ --- -last_updated: 2026-08-08 +last_updated: 2026-08-17 tags: [fanavaran, testing] source: fanavaran-module-docs --- @@ -22,6 +22,96 @@ source files/fanavaran-auth/parsian/tokens.env # then curl a lookup — see docs/external-api-curls.md ``` +## Manual Fanavaran flow test (no YARA) + +Use this when there is **no YARA claim** and you need to prove a tenant (Parsian / Tejaratno / Moallem) against Fanavaran with the **same stage order as the app**. + +Files in git: + +| Path | Role | +|------|------| +| `scripts/fanavaran-flow-test.sh` | Standalone tester | +| `scripts/data/fanavaran-flow.env.example` | Template env (section A = you fill, section B = script fills) | + +Do **not** commit a filled copy (`fanavaran-flow.moallem.env` and similar). It contains national codes, plate data, and secrets. Copy the example on the machine that runs the test. + +### 1. Copy and fill the env + +```bash +cp scripts/data/fanavaran-flow.env.example scripts/data/fanavaran-flow..env +``` + +Fill **section A** before the first run: + +- Tenant: `FANAVARAN_CLIENT`, optional auth overrides (`APP_NAME`, `CORP_ID`, `LOCATION`, …) +- Lookup ids **from that tenant** (`files/fanavaran-lookups//`): `CLAIM_EXPERT_ID`, `EXPERTISE_CLAIM_EXPERT_ID`, `CLAIM_FILE_TYPE_ID`, `VEHICLE_KIND_ID`, `DMG_SECTION_ID` +- `INSURANCE_CORP_ID`: Persian caption **or** numeric Fanavaran Id +- Case data: guilty national code, driver national code + Jalali birthday, plate/chassis/VIN if you have them +- `ATTACHMENT_FILE`: absolute path(s) to image(s) **on the host that runs the script** (comma-separated for several files) +- Leave **section B empty** (`POLICY_ID`, `CLAIM_ID`, …) + +Lookup ids from Tejaratno/Parsian files are invalid for Moallem (Fanavaran returns `نوع خودرو یافت نشد` and similar). Fetch Moallem lookups first (`GET /lookups/vehicle-kinds` with `FANAVARAN_CLIENT=moallem`, or curl Fanavaran with that tenant’s token). + +### 2. Network / IP whitelist + +Fanavaran Login is IP-restricted. From a **whitelisted server**, run with no proxy. From **localhost**, Termius dynamic port forwarding does **not** apply automatically — set: + +```env +CURL_PROXY=socks5h://127.0.0.1: +``` + +Error `کاربر … مجاز به لاگین با آی پی … نمیباشد` means curl is still using your home IP. + +### 3. Run (same sequence as the app) + +```text +auth (cached until Tehran midnight) + → policy inquiry → GEN.03 base + → driver inquiry + insurance-corp + → GEN.12 damage + → GEN.07 attachments (one request per file) + → GEN.08 expertise +``` + +From repo root (`bash`, `curl`, `node`, `awk` required): + +```bash +chmod +x scripts/fanavaran-flow-test.sh + +# Full flow (confirms before each POST) +./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env + +# One or more stages +./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env --stages base,damage +./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env --stages attachments +./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env --stages expertise + +# Build payloads / inquiries only +./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env --preview-only +``` + +Missing section-A fields can be typed when prompted; they are written back into the env file. + +### 4. Token cache (do not Login every run) + +`authenticationToken` is stored in `files/fanavaran-auth//tokens.env` until **Asia/Tehran midnight** (same as Nest). Later runs print `Reusing cached authenticationToken`. Use `--force-login` only when you must mint a new token. + +Repeated Login causes Fanavaran `لطفا پس از چند لحظه مجدد تلاش فرمایید`. + +### 5. Resume after a stage succeeds + +Section B is updated in the **same env file**. Re-run; stages with `CLAIM_ID` / `DMG_CASE_ID` already set are skipped (soft-skip). Payloads and HTTP bodies also land under `files/fanavaran-flow///` (gitignored). + +### 6. Typical failures + +| Message | What to do | +|---------|------------| +| Login IP not allowed | Run on the tenant server, or set `CURL_PROXY` to Termius SOCKS | +| Try again later | Wait; reuse cache; do not `--force-login` | +| `نوع خودرو یافت نشد` | Set `VEHICLE_KIND_ID` from **this** tenant’s `vehicle-kinds` lookup | +| File type lookup mismatch | Set `CLAIM_FILE_TYPE_ID` from this tenant’s `file-types` | +| No `authenticationToken` | Read `login.body.json` in the run folder — Fanavaran `Message` is the real error | + ## Unit / service specs | Spec | Focus | @@ -31,7 +121,7 @@ source files/fanavaran-auth/parsian/tokens.env Prefer mocks for Fanavaran HTTP in unit tests; use live calls only in controlled integration. -## Manual E2E (Parsian template) +## Manual E2E (Parsian template — via YARA) 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`. diff --git a/docs/fanavaran/README.md b/docs/fanavaran/README.md index 3df20b5..36e2d58 100644 --- a/docs/fanavaran/README.md +++ b/docs/fanavaran/README.md @@ -1,5 +1,5 @@ --- -last_updated: 2026-08-08 +last_updated: 2026-08-17 tags: [fanavaran, documentation, parsian, third-party-claim] source: fanavaran-module-docs --- @@ -32,7 +32,7 @@ source: fanavaran-module-docs | 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 | +| 10 | [10-testing.md](./10-testing.md) | Test scenarios, **manual flow-test script**, proven case | | 11 | [11-onboard-new-client.md](./11-onboard-new-client.md) | Checklist to add a new insurer | ## Related sources (code) @@ -47,6 +47,8 @@ source: fanavaran-module-docs | `.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 | +| `scripts/fanavaran-flow-test.sh` | Manual Fanavaran-only staged submit (see [10-testing.md](./10-testing.md)) | +| `scripts/data/fanavaran-flow.env.example` | Env template for the flow-test script | ## Architecture (one glance) diff --git a/scripts/data/fanavaran-flow.env.example b/scripts/data/fanavaran-flow.env.example new file mode 100644 index 0000000..3b02fec --- /dev/null +++ b/scripts/data/fanavaran-flow.env.example @@ -0,0 +1,91 @@ +# Fanavaran flow test — copy this file, fill section A, then run: +# +# cp scripts/data/fanavaran-flow.env.example scripts/data/fanavaran-flow.moallem.env +# ./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env +# +# Section A = you fill before (or when the script asks). +# Section B = leave empty. The script writes Fanavaran ids here after each stage +# so you can stop, re-run, or continue without copying ids by hand. +# +# Do not commit real secrets or national codes. + +# ============================================================================= +# A) FILL BEFORE RUN +# ============================================================================= + +# --- tenant --- +FANAVARAN_CLIENT=moallem + +# Localhost only: route curl through Termius SOCKS (Dynamic Port Forwarding). +# Example: CURL_PROXY=socks5h://127.0.0.1:1080 +# Or run the script on the Moallem server (no proxy needed). +# CURL_PROXY= + +# Optional auth overrides (omit to use built-in seeds for this client) +# APP_NAME=ItTalie +# APP_SECRET= +# FANAVARAN_USERNAME= +# FANAVARAN_PASSWORD= +# CORP_ID= +# CONTRACT_ID= +# LOCATION= + +# --- tenant lookup ids (from this insurer's Fanavaran lookups) --- +CLAIM_EXPERT_ID= +EXPERTISE_CLAIM_EXPERT_ID= +CLAIM_FILE_TYPE_ID= +VEHICLE_KIND_ID= +DMG_SECTION_ID= + +# Persian caption OR numeric Fanavaran Id +INSURANCE_CORP_ID= + +# --- GEN.03 case --- +GUILTY_NATIONAL_CODE= +ACCIDENT_DATE=1404/05/20 +ACCIDENT_TIME=12:00 + +# --- GEN.12 case --- +DRIVER_NATIONAL_CODE= +DRIVER_BIRTH_YEAR=1370 +DRIVER_BIRTH_MONTH=1 +DRIVER_BIRTH_DAY=1 +DRIVER_IS_INSURER=0 +LICENCE_NO= +DESC=سپر عقب + +# Optional vehicle / plate / policy document (leave empty if unknown) +# PLAQUE_LEFT_NO= +# PLAQUE_RIGHT_NO= +# PLAQUE_SERIAL= +# PLAQUE_MIDDLE_CODE_ID= +# PLAQUE_NO= +# CHASSIS_NO= +# MOTOR_NO= +# VIN= +# POLICY_NO= +# POLICY_CI_NUMBER= +# BEGIN_DATE= +# END_DATE= +# BUILT_YEAR= + +# --- GEN.07 --- +ATTACHMENT_FILE= + +# --- GEN.08 --- +DMG_ASSESSMENT_DATE=1404/05/20 +INSPECTION_TIME=12:00 +REPAIR_WAGE=0 +COMPONENT_REPLACEMENT_COST=0 +WASTE_VALUE=0 + +# ============================================================================= +# B) FILLED BY SCRIPT (do not set these before the first run) +# ============================================================================= +POLICY_ID= +CLAIM_ID= +CLAIM_NO= +DRIVER_ID= +INSURANCE_CORP_ID_NUM= +DMG_CASE_ID= +EXPERTISE_ID= diff --git a/scripts/fanavaran-flow-test.sh b/scripts/fanavaran-flow-test.sh new file mode 100755 index 0000000..e68f5ee --- /dev/null +++ b/scripts/fanavaran-flow-test.sh @@ -0,0 +1,1111 @@ +#!/usr/bin/env bash +# Standalone Fanavaran claim-flow tester (NO YARA). +# +# Same stage order as the app: +# 1) Auth (GetAppToken → Login) +# 2) Policy inquiry → PolicyId +# 3) GEN.03 base claim +# 4) Driver inquiry → DriverId + resolve InsuranceCorpId from Persian caption +# 5) GEN.12 damage case +# 6) GEN.07 attachments (one multipart request per file) +# 7) GEN.08 expertise +# +# Plate / chassis / VIN / policy document fields are NOT inquired here — +# put them in the env file (Tejarat/ESG may be unavailable on the host). +# +# Usage: +# cp scripts/data/fanavaran-flow.env.example scripts/data/fanavaran-flow.moallem.env +# # fill section A +# ./scripts/fanavaran-flow-test.sh --env scripts/data/fanavaran-flow.moallem.env +# +# The script writes section B (CLAIM_ID, POLICY_ID, …) back into the same env +# file after each Fanavaran response. Re-run to resume; already-set ids are skipped. +# authenticationToken is reused from files/fanavaran-auth//tokens.env until +# Asia/Tehran midnight. Pass --force-login to ignore the cache. +# +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +STAGES="all" +ENV_FILE="" +PREVIEW_ONLY=0 +ASSUME_YES=0 +FORCE_LOGIN=0 +OUT_DIR="" +TOKEN_CACHE_FILE="" + +usage() { + cat <<'USAGE' +Usage: + scripts/fanavaran-flow-test.sh --env [options] + +Fanavaran-only multi-stage submit. No YARA / no plate-provider inquiry. + +Options: + --env Required. Copy scripts/data/fanavaran-flow.env.example and fill section A. + Section B (CLAIM_ID, …) is written back into this file as stages succeed. + --stages all | base | damage | attachments | expertise + or comma list (default: all). + --yes Submit without confirmation prompts + --preview-only Build payloads + run inquiries; do not POST creates/uploads + --force-login Ignore cached authenticationToken and call GetAppToken+Login + --out Artifact directory (default: files/fanavaran-flow//) + -h, --help + +Auth token is cached in files/fanavaran-auth//tokens.env until Asia/Tehran +midnight (same as the Nest app). Re-runs reuse it; do not Login every invocation. + +Stage sequence matches the app (soft-skip when id already present): + auth → policy → GEN.03 → driver+corp resolve → GEN.12 → GEN.07 → GEN.08 +USAGE +} + +die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } +info() { printf '\n==> %s\n' "$*"; } +ok() { printf 'OK: %s\n' "$*"; } +warn() { printf 'WARN: %s\n' "$*" >&2; } + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" +} + +require_var() { + local name="$1" + local hint="${2:-}" + if [[ -z "${!name:-}" ]]; then + if [[ -n "$hint" ]]; then + die "Missing required env var: $name ($hint)" + fi + die "Missing required env var: $name" + fi +} + +# Prompt if empty (for section A values you may fill mid-run), then persist. +ask_var() { + local name="$1" + local question="$2" + local current="${!name:-}" + if [[ -n "$current" ]]; then + return 0 + fi + local answer="" + if [[ -t 0 ]]; then + read -r -p "$question: " answer || true + fi + [[ -n "$answer" ]] || die "Missing required env var: $name — set it in $ENV_FILE or type it when asked." + persist_env "$name" "$answer" +} + +persist_env() { + local key="$1" + local val="${2-}" + if [[ $# -lt 2 ]]; then + val="${!key:-}" + fi + printf -v "$key" '%s' "$val" + export "$key" + [[ -n "$val" ]] || return 0 + [[ -n "${ENV_FILE:-}" && -f "$ENV_FILE" ]] || return 0 + ENV_UPSERT_FILE="$ENV_FILE" ENV_UPSERT_KEY="$key" ENV_UPSERT_VAL="$val" node <<'NODE' +const fs = require("fs"); +const file = process.env.ENV_UPSERT_FILE; +const key = process.env.ENV_UPSERT_KEY; +const val = process.env.ENV_UPSERT_VAL ?? ""; +function quote(v) { + if (v === "") return ""; + if (/^[0-9A-Za-z._/-]+$/.test(v)) return v; + return JSON.stringify(v); +} +let text = fs.readFileSync(file, "utf8"); +if (!text.endsWith("\n")) text += "\n"; +const lines = text.split("\n"); +const re = new RegExp("^\\s*#?\\s*" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*="); +const assignment = key + "=" + quote(val); +let found = false; +for (let i = 0; i < lines.length; i++) { + if (re.test(lines[i])) { + lines[i] = assignment; + found = true; + break; + } +} +if (!found) { + const idx = lines.findIndex((l) => l.includes("B) FILLED BY SCRIPT")); + if (idx >= 0) { + let insertAt = idx + 1; + while ( + insertAt < lines.length && + (lines[insertAt].startsWith("#") || lines[insertAt].trim() === "") + ) { + insertAt++; + } + lines.splice(insertAt, 0, assignment); + } else { + lines.push("", "# filled by script", assignment); + } +} +fs.writeFileSync(file, lines.join("\n")); +NODE + ok "Saved $key=$val → $ENV_FILE" +} + +load_env_file() { + local file="$1" + [[ -f "$file" ]] || die "Env file not found: $file" + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line#"${line%%[![:space:]]*}"}" + [[ -z "$line" || "$line" == \#* ]] && continue + if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then + local key="${BASH_REMATCH[1]}" + local val="${BASH_REMATCH[2]}" + if [[ "$val" =~ ^\"(.*)\"$ ]]; then val="${BASH_REMATCH[1]}"; fi + if [[ "$val" =~ ^\'(.*)\'$ ]]; then val="${BASH_REMATCH[1]}"; fi + # Env file wins for empty CLI; do not override non-empty exported vars + if [[ -z "${!key:-}" ]]; then + printf -v "$key" '%s' "$val" + export "$key" + fi + fi + done < "$file" +} + +confirm_submit() { + local label="$1" + if [[ "$PREVIEW_ONLY" -eq 1 ]]; then + info "Preview-only: skip submit for $label" + return 1 + fi + if [[ "$ASSUME_YES" -eq 1 ]]; then + return 0 + fi + local a="" + read -r -p "Submit $label now? [y/N]: " a || true + [[ "${a,,}" == "y" || "${a,,}" == "yes" ]] +} + +stage_wanted() { + local name="$1" + [[ "$STAGES" == "all" ]] && return 0 + local p + IFS=',' read -r -a parts <<< "$STAGES" + for p in "${parts[@]}"; do + p="$(echo "$p" | tr -d '[:space:]')" + [[ "$p" == "$name" ]] && return 0 + done + return 1 +} + +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" +} + +json_get() { + local file="$1" + local expr="$2" + node -e ' +const fs = require("fs"); +const file = process.argv[1]; +const expr = process.argv[2]; +const raw = fs.readFileSync(file, "utf8"); +let data; +try { data = JSON.parse(raw); } catch (e) { + console.error("Invalid JSON in " + file); + process.exit(2); +} +let out; +try { + out = Function("data", "return (" + expr + ");")(data); +} catch (e) { + console.error(String(e)); + process.exit(3); +} +if (out === undefined || out === null) process.stdout.write(""); +else if (typeof out === "object") process.stdout.write(JSON.stringify(out)); +else process.stdout.write(String(out)); +' "$file" "$expr" +} + +pretty_json_file() { + local file="$1" + node -e ' +const fs = require("fs"); +const p = process.argv[1]; +try { + const j = JSON.parse(fs.readFileSync(p, "utf8")); + fs.writeFileSync(p, JSON.stringify(j, null, 2) + "\n"); +} catch { /* leave raw */ } +' "$file" +} + +save_ids() { + mkdir -p "$OUT_DIR" + cat > "$OUT_DIR/ids.env" < p.type !== "literal") + .map((p) => [p.type, p.value]), +); +const msIntoDay = + ((Number(parts.hour) * 60 + Number(parts.minute)) * 60 + Number(parts.second)) * + 1000; +const msPerDay = 24 * 60 * 60 * 1000; +const remaining = msPerDay - msIntoDay; +process.stdout.write(String(nowMs + (remaining === 0 ? msPerDay : remaining))); +NODE +} + +write_token_cache() { + mkdir -p "$(dirname "$TOKEN_CACHE_FILE")" + local expires_at + expires_at="$(next_tehran_midnight_ms)" + cat > "$TOKEN_CACHE_FILE" </dev/null || true + ok "Cached authenticationToken until Tehran midnight (expiresAt=$expires_at)" +} + +load_cached_token() { + [[ -f "$TOKEN_CACHE_FILE" ]] || return 1 + local cached_token="" cached_expires="" cached_fp="" cached_app="" + cached_token="$( + awk -F= '/^AUTHENTICATION_TOKEN=/{val=$2; gsub(/^'\''|'\''$/, "", val); print val; exit}' "$TOKEN_CACHE_FILE" + )" + cached_expires="$( + awk -F= '/^AUTH_EXPIRES_AT=/{val=$2; gsub(/^'\''|'\''$/, "", val); print val; exit}' "$TOKEN_CACHE_FILE" + )" + cached_fp="$( + awk -F= '/^AUTH_FINGERPRINT=/{val=$2; gsub(/^'\''|'\''$/, "", val); print val; exit}' "$TOKEN_CACHE_FILE" + )" + cached_app="$( + awk -F= '/^APP_TOKEN=/{val=$2; gsub(/^'\''|'\''$/, "", val); print val; exit}' "$TOKEN_CACHE_FILE" + )" + [[ -n "$cached_token" ]] || return 1 + if [[ -n "$cached_fp" && "$cached_fp" != "$(auth_fingerprint)" ]]; then + warn "Cached token fingerprint mismatch (credentials/location changed) — will re-login" + return 1 + fi + local now_ms + now_ms="$(node -e 'process.stdout.write(String(Date.now()))')" + if [[ -n "$cached_expires" && "$cached_expires" -gt "$now_ms" ]]; then + AUTHENTICATION_TOKEN="$cached_token" + APP_TOKEN="$cached_app" + export AUTHENTICATION_TOKEN APP_TOKEN + cp "$TOKEN_CACHE_FILE" "$OUT_DIR/tokens.env" 2>/dev/null || true + ok "Reusing cached authenticationToken until Tehran midnight" + return 0 + fi + warn "Cached authenticationToken expired at Tehran midnight — will re-login" + return 1 +} + +# Optional SOCKS/HTTP proxy (Termius dynamic port forward → socks5://127.0.0.1:PORT). +curl_proxy_args() { + local proxy="${CURL_PROXY:-${ALL_PROXY:-}}" + if [[ -n "$proxy" ]]; then + printf '%s' "--proxy $proxy" + fi +} + +fanavaran_auth_fail() { + local headers_file="$1" + local body_file="$2" + local step="$3" + local code msg + code="$(awk 'NR==1 {print $2}' "$headers_file" 2>/dev/null || true)" + msg="$(json_get "$body_file" 'data.Message || data.message || ""')" + if [[ -n "$msg" ]]; then + die "Fanavaran $step failed (HTTP ${code:-?}): $msg" + fi + die "Fanavaran $step failed (HTTP ${code:-?}) — see $headers_file / $body_file" +} + +fanavaran_login() { + FANAVARAN_BASE_URL="${FANAVARAN_BASE_URL:-https://apimanager.iraneit.com/BimeApiManager/api}" + FANAVARAN_BIME_URL="${FANAVARAN_BIME_URL:-$FANAVARAN_BASE_URL/BimeApi/v2.0}" + TOKEN_CACHE_FILE="${TOKEN_CACHE_FILE:-$ROOT_DIR/files/fanavaran-auth/$FANAVARAN_CLIENT/tokens.env}" + + if [[ -n "${CURL_PROXY:-${ALL_PROXY:-}}" ]]; then + ok "Using proxy: ${CURL_PROXY:-$ALL_PROXY}" + fi + + if [[ "$FORCE_LOGIN" -ne 1 ]] && load_cached_token; then + return 0 + fi + + local app_headers="$OUT_DIR/get-app-token.headers" + local app_body="$OUT_DIR/get-app-token.body.json" + local login_headers="$OUT_DIR/login.headers" + local login_body="$OUT_DIR/login.body.json" + + info "1) Auth — GetAppToken ($FANAVARAN_CLIENT) (no valid cache)" + # shellcheck disable=SC2046 + curl -sS -D "$app_headers" -o "$app_body" \ + $(curl_proxy_args) \ + -X POST "$FANAVARAN_BASE_URL/EITAuthentication/GetAppToken" \ + -H "appname: $APP_NAME" \ + -H "secret: $APP_SECRET" \ + -H "Content-Length: 0" + + APP_TOKEN="$(extract_header apptoken "$app_headers")" + [[ -n "$APP_TOKEN" ]] || fanavaran_auth_fail "$app_headers" "$app_body" "GetAppToken" + + info "1) Auth — Login" + # shellcheck disable=SC2046 + curl -sS -D "$login_headers" -o "$login_body" \ + $(curl_proxy_args) \ + -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="$(extract_header authenticationtoken "$login_headers")" + if [[ -z "$AUTHENTICATION_TOKEN" ]]; then + AUTHENTICATION_TOKEN="$(json_get "$login_body" 'data.authenticationToken || data.authenticationtoken || data.authentication_token || ""')" + fi + if [[ -z "$AUTHENTICATION_TOKEN" ]]; then + local msg + msg="$(json_get "$login_body" 'data.Message || data.message || ""')" + if [[ "$msg" == *"مجدد تلاش"* ]] && load_cached_token; then + warn "Login asked to retry later — reused still-valid cached token" + return 0 + fi + fanavaran_auth_fail "$login_headers" "$login_body" "Login" + fi + ok "authenticationToken acquired" + write_token_cache +} + +fv_curl_json() { + local method="$1" + local url="$2" + local out_body="$3" + local data_file="${4:-}" + local headers_file="${out_body%.json}.headers" + if [[ -n "$data_file" ]]; then + # shellcheck disable=SC2046 + curl -sS -D "$headers_file" -o "$out_body" \ + $(curl_proxy_args) \ + -X "$method" "$url" \ + -H "authenticationToken: $AUTHENTICATION_TOKEN" \ + -H "CorpId: $CORP_ID" \ + -H "ContractId: $CONTRACT_ID" \ + -H "Location: $LOCATION" \ + -H "Content-Type: application/json" \ + --data @"$data_file" + else + # shellcheck disable=SC2046 + curl -sS -D "$headers_file" -o "$out_body" \ + $(curl_proxy_args) \ + -X "$method" "$url" \ + -H "authenticationToken: $AUTHENTICATION_TOKEN" \ + -H "CorpId: $CORP_ID" \ + -H "ContractId: $CONTRACT_ID" \ + -H "Location: $LOCATION" \ + -H "Content-Type: application/json" + fi + pretty_json_file "$out_body" || true + local code + code="$(awk 'NR==1 {print $2}' "$headers_file")" + printf 'HTTP %s → %s\n' "$code" "$out_body" +} + +# ---------- inquiries ---------- +inquire_policy_id() { + if [[ -n "${POLICY_ID:-}" ]]; then + ok "Using POLICY_ID=$POLICY_ID from env (skip inquiry)" + return 0 + fi + ask_var GUILTY_NATIONAL_CODE "Guilty insurer national code (policy inquiry)" + + local out="$OUT_DIR/policy-inquiry.json" + info "2) Policy inquiry — NationalCode=$GUILTY_NATIONAL_CODE" + fv_curl_json GET \ + "$FANAVARAN_BIME_URL/common/Policies/inquiry-my-policies?InsuranceLineId=5&NationalCode=${GUILTY_NATIONAL_CODE}" \ + "$out" + + POLICY_ID="$(node -e ' +const fs = require("fs"); +const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); +const rows = Array.isArray(data) ? data : (data?.value || data?.data || []); +if (!Array.isArray(rows) || !rows.length) { console.log(""); process.exit(0); } +const withId = rows + .map(r => ({ id: Number(r.PolicyId), end: String(r.EndDate || "").trim() })) + .filter(x => Number.isFinite(x.id) && x.id > 0 && x.end); +if (!withId.length) { console.log(""); process.exit(0); } +withId.sort((a, b) => (a.end < b.end ? 1 : a.end > b.end ? -1 : 0)); +console.log(String(withId[0].id)); +' "$out")" + + [[ -n "$POLICY_ID" ]] || die "Policy inquiry returned no PolicyId. Inspect $out or set POLICY_ID in env." + ok "Selected PolicyId=$POLICY_ID (latest EndDate in response)" + persist_env POLICY_ID + save_ids +} + +inquire_driver_id() { + if [[ -n "${DRIVER_ID:-}" ]]; then + ok "Using DRIVER_ID=$DRIVER_ID from env (skip inquiry)" + return 0 + fi + ask_var DRIVER_NATIONAL_CODE "Damaged-party driver national code" + ask_var DRIVER_BIRTH_YEAR "Driver Jalali birth year" + ask_var DRIVER_BIRTH_MONTH "Driver Jalali birth month (1-12)" + ask_var DRIVER_BIRTH_DAY "Driver Jalali birth day (1-31)" + + local out="$OUT_DIR/driver-inquiry.json" + local url="$FANAVARAN_BIME_URL/common/parties/inquiry-by-unique-identifier" + url+="?NationalCode=${DRIVER_NATIONAL_CODE}" + url+="&BirthYear=${DRIVER_BIRTH_YEAR}&BirthMonth=${DRIVER_BIRTH_MONTH}&BirthDay=${DRIVER_BIRTH_DAY}" + + info "4a) Driver inquiry" + fv_curl_json GET "$url" "$out" + + local target_role=166 + if [[ "$DRIVER_IS_INSURER" == "1" || "${DRIVER_IS_INSURER,,}" == "true" ]]; then + target_role=161 + fi + + DRIVER_ID="$(node -e ' +const fs = require("fs"); +const target = Number(process.argv[2]); +const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); +const rows = Array.isArray(data) ? data : (data?.value || data?.data || []); +if (!Array.isArray(rows) || !rows.length) { console.log(""); process.exit(0); } +const match = rows.find(r => Number(r.RoleId) === target); +if (match) console.log(String(match.Id)); +else { + console.error("No RoleId=" + target + "; rows: " + rows.map(r => "Id=" + r.Id + "/RoleId=" + r.RoleId).join(", ")); + console.log(""); +} +' "$out" "$target_role")" + + [[ -n "$DRIVER_ID" ]] || die "Driver inquiry failed. Inspect $out or set DRIVER_ID in env." + ok "DriverId=$DRIVER_ID (RoleId=$target_role)" + persist_env DRIVER_ID + save_ids +} + +resolve_insurance_corp_id() { + if [[ -n "${INSURANCE_CORP_ID_NUM:-}" ]]; then + ok "Using INSURANCE_CORP_ID_NUM=$INSURANCE_CORP_ID_NUM from env" + return 0 + fi + # Numeric INSURANCE_CORP_ID is already the Fanavaran Id — skip caption lookup. + if [[ "${INSURANCE_CORP_ID:-}" =~ ^[0-9]+$ ]]; then + persist_env INSURANCE_CORP_ID_NUM "$INSURANCE_CORP_ID" + ok "Using numeric INSURANCE_CORP_ID=$INSURANCE_CORP_ID_NUM" + save_ids + return 0 + fi + ask_var INSURANCE_CORP_ID "Persian insurance-corp caption" + if [[ "${INSURANCE_CORP_ID:-}" =~ ^[0-9]+$ ]]; then + persist_env INSURANCE_CORP_ID_NUM "$INSURANCE_CORP_ID" + ok "Using numeric INSURANCE_CORP_ID=$INSURANCE_CORP_ID_NUM" + save_ids + return 0 + fi + INSURANCE_CORP_CAPTION="$INSURANCE_CORP_ID" + export INSURANCE_CORP_CAPTION + + local out="$OUT_DIR/insurance-corp.json" + info "4b) Resolve InsuranceCorpId from caption: $INSURANCE_CORP_CAPTION" + fv_curl_json GET \ + "$FANAVARAN_BIME_URL/common/code-list/insurance-corp" \ + "$out" + + INSURANCE_CORP_ID_NUM="$(node -e ' +const fs = require("fs"); +const caption = String(process.argv[2] || "").replace(/\s+/g, " ").toLowerCase().trim(); +const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); +const rows = Array.isArray(data) ? data : (data?.value || data?.data || []); +if (!Array.isArray(rows)) { console.log(""); process.exit(0); } +const match = rows.find((c) => { + if (Number(c?.IsActive) !== 1) return false; + if (typeof c?.Id !== "number") return false; + const n = String(c.Caption || "").replace(/\s+/g, " ").toLowerCase().trim(); + return n.includes(caption) || caption.includes(n); +}); +if (match) console.log(String(match.Id)); +else { + console.error("No match. Active captions:"); + rows.filter(c => Number(c?.IsActive) === 1).forEach(c => console.error(" - " + c.Caption + " (" + c.Id + ")")); + console.log(""); +} +' "$out" "$INSURANCE_CORP_CAPTION")" + + [[ -n "$INSURANCE_CORP_ID_NUM" ]] || die "Could not resolve InsuranceCorpId for caption. Set INSURANCE_CORP_ID_NUM or fix INSURANCE_CORP_ID." + ok "InsuranceCorpId=$INSURANCE_CORP_ID_NUM" + persist_env INSURANCE_CORP_ID_NUM + save_ids +} + +# ---------- payload builders ---------- +build_base_payload() { + ask_var CLAIM_EXPERT_ID "GEN.03 ClaimExpertId (مسئول پرونده مالی)" + ask_var ACCIDENT_DATE "AccidentDate Jalali YYYY/MM/DD" + ask_var ACCIDENT_TIME "AccidentTime HH:mm" + require_var POLICY_ID + + node <<'NODE' >"$OUT_DIR/base-payload.json" +const fs = require("fs"); +const e = process.env; +const n = (k) => Number(e[k]); +const payload = { + AccidentCityId: n("ACCIDENT_CITY_ID"), + AccidentReportTypeId: n("ACCIDENT_REPORT_TYPE_ID"), + AccidentVehicleUsedId: n("ACCIDENT_VEHICLE_USED_ID"), + ClaimExpertId: n("CLAIM_EXPERT_ID"), + CompensationReferenceId: n("COMPENSATION_REFERENCE_ID"), + CulpritLicenceTypeId: n("CULPRIT_LICENCE_TYPE_ID"), + CulpritTypeId: n("CULPRIT_TYPE_ID"), + AccidentCauseId: n("ACCIDENT_CAUSE_ID"), + AccidentDate: e.ACCIDENT_DATE, + AnnouncementDate: e.ACCIDENT_DATE, + DocReceivedDate: e.ACCIDENT_DATE, + AccidentTime: e.ACCIDENT_TIME, + AccidentLocationAddress: e.ACCIDENT_LOCATION_ADDRESS, + EstimateAmount: n("ESTIMATE_AMOUNT"), + ActualPremium: null, + ArchiveNo: null, + AuthorityCulpritId: null, + AccidentCulpritId: null, + ClaimCompletionDate: null, + CostSeparationToDmgSections: null, + CouponNo: null, + CourtArchiveNo: null, + CustomerFaultPercent: null, + CulpritLicenceCityId: null, + CulpritLicenceCountryId: null, + CulpritLicenceForeignCityName: null, + CulpritLicenceIssuDate: e.CULPRIT_LICENCE_ISSU_DATE, + CulpritLicenceNo: e.CULPRIT_LICENCE_NO, + DamagedCount: 1, + DmgAssessorFirstCreationTime: null, + EntryDate: null, + GlassBreakReasonId: null, + IsLicenseMatchWithVehicleKind: 1, + HasOtherCulprit: 0, + IsAccidentOutOfBorder: 0, + IsFatalAccident: 0, + IsOwnerChanged: null, + IsPlaqueChanged: 0, + IsSurplusArticleEighthLaw: null, + PlaqueCityId: null, + PlaqueKindId: null, + PlaqueLeftNo: null, + PlaqueMiddleCodeId: null, + PlaqueNo: null, + PlaqueRightNo: null, + PlaqueSampleId: null, + PlaqueSerial: null, + PoliceOfficerId: 1, + PoliceReportDesc: null, + PoliceReportSeri: null, + PoliceReportSerial: null, + PolicyId: n("POLICY_ID"), + PreviousPolicyEndDate: "", + StatusChangeDate: null, + TrackingCode: null, + IsLicenseReplacement: 0, + UnknownCulpritCauseId: null, +}; +process.stdout.write(JSON.stringify(payload, null, 2) + "\n"); +NODE + ok "Wrote $OUT_DIR/base-payload.json" +} + +build_damage_payload() { + require_var DRIVER_ID + require_var INSURANCE_CORP_ID_NUM + ask_var VEHICLE_KIND_ID "VehicleKindId (from this tenant vehicle-kinds lookup)" + ask_var LICENCE_NO "Damaged driver LicenceNo" + LICENCE_ISSU_DATE="${LICENCE_ISSU_DATE:-${DRIVER_BIRTH_YEAR}/${DRIVER_BIRTH_MONTH}/${DRIVER_BIRTH_DAY}}" + require_var LICENCE_ISSU_DATE + + # Optional plate / inquiry fields — empty → null + export PLAQUE_LEFT_NO="${PLAQUE_LEFT_NO:-}" + export PLAQUE_RIGHT_NO="${PLAQUE_RIGHT_NO:-}" + export PLAQUE_SERIAL="${PLAQUE_SERIAL:-}" + export PLAQUE_MIDDLE_CODE_ID="${PLAQUE_MIDDLE_CODE_ID:-}" + export PLAQUE_NO="${PLAQUE_NO:-}" + export CHASSIS_NO="${CHASSIS_NO:-}" + export MOTOR_NO="${MOTOR_NO:-}" + export VIN="${VIN:-}" + export POLICY_NO="${POLICY_NO:-}" + export POLICY_CI_NUMBER="${POLICY_CI_NUMBER:-}" + export BEGIN_DATE="${BEGIN_DATE:-}" + export END_DATE="${END_DATE:-}" + export BUILT_YEAR="${BUILT_YEAR:-}" + export LICENCE_ISSU_DATE DESC DRIVER_ID INSURANCE_CORP_ID_NUM VEHICLE_KIND_ID LICENCE_NO + + node <<'NODE' >"$OUT_DIR/damage-payload.json" +const e = process.env; +const numOrNull = (v) => { + if (v === undefined || v === null || String(v).trim() === "") return null; + const n = Number(v); + return Number.isFinite(n) ? n : null; +}; +const strOrNull = (v) => { + if (v === undefined || v === null || String(v).trim() === "") return null; + return String(v); +}; +const hasPlate = Boolean(strOrNull(e.PLAQUE_LEFT_NO) || strOrNull(e.PLAQUE_NO)); +const payload = { + BeginDate: strOrNull(e.BEGIN_DATE), + BuiltYear: numOrNull(e.BUILT_YEAR), + ChassisNo: strOrNull(e.CHASSIS_NO), + DmgHistoryStatus: Number(e.DMG_HISTORY_STATUS), + Desc: e.DESC, + DmgCaseTypeId: Number(e.DMG_CASE_TYPE_ID), + DriverId: Number(e.DRIVER_ID), + EndDate: strOrNull(e.END_DATE), + EstimateAmount: Number(e.ESTIMATE_AMOUNT), + FaultPercent: Number(e.FAULT_PERCENT), + InsuranceCorpId: Number(e.INSURANCE_CORP_ID_NUM), + DriverIsOwner: Number(e.DRIVER_IS_OWNER), + LicenceCityId: null, + LicenceCountryId: null, + LicenceForeignCityName: null, + LicenceIssuDate: e.LICENCE_ISSU_DATE, + LicenceNo: e.LICENCE_NO, + LicenceTypeId: Number(e.CULPRIT_LICENCE_TYPE_ID), + MotorNo: strOrNull(e.MOTOR_NO), + OwnerId: null, + PlaqueCityId: null, + PlaqueKindId: hasPlate ? Number(e.PLAQUE_KIND_ID) : null, + PlaqueLeftNo: strOrNull(e.PLAQUE_LEFT_NO), + PlaqueMiddleCodeId: numOrNull(e.PLAQUE_MIDDLE_CODE_ID), + PlaqueNo: strOrNull(e.PLAQUE_NO), + PlaqueRightNo: strOrNull(e.PLAQUE_RIGHT_NO), + PlaqueSampleId: hasPlate ? Number(e.PLAQUE_SAMPLE_ID) : null, + PlaqueSerial: strOrNull(e.PLAQUE_SERIAL), + PolicyNo: strOrNull(e.POLICY_NO), + PreviousPolicyEndDate: strOrNull(e.END_DATE) || "", + VehicleKindId: Number(e.VEHICLE_KIND_ID), + VIN: strOrNull(e.VIN), + AccidentVehicleUsedId: Number(e.ACCIDENT_VEHICLE_USED_ID), + PolicyCINumber: strOrNull(e.POLICY_CI_NUMBER), +}; +process.stdout.write(JSON.stringify(payload, null, 2) + "\n"); +NODE + ok "Wrote $OUT_DIR/damage-payload.json" +} + +build_expertise_payload() { + ask_var EXPERTISE_CLAIM_EXPERT_ID "GEN.08 ExpertiseClaimExpertId (ارزیاب)" + ask_var DMG_SECTION_ID "DmgSectionId (car-components Id)" + ask_var DMG_ASSESSMENT_DATE "DmgAssessmentDate Jalali YYYY/MM/DD" + ask_var INSPECTION_TIME "InspectionTime HH:mm" + + export DAMAGED_VEHICLE_CURRENT_PRICE="${DAMAGED_VEHICLE_CURRENT_PRICE:-}" + + node <<'NODE' >"$OUT_DIR/expertise-payload.json" +const e = process.env; +const numOrNull = (v) => { + if (v === undefined || v === null || String(v).trim() === "") return null; + const n = Number(v); + return Number.isFinite(n) ? n : null; +}; +const payload = { + ClaimExpertId: Number(e.EXPERTISE_CLAIM_EXPERT_ID), + ComponentReplacementCost: Number(e.COMPONENT_REPLACEMENT_COST), + DmgAssessmentDate: e.DMG_ASSESSMENT_DATE, + DmgCaseId: Number(e.DMG_CASE_ID), + InspectionPlaceId: Number(e.INSPECTION_PLACE_ID), + InspectionTime: e.INSPECTION_TIME, + RepairWage: Number(e.REPAIR_WAGE), + WasteValue: Number(e.WASTE_VALUE), + WentDistanceByExpert: null, + DropAmountStatus: Number(e.DROP_AMOUNT_STATUS), + DropAmountAdditionsDeductions: Number(e.DROP_AMOUNT_ADDITIONS || 0), + ComponentReplacementTaxAndToll: Number(e.COMPONENT_REPLACEMENT_TAX || 0), + DamagedVehicleCurrentPrice: numOrNull(e.DAMAGED_VEHICLE_CURRENT_PRICE), + DmgSections: [{ + Desc: e.DESC || "manual test", + DmgSectionId: Number(e.DMG_SECTION_ID), + AccidentLevel: Number(e.ACCIDENT_LEVEL), + ComponentReplacementCost: Number(e.COMPONENT_REPLACEMENT_COST), + RepairWage: Number(e.REPAIR_WAGE), + WasteValue: Number(e.WASTE_VALUE), + }], +}; +process.stdout.write(JSON.stringify(payload, null, 2) + "\n"); +NODE + ok "Wrote $OUT_DIR/expertise-payload.json" +} + +# ---------- stages ---------- +run_base() { + if [[ -n "${CLAIM_ID:-}" ]]; then + ok "CLAIM_ID=$CLAIM_ID already set — skip GEN.03 (same as app soft-skip)" + return 0 + fi + inquire_policy_id + build_base_payload + if confirm_submit "GEN.03 base claim"; then + info "3) POST GEN.03 base claim" + fv_curl_json POST \ + "$FANAVARAN_BIME_URL/car/third-party-car-financial-claims" \ + "$OUT_DIR/base-submit.json" \ + "$OUT_DIR/base-payload.json" + CLAIM_ID="$(json_get "$OUT_DIR/base-submit.json" 'data.Id ?? data.id ?? ""')" + CLAIM_NO="$(json_get "$OUT_DIR/base-submit.json" 'data.ClaimNo ?? data.claimNo ?? ""')" + [[ -n "$CLAIM_ID" ]] || die "GEN.03 response missing Id — see $OUT_DIR/base-submit.json" + ok "claimId=$CLAIM_ID claimNo=${CLAIM_NO:-?}" + persist_env CLAIM_ID + persist_env CLAIM_NO + save_ids + elif [[ "$PREVIEW_ONLY" -eq 1 ]]; then + ok "Preview-only: base payload ready (no CLAIM_ID)" + else + die "Base claim not submitted and CLAIM_ID unset — cannot continue." + fi +} + +run_damage() { + require_var CLAIM_ID "from GEN.03 or set in env to resume" + if [[ -n "${DMG_CASE_ID:-}" ]]; then + ok "DMG_CASE_ID=$DMG_CASE_ID already set — skip GEN.12" + return 0 + fi + inquire_driver_id + resolve_insurance_corp_id + build_damage_payload + if confirm_submit "GEN.12 damage case"; then + info "5) POST GEN.12 dmg-cases" + fv_curl_json POST \ + "$FANAVARAN_BIME_URL/car/third-party-car-financial-claims/${CLAIM_ID}/dmg-cases" \ + "$OUT_DIR/damage-submit.json" \ + "$OUT_DIR/damage-payload.json" + DMG_CASE_ID="$(json_get "$OUT_DIR/damage-submit.json" 'data.Id ?? data.id ?? ""')" + [[ -n "$DMG_CASE_ID" ]] || die "GEN.12 response missing Id — see $OUT_DIR/damage-submit.json" + ok "dmgCaseId=$DMG_CASE_ID" + persist_env DMG_CASE_ID + save_ids + elif [[ "$PREVIEW_ONLY" -eq 1 ]]; then + ok "Preview-only: damage payload ready (no DMG_CASE_ID)" + else + die "Damage case not submitted and DMG_CASE_ID unset — cannot continue." + fi +} + +attachment_file_list() { + # ATTACHMENT_FILES=a.jpg,b.jpg or single ATTACHMENT_FILE= + local raw="${ATTACHMENT_FILES:-${ATTACHMENT_FILE:-}}" + [[ -n "$raw" ]] || return 1 + local f + IFS=',' read -r -a files <<< "$raw" + for f in "${files[@]}"; do + f="$(echo "$f" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + [[ -n "$f" ]] && printf '%s\n' "$f" + done +} + +run_attachments() { + require_var CLAIM_ID + ask_var CLAIM_FILE_TYPE_ID "ClaimFileTypeId (must exist in this tenant file-types lookup)" + if [[ -z "${ATTACHMENT_FILE:-}" && -z "${ATTACHMENT_FILES:-}" ]]; then + ask_var ATTACHMENT_FILE "Path to an image file to upload" + fi + + local files=() + while IFS= read -r line; do + files+=("$line") + done < <(attachment_file_list || true) + + if [[ "${#files[@]}" -eq 0 ]]; then + die "Set ATTACHMENT_FILE or ATTACHMENT_FILES (comma-separated paths) in env" + fi + + local idx=0 + local path fname meta out headers + for path in "${files[@]}"; do + idx=$((idx + 1)) + [[ -f "$path" ]] || die "Attachment not found: $path" + fname="$(basename "$path")" + meta="$OUT_DIR/attachment-${idx}-content.json" + out="$OUT_DIR/attachment-${idx}-submit.json" + headers="${out%.json}.headers" + + node -e ' +const fs = require("fs"); +fs.writeFileSync(process.argv[3], JSON.stringify({ + FileName: process.argv[1], + FileTypeId: Number(process.argv[2]), + Files: [{ FileName: process.argv[1], FileTypeId: Number(process.argv[2]) }], +}) + "\n"); +' "$fname" "$CLAIM_FILE_TYPE_ID" "$meta" + + if confirm_submit "GEN.07 attachment #$idx ($fname)"; then + info "6) POST GEN.07 files — $fname (FileTypeId=$CLAIM_FILE_TYPE_ID)" + # Let curl set multipart boundary; do not set Content-Type manually. + # shellcheck disable=SC2046 + curl -sS -D "$headers" -o "$out" \ + $(curl_proxy_args) \ + -X POST "$FANAVARAN_BIME_URL/car/third-party-car-financial-claims/${CLAIM_ID}/files" \ + -H "authenticationToken: $AUTHENTICATION_TOKEN" \ + -H "CorpId: $CORP_ID" \ + -H "ContractId: $CONTRACT_ID" \ + -H "Location: $LOCATION" \ + -F "content=$(cat "$meta");type=application/json" \ + -F "files=@${path}" + pretty_json_file "$out" || true + local code + code="$(awk 'NR==1 {print $2}' "$headers")" + printf 'HTTP %s → %s\n' "$code" "$out" + fi + done +} + +run_expertise() { + require_var CLAIM_ID + require_var DMG_CASE_ID + build_expertise_payload + if confirm_submit "GEN.08 expertise"; then + info "7) POST GEN.08 expertise" + fv_curl_json POST \ + "$FANAVARAN_BIME_URL/car/third-party-car-financial-claims/${CLAIM_ID}/expertise" \ + "$OUT_DIR/expertise-submit.json" \ + "$OUT_DIR/expertise-payload.json" + EXPERTISE_ID="$(json_get "$OUT_DIR/expertise-submit.json" 'data.Id ?? data.id ?? ""')" + [[ -n "$EXPERTISE_ID" ]] || warn "Could not parse Expertise Id — see $OUT_DIR/expertise-submit.json" + ok "expertiseId=${EXPERTISE_ID:-?}" + persist_env EXPERTISE_ID + save_ids + fi +} + +# ---------- argv ---------- +need_cmd curl +need_cmd node +need_cmd awk + +while [[ $# -gt 0 ]]; do + case "$1" in + --env) ENV_FILE="${2:-}"; shift 2 ;; + --stages) STAGES="${2:-}"; shift 2 ;; + --out) OUT_DIR="${2:-}"; shift 2 ;; + --yes) ASSUME_YES=1; shift ;; + --preview-only) PREVIEW_ONLY=1; shift ;; + --force-login) FORCE_LOGIN=1; shift ;; + -h|--help) usage; exit 0 ;; + yara|fanavaran) + die "YARA mode removed. This script is Fanavaran-only. Use: --env " + ;; + *) die "Unknown option: $1 (see --help)" ;; + esac +done + +[[ -n "$ENV_FILE" ]] || die "--env is required" +[[ -f "$ENV_FILE" ]] || die "Env file not found: $ENV_FILE" +ENV_FILE="$(cd "$(dirname "$ENV_FILE")" && pwd)/$(basename "$ENV_FILE")" +load_env_file "$ENV_FILE" + +FANAVARAN_CLIENT="${FANAVARAN_CLIENT:-${CLIENT:-}}" +FANAVARAN_CLIENT="${FANAVARAN_CLIENT,,}" +ask_var FANAVARAN_CLIENT "Fanavaran client (parsian|tejaratno|moallem)" +FANAVARAN_CLIENT="${FANAVARAN_CLIENT,,}" + +apply_client_auth_defaults +apply_shared_defaults + +STAGES="$(echo "$STAGES" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" +TS="$(date +%Y%m%d-%H%M%S)" +OUT_DIR="${OUT_DIR:-$ROOT_DIR/files/fanavaran-flow/$FANAVARAN_CLIENT/$TS}" +mkdir -p "$OUT_DIR" +cp "$ENV_FILE" "$OUT_DIR/input.env" 2>/dev/null || true + +info "Fanavaran-only flow | client=$FANAVARAN_CLIENT | stages=$STAGES" +info "Env file (section B will be updated in-place): $ENV_FILE" +info "Output: $OUT_DIR" + +fanavaran_login + +# Soft-ensure order like the app: later stages create earlier ones if ids missing +if stage_wanted base; then + run_base +elif stage_wanted damage || stage_wanted attachments || stage_wanted expertise; then + if [[ -z "${CLAIM_ID:-}" ]]; then + if [[ "$PREVIEW_ONLY" -eq 1 ]]; then + warn "CLAIM_ID missing — preview-only cannot soft-ensure GEN.03 submit; set CLAIM_ID to preview later stages" + else + warn "CLAIM_ID missing — running GEN.03 first (soft-ensure)" + run_base + fi + fi +fi + +if stage_wanted damage; then + if [[ -n "${CLAIM_ID:-}" || "$PREVIEW_ONLY" -eq 0 ]]; then + run_damage + fi +elif stage_wanted expertise; then + if [[ -z "${DMG_CASE_ID:-}" ]]; then + if [[ "$PREVIEW_ONLY" -eq 1 ]]; then + warn "DMG_CASE_ID missing — set it to preview expertise" + else + warn "DMG_CASE_ID missing — running GEN.12 first (soft-ensure)" + run_damage + fi + fi +fi + +if stage_wanted attachments; then + if [[ -n "${CLAIM_ID:-}" ]]; then + run_attachments + else + warn "Skip attachments: CLAIM_ID required" + fi +fi + +if stage_wanted expertise; then + if [[ -n "${CLAIM_ID:-}" && -n "${DMG_CASE_ID:-}" ]]; then + run_expertise + elif [[ "$PREVIEW_ONLY" -eq 1 && -n "${DMG_CASE_ID:-}" ]]; then + run_expertise + else + warn "Skip expertise: need CLAIM_ID and DMG_CASE_ID (or run full flow)" + fi +fi + +save_ids +info "Done. Section B of $ENV_FILE now holds Fanavaran ids for resume." +if [[ -f "$OUT_DIR/ids.env" ]]; then + printf 'Ids:\n' + cat "$OUT_DIR/ids.env" +fi