- 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
213 lines
9.2 KiB
Bash
Executable File
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
|