Files
proxmox/scripts/omnl/omnl-m1-clearing-transfer-between-offices.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

213 lines
9.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# OMNL Fineract — Move M1 clearing-style position (GL 2100 / 1410) from one office to another.
#
# Accounting (same structure as omnl-pvp-post-clearing-bank-kanaya.sh branch leg):
# - Unwind source office: Dr 1410 / Cr 2100 (reverses Dr 2100 / Cr 1410)
# - Book target office: Dr 2100 / Cr 1410 (same as PvP beneficiary branch leg)
# Head office leg (Dr 2410 / Cr 2100) is unchanged — beneficiary reallocates at branch level only.
#
# Compliance (live post, DRY_RUN=0):
# - Set COMPLIANCE_AUTH_REF (e.g. committee minute id, ticket, legal opinion ref).
# - Set COMPLIANCE_APPROVER (human name) for material amounts (>= MATERIAL_THRESHOLD_COMPLIANCE, default 10_000_000).
# - Stable REFERENCE_BASE in journal referenceNumber (default HYBX-BATCH-001-BEN-REALLOC).
# - Run DRY_RUN=1 first; use maker-checker (WRITE_MAKER_PAYLOADS=1) if policy requires segregated duties.
# IPSAS / IFRS (IFGA default): comments append COMPLIANCE_STANDARD_MEMO — see
# docs/04-configuration/mifos-omnl-central-bank/OMNL_IPSAS_IFRS_INTEROFFICE_COMPLIANCE.md
#
# Amount:
# - Default: FETCH_AMOUNT_FROM_API=1 sums non-reversed DEBIT lines on GL 2100 at FROM_OFFICE (matches posted PvP Kanaya legs).
# - Override: AMOUNT=<same numeric scale as existing JEs on the tenant> (required if fetch yields 0).
#
# Usage:
# DRY_RUN=1 bash scripts/omnl/omnl-m1-clearing-transfer-between-offices.sh
# FROM_OFFICE=21 TO_OFFICE=22 DRY_RUN=0 COMPLIANCE_AUTH_REF=DIR-2026-0330 COMPLIANCE_APPROVER="CFO Name" \
# bash scripts/omnl/omnl-m1-clearing-transfer-between-offices.sh
#
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
DRY_RUN="${DRY_RUN:-1}"
TRANSACTION_DATE="${TRANSACTION_DATE:-$(date +%Y-%m-%d)}"
FROM_OFFICE="${FROM_OFFICE:-21}"
TO_OFFICE="${TO_OFFICE:-22}"
REFERENCE_BASE="${REFERENCE_BASE:-HYBX-BATCH-001-BEN-REALLOC}"
SETTLEMENT_CONTEXT="${SETTLEMENT_CONTEXT:-HYBX-BATCH-001 multilateral net beneficiary realloc Bank Kanaya to PT CAKRA}"
FETCH_AMOUNT_FROM_API="${FETCH_AMOUNT_FROM_API:-1}"
AMOUNT="${AMOUNT:-}"
MATERIAL_THRESHOLD_COMPLIANCE="${MATERIAL_THRESHOLD_COMPLIANCE:-10000000}"
COMPLIANCE_AUTH_REF="${COMPLIANCE_AUTH_REF:-}"
COMPLIANCE_APPROVER="${COMPLIANCE_APPROVER:-}"
WRITE_MAKER_PAYLOADS="${WRITE_MAKER_PAYLOADS:-0}"
# Appended to Fineract comments (IPSAS + IFRS; IFGA = IFRS unless org defines otherwise)
COMPLIANCE_STANDARD_MEMO="${COMPLIANCE_STANDARD_MEMO:-IPSAS:1,3,9,28,29 accrual double-entry inter-office M1 realloc no revenue. IFRS/IFGA-default: IAS32 IFRS7 IFRS9 amortised cost no PnL on symmetric 1410/2100 legs.}"
if [ -f "${REPO_ROOT}/omnl-fineract/.env" ]; then
set +u
# shellcheck disable=SC1090
source "${REPO_ROOT}/omnl-fineract/.env" 2>/dev/null || true
set -u
elif [ -f "${REPO_ROOT}/.env" ]; then
set +u
# shellcheck disable=SC1090
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_GET=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
CURL_POST=(-s -S -w "\n%{http_code}" -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
fetch_office_journal_all() {
local oid="$1"
local offset=0
local limit=500
local acc='[]'
while true; do
local resp
resp=$(curl "${CURL_GET[@]}" "${BASE_URL}/journalentries?officeId=${oid}&offset=${offset}&limit=${limit}")
local batch
batch=$(echo "$resp" | jq -c '.pageItems // []')
local n
n=$(echo "$batch" | jq 'length')
acc=$(jq -n --argjson a "$acc" --argjson b "$batch" '$a + $b')
local total
total=$(echo "$resp" | jq -r '.totalFilteredRecords // 0')
offset=$((offset + n))
if [ "$n" -lt "$limit" ] || [ "$offset" -ge "$total" ]; then
break
fi
done
echo "$acc"
}
sum_2100_debits() {
local items="$1"
echo "$items" | jq '[.[] | select((.reversed // false) | not) | select(.glAccountCode == "2100") | select((.entryType.value // .entryType // "") | ascii_downcase | test("debit"))] | map(.amount | tonumber) | add // 0'
}
GL_RAW=$(curl "${CURL_GET[@]}" "${BASE_URL}/glaccounts")
GL_JSON=$(echo "$GL_RAW" | jq -c 'if type == "array" then . else (.pageItems // []) end' 2>/dev/null || echo "[]")
get_gl_id() {
local code="$1"
echo "$GL_JSON" | jq -r --arg c "$code" '.[]? | select(.glCode == $c) | .id // empty' 2>/dev/null | head -n1
}
ID_1410="$(get_gl_id "1410")"
ID_2100="$(get_gl_id "2100")"
if [ -z "$ID_1410" ] || [ -z "$ID_2100" ]; then
echo "ERROR: Missing GL 1410 or 2100." >&2
exit 1
fi
FROM_NAME=$(curl "${CURL_GET[@]}" "${BASE_URL}/offices" | jq -r --argjson id "$FROM_OFFICE" '.[] | select(.id == $id) | .name // empty' | head -1)
TO_NAME=$(curl "${CURL_GET[@]}" "${BASE_URL}/offices" | jq -r --argjson id "$TO_OFFICE" '.[] | select(.id == $id) | .name // empty' | head -1)
if [ -z "$FROM_NAME" ] || [ -z "$TO_NAME" ]; then
echo "ERROR: Could not resolve office name for FROM_OFFICE=$FROM_OFFICE or TO_OFFICE=$TO_OFFICE" >&2
exit 1
fi
if [ -n "$AMOUNT" ]; then
TRANSFER_AMT="$AMOUNT"
elif [ "$FETCH_AMOUNT_FROM_API" = "1" ]; then
ITEMS=$(fetch_office_journal_all "$FROM_OFFICE")
TRANSFER_AMT=$(sum_2100_debits "$ITEMS")
else
echo "ERROR: Set AMOUNT= or FETCH_AMOUNT_FROM_API=1" >&2
exit 1
fi
if ! awk -v a="$TRANSFER_AMT" 'BEGIN { if (a + 0 > 0) exit 0; exit 1 }'; then
echo "ERROR: Transfer amount must be > 0 (got ${TRANSFER_AMT}). Set AMOUNT explicitly or ensure GL 2100 debits exist at FROM_OFFICE." >&2
exit 1
fi
REF_UNWIND="${REFERENCE_BASE}-UNWIND-${FROM_OFFICE}"
REF_BOOK="${REFERENCE_BASE}-BOOK-${TO_OFFICE}"
NARR_UNWIND="M1 clearing beneficiary realloc: unwind at ${FROM_NAME} (office ${FROM_OFFICE}). Auth: ${COMPLIANCE_AUTH_REF:-n/a}. ${SETTLEMENT_CONTEXT} | ${COMPLIANCE_STANDARD_MEMO}"
NARR_BOOK="M1 clearing beneficiary realloc: book at ${TO_NAME} (office ${TO_OFFICE}). Auth: ${COMPLIANCE_AUTH_REF:-n/a}. ${SETTLEMENT_CONTEXT} | ${COMPLIANCE_STANDARD_MEMO}"
if [ "$DRY_RUN" != "1" ]; then
if [ -z "$COMPLIANCE_AUTH_REF" ]; then
echo "ERROR: Live post requires COMPLIANCE_AUTH_REF (governance / ticket / minute reference)." >&2
exit 1
fi
if awk -v a="$TRANSFER_AMT" -v t="$MATERIAL_THRESHOLD_COMPLIANCE" 'BEGIN { exit !(a >= t) }'; then
if [ -z "$COMPLIANCE_APPROVER" ]; then
echo "ERROR: Amount ${TRANSFER_AMT} >= ${MATERIAL_THRESHOLD_COMPLIANCE}; set COMPLIANCE_APPROVER for dual-control attestation." >&2
exit 1
fi
fi
fi
post_je() {
local office_id="$1"
local debit_id="$2"
local credit_id="$3"
local ref="$4"
local memo="$5"
local body
body=$(jq -n \
--argjson officeId "$office_id" \
--arg transactionDate "$TRANSACTION_DATE" \
--arg comments "$memo" \
--arg referenceNumber "$ref" \
--argjson debitId "$debit_id" \
--argjson creditId "$credit_id" \
--argjson amount "$TRANSFER_AMT" \
'{ officeId: $officeId, transactionDate: $transactionDate, dateFormat: "yyyy-MM-dd", locale: "en", currencyCode: "USD", comments: $comments, referenceNumber: $referenceNumber, debits: [ { glAccountId: $debitId, amount: $amount } ], credits: [ { glAccountId: $creditId, amount: $amount } ] }')
if [ "$WRITE_MAKER_PAYLOADS" = "1" ]; then
local pdir="${REPO_ROOT}/reconciliation"
mkdir -p "$pdir"
local safe
safe=$(echo "$ref" | tr -c 'A-Za-z0-9_-' '_')
local to_write="$body"
if awk -v a="$TRANSFER_AMT" -v t="$MATERIAL_THRESHOLD_COMPLIANCE" 'BEGIN { exit !(a >= t) }' \
&& [ -n "${COMPLIANCE_APPROVER:-}" ]; then
to_write=$(echo "$body" | jq --arg approver "$COMPLIANCE_APPROVER" --arg approvedAt "$(date -u -Iseconds)" \
'. + { approvalMetadata: { approver: $approver, approvedAt: $approvedAt } }')
fi
echo "$to_write" > "${pdir}/je-${safe}.payload.json"
sha256sum "${pdir}/je-${safe}.payload.json" | awk '{print $1}' > "${pdir}/je-${safe}.payload.sha256"
echo "Wrote maker payload ${pdir}/je-${safe}.payload.json (post: PAYLOAD_FILE=... DRY_RUN=0 bash scripts/omnl/omnl-je-checker.sh)" >&2
fi
if [ "$DRY_RUN" = "1" ]; then
echo "DRY_RUN JE: office=$office_id ref=$ref" >&2
echo "$body" | jq .
return 0
fi
local out code resp
out=$(curl "${CURL_POST[@]}" -X POST -d "$body" "${BASE_URL}/journalentries" 2>/dev/null)
code=$(echo "$out" | tail -n1)
resp=$(echo "$out" | sed '$d')
if [ "$code" = "200" ] || [ "${code:0:1}" = "2" ]; then
echo "OK office=$office_id ref=$ref HTTP $code" >&2
echo "$resp" | jq . 2>/dev/null || echo "$resp"
else
echo "FAIL office=$office_id ref=$ref HTTP $code: $resp" >&2
return 1
fi
}
echo "M1 clearing transfer | from office ${FROM_OFFICE} (${FROM_NAME}) → ${TO_OFFICE} (${TO_NAME}) | amount=${TRANSFER_AMT} | DRY_RUN=${DRY_RUN}" >&2
echo "JE1 unwind: office ${FROM_OFFICE} Dr 1410 Cr 2100 | ref ${REF_UNWIND}" >&2
echo "JE2 book: office ${TO_OFFICE} Dr 2100 Cr 1410 | ref ${REF_BOOK}" >&2
# Unwind source (mirror of PvP branch leg)
post_je "$FROM_OFFICE" "$ID_1410" "$ID_2100" "$REF_UNWIND" "$NARR_UNWIND"
# Book target
post_je "$TO_OFFICE" "$ID_2100" "$ID_1410" "$REF_BOOK" "$NARR_BOOK"
echo "Done." >&2