#!/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