Files
proxmox/scripts/omnl/generate-3way-reconciliation-evidence.sh
defiQUG 7ac74f432b chore: sync docs, config schemas, scripts, and meta task alignment
- Institutional / JVMTM / reserve-provenance / GRU transport + standards JSON
- Validation and verify scripts (Blockscout labels, x402, GRU preflight, P1 local path)
- Wormhole wiring in AGENTS, MCP_SETUP, MASTER_INDEX, 04-configuration README
- Meta docs, integration gaps, live verification log, architecture updates
- CI validate-config workflow updates

Operator/LAN items, submodule working trees, and public token-aggregation edge
routes remain follow-up (see TODOS_CONSOLIDATED P1).

Made-with: Cursor
2026-03-31 22:31:39 -07:00

270 lines
9.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Generate three-way reconciliation JSON from Fineract (ledger) + optional bank file/env + Chain 138 ERC20 balance.
# Operational evidence: bank leg requires operator-supplied statement/API (file or env). See
# config/jvmtm-regulatory-closure/OPERATIONAL_EVIDENCE_VS_TEMPLATES.md
#
# Env (after sourcing load-project-env):
# OMNL_FINERACT_BASE_URL, OMNL_FINERACT_USER, OMNL_FINERACT_PASSWORD, OMNL_FINERACT_TENANT (default omnl)
# RECON_OFFICE_ID (default 21), RECON_GL_CODE (default 2100)
# RECON_TOKEN_ADDRESS (default canonical cUSDT on 138), RECON_CHAIN_HOLDER (default deployer), RECON_TOKEN_DECIMALS (default 6)
# JVMTM_CORRELATION_ID — use real UUID for examination (not literal PLACEHOLDER)
# JVMTM_BANK_BALANCE_JSON — path: {"value_major","statement_ref","fetched_at"?}
# JVMTM_BANK_BALANCE_MAJOR + JVMTM_BANK_STATEMENT_REF — alternative
# JVMTM_EVIDENCE_DIR — default REPO/output/jvmtm-evidence
# AS_OF — YYYY-MM-DD (default UTC today)
#
# Output: 3way-<AS_OF>.json + latest-3way-result.json
#
set -eo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# shellcheck source=scripts/lib/load-project-env.sh
set +u
source "${REPO_ROOT}/scripts/lib/load-project-env.sh"
if [[ -f "${REPO_ROOT}/omnl-fineract/.env" ]]; then
set -a
# shellcheck disable=SC1090
source "${REPO_ROOT}/omnl-fineract/.env" 2>/dev/null || true
set +a
fi
set -euo pipefail
AS_OF="${AS_OF:-$(date -u +%Y-%m-%d)}"
OUT_DIR="${JVMTM_EVIDENCE_DIR:-${REPO_ROOT}/output/jvmtm-evidence}"
mkdir -p "$OUT_DIR"
OFFICE_ID="${RECON_OFFICE_ID:-21}"
GL_CODE="${RECON_GL_CODE:-2100}"
CORR="${JVMTM_CORRELATION_ID:-PLACEHOLDER}"
TOKEN_ADDR="${RECON_TOKEN_ADDRESS:-0x93E66202A11B1772E55407B32B44e5Cd8eda7f22}"
HOLDER="${RECON_CHAIN_HOLDER:-0x4A666F96fC8764181194447A7dFdb7d471b301C8}"
DECIMALS="${RECON_TOKEN_DECIMALS:-6}"
RPC="${RPC_URL_138:-http://192.168.11.211:8545}"
REPORT_ID="3WAY-GEN-${AS_OF}-$(date -u +%H%M%S)"
GAPS=()
LEDGER_SOURCE="fineract:/glaccounts"
CHAIN_SOURCE="cast:erc20_balanceOf"
RPC_HOST="$(RPC_URL="$RPC" python3 -c "from urllib.parse import urlparse; import os; print(urlparse(os.environ['RPC_URL']).hostname or os.environ['RPC_URL'])")"
BASE_URL="${OMNL_FINERACT_BASE_URL:-}"
PASS="${OMNL_FINERACT_PASSWORD:-}"
USER="${OMNL_FINERACT_USER:-app.omnl}"
TENANT="${OMNL_FINERACT_TENANT:-omnl}"
if [[ -n "$BASE_URL" && -n "$PASS" ]]; then
GL_RAW="$(curl -sS -H "Fineract-Platform-TenantId: ${TENANT}" -u "${USER}:${PASS}" "${BASE_URL}/glaccounts" || true)"
LEDGER_BLOCK="$(GL_RAW="$GL_RAW" OFFICE_ID="$OFFICE_ID" GL_CODE="$GL_CODE" python3 <<'PY'
import json, os
from datetime import datetime, timezone
office = int(os.environ["OFFICE_ID"])
code = os.environ["GL_CODE"]
raw = os.environ.get("GL_RAW", "[]")
try:
data = json.loads(raw)
except json.JSONDecodeError:
data = []
if isinstance(data, dict) and "pageItems" in data:
data = data["pageItems"]
rows = [
x for x in data
if isinstance(x, dict) and str(x.get("glCode")) == code
and (x.get("officeId") == office or x.get("officeId") is None)
]
acc = rows[0] if rows else {}
bal = acc.get("organizationRunningBalance")
if bal is None:
bal = acc.get("runningBalance")
if bal is None:
s = acc.get("summary")
if isinstance(s, dict):
bal = s.get("runningBalance")
out = {
"value_major": None,
"source": "fineract:/glaccounts",
"fetched_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"gl_code": code,
"office_id": office,
"gl_account_id": acc.get("id"),
"raw_field": "organizationRunningBalance|runningBalance",
}
if bal is not None:
out["value_major"] = str(bal)
print(json.dumps({"ledger_line": out, "found": bool(acc)}))
PY
)"
LEDGER_VAL="$(echo "$LEDGER_BLOCK" | jq -r '.ledger_line.value_major // empty')"
if [[ "$(echo "$LEDGER_BLOCK" | jq -r '.found')" != "true" ]] || [[ -z "$LEDGER_VAL" ]]; then
GAPS+=("fineract_gl_balance_missing")
fi
LEDGER_JSON="$(echo "$LEDGER_BLOCK" | jq -c '.ledger_line')"
else
GAPS+=("fineract_unreachable_or_unconfigured")
LEDGER_JSON="$(jq -nc \
--arg s "$LEDGER_SOURCE" \
--arg ft "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--argjson oid "$OFFICE_ID" \
--arg gc "$GL_CODE" \
'{value_major: null, source: $s, fetched_at: $ft, gl_code: $gc, office_id: $oid, raw_field: "n/a"}')"
fi
if command -v cast &>/dev/null; then
RAW_BAL="$(cast call "$TOKEN_ADDR" "balanceOf(address)(uint256)" "$HOLDER" --rpc-url "$RPC" 2>/dev/null || echo "")"
if [[ -n "$RAW_BAL" ]]; then
RAW_ONE="$(echo "$RAW_BAL" | awk '{print $1}')"
CHAIN_JSON="$(RAW_BAL="$RAW_ONE" DECIMALS="$DECIMALS" TOKEN_ADDR="$TOKEN_ADDR" HOLDER="$HOLDER" RPC_HOST="$RPC_HOST" python3 <<'PY'
import os, json
from decimal import Decimal
from datetime import datetime, timezone
raw = int(os.environ["RAW_BAL"].strip(), 0)
dec = int(os.environ["DECIMALS"])
major = str(Decimal(raw) / (Decimal(10) ** dec))
out = {
"value_major": major,
"source": "cast:erc20_balanceOf",
"fetched_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"rpc_url_host": os.environ.get("RPC_HOST", ""),
"chain_id": 138,
"token_address": os.environ["TOKEN_ADDR"],
"holder_address": os.environ["HOLDER"],
"decimals": dec,
}
print(json.dumps(out))
PY
)"
else
GAPS+=("chain_balance_query_failed")
CHAIN_JSON="$(jq -nc \
--arg s "$CHAIN_SOURCE" \
--arg ft "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg th "$RPC_HOST" \
--arg ta "$TOKEN_ADDR" \
--arg hd "$HOLDER" \
--argjson dec "$DECIMALS" \
'{value_major: null, source: $s, fetched_at: $ft, rpc_url_host: $th, chain_id: 138, token_address: $ta, holder_address: $hd, decimals: $dec}')"
fi
else
GAPS+=("cast_not_installed")
CHAIN_JSON="$(jq -nc \
--arg s "$CHAIN_SOURCE" \
--arg ft "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg th "$RPC_HOST" \
--arg ta "$TOKEN_ADDR" \
--arg hd "$HOLDER" \
--argjson dec "$DECIMALS" \
'{value_major: null, source: $s, fetched_at: $ft, rpc_url_host: $th, chain_id: 138, token_address: $ta, holder_address: $hd, decimals: $dec}')"
fi
if [[ -n "${JVMTM_BANK_BALANCE_JSON:-}" && -f "${JVMTM_BANK_BALANCE_JSON}" ]]; then
BANK_JSON="$(jq -c \
--arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{value_major: .value_major, source: (.source // "operator:jvmtm_bank_json_file"), fetched_at: (.fetched_at // $now), statement_ref: .statement_ref}' \
"${JVMTM_BANK_BALANCE_JSON}")"
BANK_VAL="$(echo "$BANK_JSON" | jq -r '.value_major // empty')"
if [[ -z "$BANK_VAL" ]]; then
GAPS+=("bank_file_missing_value_major")
BANK_JSON="null"
fi
elif [[ -n "${JVMTM_BANK_BALANCE_MAJOR:-}" ]]; then
BANK_JSON="$(jq -nc \
--arg v "${JVMTM_BANK_BALANCE_MAJOR}" \
--arg r "${JVMTM_BANK_STATEMENT_REF:-nostro-export}" \
--arg ft "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{value_major: $v, source: "operator:env_JVMTM_BANK_BALANCE_MAJOR", fetched_at: $ft, statement_ref: $r}')"
else
GAPS+=("bank_statement_not_supplied")
BANK_JSON="null"
fi
[[ ${#CORR} -lt 8 ]] && GAPS+=("correlation_id_too_short_use_JVMTM_CORRELATION_ID")
[[ "$CORR" == "PLACEHOLDER" ]] && GAPS+=("correlation_id_placeholder_not_examination_grade")
GAPS_JSON="$(printf '%s\n' "${GAPS[@]}" | jq -R -s -c 'split("\n") | map(select(length>0))')"
ARGV_JSON="$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1:]))' -- "$@")"
export LEDGER_JSON CHAIN_JSON BANK_JSON GAPS_JSON CORR REPORT_ID AS_OF ARGV_JSON
FINAL_JSON="$(python3 <<'PY'
import json, os
from decimal import Decimal, InvalidOperation
def D(x):
if x is None or x == "":
return None
try:
return Decimal(str(x))
except InvalidOperation:
return None
ledger = json.loads(os.environ["LEDGER_JSON"])
chain = json.loads(os.environ["CHAIN_JSON"])
bank_s = os.environ["BANK_JSON"]
bank = json.loads(bank_s) if bank_s != "null" else None
gaps = json.loads(os.environ["GAPS_JSON"])
corr = os.environ["CORR"]
report_id = os.environ["REPORT_ID"]
as_of = os.environ["AS_OF"]
argv = json.loads(os.environ["ARGV_JSON"])
lv = D(ledger.get("value_major"))
cv = D(chain.get("value_major") if chain else None)
bv = D(bank.get("value_major") if bank else None)
eps = Decimal("0.01")
def sub(a, b):
if a is None or b is None:
return None
return str(a - b)
var = {
"ledger_vs_bank_major": sub(lv, bv) if bv is not None else "n/a",
"ledger_vs_chain_major": sub(lv, cv) if cv is not None else "n/a",
"bank_vs_chain_major": sub(bv, cv) if bv is not None and cv is not None else "n/a",
}
matched = False
if lv is not None and cv is not None and bv is not None:
matched = abs(lv - cv) <= eps and abs(lv - bv) <= eps and abs(bv - cv) <= eps
elif lv is not None and cv is not None and bv is None:
matched = abs(lv - cv) <= eps
if any(g in gaps for g in ("bank_statement_not_supplied", "bank_file_missing_value_major")):
tier = "GENERATED_PARTIAL"
elif not gaps:
tier = "GENERATED_FULL" if matched and bv is not None else "GENERATED_PARTIAL"
else:
tier = "INCOMPLETE"
from datetime import datetime, timezone
import socket
gen_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
out = {
"schema_version": 1,
"report_id": report_id,
"as_of": as_of,
"correlation_id": corr,
"currency": "USD",
"evidence_tier": tier,
"evidence_gaps": gaps,
"ledger": ledger,
"bank": bank,
"chain": chain,
"variance": var,
"matched": matched,
"generated_at": gen_at,
"generator": {
"script": "scripts/omnl/generate-3way-reconciliation-evidence.sh",
"argv": argv,
"host": socket.gethostname(),
},
}
print(json.dumps(out, indent=2))
PY
)"
echo "$FINAL_JSON" | tee "${OUT_DIR}/3way-${AS_OF}.json" > "${OUT_DIR}/latest-3way-result.json"
echo "Wrote ${OUT_DIR}/3way-${AS_OF}.json and latest-3way-result.json" >&2