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
180 lines
6.7 KiB
Bash
180 lines
6.7 KiB
Bash
#!/usr/bin/env bash
|
|
# OMNL Fineract — Post journal entries from the matrix JSON (OMNL_JOURNAL_LEDGER_MATRIX / omnl-journal-matrix.json).
|
|
# Resolves glCode → GL account id via GET /glaccounts; posts each entry via POST /journalentries to OMNL Hybx.
|
|
# Usage: run from repo root. Set DRY_RUN=1 to print payloads only.
|
|
# JOURNAL_MATRIX=<path> Default: docs/04-configuration/mifos-omnl-central-bank/omnl-journal-matrix.json
|
|
# TRANSACTION_DATE=yyyy-MM-dd Default: today
|
|
# Requires: curl, jq.
|
|
|
|
set -euo pipefail
|
|
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
DRY_RUN="${DRY_RUN:-0}"
|
|
TRANSACTION_DATE="${TRANSACTION_DATE:-$(date +%Y-%m-%d)}"
|
|
JOURNAL_MATRIX="${JOURNAL_MATRIX:-${REPO_ROOT}/docs/04-configuration/mifos-omnl-central-bank/omnl-journal-matrix.json}"
|
|
|
|
if [ ! -f "$JOURNAL_MATRIX" ]; then
|
|
echo "Journal matrix not found: $JOURNAL_MATRIX" >&2
|
|
exit 1
|
|
fi
|
|
|
|
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 (e.g. omnl-fineract/.env)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
CURL_OPTS=(-s -S -w "\n%{http_code}" -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
|
|
CURRENCY="${CURRENCY:-USD}"
|
|
DATE_FORMAT="${DATE_FORMAT:-yyyy-MM-dd}"
|
|
LOCALE="${LOCALE:-en}"
|
|
|
|
# Resolve glCode -> id from GET /glaccounts
|
|
GL_ACCOUNTS=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/glaccounts" 2>/dev/null | sed '$d')
|
|
get_gl_id() {
|
|
local code="$1"
|
|
echo "$GL_ACCOUNTS" | jq -r --arg c "$code" '.[] | select(.glCode == $c) | .id // empty' 2>/dev/null || true
|
|
}
|
|
|
|
# Post one journal entry (Fineract: officeId, transactionDate, comments, debits[], credits[]; referenceNumber for idempotency)
|
|
post_entry() {
|
|
local office_id="$1"
|
|
local debit_id="$2"
|
|
local credit_id="$3"
|
|
local amount="$4"
|
|
local comments="$5"
|
|
local ref="${6:-}"
|
|
local date_no_dash="${TRANSACTION_DATE//-/}"
|
|
[ -z "$ref" ] && ref="OMNL-JE-${office_id}-${date_no_dash}-${entry_index:-0}"
|
|
local body
|
|
body=$(jq -n \
|
|
--argjson officeId "$office_id" \
|
|
--arg transactionDate "$TRANSACTION_DATE" \
|
|
--arg comments "$comments" \
|
|
--arg referenceNumber "$ref" \
|
|
--arg dateFormat "$DATE_FORMAT" \
|
|
--arg locale "$LOCALE" \
|
|
--arg currencyCode "$CURRENCY" \
|
|
--argjson debitId "$debit_id" \
|
|
--argjson creditId "$credit_id" \
|
|
--argjson amount "$amount" \
|
|
'{ officeId: $officeId, transactionDate: $transactionDate, dateFormat: $dateFormat, locale: $locale, currencyCode: $currencyCode, comments: $comments, referenceNumber: $referenceNumber, credits: [ { glAccountId: $creditId, amount: $amount } ], debits: [ { glAccountId: $debitId, amount: $amount } ] }' 2>/dev/null)
|
|
if [ -z "$body" ]; then
|
|
echo "jq build failed for $comments" >&2
|
|
return 1
|
|
fi
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
echo "DRY_RUN: $comments -> (debit=$debit_id credit=$credit_id amount=$amount)" >&2
|
|
return 0
|
|
fi
|
|
local out
|
|
out=$(curl "${CURL_OPTS[@]}" -X POST -d "$body" "${BASE_URL}/journalentries" 2>/dev/null)
|
|
local code
|
|
code=$(echo "$out" | tail -n1)
|
|
local resp
|
|
resp=$(echo "$out" | sed '$d')
|
|
if [ "$code" = "200" ] || [ "${code:0:1}" = "2" ]; then
|
|
echo "OK $comments (HTTP $code)" >&2
|
|
else
|
|
echo "FAIL $comments HTTP $code: $resp" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# --- Guardrails: idempotency-by-enforcement + sanity checks
|
|
MAX_POST_AMOUNT="${MAX_POST_AMOUNT:-}"
|
|
ALLOWED_OFFICE_IDS="${ALLOWED_OFFICE_IDS:-1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20}"
|
|
POSTED_REFS_FILE="${POSTED_REFS_FILE:-${REPO_ROOT}/reconciliation/.posted_refs}"
|
|
# Optional: sync shared refs (e.g. S3). Pull before posting; push after each append or once at end.
|
|
POSTED_REFS_SYNC_PULL_CMD="${POSTED_REFS_SYNC_PULL_CMD:-}"
|
|
POSTED_REFS_SYNC_PUSH_CMD="${POSTED_REFS_SYNC_PUSH_CMD:-}"
|
|
|
|
[ -n "$POSTED_REFS_SYNC_PULL_CMD" ] && eval "$POSTED_REFS_SYNC_PULL_CMD" 2>/dev/null || true
|
|
|
|
guardrail_validate() {
|
|
local amount="$1"
|
|
local office_id="$2"
|
|
local memo="$3"
|
|
if [ -z "$amount" ] || [ "${amount:-0}" -le 0 ] 2>/dev/null; then
|
|
echo "GUARDRAIL: Skip $memo — amount must be positive (got: $amount)" >&2
|
|
return 1
|
|
fi
|
|
if [ -n "$MAX_POST_AMOUNT" ] && [ "${amount:-0}" -gt "${MAX_POST_AMOUNT}" ] 2>/dev/null; then
|
|
echo "GUARDRAIL: Skip $memo — amount exceeds MAX_POST_AMOUNT ($MAX_POST_AMOUNT)" >&2
|
|
return 1
|
|
fi
|
|
if [[ ! ",${ALLOWED_OFFICE_IDS}," =~ ,${office_id}, ]]; then
|
|
echo "GUARDRAIL: Skip $memo — officeId $office_id not in ALLOWED_OFFICE_IDS" >&2
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
ref_already_posted() {
|
|
local ref="$1"
|
|
[ -z "$ref" ] && return 1
|
|
[ -f "$POSTED_REFS_FILE" ] && grep -Fxq "$ref" "$POSTED_REFS_FILE" 2>/dev/null && return 0
|
|
return 1
|
|
}
|
|
|
|
record_posted_ref() {
|
|
local ref="$1"
|
|
[ -z "$ref" ] && return
|
|
mkdir -p "$(dirname "$POSTED_REFS_FILE")"
|
|
echo "$ref" >> "$POSTED_REFS_FILE"
|
|
[ -n "$POSTED_REFS_SYNC_PUSH_CMD" ] && eval "$POSTED_REFS_SYNC_PUSH_CMD" 2>/dev/null || true
|
|
}
|
|
|
|
entry_count=$(jq -r '.entries | length' "$JOURNAL_MATRIX")
|
|
if [ -z "$entry_count" ] || [ "$entry_count" = "0" ]; then
|
|
echo "No entries in $JOURNAL_MATRIX" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Posting $entry_count journal entries (date=$TRANSACTION_DATE) from matrix..." >&2
|
|
posted=0
|
|
for i in $(seq 0 $((entry_count - 1))); do
|
|
memo=$(jq -r ".entries[$i].memo" "$JOURNAL_MATRIX")
|
|
office_id=$(jq -r ".entries[$i].officeId" "$JOURNAL_MATRIX")
|
|
debit_code=$(jq -r ".entries[$i].debitGlCode" "$JOURNAL_MATRIX")
|
|
credit_code=$(jq -r ".entries[$i].creditGlCode" "$JOURNAL_MATRIX")
|
|
amount=$(jq -r ".entries[$i].amount" "$JOURNAL_MATRIX")
|
|
narrative=$(jq -r ".entries[$i].narrative" "$JOURNAL_MATRIX")
|
|
comments="${memo} — ${narrative}"
|
|
date_no_dash="${TRANSACTION_DATE//-/}"
|
|
ref="OMNL-JE-${office_id}-${date_no_dash}-${i}"
|
|
|
|
guardrail_validate "$amount" "$office_id" "$memo" || continue
|
|
if ref_already_posted "$ref"; then
|
|
echo "Skip $memo: duplicate referenceNumber=$ref (idempotent skip)" >&2
|
|
continue
|
|
fi
|
|
|
|
debit_id=$(get_gl_id "$debit_code")
|
|
credit_id=$(get_gl_id "$credit_code")
|
|
if [ -z "$debit_id" ] || [ -z "$credit_id" ]; then
|
|
echo "Skip $memo: GL account not found (debit=$debit_code id=$debit_id, credit=$credit_code id=$credit_id). Create GL accounts first." >&2
|
|
continue
|
|
fi
|
|
entry_index="$i"
|
|
if post_entry "$office_id" "$debit_id" "$credit_id" "$amount" "$comments" "$ref"; then
|
|
record_posted_ref "$ref"
|
|
((posted++)) || true
|
|
fi
|
|
done
|
|
|
|
echo "Done: $posted entries posted." >&2
|