Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
- Config, docs, scripts, and backup manifests - Submodule refs unchanged (m = modified content in submodules) Made-with: Cursor
129 lines
6.4 KiB
Bash
Executable File
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
|