Files
yara724-api/scripts/fanavaran-flow-test.sh
s.hajizadeh 0f9f702a00 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 <cursoragent@cursor.com>
2026-08-17 17:05:33 +03:30

1112 lines
37 KiB
Bash
Executable File

#!/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/<client>/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 <file> [options]
Fanavaran-only multi-stage submit. No YARA / no plate-provider inquiry.
Options:
--env <file> 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 <list> 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 <dir> Artifact directory (default: files/fanavaran-flow/<client>/<ts>)
-h, --help
Auth token is cached in files/fanavaran-auth/<client>/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" <<EOF
CLAIM_ID='${CLAIM_ID:-}'
CLAIM_NO='${CLAIM_NO:-}'
POLICY_ID='${POLICY_ID:-}'
DRIVER_ID='${DRIVER_ID:-}'
INSURANCE_CORP_ID_NUM='${INSURANCE_CORP_ID_NUM:-}'
DMG_CASE_ID='${DMG_CASE_ID:-}'
EXPERTISE_ID='${EXPERTISE_ID:-}'
EOF
}
apply_client_auth_defaults() {
case "$FANAVARAN_CLIENT" in
tejaratno)
APP_NAME="${APP_NAME:-fanhab}"
APP_SECRET="${APP_SECRET:-5Fa@N#A2B}"
FANAVARAN_USERNAME="${FANAVARAN_USERNAME:-fanhabUser}"
FANAVARAN_PASSWORD="${FANAVARAN_PASSWORD:-Fan#@2U\$3er}"
CORP_ID="${CORP_ID:-3539}"
CONTRACT_ID="${CONTRACT_ID:-263}"
LOCATION="${LOCATION:-100}"
;;
parsian)
APP_NAME="${APP_NAME:-ParsianService}"
APP_SECRET="${APP_SECRET:-P@r30@n\$erv!ce}"
FANAVARAN_USERNAME="${FANAVARAN_USERNAME:-ParsianServiceUser}"
FANAVARAN_PASSWORD="${FANAVARAN_PASSWORD:-P@r30@n123}"
CORP_ID="${CORP_ID:-543}"
CONTRACT_ID="${CONTRACT_ID:-28}"
LOCATION="${LOCATION:-210050}"
;;
moallem)
APP_NAME="${APP_NAME:-ItTalie}"
APP_SECRET="${APP_SECRET:-itT@l!3@api}"
FANAVARAN_USERNAME="${FANAVARAN_USERNAME:-itTalieUser}"
FANAVARAN_PASSWORD="${FANAVARAN_PASSWORD:-itT@l!3@user}"
CORP_ID="${CORP_ID:-5650}"
CONTRACT_ID="${CONTRACT_ID:-304}"
LOCATION="${LOCATION:-30900}"
;;
*)
die "Unknown FANAVARAN_CLIENT=$FANAVARAN_CLIENT (parsian|tejaratno|moallem)"
;;
esac
}
apply_shared_defaults() {
ACCIDENT_CITY_ID="${ACCIDENT_CITY_ID:-701}"
ACCIDENT_REPORT_TYPE_ID="${ACCIDENT_REPORT_TYPE_ID:-155}"
ACCIDENT_VEHICLE_USED_ID="${ACCIDENT_VEHICLE_USED_ID:-1}"
COMPENSATION_REFERENCE_ID="${COMPENSATION_REFERENCE_ID:-167}"
CULPRIT_LICENCE_TYPE_ID="${CULPRIT_LICENCE_TYPE_ID:-2}"
CULPRIT_TYPE_ID="${CULPRIT_TYPE_ID:-337}"
ACCIDENT_CAUSE_ID="${ACCIDENT_CAUSE_ID:-6}"
DMG_CASE_TYPE_ID="${DMG_CASE_TYPE_ID:-175}"
DMG_HISTORY_STATUS="${DMG_HISTORY_STATUS:-5214}"
PLAQUE_KIND_ID="${PLAQUE_KIND_ID:-8}"
PLAQUE_SAMPLE_ID="${PLAQUE_SAMPLE_ID:-10}"
DRIVER_IS_INSURER="${DRIVER_IS_INSURER:-0}"
if [[ "$DRIVER_IS_INSURER" == "1" || "${DRIVER_IS_INSURER,,}" == "true" ]]; then
DRIVER_IS_OWNER="${DRIVER_IS_OWNER:-1}"
else
DRIVER_IS_OWNER="${DRIVER_IS_OWNER:-0}"
fi
FAULT_PERCENT="${FAULT_PERCENT:-100}"
ESTIMATE_AMOUNT="${ESTIMATE_AMOUNT:-1000}"
INSPECTION_PLACE_ID="${INSPECTION_PLACE_ID:-282}"
DROP_AMOUNT_STATUS="${DROP_AMOUNT_STATUS:-5458}"
ACCIDENT_LEVEL="${ACCIDENT_LEVEL:-5456}"
ACCIDENT_LOCATION_ADDRESS="${ACCIDENT_LOCATION_ADDRESS:-استان تهران شهر تهران}"
CULPRIT_LICENCE_NO="${CULPRIT_LICENCE_NO:-${FANAVARAN_DUMMY_LICENCE_NO:-9705463515}}"
CULPRIT_LICENCE_ISSU_DATE="${CULPRIT_LICENCE_ISSU_DATE:-1394/10/13}"
LICENCE_NO="${LICENCE_NO:-$CULPRIT_LICENCE_NO}"
DESC="${DESC:-موارد آسیب دیده خودرو}"
REPAIR_WAGE="${REPAIR_WAGE:-0}"
COMPONENT_REPLACEMENT_COST="${COMPONENT_REPLACEMENT_COST:-0}"
WASTE_VALUE="${WASTE_VALUE:-0}"
DROP_AMOUNT_ADDITIONS="${DROP_AMOUNT_ADDITIONS:-0}"
COMPONENT_REPLACEMENT_TAX="${COMPONENT_REPLACEMENT_TAX:-0}"
INSURANCE_CORP_CAPTION="${INSURANCE_CORP_CAPTION:-${INSURANCE_CORP_ID:-}}"
export \
ACCIDENT_CITY_ID ACCIDENT_REPORT_TYPE_ID ACCIDENT_VEHICLE_USED_ID \
COMPENSATION_REFERENCE_ID CULPRIT_LICENCE_TYPE_ID CULPRIT_TYPE_ID \
ACCIDENT_CAUSE_ID DMG_CASE_TYPE_ID DMG_HISTORY_STATUS \
PLAQUE_KIND_ID PLAQUE_SAMPLE_ID DRIVER_IS_OWNER FAULT_PERCENT \
ESTIMATE_AMOUNT INSPECTION_PLACE_ID DROP_AMOUNT_STATUS ACCIDENT_LEVEL \
ACCIDENT_LOCATION_ADDRESS CULPRIT_LICENCE_NO CULPRIT_LICENCE_ISSU_DATE \
LICENCE_NO DESC REPAIR_WAGE COMPONENT_REPLACEMENT_COST WASTE_VALUE \
DROP_AMOUNT_ADDITIONS COMPONENT_REPLACEMENT_TAX DRIVER_IS_INSURER \
INSURANCE_CORP_CAPTION INSURANCE_CORP_ID \
CLAIM_EXPERT_ID EXPERTISE_CLAIM_EXPERT_ID CLAIM_FILE_TYPE_ID \
ACCIDENT_DATE ACCIDENT_TIME GUILTY_NATIONAL_CODE \
DRIVER_NATIONAL_CODE DRIVER_BIRTH_YEAR DRIVER_BIRTH_MONTH DRIVER_BIRTH_DAY \
VEHICLE_KIND_ID DMG_SECTION_ID DMG_ASSESSMENT_DATE INSPECTION_TIME \
FANAVARAN_CLIENT CORP_ID CONTRACT_ID LOCATION CURL_PROXY
}
auth_fingerprint() {
printf '%s' "${APP_NAME}|${FANAVARAN_USERNAME}|${CORP_ID}|${CONTRACT_ID}|${LOCATION}"
}
next_tehran_midnight_ms() {
node <<'NODE'
const nowMs = Date.now();
const parts = Object.fromEntries(
new Intl.DateTimeFormat("en-US", {
timeZone: "Asia/Tehran",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
})
.formatToParts(new Date(nowMs))
.filter((p) => 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" <<EOF
FANAVARAN_CLIENT='$FANAVARAN_CLIENT'
FANAVARAN_BASE_URL='$FANAVARAN_BASE_URL'
FANAVARAN_BIME_URL='$FANAVARAN_BIME_URL'
APP_TOKEN='$APP_TOKEN'
AUTHENTICATION_TOKEN='$AUTHENTICATION_TOKEN'
CORP_ID='$CORP_ID'
CONTRACT_ID='$CONTRACT_ID'
LOCATION='$LOCATION'
AUTH_EXPIRES_AT='$expires_at'
AUTH_FINGERPRINT='$(auth_fingerprint)'
EOF
cp "$TOKEN_CACHE_FILE" "$OUT_DIR/tokens.env" 2>/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 <file>"
;;
*) die "Unknown option: $1 (see --help)" ;;
esac
done
[[ -n "$ENV_FILE" ]] || die "--env <file> 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