Files
proxmox/scripts/omnl/omnl-audit-packet-office20.sh
defiQUG b3a8fe4496
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
chore: sync all changes to Gitea
- Config, docs, scripts, and backup manifests
- Submodule refs unchanged (m = modified content in submodules)

Made-with: Cursor
2026-03-02 11:37:34 -08:00

129 lines
6.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# OMNL Fineract — Office 20 Audit Packet: one folder per run with snapshot, computed balances, recent JEs, manifest.
# Makes the 5B position defensible for audit. Run daily or after material postings.
# Usage: from repo root. OUT_BASE=./reconciliation (default). See OPERATING_RAILS.md and OFFICE_20_AUDIT_PACKET.md.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
OUT_BASE="${OUT_BASE:-${REPO_ROOT}/reconciliation}"
OFFICE_ID="${OFFICE_ID:-20}"
RECENT_JE_DAYS="${RECENT_JE_DAYS:-90}"
TIMESTAMP="${TIMESTAMP:-$(date -u +%Y%m%d-%H%M%S)}"
PACKET_DIR="${OUT_BASE}/audit-office${OFFICE_ID}-${TIMESTAMP}"
if [ -f "${REPO_ROOT}/omnl-fineract/.env" ]; then
set +u
source "${REPO_ROOT}/omnl-fineract/.env" 2>/dev/null || true
set -u
elif [ -f "${REPO_ROOT}/.env" ]; then
set +u
source "${REPO_ROOT}/.env" 2>/dev/null || true
set -u
fi
BASE_URL="${OMNL_FINERACT_BASE_URL:-}"
TENANT="${OMNL_FINERACT_TENANT:-omnl}"
USER="${OMNL_FINERACT_USER:-app.omnl}"
PASS="${OMNL_FINERACT_PASSWORD:-}"
if [ -z "$BASE_URL" ] || [ -z "$PASS" ]; then
echo "Set OMNL_FINERACT_BASE_URL and OMNL_FINERACT_PASSWORD" >&2
exit 1
fi
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
mkdir -p "$PACKET_DIR"
# --- 1) Snapshot: offices + GL (1410, 2100, 2410)
offices=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/offices" 2>/dev/null)
offices_json=$(echo "$offices" | jq --argjson o "$OFFICE_ID" '[.[] | select(.id == $o or .id == 1)]' 2>/dev/null || echo "[]")
glaccounts=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/glaccounts" 2>/dev/null)
gl_json=$(echo "$glaccounts" | jq '[.[] | select(.glCode == "1410" or .glCode == "2100" or .glCode == "2410") | {glCode, id, name, organizationRunningBalance: (.organizationRunningBalance // "n/a")}]' 2>/dev/null || echo "[]")
jq -n \
--arg ts "$(date -u -Iseconds)" \
--argjson oid "$OFFICE_ID" \
--arg op "${OPERATOR_ID:-manual}" \
--argjson ofc "$offices_json" \
--argjson gl "$gl_json" \
'{ timestamp: $ts, officeId: $oid, operator: $op, offices: $ofc, glRelevant: $gl }' \
> "${PACKET_DIR}/snapshot.json"
# --- 2) Snapshot.meta (sha256, timestamp)
SNAPSHOT_SHA=$(sha256sum "${PACKET_DIR}/snapshot.json" 2>/dev/null | awk '{print $1}')
cat > "${PACKET_DIR}/snapshot.meta" <<EOF
timestamp=$TIMESTAMP
snapshot_file=snapshot.json
sha256=$SNAPSHOT_SHA
officeId=$OFFICE_ID
EOF
# --- 3) Recent journal entries (office 20). API returns pageItems of line-level entries; require dateFormat+locale.
from_date=$(date -u -d "-${RECENT_JE_DAYS} days" +%Y-%m-%d 2>/dev/null || date -u -v-${RECENT_JE_DAYS}d +%Y-%m-%d 2>/dev/null || echo "2020-01-01")
to_date=$(date -u -d "+1 day" +%Y-%m-%d 2>/dev/null || date -u -v+1d +%Y-%m-%d 2>/dev/null || date -u +%Y-%m-%d)
je_url="${BASE_URL}/journalentries?officeId=${OFFICE_ID}&fromDate=${from_date}&toDate=${to_date}&dateFormat=yyyy-MM-dd&locale=en"
je_resp=$(curl "${CURL_OPTS[@]}" "$je_url" 2>/dev/null) || je_resp=""
# Normalize: API returns { totalFilteredRecords, pageItems } with each item = one line (glAccountId, amount, entryType DEBIT/CREDIT)
je_json=$(echo "$je_resp" | jq 'if type == "array" then . else (.pageItems // .) end | if type == "array" then . else [] end' 2>/dev/null || echo "[]")
je_list=$(echo "$je_json" | jq '
(if type == "array" then . else [] end) |
map({
id: (.id // .resourceId),
officeId: (.officeId // .office.id),
glAccountId: (.glAccountId // .glAccount.id),
glAccountCode: (.glAccountCode // ""),
amount: (.amount // 0),
entryType: (if .entryType then (.entryType | if type == "string" then . else .value end) else "n/a" end),
transactionDate: (if .transactionDate | type == "array" then (.transactionDate | [.[0], (.[1] // 1), (.[2] // 1)] | map(tostring) | (.[0] + "-" + (.[1] | if length == 1 then "0" + . else . end) + "-" + (.[2] | if length == 1 then "0" + . else . end))) else (.transactionDate // .transaction_date // "n/a") end),
transactionId: (.transactionId // ""),
referenceNumber: (.referenceNumber // .reference_number // ""),
comments: (.comments // "")
})
' 2>/dev/null || echo "[]")
echo "$je_list" > "${PACKET_DIR}/recent_journal_entries.json"
# --- 4) Computed balances: from line-level entries, sum signed amount by glAccountId (DEBIT +, CREDIT -)
if [ -s "${PACKET_DIR}/recent_journal_entries.json" ]; then
balances=$(jq -s '
(.[0] | if type == "array" then . else [] end) |
reduce .[] as $e (
{};
($e.glAccountId | tostring) as $k |
(if (($e.entryType // "") | test("debit"; "i")) then ($e.amount // 0) else -(($e.amount // 0)) end) as $signed |
.[$k] = ((.[$k] // 0) + $signed)
)
' "${PACKET_DIR}/recent_journal_entries.json" 2>/dev/null || echo "{}")
else
balances="{}"
fi
jq -n \
--arg ts "$(date -u -Iseconds)" \
--argjson oid "$OFFICE_ID" \
--argjson bal "$balances" \
'{ timestamp: $ts, officeId: $oid, computedBy: "sum_of_journal_entries", balancesByGlId: $bal, note: "Deterministic from JE lines; use trial balance to compare reported." }' \
> "${PACKET_DIR}/computed_balances.json"
# --- 5) Manifest (scripts, env version, git, operator)
GIT_COMMIT=$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || echo "n/a")
GIT_BRANCH=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "n/a")
MANIFEST=$(jq -n \
--arg ts "$(date -u -Iseconds)" \
--arg script "omnl-audit-packet-office20.sh" \
--arg repo "$REPO_ROOT" \
--arg gitCommit "$GIT_COMMIT" \
--arg gitBranch "$GIT_BRANCH" \
--arg operator "${OPERATOR_ID:-manual}" \
--arg envBaseUrl "(redacted)" \
--arg tenant "$TENANT" \
'{ timestamp: $ts, script: $script, repo: $repo, gitCommit: $gitCommit, gitBranch: $gitBranch, operator: $operator, tenant: $tenant, envBaseUrl: $envBaseUrl }')
echo "$MANIFEST" > "${PACKET_DIR}/manifest.json"
# --- 6) Chain-of-custody: append folder hash to audit log (outside reconciliation/)
AUDIT_LOG_PATH="${AUDIT_LOG_PATH:-${REPO_ROOT}/audit_log.jsonl}"
FOLDER_SHA=$(find "$PACKET_DIR" -type f -exec sha256sum {} \; 2>/dev/null | sort | sha256sum | awk '{print $1}')
echo "{\"timestamp\": \"$(date -u -Iseconds)\", \"packetDir\": \"$PACKET_DIR\", \"folderSha256\": \"$FOLDER_SHA\", \"officeId\": $OFFICE_ID}" >> "$AUDIT_LOG_PATH" 2>/dev/null || true
echo "Audit packet: $PACKET_DIR" >&2
echo " snapshot.json, snapshot.meta, computed_balances.json, recent_journal_entries.json, manifest.json" >&2
echo " sha256: $SNAPSHOT_SHA | folderSha256: $FOLDER_SHA (appended to $AUDIT_LOG_PATH)" >&2