chore: sync all changes to Gitea
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
This commit is contained in:
defiQUG
2026-03-02 11:37:34 -08:00
parent ed85135249
commit b3a8fe4496
883 changed files with 73580 additions and 4796 deletions

View File

@@ -4,10 +4,59 @@ Scripts for the **OMNL** tenancy ([omnl.hybxfinance.io](https://omnl.hybxfinance
| Script | Purpose |
|--------|---------|
| **omnl-gl-accounts-create.sh** | Create the five migration GL accounts (1000, 1050, 2000, 2100, 3000) via `POST /glaccounts`. Idempotent (skips if exists). Run **before** ledger post. |
| **omnl-gl-accounts-create.sh** | Create the five migration GL accounts (1000, 1050, 2000, 2100, 3000) via `POST /glaccounts`. Idempotent (skips if exists). Run **before** ledger post. See [OMNL_GL_ACCOUNTS_REQUIRED.md](../../docs/04-configuration/mifos-omnl-central-bank/OMNL_GL_ACCOUNTS_REQUIRED.md). |
| **omnl-gl-accounts-fx-gru-create.sh** | Create FX and GRU (M00) GL accounts from Chart of Accounts (12xxx/13xxx, 21xxx, 42xxx/52xxx). See [OMNL_GL_ACCOUNTS_FX_GRU.md](../../docs/04-configuration/mifos-omnl-central-bank/OMNL_GL_ACCOUNTS_FX_GRU.md). |
| **omnl-discovery.sh** | GET offices, clients, savings/FD/RD products and accounts; output JSON. Set `OUT_DIR=<dir>` to write files. |
| **omnl-ledger-post.sh** | Post ledger allocation entries T-001, T-001B, T-002AT-008 per [LEDGER_ALLOCATION_POSTING_RUNBOOK.md](../../docs/04-configuration/mifos-omnl-central-bank/LEDGER_ALLOCATION_POSTING_RUNBOOK.md). Resolves GL account IDs from `GET /glaccounts`. Set `DRY_RUN=1` to print payloads only; `TRANSACTION_DATE=yyyy-MM-dd`, `OFFICE_ID=1` optional. |
| **omnl-ledger-post.sh** | Post ledger allocation entries T-001T-008 per [LEDGER_ALLOCATION_POSTING_RUNBOOK.md](../../docs/04-configuration/mifos-omnl-central-bank/LEDGER_ALLOCATION_POSTING_RUNBOOK.md). Resolves GL from `GET /glaccounts`. `DRY_RUN=1`, `TRANSACTION_DATE`, `OFFICE_ID=1` optional. |
| **omnl-ledger-post-from-matrix.sh** | Post journal entries from [omnl-journal-matrix.json](../../docs/04-configuration/mifos-omnl-central-bank/omnl-journal-matrix.json) (matrix + full GL + IPSAS). Resolves glCode→id; posts to OMNL Hybx. `JOURNAL_MATRIX=<path>`, `DRY_RUN=1`, `TRANSACTION_DATE` optional. See [OMNL_JOURNAL_LEDGER_MATRIX.md](../../docs/04-configuration/mifos-omnl-central-bank/OMNL_JOURNAL_LEDGER_MATRIX.md). |
| **omnl-deposit-one.sh** | Post a single deposit to an existing savings account. `ACCOUNT_ID=<id> AMOUNT=<number> [DATE=yyyy-MM-dd]`. Use discovery output for account IDs; for bulk, loop over a CSV or discovery JSON. |
| **omnl-client-names-fix.sh** | Set client `firstname`/`lastname` to canonical entity names when blank. `DRY_RUN=1` to print only. See [OMNL_CLIENT_NAMES_FIX.md](../../docs/04-configuration/mifos-omnl-central-bank/OMNL_CLIENT_NAMES_FIX.md). |
| **omnl-entity-data-apply.sh** | Apply full entity master data (name, LEI, address, contacts) from [OMNL_ENTITY_MASTER_DATA.json](../../docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json). `ENTITY_DATA=<path>` optional; `DRY_RUN=1` to print only. See [OMNL_ENTITY_MASTER_DATA.md](../../docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.md). |
| **omnl-clients-create-9-15.sh** | Create clients 915 in Fineract (FIDIS, Alpha Omega Holdings, …). Idempotent. `DRY_RUN=1` to print only. *(Deprecated if using entities as offices instead.)* |
| **omnl-offices-populate-15.sh** | Populate the 15 entities as **Offices** (Organization / Manage Offices): update office 1 name, create offices 215 as children. Uses [OMNL_ENTITY_MASTER_DATA.json](../../docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json). `DRY_RUN=1` to print only; `OPENING_DATE=yyyy-MM-dd` optional. |
| **omnl-clients-remove-15.sh** | Remove the 15 clients (ids 115). Run after populating entities as offices. Requires `CONFIRM_REMOVE=1`; `DRY_RUN=1` to preview. |
| **omnl-user-shamrayan-office-create.sh** | Create Staff for office 2 (Shamrayan) and User `shamrayan.admin` with full admin access to that office only. Requires `OMNL_SHAMRAYAN_ADMIN_PASSWORD`. See [OMNL_OFFICE_LOGINS_AND_CREDENTIALS.md](../../docs/04-configuration/mifos-omnl-central-bank/OMNL_OFFICE_LOGINS_AND_CREDENTIALS.md). |
| **omnl-office2-access-security-test.sh** | Security test: office-2 user must not see other offices data or achieve path traversal/command injection. Set office-2 user and password (e.g. `OMNL_OFFICE2_TEST_USER`, `OMNL_OFFICE2_TEST_PASSWORD`). See [OMNL_OFFICE_2_ACCESS_SECURITY_TEST.md](../../docs/04-configuration/mifos-omnl-central-bank/OMNL_OFFICE_2_ACCESS_SECURITY_TEST.md). |
| **omnl-office-create-samama.sh** | Create Office for Samama Group LLC (Azerbaijan) and post 5B USD M1 from Head Office (Phase C pattern: HO Dr 2100 Cr 2410; office Dr 1410 Cr 2100). Idempotent by externalId. `SKIP_TRANSFER=1` to create office only. See [SAMAMA_OFFICE_AND_5B_M1_TRANSFER.md](../../docs/04-configuration/mifos-omnl-central-bank/SAMAMA_OFFICE_AND_5B_M1_TRANSFER.md). |
| **omnl-office-create-pelican.sh** | Create Office for Pelican Motors And Finance LLC (Chalmette, LA). Idempotent by externalId `PEL-MOTORS-CHALMETTE-LA`. Use with omnl.hybx.global by setting `OMNL_FINERACT_BASE_URL`. See [PELICAN_MOTORS_OFFICE_RUNBOOK.md](../../docs/04-configuration/mifos-omnl-central-bank/PELICAN_MOTORS_OFFICE_RUNBOOK.md). |
| **resolve_ids.sh** | Resolve GL IDs (1410, 2100, 2410) and payment type; write `ids.env`. Run before closures/reconciliation/templates. See [OPERATING_RAILS.md](../../docs/04-configuration/mifos-omnl-central-bank/OPERATING_RAILS.md). |
| **omnl-gl-closures-post.sh** | Post GL closures for Office 20 and HO (idempotent). `CLOSING_DATE=yyyy-MM-dd`, `DRY_RUN=1`. See [OPERATING_RAILS.md](../../docs/04-configuration/mifos-omnl-central-bank/OPERATING_RAILS.md). |
| **omnl-reconciliation-office20.sh** | Snapshot Office 20 (offices + GL + trial balance), timestamp, sha256. `OUT_DIR=./reconciliation`. See [OPERATING_RAILS.md](../../docs/04-configuration/mifos-omnl-central-bank/OPERATING_RAILS.md). |
| **omnl-operator-rail.sh** | One-command rail: resolve IDs, closures, verify, reconciliation, A/B/C readiness, print templates. `SKIP_CLOSURES=1` / `SKIP_RECON=1` optional. See [OPERATING_RAILS.md](../../docs/04-configuration/mifos-omnl-central-bank/OPERATING_RAILS.md). |
| **omnl-audit-packet-office20.sh** | Audit packet: snapshot.json, snapshot.meta, computed_balances.json, recent_journal_entries.json, manifest.json. See [OFFICE_20_AUDIT_PACKET.md](../../docs/04-configuration/mifos-omnl-central-bank/OFFICE_20_AUDIT_PACKET.md). |
| **omnl-je-reverse-by-reference.sh** | Reverse JE by referenceNumber. `REFERENCE_NUMBER=...` See [OFFICE_20_DR_RUNBOOK.md](../../docs/04-configuration/mifos-omnl-central-bank/OFFICE_20_DR_RUNBOOK.md). |
| **omnl-je-maker.sh** / **omnl-je-checker.sh** | Maker-checker: maker writes payload + sha256; checker validates and posts. |
| **omnl-monitor-office20-movement.sh** | Exit 2 if Office 20 movement in last N days (alert payload). |
| **omnl-config-hash.sh** | Output hashes of payment types, GL, office 20 (drift detection). |
| **validate-rail.sh** | CI: .gitignore (ids.env, reconciliation), resolve_ids pattern, shellcheck. |
**Populate 15 entities as Offices (remove as Clients)**
From repo root with `omnl-fineract/.env` set:
```bash
# 1. Populate entities as offices (update office 1, create offices 215)
DRY_RUN=1 bash scripts/omnl/omnl-offices-populate-15.sh
bash scripts/omnl/omnl-offices-populate-15.sh
# 2. Remove the 15 clients (requires confirmation)
DRY_RUN=1 bash scripts/omnl/omnl-clients-remove-15.sh
CONFIRM_REMOVE=1 bash scripts/omnl/omnl-clients-remove-15.sh
```
**Complete all clients (115) in one go** *(only if keeping entities as clients)*
From repo root with `omnl-fineract/.env` set (OMNL_FINERACT_BASE_URL, OMNL_FINERACT_PASSWORD):
```bash
# 1. Create clients 915 in Fineract (no-op if they already exist)
bash scripts/omnl/omnl-clients-create-9-15.sh
# 2. Set names for all 15 + apply LEI/address/contacts from OMNL_ENTITY_MASTER_DATA.json
bash scripts/omnl/omnl-entity-data-apply.sh
```
Optional: run `DRY_RUN=1` before each step to preview. To only fix names (no LEI/address/contact), run `bash scripts/omnl/omnl-client-names-fix.sh` after step 1.
**Run from repo root:**
@@ -15,8 +64,11 @@ Scripts for the **OMNL** tenancy ([omnl.hybxfinance.io](https://omnl.hybxfinance
# 1. Create GL accounts (run first; idempotent)
bash scripts/omnl/omnl-gl-accounts-create.sh
# 2. Post ledger entries (T-001T-008)
# 2. Post ledger entries (T-001T-008) — from runbook or from matrix JSON
bash scripts/omnl/omnl-ledger-post.sh
# Or from matrix (full GL + IPSAS): omnl-ledger-post-from-matrix.sh
DRY_RUN=1 bash scripts/omnl/omnl-ledger-post-from-matrix.sh
bash scripts/omnl/omnl-ledger-post-from-matrix.sh
# Discovery (list products, clients, accounts)
bash scripts/omnl/omnl-discovery.sh
@@ -27,6 +79,34 @@ DRY_RUN=1 bash scripts/omnl/omnl-ledger-post.sh
# Single deposit (ACCOUNT_ID from discovery)
ACCOUNT_ID=1 AMOUNT=100 DATE=2026-02-10 bash scripts/omnl/omnl-deposit-one.sh
# Fix blank client names (set canonical entity names)
DRY_RUN=1 bash scripts/omnl/omnl-client-names-fix.sh
bash scripts/omnl/omnl-client-names-fix.sh
# Apply full entity data (names + LEI + address + contacts from OMNL_ENTITY_MASTER_DATA.json)
ENTITY_DATA=docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json DRY_RUN=1 bash scripts/omnl/omnl-entity-data-apply.sh
bash scripts/omnl/omnl-entity-data-apply.sh
# Create clients 915 (idempotent)
DRY_RUN=1 bash scripts/omnl/omnl-clients-create-9-15.sh
bash scripts/omnl/omnl-clients-create-9-15.sh
# Populate 15 entities as offices (Organization / Manage Offices)
DRY_RUN=1 bash scripts/omnl/omnl-offices-populate-15.sh
bash scripts/omnl/omnl-offices-populate-15.sh
# Remove the 15 clients (after populating as offices)
CONFIRM_REMOVE=1 bash scripts/omnl/omnl-clients-remove-15.sh
# Samama Group LLC — create office and 5B USD M1 transfer (Phase C interoffice)
DRY_RUN=1 bash scripts/omnl/omnl-office-create-samama.sh
bash scripts/omnl/omnl-office-create-samama.sh
# Office only (no transfer): SKIP_TRANSFER=1 bash scripts/omnl/omnl-office-create-samama.sh
# Pelican Motors And Finance LLC — create office (omnl.hybx.global or omnl.hybxfinance.io)
DRY_RUN=1 bash scripts/omnl/omnl-office-create-pelican.sh
bash scripts/omnl/omnl-office-create-pelican.sh
```
**Requirements:** `curl`, `jq` (for ledger posting and pretty-print in discovery).

View File

@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# OMNL — Create one new office (by name, parentId, externalId, openingDate) and fund it with a two-leg M1 transfer from HO.
# Rail B: Leg 1 at HO (Dr 2100 / Cr 1410), Leg 2 at new office (Dr 1410 / Cr 2100).
# Usage:
# OFFICE_NAME="Crunchygalaxy Unip Lda - Portugal" EXTERNAL_ID="CRUNCHYGALAXY-515159573" \
# OPENING_DATE="2026-02-24" AMOUNT=1000000000 APPROVER="<name>" \
# bash scripts/omnl/create-office-and-fund.sh
# Optional: SKIP_OFFICE_CREATE=1 (use existing office by EXTERNAL_ID), SKIP_TRANSFER=1 (create office only), DRY_RUN=1, SKIP_INITIAL_CLOSURES=1 (do not run closures before legs; use when posting on same day as existing closure would block).
# Requires: resolve_ids.sh, omnl-gl-closures-post.sh, omnl-je-maker.sh, omnl-je-checker.sh, omnl-audit-packet-office20.sh.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
SKIP_OFFICE_CREATE="${SKIP_OFFICE_CREATE:-0}"
SKIP_TRANSFER="${SKIP_TRANSFER:-0}"
SKIP_INITIAL_CLOSURES="${SKIP_INITIAL_CLOSURES:-0}"
DRY_RUN="${DRY_RUN:-0}"
OPENING_DATE="${OPENING_DATE:-$(date +%Y-%m-%d)}"
TX_DATE="${TX_DATE:-$(date +%Y-%m-%d)}"
: "${OFFICE_NAME:?Set OFFICE_NAME}"
: "${EXTERNAL_ID:?Set EXTERNAL_ID}"
: "${AMOUNT:?Set AMOUNT (minor units, e.g. 1000000000 for 1B)}"
# Material (≥10M) requires APPROVER
if [ "${AMOUNT:-0}" -ge 10000000 ] 2>/dev/null; then
: "${APPROVER:?Set APPROVER for material posting}"
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:-}"
[ -z "$BASE_URL" ] || [ -z "$PASS" ] && { echo "Set OMNL_FINERACT_BASE_URL and OMNL_FINERACT_PASSWORD" >&2; exit 1; }
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
# --- Resolve GL IDs
echo "=== Resolve IDs ===" >&2
bash "${REPO_ROOT}/scripts/omnl/resolve_ids.sh"
source "${REPO_ROOT}/ids.env" 2>/dev/null || source ids.env 2>/dev/null
: "${ID_1410:?}"
: "${ID_2100:?}"
# --- Create or find office
offices_json=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/offices" 2>/dev/null)
NEW_OFFICE_ID=$(echo "$offices_json" | jq -r --arg e "$EXTERNAL_ID" '.[] | select(.externalId == $e) | .id' 2>/dev/null | head -1)
if [ -n "$NEW_OFFICE_ID" ] && [ "$NEW_OFFICE_ID" != "null" ]; then
echo "Office already exists: officeId=$NEW_OFFICE_ID (externalId=$EXTERNAL_ID)" >&2
else
if [ "$SKIP_OFFICE_CREATE" = "1" ]; then
echo "SKIP_OFFICE_CREATE=1 and office not found for externalId=$EXTERNAL_ID" >&2
exit 1
fi
payload=$(jq -n \
--arg name "$OFFICE_NAME" \
--arg openingDate "$OPENING_DATE" \
--arg externalId "$EXTERNAL_ID" \
'{ name: $name, parentId: 1, openingDate: $openingDate, externalId: $externalId, dateFormat: "yyyy-MM-dd", locale: "en" }')
if [ "$DRY_RUN" = "1" ]; then
echo "DRY_RUN: would POST /offices $payload" >&2
exit 0
fi
res=$(curl "${CURL_OPTS[@]}" -X POST -d "$payload" "${BASE_URL}/offices" 2>/dev/null) || true
NEW_OFFICE_ID=$(echo "$res" | jq -r '.resourceId // .officeId // empty')
[ -z "$NEW_OFFICE_ID" ] && { echo "Failed to create office: $res" >&2; exit 1; }
echo "Created office: officeId=$NEW_OFFICE_ID" >&2
fi
if [ "$SKIP_TRANSFER" = "1" ]; then
echo "SKIP_TRANSFER=1. NEW_OFFICE_ID=$NEW_OFFICE_ID" >&2
exit 0
fi
# --- Closures (idempotent; skip if SKIP_INITIAL_CLOSURES=1 to allow same-day post when server blocks on closure date)
if [ "$SKIP_INITIAL_CLOSURES" != "1" ]; then
echo "=== GL closures ===" >&2
CLOSING_DATE="${TX_DATE}" bash "${REPO_ROOT}/scripts/omnl/omnl-gl-closures-post.sh" 2>/dev/null || true
fi
# --- Leg 1: HO (Office 1) Dr 2100 / Cr 1410
REF_L1="CRUNCHY-1-$(date +%Y%m%d)-TR1-1B-L1"
REF_L2="CRUNCHY-${NEW_OFFICE_ID}-$(date +%Y%m%d)-TR1-1B-L2"
echo "=== Leg 1 (HO reduction) ===" >&2
if [ "$DRY_RUN" = "1" ]; then
echo "Would maker+checker REF=$REF_L1 OFFICE_ID=1 Dr 2100 Cr 1410 AMOUNT=$AMOUNT" >&2
else
REQUIRES_APPROVAL=1 APPROVER="${APPROVER:-}" REFERENCE_NUMBER="$REF_L1" TX_DATE="$TX_DATE" OFFICE_ID=1 \
DEBIT_GL_ID="$ID_2100" CREDIT_GL_ID="$ID_1410" AMOUNT="$AMOUNT" \
bash "${REPO_ROOT}/scripts/omnl/omnl-je-maker.sh"
PAYLOAD_FILE=$(ls "${REPO_ROOT}/reconciliation/je-${REF_L1}"*.payload.json 2>/dev/null | head -1)
[ -z "$PAYLOAD_FILE" ] && PAYLOAD_FILE="${REPO_ROOT}/reconciliation/je-${REF_L1}.payload.json"
PAYLOAD_FILE="$PAYLOAD_FILE" bash "${REPO_ROOT}/scripts/omnl/omnl-je-checker.sh"
fi
# --- Leg 2: New office Dr 1410 / Cr 2100
echo "=== Leg 2 (Office $NEW_OFFICE_ID funding) ===" >&2
if [ "$DRY_RUN" = "1" ]; then
echo "Would maker+checker REF=$REF_L2 OFFICE_ID=$NEW_OFFICE_ID Dr 1410 Cr 2100 AMOUNT=$AMOUNT" >&2
else
REQUIRES_APPROVAL=1 APPROVER="${APPROVER:-}" REFERENCE_NUMBER="$REF_L2" TX_DATE="$TX_DATE" OFFICE_ID="$NEW_OFFICE_ID" \
DEBIT_GL_ID="$ID_1410" CREDIT_GL_ID="$ID_2100" AMOUNT="$AMOUNT" \
bash "${REPO_ROOT}/scripts/omnl/omnl-je-maker.sh"
PAYLOAD_FILE=$(ls "${REPO_ROOT}/reconciliation/je-${REF_L2}"*.payload.json 2>/dev/null | head -1)
[ -z "$PAYLOAD_FILE" ] && PAYLOAD_FILE="${REPO_ROOT}/reconciliation/je-${REF_L2}.payload.json"
PAYLOAD_FILE="$PAYLOAD_FILE" bash "${REPO_ROOT}/scripts/omnl/omnl-je-checker.sh"
fi
# --- Post-audit for new office
echo "=== Post-audit (Office $NEW_OFFICE_ID) ===" >&2
if [ "$DRY_RUN" != "1" ]; then
OUT_BASE="${REPO_ROOT}/reconciliation" OFFICE_ID="$NEW_OFFICE_ID" bash "${REPO_ROOT}/scripts/omnl/omnl-audit-packet-office20.sh"
PACKET_DIR=$(find "${REPO_ROOT}/reconciliation" -maxdepth 1 -type d -name "audit-office${NEW_OFFICE_ID}-*" 2>/dev/null | sort -r | head -1)
echo "Audit packet: $PACKET_DIR" >&2
fi
# --- Re-lock
echo "=== Re-lock closures ===" >&2
CLOSING_DATE="${TX_DATE}" bash "${REPO_ROOT}/scripts/omnl/omnl-gl-closures-post.sh" 2>/dev/null || true
[ -n "${PACKET_DIR:-}" ] && echo "Package: cd reconciliation && zip -r ../CRUNCHY-${NEW_OFFICE_ID}-$(date +%Y%m%d)-TR1-1B-AUDIT.zip $(basename "$PACKET_DIR") && cd .." >&2
echo "Done. NEW_OFFICE_ID=$NEW_OFFICE_ID" >&2

View File

@@ -0,0 +1,268 @@
#!/usr/bin/env bash
# Office 2 (Shamrayan) — Full runbook execution: API 3-step send → settlement confirmation → mirror JE → audit → closures.
# Usage: from repo root. Set P2P_BEARER_TOKEN, SENDER_SERVER_IP, SOURCE_ACCOUNT_NAME, SOURCE_ACCOUNT_NUMBER, APPROVER. P2P_API_KEY optional (PDF uses Bearer only).
# Optional: SKIP_POLL=1 and SETTLED=1 to skip polling; OFFICE2_BANK_SERVER_ID and/or OFFICE2_BANK_ACCOUNT_ID to skip step 1/2; BASE_URL or P2P_TRANSACTIONS_ENDPOINT (or P2P_ENDPOINT_TRANSACTIONS) to override API base/path.
# See docs/04-configuration/mifos-omnl-central-bank/OFFICE_2_SHAMRAYAN_RUNBOOK.md
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
cd "$REPO_ROOT"
# --- CONFIG (locked) ---
export OMNL_OFFICE_ID="2"
# OMNL_AMOUNT: default-only so export OMNL_AMOUNT=100 overrides; set before Step 3
export OMNL_CURRENCY="USD"
export OMNL_TX_DATE="${OMNL_TX_DATE:-$(date +%Y-%m-%d)}"
export BASE_URL="${BASE_URL:-https://banktransfer.devmindgroup.com}"
TRANSACTIONS_ENDPOINT="${P2P_TRANSACTIONS_ENDPOINT:-${P2P_ENDPOINT_TRANSACTIONS:-/api/transactions}}"
export EVID_DIR="reconciliation/p2p-office2-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$EVID_DIR"
echo "===== OFFICE 2 (SHAMRAYAN) 5B FULL EXECUTION ====="
echo "Evidence dir: $EVID_DIR"
# --- API connection test ---
echo "[0] Testing API connection..."
HTTP=$(curl -sS -o /dev/null -w "%{http_code}" --connect-timeout 10 "$BASE_URL/" 2>/dev/null || echo "000")
if [ "$HTTP" = "000" ]; then
echo "ERROR: Cannot reach $BASE_URL (connection failed)."
exit 1
fi
echo "OK: API reachable (HTTP $HTTP)."
# --- Required env (PDF: Bearer required; x-api-key optional) ---
: "${P2P_BEARER_TOKEN:?Set P2P_BEARER_TOKEN (from Shamrayan PDF / vault path omnl/offices/2/p2p)}"
: "${SENDER_SERVER_IP:?Set SENDER_SERVER_IP (public sender IP; PDF IP is example only)}"
: "${SOURCE_ACCOUNT_NAME:?Set SOURCE_ACCOUNT_NAME}"
: "${SOURCE_ACCOUNT_NUMBER:?Set SOURCE_ACCOUNT_NUMBER}"
: "${APPROVER:?Set APPROVER for mirror JE (maker-checker)}"
# P2P_API_KEY optional (PDF example does not show it; only if provider requires)
# : "${P2P_API_KEY:?Set P2P_API_KEY if required by provider}"
SENDER_SERVER_NAME="${SENDER_SERVER_NAME:-OMNL-OFF2-SHAMRAYAN}"
# --- Step 1: Create bank server (or use existing if OFFICE2_BANK_SERVER_ID set) ---
if [ -n "${OFFICE2_BANK_SERVER_ID:-}" ]; then
echo "[1] Using existing BANK_SERVER_ID (OFFICE2_BANK_SERVER_ID=$OFFICE2_BANK_SERVER_ID)"
BANK_SERVER_ID="$OFFICE2_BANK_SERVER_ID"
echo "$BANK_SERVER_ID" > "$EVID_DIR/01_bank_server.id.txt"
echo "{\"id\": $BANK_SERVER_ID, \"name\": \"$SENDER_SERVER_NAME\", \"server_ip_address\": \"$SENDER_SERVER_IP\"}" > "$EVID_DIR/01_bank_server.response.json"
else
echo "[1] POST /api/bank-servers..."
jq -n --arg name "$SENDER_SERVER_NAME" --arg ip "$SENDER_SERVER_IP" \
'{name: $name, server_ip_address: $ip}' > "$EVID_DIR/01_bank_server.request.json"
HTTP_STEP1=$(curl -sS -w "%{http_code}" -o "$EVID_DIR/01_bank_server.response.json" -X POST "$BASE_URL/api/bank-servers" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $P2P_BEARER_TOKEN" \
${P2P_API_KEY:+ -H "x-api-key: $P2P_API_KEY"} \
--data @"$EVID_DIR/01_bank_server.request.json")
if [ "$HTTP_STEP1" = "422" ] && grep -q 'payload.*id' "$EVID_DIR/01_bank_server.response.json" 2>/dev/null; then
echo "Step 1: 422 (live API expects different schema). Retrying with id: 1..."
jq -n --argjson id 1 --arg name "$SENDER_SERVER_NAME" --arg ip "$SENDER_SERVER_IP" \
'{id: $id, name: $name, server_ip_address: $ip}' > "$EVID_DIR/01_bank_server.request.json"
HTTP_STEP1=$(curl -sS -w "%{http_code}" -o "$EVID_DIR/01_bank_server.response.json" -X POST "$BASE_URL/api/bank-servers" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $P2P_BEARER_TOKEN" \
${P2P_API_KEY:+ -H "x-api-key: $P2P_API_KEY"} \
--data @"$EVID_DIR/01_bank_server.request.json")
fi
echo "Step 1 HTTP: $HTTP_STEP1"
if [ "$HTTP_STEP1" != "200" ] && [ "$HTTP_STEP1" != "201" ]; then
echo "ERROR: Step 1 failed (HTTP $HTTP_STEP1). To skip and use existing server: set OFFICE2_BANK_SERVER_ID=1 (or your server id). Response:"
cat "$EVID_DIR/01_bank_server.response.json" | head -c 2000
exit 2
fi
BANK_SERVER_ID=$(jq -r '.id // .payload.id // .data.id // empty' "$EVID_DIR/01_bank_server.response.json" 2>/dev/null || true)
if [ -z "$BANK_SERVER_ID" ] || [ "$BANK_SERVER_ID" = "null" ]; then
echo "ERROR: Step 1 failed (no server id in response). Response:"
cat "$EVID_DIR/01_bank_server.response.json" | jq '.' 2>/dev/null || cat "$EVID_DIR/01_bank_server.response.json"
exit 2
fi
echo "BANK_SERVER_ID=$BANK_SERVER_ID" | tee "$EVID_DIR/01_bank_server.id.txt"
fi
# --- Step 2: Create bank account (or use existing if OFFICE2_BANK_ACCOUNT_ID set) ---
if [ -n "${OFFICE2_BANK_ACCOUNT_ID:-}" ]; then
echo "[2] Using existing SOURCE_ACCOUNT_ID (OFFICE2_BANK_ACCOUNT_ID=$OFFICE2_BANK_ACCOUNT_ID)"
SOURCE_ACCOUNT_ID="$OFFICE2_BANK_ACCOUNT_ID"
echo "$SOURCE_ACCOUNT_ID" > "$EVID_DIR/02_bank_account.id.txt"
else
echo "[2] POST /api/bank-accounts..."
jq -n --argjson bs "$BANK_SERVER_ID" --arg an "$SOURCE_ACCOUNT_NAME" --arg anum "$SOURCE_ACCOUNT_NUMBER" \
'{bank_server: $bs, account_name: $an, account_number: $anum}' > "$EVID_DIR/02_bank_account.request.json"
HTTP_STEP2=$(curl -sS -w "%{http_code}" -o "$EVID_DIR/02_bank_account.response.json" -X POST "$BASE_URL/api/bank-accounts" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $P2P_BEARER_TOKEN" \
${P2P_API_KEY:+ -H "x-api-key: $P2P_API_KEY"} \
--data @"$EVID_DIR/02_bank_account.request.json")
echo "Step 2 HTTP: $HTTP_STEP2"
if [ "$HTTP_STEP2" != "200" ] && [ "$HTTP_STEP2" != "201" ]; then
echo "ERROR: Step 2 failed (HTTP $HTTP_STEP2). To skip: set OFFICE2_BANK_ACCOUNT_ID=<id>. Response:"
cat "$EVID_DIR/02_bank_account.response.json" | head -c 2000
exit 3
fi
SOURCE_ACCOUNT_ID=$(jq -r '.id // .payload.id // .data.id // empty' "$EVID_DIR/02_bank_account.response.json" 2>/dev/null || true)
if [ -z "$SOURCE_ACCOUNT_ID" ] || [ "$SOURCE_ACCOUNT_ID" = "null" ]; then
echo "ERROR: Step 2 failed (no account id in response). Response:"
cat "$EVID_DIR/02_bank_account.response.json" | jq '.' 2>/dev/null || cat "$EVID_DIR/02_bank_account.response.json"
exit 3
fi
echo "SOURCE_ACCOUNT_ID=$SOURCE_ACCOUNT_ID" | tee "$EVID_DIR/02_bank_account.id.txt"
fi
# --- Step 3: Create transaction ---
: "${OMNL_AMOUNT:=5000000000}"
OMNL_AMOUNT="${OMNL_AMOUNT//,/}"
export OMNL_AMOUNT
P2P_CHANNEL="${P2P_CHANNEL:-Instant Server Settlement}"
P2P_BENEFICIARY_NAME="${P2P_BENEFICIARY_NAME:-}"
P2P_PURPOSE="${P2P_PURPOSE:-}"
P2P_TARGET_IBAN="${P2P_TARGET_IBAN:-}"
export IDEMPOTENCY_KEY="OFF2-SHAMRAYAN-5B-$(date +%Y%m%d)-$(date +%H%M%S)"
echo "$IDEMPOTENCY_KEY" | tee "$EVID_DIR/03_idempotency_key.txt"
echo "[3] POST $TRANSACTIONS_ENDPOINT (amount=$OMNL_AMOUNT, channel=$P2P_CHANNEL)..."
# Full payload: omit target_iban when empty; channel from env; optional beneficiary_name, purpose_of_payment
jq -n \
--argjson amt "$OMNL_AMOUNT" \
--argjson src "$SOURCE_ACCOUNT_ID" \
--arg ref "$IDEMPOTENCY_KEY" \
--arg ch "$P2P_CHANNEL" \
--arg iban "$P2P_TARGET_IBAN" \
--arg ben "$P2P_BENEFICIARY_NAME" \
--arg pur "$P2P_PURPOSE" \
'(
(if $iban != "" then {target_iban: $iban} else {} end) +
{
transaction_type: "bank_transfer",
amount: $amt,
source_account: $src,
target_swift_code: "DFCUUGKA",
target_bank_account_number: "02650010158937",
target_bank_name: "DFCU Bank Limited",
target_country: "Uganda",
provider: "SWIFT",
reference: $ref,
channel: $ch
} +
(if $ben != "" then {beneficiary_name: $ben} else {} end) +
(if $pur != "" then {purpose_of_payment: $pur} else {} end)
)' > "$EVID_DIR/03_transaction.request.json"
# Minimal payload: same but no reference; omit target_iban when empty; channel + optional beneficiary/purpose
jq -n \
--argjson amt "$OMNL_AMOUNT" \
--argjson src "$SOURCE_ACCOUNT_ID" \
--arg ch "$P2P_CHANNEL" \
--arg iban "$P2P_TARGET_IBAN" \
--arg ben "$P2P_BENEFICIARY_NAME" \
--arg pur "$P2P_PURPOSE" \
'(
(if $iban != "" then {target_iban: $iban} else {} end) +
{
transaction_type: "bank_transfer",
amount: $amt,
source_account: $src,
target_swift_code: "DFCUUGKA",
target_bank_account_number: "02650010158937",
target_bank_name: "DFCU Bank Limited",
target_country: "Uganda",
provider: "SWIFT",
channel: $ch
} +
(if $ben != "" then {beneficiary_name: $ben} else {} end) +
(if $pur != "" then {purpose_of_payment: $pur} else {} end)
)' > "$EVID_DIR/03_transaction.request.minimal.json"
HTTP_STEP3=$(curl -sS -w "%{http_code}" -o "$EVID_DIR/03_transaction.response.json" -X POST "$BASE_URL$TRANSACTIONS_ENDPOINT" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $P2P_BEARER_TOKEN" \
${P2P_API_KEY:+ -H "x-api-key: $P2P_API_KEY"} \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
--data @"$EVID_DIR/03_transaction.request.json")
echo "Step 3 HTTP: $HTTP_STEP3"
if [ "$HTTP_STEP3" != "200" ] && [ "$HTTP_STEP3" != "201" ]; then
echo "Retrying Step 3 with provider-exact minimal payload (no reference, channel, or Idempotency-Key)..."
HTTP_STEP3=$(curl -sS -w "%{http_code}" -o "$EVID_DIR/03_transaction.response.json" -X POST "$BASE_URL$TRANSACTIONS_ENDPOINT" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $P2P_BEARER_TOKEN" \
${P2P_API_KEY:+ -H "x-api-key: $P2P_API_KEY"} \
--data @"$EVID_DIR/03_transaction.request.minimal.json")
echo "Step 3 retry HTTP: $HTTP_STEP3"
fi
if [ "$HTTP_STEP3" != "200" ] && [ "$HTTP_STEP3" != "201" ]; then
echo "ERROR: Step 3 failed (HTTP $HTTP_STEP3). Response:"
cat "$EVID_DIR/03_transaction.response.json" | head -c 2000
echo ""
if [ "$HTTP_STEP3" = "404" ]; then
echo "404: Provider may not have this path enabled. Confirm with provider that POST $BASE_URL$TRANSACTIONS_ENDPOINT is correct."
echo "To try an alternate path, set e.g. P2P_TRANSACTIONS_ENDPOINT=/api/v1/transactions and re-run."
fi
exit 4
fi
TX_ID=$(jq -r '.id // .payload.id // .transactionId // .data.id // empty' "$EVID_DIR/03_transaction.response.json" 2>/dev/null || true)
TX_STATUS=$(jq -r '.status // .payload.status // .data.status // empty' "$EVID_DIR/03_transaction.response.json" 2>/dev/null || true)
echo "TX_ID=$TX_ID" | tee "$EVID_DIR/03_transaction.id.txt"
echo "TX_STATUS=$TX_STATUS" | tee "$EVID_DIR/03_transaction.status.txt"
if [ -z "$TX_ID" ] && [ -z "$TX_STATUS" ]; then
echo "ERROR: Step 3 failed (no tx id/status in response). Response:"
cat "$EVID_DIR/03_transaction.response.json" | jq '.' 2>/dev/null || cat "$EVID_DIR/03_transaction.response.json"
exit 4
fi
echo "OK: Transaction submitted (TX_ID=$TX_ID, status=$TX_STATUS)."
# --- Settlement confirmation ---
if [ "${SKIP_POLL:-0}" = "1" ] && [ "${SETTLED:-0}" = "1" ]; then
echo "[4] Skipping poll (SKIP_POLL=1 SETTLED=1 — out-of-band confirmation)."
echo "SETTLED" > "$EVID_DIR/04_settlement.status.txt"
else
echo "[4] Settlement probe + polling (15s x 120 = 30 min max)..."
TRANSACTIONS_BASE="${TRANSACTIONS_ENDPOINT%/}"
for path in "$TRANSACTIONS_BASE/$TX_ID" "$TRANSACTIONS_BASE?id=$TX_ID"; do
curl -sS "$BASE_URL$path" -H "Content-Type: application/json" -H "Authorization: Bearer $P2P_BEARER_TOKEN" ${P2P_API_KEY:+ -H "x-api-key: $P2P_API_KEY"} > "$EVID_DIR/04_settlement.response.json" 2>/dev/null || true
STATUS=$(jq -r '.status // .data.status // empty' "$EVID_DIR/04_settlement.response.json" 2>/dev/null || true)
[ -n "$STATUS" ] && break
done
POLL_INTERVAL=15
MAX_POLL=120
SETTLED=0
for i in $(seq 1 $MAX_POLL); do
[ -n "$TX_ID" ] && curl -sS "$BASE_URL$TRANSACTIONS_BASE/$TX_ID" -H "Content-Type: application/json" -H "Authorization: Bearer $P2P_BEARER_TOKEN" ${P2P_API_KEY:+ -H "x-api-key: $P2P_API_KEY"} > "$EVID_DIR/04_settlement.response.json" 2>/dev/null || true
STATUS=$(jq -r '.status // .data.status // empty' "$EVID_DIR/04_settlement.response.json" 2>/dev/null || true)
echo "$(date -Is) iteration=$i status=$STATUS" | tee -a "$EVID_DIR/04_settlement.poll.log"
case "$STATUS" in
SETTLED|COMPLETED|SUCCESS) SETTLED=1; break ;;
FAILED|REJECTED|CANCELED) echo "Settlement failed: $STATUS"; exit 5 ;;
*) sleep $POLL_INTERVAL ;;
esac
done
if [ "$SETTLED" -ne 1 ]; then
echo "ESCALATE: 30 min polling exceeded. TX_ID=$TX_ID. Obtain out-of-band confirmation then re-run with SKIP_POLL=1 SETTLED=1 for mirror only." | tee "$EVID_DIR/04_settlement.ESCALATE.txt"
exit 6
fi
echo "SETTLED" | tee "$EVID_DIR/04_settlement.status.txt"
fi
# --- Mirror JE (only after settlement) ---
echo "[5] Mirror JE (Office 2, Dr 2100 / Cr 1410, $OMNL_AMOUNT)..."
source omnl-fineract/.env 2>/dev/null || true
bash scripts/omnl/resolve_ids.sh
source ids.env
export REFERENCE_NUMBER="OFF2-SHAMRAYAN-SETTLED-${OMNL_TX_DATE//-/}-${OMNL_AMOUNT}"
REQUIRES_APPROVAL=1 APPROVER="$APPROVER" REFERENCE_NUMBER="$REFERENCE_NUMBER" TX_DATE="$OMNL_TX_DATE" OFFICE_ID="$OMNL_OFFICE_ID" CURRENCY="$OMNL_CURRENCY" DEBIT_GL_ID="$ID_2100" CREDIT_GL_ID="$ID_1410" AMOUNT="$OMNL_AMOUNT" bash scripts/omnl/omnl-je-maker.sh
PAYLOAD_FILE="reconciliation/je-${REFERENCE_NUMBER}.payload.json"
[ -f "$PAYLOAD_FILE" ] || PAYLOAD_FILE="reconciliation/je-${REFERENCE_NUMBER}_.payload.json"
PAYLOAD_FILE="$PAYLOAD_FILE" bash scripts/omnl/omnl-je-checker.sh
# --- Audit + closures + archive instruction ---
echo "[6] Post-audit (Office 2)..."
OFFICE_ID="$OMNL_OFFICE_ID" bash scripts/omnl/omnl-audit-packet-office20.sh
echo "[7] Re-lock GL closures..."
bash scripts/omnl/omnl-gl-closures-post.sh
echo "Archive off-box: $EVID_DIR and reconciliation/audit-office${OMNL_OFFICE_ID}-<timestamp>/" > "reconciliation/p2p-office2-archive-instructions.txt"
echo ""
echo "===== FULL EXECUTION COMPLETE ====="
echo "Evidence: $EVID_DIR"
echo "Mirror ref: $REFERENCE_NUMBER"

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env bash
# Office 2 / SHAMRAYAN — DRY RUN (NO SEND). Builds payloads and curl commands per API CIS & Procedure SHAMRAYAN PDF.
# Usage: from repo root. Optional: SENDER_SERVER_IP, SOURCE_ACCOUNT_NAME, SOURCE_ACCOUNT_NUMBER (or placeholders).
# Optional: DRYRUN_SKIP_ACK=1 to skip interactive confirmation; DRYRUN_ACK=YES to pre-set acknowledgment.
# See docs/04-configuration/mifos-omnl-central-bank/OFFICE_2_SHAMRAYAN_RUNBOOK.md
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
cd "$REPO_ROOT"
# =========================
# Office 2 / SHAMRAYAN — DRY RUN
# =========================
# ---- OMNL side (ledger mirror happens ONLY after settled; DRY RUN will NOT mirror/post) ----
export OMNL_OFFICE_ID="2" # SHAMRAYAN
export OMNL_AMOUNT="5000000000" # 5B
export OMNL_CURRENCY="USD"
export OMNL_TX_DATE="${OMNL_TX_DATE:-$(date +%F)}"
# ---- P2P banking rail (from SHAMRAYAN PDF) ----
export P2P_BASE_URL="https://banktransfer.devmindgroup.com"
export P2P_ENDPOINT_BANK_SERVERS="/api/bank-servers"
export P2P_ENDPOINT_BANK_ACCOUNTS="/api/bank-accounts"
export P2P_ENDPOINT_TRANSACTIONS="/api/transactions"
# Receiver (from runbook / package)
export RECEIVER_BANK_NAME="DFCU Bank Limited"
export RECEIVER_SWIFT="DFCUUGKA"
export RECEIVER_ACCOUNT_NAME="SHAMRAYAN ENTERPRISES"
export RECEIVER_ACCOUNT_NUMBER="02650010158937"
export RECEIVER_COUNTRY="Uganda"
export PROVIDER="SWIFT"
export CHANNEL="Instant Server Settlement"
# Auth: vault-only. Do not commit tokens. Set P2P_BEARER_TOKEN and P2P_API_KEY when running EXECUTE.
# export P2P_API_KEY="<from vault omnl/offices/2/p2p>"
# export P2P_BEARER_TOKEN="<from vault omnl/offices/2/p2p>"
# ---- Required operator fields (placeholders for artifact building) ----
export APPROVER="${APPROVER:-<<APPROVER_NAME>>}"
# Sender-side placeholders (set to real values for EXECUTE)
export SENDER_SERVER_NAME="${SENDER_SERVER_NAME:-OMNL-OFF2-SHAMRAYAN}"
export SENDER_SERVER_IP="${SENDER_SERVER_IP:-<<SENDER_SERVER_PUBLIC_IP>>}"
export SOURCE_ACCOUNT_NAME="${SOURCE_ACCOUNT_NAME:-<<HYBX_SOURCE_ACCOUNT_NAME>>}"
export SOURCE_ACCOUNT_NUMBER="${SOURCE_ACCOUNT_NUMBER:-<<HYBX_SOURCE_ACCOUNT_NUMBER>>}"
# ---- Idempotency (runbook rule) ----
TS="$(date +%Y%m%d)-$(date +%H%M%S)"
export IDEMPOTENCY_KEY="OFF2-SHAMRAYAN-5B-${TS}"
# ---- Evidence folder (runbook package) ----
OUT_DIR="reconciliation/p2p-office2-${TS}"
mkdir -p "$OUT_DIR"
printf "%s\n" "$IDEMPOTENCY_KEY" > "${OUT_DIR}/03_idempotency_key.txt"
echo "== DRY RUN: Office 2 SHAMRAYAN (NO SEND) =="
echo "Evidence folder: $OUT_DIR"
echo "Idempotency key: $IDEMPOTENCY_KEY"
echo
# =========================
# [A] API connectivity check (safe)
# =========================
echo "== [A] API connectivity check =="
curl -sS -I "${P2P_BASE_URL}" 2>/dev/null | head -n 5 | tee "${OUT_DIR}/00_api_head.txt" || true
echo
# =========================
# [B] Build the three payloads (NO POST)
# =========================
echo "== [B] Building payloads (NO POST) =="
# Step 1 per API doc: name + server_ip_address only (no id, channel, or idempotency_key in doc)
cat > "${OUT_DIR}/01_bank_server.request.json" <<JSON
{
"name": "${SENDER_SERVER_NAME}",
"server_ip_address": "${SENDER_SERVER_IP}"
}
JSON
# Step 2 per API doc: bank_server, account_name, account_number only
cat > "${OUT_DIR}/02_bank_account.request.json" <<JSON
{
"bank_server": "<<BANK_SERVER_ID_FROM_STEP_1>>",
"account_name": "${SOURCE_ACCOUNT_NAME}",
"account_number": "${SOURCE_ACCOUNT_NUMBER}"
}
JSON
# Step 3 per API doc: transaction_type bank_transfer, channel optional
cat > "${OUT_DIR}/03_transaction.request.json" <<JSON
{
"transaction_type": "bank_transfer",
"amount": ${OMNL_AMOUNT},
"currency": "${OMNL_CURRENCY}",
"source_account": "<<BANK_ACCOUNT_ID_FROM_STEP_2>>",
"target_iban": null,
"target_swift_code": "${RECEIVER_SWIFT}",
"target_bank_account_number": "${RECEIVER_ACCOUNT_NUMBER}",
"target_bank_name": "${RECEIVER_BANK_NAME}",
"target_country": "${RECEIVER_COUNTRY}",
"provider": "${PROVIDER}",
"reference": "${IDEMPOTENCY_KEY}",
"channel": "${CHANNEL}"
}
JSON
echo "Wrote:"
ls -1 "${OUT_DIR}/01_bank_server.request.json" "${OUT_DIR}/02_bank_account.request.json" "${OUT_DIR}/03_transaction.request.json"
echo
# =========================
# [C] Print the EXACT curl commands (still NO POST)
# =========================
echo "== [C] Commands that would be run in EXECUTE mode (NOT RUN NOW) =="
RUN_DIR="p2p-office2-${TS}"
cat > "${OUT_DIR}/DRYRUN.commands.txt" <<TXT
# 1) Create bank server
curl -sS -X POST "\${P2P_BASE_URL}\${P2P_ENDPOINT_BANK_SERVERS}" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer \${P2P_BEARER_TOKEN}" \\
-H "x-api-key: \${P2P_API_KEY}" \\
-d @reconciliation/${RUN_DIR}/01_bank_server.request.json | tee reconciliation/${RUN_DIR}/01_bank_server.response.json
# 2) Create bank account (replace <<BANK_SERVER_ID_FROM_STEP_1>> in 02 request with id from step 1 response)
curl -sS -X POST "\${P2P_BASE_URL}\${P2P_ENDPOINT_BANK_ACCOUNTS}" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer \${P2P_BEARER_TOKEN}" \\
-H "x-api-key: \${P2P_API_KEY}" \\
-d @reconciliation/${RUN_DIR}/02_bank_account.request.json | tee reconciliation/${RUN_DIR}/02_bank_account.response.json
# 3) Create transaction (send) — replace <<BANK_ACCOUNT_ID_FROM_STEP_2>> in 03 request with id from step 2 response
curl -sS -X POST "\${P2P_BASE_URL}\${P2P_ENDPOINT_TRANSACTIONS}" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer \${P2P_BEARER_TOKEN}" \\
-H "x-api-key: \${P2P_API_KEY}" \\
-d @reconciliation/${RUN_DIR}/03_transaction.request.json | tee reconciliation/${RUN_DIR}/03_transaction.response.json
TXT
echo "Saved: ${OUT_DIR}/DRYRUN.commands.txt"
echo
# =========================
# [D] Operator confirmation gate (still NO SEND)
# =========================
echo "== [D] CONFIRMATION GATE =="
echo "Check these before any real send:"
echo " - SENDER_SERVER_IP is correct (public sender IP): ${SENDER_SERVER_IP}"
echo " - SOURCE_ACCOUNT_* are your real HYBX source identifiers"
echo " - Amount/currency match: ${OMNL_AMOUNT} ${OMNL_CURRENCY}"
echo " - Receiver: ${RECEIVER_BANK_NAME}, SWIFT ${RECEIVER_SWIFT}, ACCT ${RECEIVER_ACCOUNT_NUMBER}, ${RECEIVER_COUNTRY}"
echo " - Idempotency key recorded: ${IDEMPOTENCY_KEY}"
echo
if [ -t 0 ] && [ "${DRYRUN_SKIP_ACK:-0}" != "1" ]; then
read -r -p 'Type YES to acknowledge DRY RUN artifacts look correct (NO SEND happens either way): ' ACK
else
ACK="${DRYRUN_ACK:-SKIPPED}"
echo "Non-interactive: ACK=${ACK}"
fi
echo "ACK=${ACK}" | tee "${OUT_DIR}/DRYRUN.ack.txt"
echo
echo "DRY RUN COMPLETE. Nothing was sent."
echo ""
echo "--- EXECUTE (only after you confirm; load vault secrets first) ---"
echo "export P2P_BEARER_TOKEN=\"<<from vault>>\""
echo "export P2P_API_KEY=\"<<from vault if required>>\""
echo "export P2P_BASE_URL=\"https://banktransfer.devmindgroup.com\""
echo "export P2P_ENDPOINT_BANK_SERVERS=\"/api/bank-servers\""
echo "export P2P_ENDPOINT_BANK_ACCOUNTS=\"/api/bank-accounts\""
echo "export P2P_ENDPOINT_TRANSACTIONS=\"/api/transactions\""
echo "RUN_DIR=\"$(basename "$OUT_DIR")\""
echo "bash -c \"set -euo pipefail; source omnl-fineract/.env 2>/dev/null || true; cat reconciliation/\\\${RUN_DIR}/DRYRUN.commands.txt | bash\""
echo "---"

View File

@@ -0,0 +1,128 @@
#!/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

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# OMNL — Create Day 0 baseline: config-hash + audit packet, saved under reconciliation/baseline/<YYYYMMDD>/.
# Run once after confirming 5B in UI. Then copy baseline off-box (S3/Drive/Vault). See PRODUCTION_OPS_OFFICE20.md.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
BASELINE_DATE="${BASELINE_DATE:-$(date -u +%Y%m%d)}"
BASELINE_ROOT="${BASELINE_ROOT:-${REPO_ROOT}/reconciliation/baseline/${BASELINE_DATE}}"
mkdir -p "$BASELINE_ROOT"
echo "Creating baseline at $BASELINE_ROOT (date=$BASELINE_DATE)..." >&2
# 1) Config hash
bash "${REPO_ROOT}/scripts/omnl/omnl-config-hash.sh" > "${BASELINE_ROOT}/config-hash.json" 2>/dev/null || echo "{\"error\": \"config-hash failed\"}" > "${BASELINE_ROOT}/config-hash.json"
echo " config-hash.json" >&2
# 2) Audit packet (folder inside baseline)
OUT_BASE="${BASELINE_ROOT}" TIMESTAMP="${BASELINE_DATE}-000000" bash "${REPO_ROOT}/scripts/omnl/omnl-audit-packet-office20.sh" >/dev/null 2>&1 || true
# Packet dir will be ${BASELINE_ROOT}/audit-office20-${BASELINE_DATE}-000000
packet_dir=$(find "$BASELINE_ROOT" -maxdepth 1 -type d -name "audit-office20-*" 2>/dev/null | head -1)
if [ -n "$packet_dir" ]; then
echo " $packet_dir" >&2
else
echo " (audit packet folder not found; run omnl-audit-packet-office20.sh with OUT_BASE=$BASELINE_ROOT)" >&2
fi
echo "Baseline created. Next: copy $BASELINE_ROOT off-box (S3/Drive/Vault) for immutable Day 0." >&2
echo "BASELINE_ROOT=$BASELINE_ROOT"

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# OMNL Fineract — Set client firstname/lastname to canonical operating-entity names.
# Usage: run from repo root; sources omnl-fineract/.env or .env.
# DRY_RUN=1 print payloads only, do not PUT.
# Requires: curl, jq.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
DRY_RUN="${DRY_RUN:-0}"
# Canonical names: Client 1 = Head Office, Client 215 = operating entities (by account no 000000001000000015)
declare -A CLIENT_NAMES=(
[1]="OMNL Head Office (DBIS) Central Bank"
[2]="Shamrayan Enterprises"
[3]="HYBX"
[4]="TAJ Private Single Family Office"
[5]="Aseret Mortgage Bank"
[6]="Mann Li Family Offices"
[7]="Sovereign Order of Malta OSJ"
[8]="Alltra Mainnet"
[9]="FIDIS"
[10]="Alpha Omega Holdings"
[11]="SGI Capital"
[12]="Titan Financial"
[13]="Roy Walker PLLC"
[14]="SGI Partners LLC"
[15]="Tsunami Holdings AG"
)
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. in omnl-fineract/.env)" >&2
exit 1
fi
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
clients_json=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/clients")
if ! echo "$clients_json" | jq -e '.pageItems // .' >/dev/null 2>&1; then
echo "Unexpected clients response (no pageItems or array)." >&2
echo "$clients_json" | head -c 500
exit 1
fi
# Normalize: Fineract may return { pageItems: [...] } or direct array
if echo "$clients_json" | jq -e '.pageItems' >/dev/null 2>&1; then
items=$(echo "$clients_json" | jq -c '.pageItems[]')
else
items=$(echo "$clients_json" | jq -c '.[]')
fi
updated=0
skipped=0
while IFS= read -r row; do
[ -z "$row" ] && continue
id=$(echo "$row" | jq -r '.id')
account_no=$(echo "$row" | jq -r '.accountNo // ""')
ext_id=$(echo "$row" | jq -r '.externalId // ""')
first=$(echo "$row" | jq -r '.firstname // ""')
last=$(echo "$row" | jq -r '.lastname // ""')
display=$(echo "$row" | jq -r '.displayName // ""')
# Resolve client number from accountNo (000000001 -> 1, etc.)
client_num=""
if [ -n "$account_no" ]; then
client_num=$(echo "$account_no" | sed 's/^0*//')
[ -z "$client_num" ] && client_num="0"
fi
display_name="${CLIENT_NAMES[$client_num]:-}"
# Skip if no canonical name for this client number
if [ -z "$display_name" ]; then
echo "Skip clientId=$id accountNo=$account_no (no canonical name for Client $client_num)" >&2
((skipped++)) || true
continue
fi
# Idempotent: skip if already set to this exact name
if [ -n "$display" ] && [ "$display" = "$display_name" ]; then
echo "Skip clientId=$id (already set: $display)" >&2
((skipped++)) || true
continue
fi
# Set full name in firstname; lastname must be non-blank (tenant validation)
firstname="$display_name"
lastname="."
payload=$(jq -n --arg f "$firstname" --arg l "$lastname" '{ firstname: $f, lastname: $l }')
echo "Client id=$id accountNo=$account_no (Client $client_num) -> $display_name" >&2
if [ "$DRY_RUN" = "1" ]; then
echo " [DRY RUN] PUT ${BASE_URL}/clients/${id} $payload" >&2
((updated++)) || true
continue
fi
res=$(curl "${CURL_OPTS[@]}" -X PUT -d "$payload" "${BASE_URL}/clients/${id}" 2>/dev/null) || true
if echo "$res" | jq -e '.resourceId // .clientId' >/dev/null 2>&1; then
echo " Updated clientId=$id" >&2
((updated++)) || true
else
echo " Failed clientId=$id: $res" >&2
fi
done <<< "$items"
echo "Done: $updated updated, $skipped skipped (already had name)." >&2

View File

@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# OMNL Fineract — Create clients 915 (FIDIS, Alpha Omega Holdings, …) via POST /clients.
# Idempotent: skips if a client with the same externalId already exists.
# Usage: run from repo root; sources omnl-fineract/.env or .env.
# ENTITY_DATA=<path> JSON entity data (default: docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json)
# DRY_RUN=1 print payloads only, do not POST.
# Requires: curl, jq.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
DRY_RUN="${DRY_RUN:-0}"
ENTITY_DATA="${ENTITY_DATA:-${REPO_ROOT}/docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json}"
if [ ! -f "$ENTITY_DATA" ]; then
echo "Entity data file not found: $ENTITY_DATA" >&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:-}"
OFFICE_ID="${OFFICE_ID:-1}"
# legalFormId from GET /clients/template: 1=Person, 2=Entity
LEGAL_FORM_ID="${LEGAL_FORM_ID:-2}"
if [ -z "$BASE_URL" ] || [ -z "$PASS" ]; then
echo "Set OMNL_FINERACT_BASE_URL and OMNL_FINERACT_PASSWORD (e.g. in omnl-fineract/.env)" >&2
exit 1
fi
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
SUBMITTED_DATE="${SUBMITTED_DATE:-$(date +%Y-%m-%d)}"
# Return client id if a client with this externalId exists, else empty
get_client_id_by_external_id() {
local ext_id="$1"
local clients_json="$2"
if echo "$clients_json" | jq -e '.pageItems' >/dev/null 2>&1; then
echo "$clients_json" | jq -r --arg e "$ext_id" '.pageItems[] | select(.externalId == $e) | .id'
else
echo "$clients_json" | jq -r --arg e "$ext_id" '.[] | select(.externalId == $e) | .id'
fi
}
clients_json=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/clients")
if ! echo "$clients_json" | jq -e '.pageItems // .' >/dev/null 2>&1; then
echo "Unexpected clients response." >&2
exit 1
fi
created=0
skipped=0
for client_num in 9 10 11 12 13 14 15; do
entity=$(jq -c ".entities[] | select(.clientNumber == $client_num)" "$ENTITY_DATA")
if [ -z "$entity" ]; then
echo "Skip: no entity with clientNumber=$client_num in $ENTITY_DATA" >&2
continue
fi
entity_name=$(echo "$entity" | jq -r '.entityName')
account_no=$(echo "$entity" | jq -r '.accountNo')
ext_id="OMNL-${client_num}"
existing=$(get_client_id_by_external_id "$ext_id" "$clients_json")
if [ -n "$existing" ] && [ "$existing" != "null" ]; then
echo "Skip client $client_num: already exists (externalId=$ext_id, id=$existing)" >&2
((skipped++)) || true
continue
fi
payload=$(jq -n \
--argjson officeId "$OFFICE_ID" \
--argjson legalFormId "$LEGAL_FORM_ID" \
--arg firstname "$entity_name" \
--arg externalId "$ext_id" \
--arg submittedOnDate "$SUBMITTED_DATE" \
'{
officeId: $officeId,
legalFormId: $legalFormId,
firstname: $firstname,
lastname: ".",
externalId: $externalId,
dateFormat: "yyyy-MM-dd",
locale: "en",
active: false,
submittedOnDate: $submittedOnDate
}')
echo "Create client $client_num: $entity_name (externalId=$ext_id)" >&2
if [ "$DRY_RUN" = "1" ]; then
echo " [DRY RUN] POST clients $payload" >&2
((created++)) || true
continue
fi
res=$(curl "${CURL_OPTS[@]}" -X POST -d "$payload" "${BASE_URL}/clients" 2>/dev/null) || true
if echo "$res" | jq -e '.resourceId // .clientId' >/dev/null 2>&1; then
new_id=$(echo "$res" | jq -r '.resourceId // .clientId')
echo " Created clientId=$new_id" >&2
((created++)) || true
else
echo " Failed: $res" >&2
fi
done
echo "Done: $created created, $skipped skipped (already existed)." >&2

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# OMNL Fineract — Remove the 15 clients (ids 115) that were created as entities.
# Run this after populating the 15 entities as Offices (omnl-offices-populate-15.sh).
# Usage: run from repo root; sources omnl-fineract/.env or .env.
# CONFIRM_REMOVE=1 Required to actually delete (safety).
# DRY_RUN=1 print only, do not DELETE.
# Requires: curl, jq.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
DRY_RUN="${DRY_RUN:-0}"
CONFIRM_REMOVE="${CONFIRM_REMOVE:-0}"
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. in omnl-fineract/.env)" >&2
exit 1
fi
if [ "$CONFIRM_REMOVE" != "1" ] && [ "$DRY_RUN" != "1" ]; then
echo "Safety: set CONFIRM_REMOVE=1 to actually delete the 15 clients." >&2
echo "Example: CONFIRM_REMOVE=1 bash scripts/omnl/omnl-clients-remove-15.sh" >&2
exit 1
fi
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
removed=0
failed=0
# Delete in reverse order (15..1) in case of constraints
for id in 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1; do
if [ "$DRY_RUN" = "1" ]; then
echo " [DRY RUN] DELETE clients/$id" >&2
((removed++)) || true
continue
fi
if [ "$CONFIRM_REMOVE" != "1" ]; then
continue
fi
res=$(curl "${CURL_OPTS[@]}" -X DELETE "${BASE_URL}/clients/${id}" 2>/dev/null) || true
if echo "$res" | jq -e '.resourceId' >/dev/null 2>&1 || [ -z "$res" ]; then
echo " Deleted clientId=$id" >&2
((removed++)) || true
else
echo " Failed clientId=$id: $res" >&2
((failed++)) || true
fi
done
echo "Done: $removed deleted, $failed failed." >&2

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# OMNL — Config drift: output sha256 hashes of payment types, GL (1410/2100/2410), office 20 config.
# Compare output to a baseline to detect drift. For CI or alerting.
# Usage: bash scripts/omnl/omnl-config-hash.sh; store output as baseline and diff on next run.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
if [ -f "${REPO_ROOT}/omnl-fineract/.env" ]; then
set +u
source "${REPO_ROOT}/omnl-fineract/.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:-}"
[ -z "$BASE_URL" ] || [ -z "$PASS" ] && { echo "{}"; exit 0; }
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
pt=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/paymenttypes" 2>/dev/null)
gl=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/glaccounts" 2>/dev/null)
offices=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/offices" 2>/dev/null)
pt_hash=$(echo "$pt" | jq -c 'sort_by(.id)' 2>/dev/null | sha256sum | awk '{print $1}')
gl_relevant=$(echo "$gl" | jq '[.[] | select(.glCode == "1410" or .glCode == "2100" or .glCode == "2410")] | sort_by(.id)' 2>/dev/null)
gl_hash=$(echo "$gl_relevant" | sha256sum | awk '{print $1}')
o20=$(echo "$offices" | jq '.[] | select(.id == 20)' 2>/dev/null)
o20_hash=$(echo "$o20" | sha256sum | awk '{print $1}')
echo "{\"paymentTypes\": \"$pt_hash\", \"glRelevant\": \"$gl_hash\", \"office20\": \"$o20_hash\", \"timestamp\": \"$(date -u -Iseconds)\"}"

View File

@@ -0,0 +1,183 @@
#!/usr/bin/env bash
# OMNL Fineract — Apply entity master data (name, LEI, address, contacts) to clients.
# Reads OMNL_ENTITY_MASTER_DATA.json; maps by accountNo to clientId; updates names, identifiers (LEI), addresses, contacts.
# Usage: run from repo root; sources omnl-fineract/.env or .env.
# ENTITY_DATA=<path> JSON entity data (default: docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json)
# DRY_RUN=1 print only, do not PUT/POST.
# Requires: curl, jq.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
DRY_RUN="${DRY_RUN:-0}"
ENTITY_DATA="${ENTITY_DATA:-${REPO_ROOT}/docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json}"
if [ ! -f "$ENTITY_DATA" ]; then
echo "Entity data file not found: $ENTITY_DATA" >&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. in omnl-fineract/.env)" >&2
exit 1
fi
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
# Resolve clientId by accountNo (000000001 -> id)
get_client_id_by_account() {
local account_no="$1"
local clients_json="$2"
if echo "$clients_json" | jq -e '.pageItems' >/dev/null 2>&1; then
echo "$clients_json" | jq -r --arg an "$account_no" '.pageItems[] | select(.accountNo == $an) | .id'
else
echo "$clients_json" | jq -r --arg an "$account_no" '.[] | select(.accountNo == $an) | .id'
fi
}
# Resolve clientId by externalId (e.g. OMNL-9) when accountNo not found
get_client_id_by_external_id() {
local ext_id="$1"
local clients_json="$2"
if echo "$clients_json" | jq -e '.pageItems' >/dev/null 2>&1; then
echo "$clients_json" | jq -r --arg e "$ext_id" '.pageItems[] | select(.externalId == $e) | .id'
else
echo "$clients_json" | jq -r --arg e "$ext_id" '.[] | select(.externalId == $e) | .id'
fi
}
# Resolve LEI document type ID from identifiers template (first type whose name contains LEI, or first type)
get_lei_document_type_id() {
local client_id="$1"
local template
template=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/clients/${client_id}/identifiers/template" 2>/dev/null) || true
if [ -z "$template" ]; then
echo ""
return
fi
local id
id=$(echo "$template" | jq -r '(.allowedDocumentTypes // [])[] | select(.name | ascii_upcase | test("LEI")) | .id' 2>/dev/null | head -1)
if [ -z "$id" ] || [ "$id" = "null" ]; then
id=$(echo "$template" | jq -r '(.allowedDocumentTypes // [])[0].id // empty' 2>/dev/null)
fi
echo "$id"
}
clients_json=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/clients")
if ! echo "$clients_json" | jq -e '.pageItems // .' >/dev/null 2>&1; then
echo "Unexpected clients response." >&2
exit 1
fi
entity_count=$(jq -r '.entities | length' "$ENTITY_DATA")
updated_names=0
updated_lei=0
updated_address=0
updated_contact=0
for i in $(seq 0 $((entity_count - 1))); do
entity=$(jq -c ".entities[$i]" "$ENTITY_DATA")
client_num=$(echo "$entity" | jq -r '.clientNumber')
account_no=$(echo "$entity" | jq -r '.accountNo')
entity_name=$(echo "$entity" | jq -r '.entityName')
lei=$(echo "$entity" | jq -r '.lei // ""')
mobile=$(echo "$entity" | jq -r '.contact.mobileNo // ""')
email=$(echo "$entity" | jq -r '.contact.emailAddress // ""')
client_id=$(get_client_id_by_account "$account_no" "$clients_json")
if [ -z "$client_id" ] || [ "$client_id" = "null" ]; then
client_id=$(get_client_id_by_external_id "OMNL-${client_num}" "$clients_json")
fi
if [ -z "$client_id" ] || [ "$client_id" = "null" ]; then
echo "Skip: no client with accountNo=$account_no or externalId=OMNL-$client_num" >&2
continue
fi
echo "=== Client $client_num (id=$client_id) $entity_name ===" >&2
# 1. Name (lastname non-blank required by tenant validation)
payload_name=$(jq -n --arg f "$entity_name" --arg l "." '{ firstname: $f, lastname: $l }')
if [ "$DRY_RUN" = "1" ]; then
echo " [DRY RUN] PUT clients/${client_id} name=$entity_name" >&2
else
res=$(curl "${CURL_OPTS[@]}" -X PUT -d "$payload_name" "${BASE_URL}/clients/${client_id}" 2>/dev/null) || true
if echo "$res" | jq -e '.resourceId // .clientId' >/dev/null 2>&1; then
((updated_names++)) || true
fi
fi
# 2. LEI identifier (if lei non-empty)
if [ -n "$lei" ] && [ "$lei" != "null" ]; then
lei_type_id=$(get_lei_document_type_id "$client_id")
if [ -n "$lei_type_id" ] && [ "$lei_type_id" != "null" ]; then
payload_lei=$(jq -n --arg key "$lei" --argjson typeId "$lei_type_id" '{ documentKey: $key, documentTypeId: $typeId, description: "LEI", status: "Active" }')
if [ "$DRY_RUN" = "1" ]; then
echo " [DRY RUN] POST clients/${client_id}/identifiers LEI=$lei" >&2
else
res=$(curl "${CURL_OPTS[@]}" -X POST -d "$payload_lei" "${BASE_URL}/clients/${client_id}/identifiers" 2>/dev/null) || true
if echo "$res" | jq -e '.resourceId // .clientId' >/dev/null 2>&1; then
((updated_lei++)) || true
fi
fi
else
echo " Skip LEI: no LEI document type in tenant (add via Admin or codes)" >&2
fi
fi
# 3. Address (if any address field set and countryId present)
country_id=$(echo "$entity" | jq -r '.address.countryId // empty')
street=$(echo "$entity" | jq -r '.address.street // ""')
line1=$(echo "$entity" | jq -r '.address.addressLine1 // ""')
city=$(echo "$entity" | jq -r '.address.city // ""')
if [ -n "$country_id" ] && [ "$country_id" != "null" ] && { [ -n "$street" ] || [ -n "$line1" ] || [ -n "$city" ]; }; then
payload_addr=$(echo "$entity" | jq -c '
.address | {
street: (.street // ""),
addressLine1: (.addressLine1 // ""),
addressLine2: (.addressLine2 // ""),
addressLine3: (.addressLine3 // ""),
city: (.city // ""),
postalCode: (if .postalCode != null and .postalCode != "" then .postalCode else "" end),
countryId: .countryId,
isActive: true
} + (if .stateProvinceId != null and .stateProvinceId != "" then { stateProvinceId: .stateProvinceId } else {} end)
')
if [ "$DRY_RUN" = "1" ]; then
echo " [DRY RUN] POST client/${client_id}/addresses" >&2
else
res=$(curl "${CURL_OPTS[@]}" -X POST -d "$payload_addr" "${BASE_URL}/client/${client_id}/addresses" 2>/dev/null) || true
if echo "$res" | jq -e '.resourceId // .addressId' >/dev/null 2>&1; then
((updated_address++)) || true
fi
fi
fi
# 4. Contact (mobileNo, emailAddress) via PUT client
if [ -n "$mobile" ] || [ -n "$email" ]; then
payload_contact=$(jq -n --arg m "$mobile" --arg e "$email" '{ mobileNo: $m, emailAddress: $e }')
if [ "$DRY_RUN" = "1" ]; then
echo " [DRY RUN] PUT clients/${client_id} contact mobile=$mobile email=$email" >&2
else
res=$(curl "${CURL_OPTS[@]}" -X PUT -d "$payload_contact" "${BASE_URL}/clients/${client_id}" 2>/dev/null) || true
if echo "$res" | jq -e '.resourceId // .clientId' >/dev/null 2>&1; then
((updated_contact++)) || true
fi
fi
fi
done
echo "Done: names=$updated_names, LEI=$updated_lei, addresses=$updated_address, contacts=$updated_contact" >&2

View File

@@ -85,6 +85,8 @@ create_gl "1050" "USD Treasury Conversion Reserve (M0)" 1 "Treasury Conversion R
create_gl "2000" "USD Central Deposits" 2 "Central bank customer deposits; M1-denominated claims backed by 1050 where applicable"
create_gl "2100" "USD Restricted Liabilities" 2 "Restricted / held deposits"
create_gl "3000" "Opening Balance Control" 3 "Migration control account"
create_gl "1410" "Due From Head Office (Interoffice Receivable)" 1 "Interoffice receivable at branch"
create_gl "2410" "Due To Offices (Interoffice Payable)" 2 "Interoffice payable at Head Office"
echo ""
echo "Done. Run scripts/omnl/omnl-ledger-post.sh to post T-001T-008."

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# Create FX and GRU (M00) GL accounts from CHART_OF_ACCOUNTS.md in OMNL Fineract.
# Idempotent: skips if glCode exists. Creates parents before children. Run from repo root.
# Requires: curl, jq.
# See: docs/04-configuration/mifos-omnl-central-bank/CHART_OF_ACCOUNTS.md, OMNL_GL_ACCOUNTS_FX_GRU.md
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
# usage: 1=DETAIL, 2=HEADER
USAGE_DETAIL=1
USAGE_HEADER=2
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}")
# Build glCode -> id map from existing GL accounts
get_gl_map() {
local json
json=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/glaccounts" 2>/dev/null | sed '$d')
echo "$json" | jq -r '.[] | "\(.glCode)|\(.id)"' 2>/dev/null || true
}
# Create one GL account; optional parent glCode; usage 1=DETAIL 2=HEADER
create_gl() {
local gl_code="$1" name="$2" type_id="$3" usage_id="$4" parent_gl="$5" desc="$6"
local body parent_id
if [ -n "${GL_ID_MAP[$gl_code]:-}" ]; then
echo " [skip] $gl_code$name (exists)"
return 0
fi
parent_id=""
if [ -n "$parent_gl" ] && [ -n "${GL_ID_MAP[$parent_gl]:-}" ]; then
parent_id="${GL_ID_MAP[$parent_gl]}"
fi
if [ -n "$parent_id" ]; then
body=$(jq -n \
--arg code "$gl_code" \
--arg name "$name" \
--argjson type "$type_id" \
--argjson usage "$usage_id" \
--argjson parentId "$parent_id" \
--arg desc "$desc" \
'{ glCode: $code, name: $name, type: $type, usage: $usage, parentId: $parentId, manualEntriesAllowed: true, description: $desc }')
else
body=$(jq -n \
--arg code "$gl_code" \
--arg name "$name" \
--argjson type "$type_id" \
--argjson usage "$usage_id" \
--arg desc "$desc" \
'{ glCode: $code, name: $name, type: $type, usage: $usage, manualEntriesAllowed: true, description: $desc }')
fi
local out
out=$(curl "${CURL_OPTS[@]}" -X POST -d "$body" "${BASE_URL}/glaccounts" 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
local new_id
new_id=$(echo "$resp" | jq -r '.resourceId // empty')
if [ -n "$new_id" ]; then
GL_ID_MAP[$gl_code]=$new_id
fi
echo " [created] $gl_code$name"
else
echo " [fail] $gl_code$name HTTP $code: $resp" >&2
return 1
fi
}
# Load initial map
declare -A GL_ID_MAP
while IFS='|' read -r code id; do
[ -n "$code" ] && [ -n "$id" ] && GL_ID_MAP[$code]=$id
done < <(get_gl_map)
echo "=== OMNL Fineract — Create FX and GRU (M00) GL accounts ==="
echo "Base URL: $BASE_URL"
echo ""
# Type: 1=ASSET, 2=LIABILITY, 4=INCOME, 5=EXPENSE
# Order: parents before children
echo "--- Assets (FX reserves & settlement) ---"
create_gl "10000" "Assets (header)" 1 $USAGE_HEADER "" "Total assets"
create_gl "12000" "Foreign currency reserves (header)" 1 $USAGE_HEADER "10000" "FX reserves header"
create_gl "12010" "FX reserves — USD" 1 $USAGE_DETAIL "12000" "Foreign currency reserves — USD"
create_gl "12020" "FX reserves — EUR" 1 $USAGE_DETAIL "12000" "Foreign currency reserves — EUR"
create_gl "12090" "FX reserves — other" 1 $USAGE_DETAIL "12000" "Other ISO-4217 and special units"
create_gl "13000" "FX settlement balances (header)" 1 $USAGE_HEADER "10000" "FX settlement header"
create_gl "13010" "FX settlement — nostro" 1 $USAGE_DETAIL "13000" "Settlement balances with counterparties"
echo "--- Liabilities (GRU / M00) ---"
create_gl "20000" "Liabilities (header)" 2 $USAGE_HEADER "" "Total liabilities"
create_gl "21000" "M00 — Base reserve (header)" 2 $USAGE_HEADER "20000" "Central bank reserve unit; GRU-denominated; non-circulating except authorized issuance"
create_gl "21010" "M00 — Bank reserves (control)" 2 $USAGE_DETAIL "21000" "Control account for M00"
echo "--- Income (FX gains) ---"
create_gl "40000" "Income (header)" 4 $USAGE_HEADER "" "Total income"
create_gl "42000" "FX gains (realized)" 4 $USAGE_DETAIL "40000" "Realized foreign exchange gains"
create_gl "42100" "Unrealized FX gain (P&L)" 4 $USAGE_DETAIL "40000" "Unrealized FX gain (revaluation)"
echo "--- Expenses (FX losses) ---"
create_gl "50000" "Expenses (header)" 5 $USAGE_HEADER "" "Total expenses"
create_gl "51000" "FX losses (realized)" 5 $USAGE_DETAIL "50000" "Realized foreign exchange losses"
create_gl "52100" "Unrealized FX loss (P&L)" 5 $USAGE_DETAIL "50000" "Unrealized FX loss (revaluation)"
echo ""
echo "Done. See CHART_OF_ACCOUNTS.md and FX_AND_VALUATION.md for usage."

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# OMNL Fineract — Post GL closures for Office 20 (Samama) and Head Office (1) to lock period through a given date.
# Idempotent: skips if a closure already exists for that office. Use after funding to prevent backdating.
# Usage: from repo root. CLOSING_DATE=yyyy-MM-dd (default: 2026-02-24). DRY_RUN=1 to print only.
# See docs/04-configuration/mifos-omnl-central-bank/OPERATING_RAILS.md
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
DRY_RUN="${DRY_RUN:-0}"
CLOSING_DATE="${CLOSING_DATE:-2026-02-24}"
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 -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
# GET glclosures (response may be array or object with pageItems)
closures_json=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/glclosures" 2>/dev/null)
has_closure_for_office() {
local oid="$1"
echo "$closures_json" | jq -e --argjson o "$oid" '
(if type == "array" then . else (.pageItems // .) end) |
map(select(.officeId == $o or .office.id == $o)) |
length > 0
' >/dev/null 2>&1
}
post_closure() {
local office_id="$1"
local comments="$2"
local body
body=$(jq -n \
--argjson officeId "$office_id" \
--arg closingDate "$CLOSING_DATE" \
--arg comments "$comments" \
--arg dateFormat "yyyy-MM-dd" \
--arg locale "en" \
'{ officeId: $officeId, closingDate: $closingDate, comments: $comments, dateFormat: $dateFormat, locale: $locale }')
if [ "$DRY_RUN" = "1" ]; then
echo "DRY_RUN: would POST /glclosures officeId=$office_id closingDate=$CLOSING_DATE" >&2
return 0
fi
local out
out=$(curl "${CURL_OPTS[@]}" -X POST -d "$body" "${BASE_URL}/glclosures" 2>/dev/null)
if echo "$out" | jq -e '.resourceId // .officeId' >/dev/null 2>&1; then
echo "Posted GL closure for office $office_id (date=$CLOSING_DATE)" >&2
return 0
else
echo "POST /glclosures failed for office $office_id: $out" >&2
return 1
fi
}
for office_id in 20 1; do
if has_closure_for_office "$office_id"; then
echo "Skip office $office_id: closure already exists" >&2
else
case "$office_id" in
20) comments="Samama Office funded; lock period through funding date" ;;
1) comments="HO closure through Samama funding date" ;;
*) comments="GL closure $CLOSING_DATE" ;;
esac
post_closure "$office_id" "$comments" || true
fi
done
echo "Verify: GET /glclosures?officeId=20 and GET /glclosures?officeId=1" >&2

85
scripts/omnl/omnl-je-checker.sh Executable file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# OMNL Fineract — Checker: validate payload file (hash + sanity) and POST journal entry (maker-checker).
# Usage: PAYLOAD_FILE=reconciliation/je-<ref>.payload.json bash scripts/omnl/omnl-je-checker.sh
# Optional: SKIP_HASH=1 to skip sha256 check. DRY_RUN=1 to validate only, do not post.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
PAYLOAD_FILE="${PAYLOAD_FILE:?Set PAYLOAD_FILE}"
SKIP_HASH="${SKIP_HASH:-0}"
DRY_RUN="${DRY_RUN:-0}"
if [ ! -f "$PAYLOAD_FILE" ]; then
echo "Payload file not found: $PAYLOAD_FILE" >&2
exit 1
fi
hash_file="${PAYLOAD_FILE%.json}.sha256"
if [ "$SKIP_HASH" != "1" ] && [ -f "$hash_file" ]; then
expected=$(cat "$hash_file")
actual=$(sha256sum "$PAYLOAD_FILE" | awk '{print $1}')
if [ "$expected" != "$actual" ]; then
echo "Checker: hash mismatch (expected $expected, got $actual). Abort." >&2
exit 1
fi
fi
body=$(cat "$PAYLOAD_FILE")
amount=$(echo "$body" | jq -r '.debits[0].amount // 0')
office_id=$(echo "$body" | jq -r '.officeId')
MATERIAL_THRESHOLD_MAKER_CHECKER="${MATERIAL_THRESHOLD_MAKER_CHECKER:-10000000}"
if [ -z "$amount" ] || [ "${amount:-0}" -le 0 ]; then
echo "Checker: invalid amount in payload" >&2
exit 1
fi
# Material policy: amount >= threshold requires approvalMetadata in payload
if [ "${amount:-0}" -ge "${MATERIAL_THRESHOLD_MAKER_CHECKER}" ] 2>/dev/null; then
approver=$(echo "$body" | jq -r '.approvalMetadata.approver // empty')
approved_at=$(echo "$body" | jq -r '.approvalMetadata.approvedAt // empty')
if [ -z "$approver" ] || [ -z "$approved_at" ]; then
echo "Checker: amount >= $MATERIAL_THRESHOLD_MAKER_CHECKER requires approvalMetadata (approver, approvedAt) in payload" >&2
exit 1
fi
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" >&2
exit 1
fi
if [ "$DRY_RUN" = "1" ]; then
echo "Checker: DRY_RUN — payload valid, not posting" >&2
exit 0
fi
# Strip approvalMetadata before POST (Fineract does not expect it)
post_body=$(echo "$body" | jq 'del(.approvalMetadata)' 2>/dev/null || echo "$body")
CURL_OPTS=(-s -S -w "\n%{http_code}" -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
out=$(curl "${CURL_OPTS[@]}" -X POST -d "$post_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 "Checker: posted successfully (HTTP $code)" >&2
echo "$resp" | jq '.' 2>/dev/null || echo "$resp"
else
echo "Checker: POST failed HTTP $code: $resp" >&2
exit 1
fi

56
scripts/omnl/omnl-je-maker.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# OMNL — Maker: write single JE payload + sha256 for checker (maker-checker).
# Usage: OFFICE_ID=20 DEBIT_GL_ID=... CREDIT_GL_ID=... AMOUNT=... REF=... [COMMENTS=...] bash scripts/omnl/omnl-je-maker.sh
# Aliases: REFERENCE_NUMBER=REF, TX_DATE=TRANSACTION_DATE. For ≥10M set REQUIRES_APPROVAL=1 and APPROVER=<name>.
# Output: reconciliation/je-<ref>.payload.json and .payload.sha256
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
PAYLOAD_DIR="${PAYLOAD_DIR:-${REPO_ROOT}/reconciliation}"
REF="${REF:-$REFERENCE_NUMBER}"
TRANSACTION_DATE="${TRANSACTION_DATE:-$TX_DATE}"
TRANSACTION_DATE="${TRANSACTION_DATE:-$(date +%Y-%m-%d)}"
: "${OFFICE_ID:?Set OFFICE_ID}"
: "${DEBIT_GL_ID:?Set DEBIT_GL_ID}"
: "${CREDIT_GL_ID:?Set CREDIT_GL_ID}"
: "${AMOUNT:?Set AMOUNT}"
: "${REF:?Set REF or REFERENCE_NUMBER (referenceNumber)}"
COMMENTS="${COMMENTS:-JE from maker}"
MATERIAL_THRESHOLD_MAKER_CHECKER="${MATERIAL_THRESHOLD_MAKER_CHECKER:-10000000}"
# Material policy: if amount >= threshold, require REQUIRES_APPROVAL=1 and APPROVER
if [ "${AMOUNT:-0}" -ge "${MATERIAL_THRESHOLD_MAKER_CHECKER}" ] 2>/dev/null; then
if [ "${REQUIRES_APPROVAL:-0}" != "1" ] || [ -z "${APPROVER:-}" ]; then
echo "Maker: amount >= $MATERIAL_THRESHOLD_MAKER_CHECKER requires REQUIRES_APPROVAL=1 and APPROVER=<name>" >&2
exit 1
fi
fi
mkdir -p "$PAYLOAD_DIR"
safe_ref=$(echo "$REF" | tr -c 'A-Za-z0-9_-' '_')
payload_file="${PAYLOAD_DIR}/je-${safe_ref}.payload.json"
hash_file="${PAYLOAD_DIR}/je-${safe_ref}.payload.sha256"
body=$(jq -n \
--argjson officeId "$OFFICE_ID" \
--arg transactionDate "$TRANSACTION_DATE" \
--arg dateFormat "yyyy-MM-dd" \
--arg locale "en" \
--arg currencyCode "USD" \
--arg comments "$COMMENTS" \
--arg referenceNumber "$REF" \
--argjson debitId "$DEBIT_GL_ID" \
--argjson creditId "$CREDIT_GL_ID" \
--argjson amount "$AMOUNT" \
'{ officeId: $officeId, transactionDate: $transactionDate, dateFormat: $dateFormat, locale: $locale, currencyCode: $currencyCode, comments: $comments, referenceNumber: $referenceNumber, debits: [ { glAccountId: $debitId, amount: $amount } ], credits: [ { glAccountId: $creditId, amount: $amount } ] }')
# Append approvalMetadata for material postings (checker enforces; Fineract ignores unknown fields)
if [ "${AMOUNT:-0}" -ge "${MATERIAL_THRESHOLD_MAKER_CHECKER}" ] 2>/dev/null && [ -n "${APPROVER:-}" ]; then
body=$(echo "$body" | jq --arg approver "$APPROVER" --arg approvedAt "$(date -u -Iseconds)" \
'. + { approvalMetadata: { approver: $approver, approvedAt: $approvedAt } }')
fi
echo "$body" > "$payload_file"
sha256sum "$payload_file" | awk '{print $1}' > "$hash_file"
echo "Maker: wrote $payload_file and $hash_file" >&2

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# OMNL Fineract — Reverse a journal entry by referenceNumber: find JE and post opposite debits/credits.
# Usage: REFERENCE_NUMBER=<ref> bash scripts/omnl/omnl-je-reverse-by-reference.sh
# Optional: DRY_RUN=1 to print payload only. Reversal is a new JE with same officeId/date, swapped debits/credits, comment "REVERSAL: <ref>".
# See OPERATING_RAILS.md and OFFICE_20_DR_RUNBOOK.md.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
REFERENCE_NUMBER="${REFERENCE_NUMBER:-}"
DRY_RUN="${DRY_RUN:-0}"
if [ -z "$REFERENCE_NUMBER" ]; then
echo "Set REFERENCE_NUMBER (e.g. SAMAMA-20-20260224-HO)" >&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:-}"
TRANSACTION_DATE="${TRANSACTION_DATE:-$(date +%Y-%m-%d)}"
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}")
# Try to find JE by referenceNumber (Fineract may support referenceNumber in list or search)
# GET journalentries often takes fromDate, toDate, officeId - we may need to fetch and filter
from_date=$(date -d "${TRANSACTION_DATE} -30 days" +%Y-%m-%d 2>/dev/null || echo "2020-01-01")
je_list=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/journalentries?fromDate=${from_date}&toDate=${TRANSACTION_DATE}" 2>/dev/null) || je_list="[]"
je_match=$(echo "$je_list" | jq --arg ref "$REFERENCE_NUMBER" '
(if type == "array" then . else (.pageItems // .) end) |
if type == "array" then . else [] end |
map(select(.referenceNumber == $ref or .reference_number == $ref)) | .[0]
' 2>/dev/null)
if [ -z "$je_match" ] || [ "$je_match" = "null" ]; then
echo "No journal entry found with referenceNumber=$REFERENCE_NUMBER. List API may not support reference filter; check UI or run audit packet to get JE id." >&2
echo "To reverse manually: 1) GET journalentries or use UI to find JE id and its debits/credits. 2) POST a new JE with officeId/transactionDate, comments=\"REVERSAL: $REFERENCE_NUMBER\", debits=original credits, credits=original debits." >&2
exit 1
fi
office_id=$(echo "$je_match" | jq -r '.officeId // .office.id // 1')
debits=$(echo "$je_match" | jq -c '.debits // .debitAccounts // []')
credits=$(echo "$je_match" | jq -c '.credits // .creditAccounts // []')
amount=$(echo "$je_match" | jq -r '.debits[0].amount // .credits[0].amount // 0')
# Swap: reversal debits = original credits, reversal credits = original debits
rev_debits="$credits"
rev_credits="$debits"
reversal_ref="REV-${REFERENCE_NUMBER}"
body=$(jq -n \
--argjson officeId "$office_id" \
--arg transactionDate "$TRANSACTION_DATE" \
--arg comments "REVERSAL: $REFERENCE_NUMBER" \
--arg referenceNumber "$reversal_ref" \
--arg dateFormat "yyyy-MM-dd" \
--arg locale "en" \
--arg currencyCode "USD" \
--argjson revDebits "$rev_debits" \
--argjson revCredits "$rev_credits" \
'{ officeId: $officeId, transactionDate: $transactionDate, dateFormat: $dateFormat, locale: $locale, currencyCode: $currencyCode, comments: $comments, referenceNumber: $referenceNumber, debits: $revDebits, credits: $revCredits }')
if [ "$DRY_RUN" = "1" ]; then
echo "DRY_RUN: would POST reversal for $REFERENCE_NUMBER (officeId=$office_id amount=$amount)" >&2
echo "$body" | jq '.' >&2
exit 0
fi
out=$(curl "${CURL_OPTS[@]}" -X POST -d "$body" "${BASE_URL}/journalentries" 2>/dev/null)
if echo "$out" | jq -e '.resourceId // .officeId' >/dev/null 2>&1; then
echo "Posted reversal: $reversal_ref for original $REFERENCE_NUMBER" >&2
echo "$out" | jq '.' >&2
else
echo "POST reversal failed: $out" >&2
exit 1
fi

View File

@@ -0,0 +1,179 @@
#!/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

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# OMNL — Monitoring stub: detect Office 20 movement (JE with officeId=20 touching 1410/2100 or amount > threshold).
# Outputs alert payload JSON to stdout and exits 0 if no alert, 2 if movement detected (for alerting integration).
# Usage: OFFICE_ID=20 AMOUNT_THRESHOLD=0 RECENT_DAYS=1 bash scripts/omnl/omnl-monitor-office20-movement.sh
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
OFFICE_ID="${OFFICE_ID:-20}"
AMOUNT_THRESHOLD="${AMOUNT_THRESHOLD:-0}"
RECENT_DAYS="${RECENT_DAYS:-1}"
if [ -f "${REPO_ROOT}/omnl-fineract/.env" ]; then
set +u
source "${REPO_ROOT}/omnl-fineract/.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:-}"
[ -z "$BASE_URL" ] || [ -z "$PASS" ] && exit 0
from_date=$(date -u -d "-${RECENT_DAYS} days" +%Y-%m-%d 2>/dev/null || date -u -v-${RECENT_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)
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
je_url="${BASE_URL}/journalentries?officeId=${OFFICE_ID}&fromDate=${from_date}&toDate=${to_date}&dateFormat=yyyy-MM-dd&locale=en"
je_list=$(curl "${CURL_OPTS[@]}" "$je_url" 2>/dev/null) || je_list="[]"
# API returns pageItems of line-level entries (each has amount, glAccountId, entryType, transactionId, referenceNumber)
matches=$(echo "$je_list" | jq --argjson oid "$OFFICE_ID" --argjson thresh "$AMOUNT_THRESHOLD" '
(if type == "array" then . else (.pageItems // .) end) | if type != "array" then [] else . end |
map(select(.officeId == $oid or (.office and .office.id == $oid))) |
map(select(($thresh == 0) or ((.amount // 0) > $thresh))) |
[.[] | { id, transactionId: (.transactionId // ""), referenceNumber: (.referenceNumber // .reference_number // ""), amount, transactionDate: (.transactionDate // .transaction_date), entryType: (.entryType | if type == "string" then . else .value end) }] |
unique_by(.transactionId) | if length == 0 then [] else . end
' 2>/dev/null) || matches="[]"
count=$(echo "$matches" | jq 'length' 2>/dev/null || echo "0")
if [ "${count:-0}" -gt 0 ]; then
echo "{\"alert\": \"office20_movement\", \"officeId\": $OFFICE_ID, \"count\": $count, \"entries\": $matches}" | jq '.'
exit 2
fi
echo "{\"alert\": null, \"officeId\": $OFFICE_ID, \"count\": 0}"
exit 0

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# OMNL Fineract — Create one Office for Pelican Motors And Finance LLC (Chalmette, LA).
# Uses Fineract POST /offices (name, parentId, openingDate, externalId).
# See docs/04-configuration/mifos-omnl-central-bank/PELICAN_MOTORS_OFFICE_RUNBOOK.md
#
# Usage: run from repo root.
# OPENING_DATE=2026-02-26 (default)
# DRY_RUN=1 to print payload only, do not POST.
#
# For omnl.hybx.global set in .env:
# OMNL_FINERACT_BASE_URL=https://omnl.hybx.global/fineract-provider/api/v1
#
# Requires: curl, jq.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
DRY_RUN="${DRY_RUN:-0}"
OPENING_DATE="${OPENING_DATE:-2026-02-26}"
PELICAN_EXTERNAL_ID="${PELICAN_EXTERNAL_ID:-PEL-MOTORS-CHALMETTE-LA}"
PELICAN_OFFICE_NAME="${PELICAN_OFFICE_NAME:-Pelican Motors And Finance LLC}"
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 or .env)." >&2
echo "For omnl.hybx.global use: OMNL_FINERACT_BASE_URL=https://omnl.hybx.global/fineract-provider/api/v1" >&2
exit 1
fi
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
# Resolve existing office by externalId (idempotent)
offices_json=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/offices" 2>/dev/null)
existing_id=$(echo "$offices_json" | jq -r --arg e "$PELICAN_EXTERNAL_ID" '.[] | select(.externalId == $e) | .id' 2>/dev/null | head -1)
if [ -n "$existing_id" ] && [ "$existing_id" != "null" ]; then
echo "Pelican Motors office already exists: officeId=$existing_id (externalId=$PELICAN_EXTERNAL_ID)" >&2
echo "OFFICE_ID_PELICAN=$existing_id"
exit 0
fi
payload=$(jq -n \
--arg name "$PELICAN_OFFICE_NAME" \
--arg openingDate "$OPENING_DATE" \
--arg externalId "$PELICAN_EXTERNAL_ID" \
'{ name: $name, parentId: 1, openingDate: $openingDate, externalId: $externalId, dateFormat: "yyyy-MM-dd", locale: "en" }')
if [ "$DRY_RUN" = "1" ]; then
echo "DRY_RUN: would POST /offices with name=$PELICAN_OFFICE_NAME externalId=$PELICAN_EXTERNAL_ID openingDate=$OPENING_DATE" >&2
echo "Payload: $payload" >&2
exit 0
fi
res=$(curl "${CURL_OPTS[@]}" -X POST -d "$payload" "${BASE_URL}/offices" 2>/dev/null) || true
if echo "$res" | jq -e '.resourceId // .officeId' >/dev/null 2>&1; then
PELICAN_OFFICE_ID=$(echo "$res" | jq -r '.resourceId // .officeId')
echo "Created Pelican Motors office: officeId=$PELICAN_OFFICE_ID" >&2
echo "OFFICE_ID_PELICAN=$PELICAN_OFFICE_ID"
else
echo "Failed to create office: $res" >&2
exit 1
fi

View File

@@ -0,0 +1,135 @@
#!/usr/bin/env bash
# OMNL Fineract — Create one Office for Samama Group LLC (Azerbaijan) and optionally run 5B USD M1 transfer.
# Uses Fineract POST /offices (name, parentId, openingDate, externalId). See docs/04-configuration/mifos-omnl-central-bank/SAMAMA_OFFICE_AND_5B_M1_TRANSFER.md.
# Usage: run from repo root. Set DRY_RUN=1 to print only. Set SKIP_TRANSFER=1 to create office only.
# OPENING_DATE=2024-01-10 (default)
# Requires: curl, jq.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
DRY_RUN="${DRY_RUN:-0}"
SKIP_TRANSFER="${SKIP_TRANSFER:-0}"
OPENING_DATE="${OPENING_DATE:-2024-01-10}"
SAMAMA_EXTERNAL_ID="${SAMAMA_EXTERNAL_ID:-SAMAMA-AZ-1703722701}"
SAMAMA_OFFICE_NAME="${SAMAMA_OFFICE_NAME:-Samama Group LLC - Azerbaijan}"
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 -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
# Resolve existing office by externalId
offices_json=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/offices" 2>/dev/null)
existing_id=$(echo "$offices_json" | jq -r --arg e "$SAMAMA_EXTERNAL_ID" '.[] | select(.externalId == $e) | .id' 2>/dev/null | head -1)
if [ -n "$existing_id" ] && [ "$existing_id" != "null" ]; then
echo "Samama office already exists: officeId=$existing_id (externalId=$SAMAMA_EXTERNAL_ID)" >&2
SAMAMA_OFFICE_ID="$existing_id"
else
payload=$(jq -n \
--arg name "$SAMAMA_OFFICE_NAME" \
--arg openingDate "$OPENING_DATE" \
--arg externalId "$SAMAMA_EXTERNAL_ID" \
'{ name: $name, parentId: 1, openingDate: $openingDate, externalId: $externalId, dateFormat: "yyyy-MM-dd", locale: "en" }')
if [ "$DRY_RUN" = "1" ]; then
echo "DRY_RUN: would POST /offices with name=$SAMAMA_OFFICE_NAME externalId=$SAMAMA_EXTERNAL_ID openingDate=$OPENING_DATE" >&2
echo "Payload: $payload" >&2
exit 0
fi
res=$(curl "${CURL_OPTS[@]}" -X POST -d "$payload" "${BASE_URL}/offices" 2>/dev/null) || true
if echo "$res" | jq -e '.resourceId // .officeId' >/dev/null 2>&1; then
SAMAMA_OFFICE_ID=$(echo "$res" | jq -r '.resourceId // .officeId')
echo "Created Samama office: officeId=$SAMAMA_OFFICE_ID" >&2
else
echo "Failed to create office: $res" >&2
exit 1
fi
fi
if [ "$SKIP_TRANSFER" = "1" ]; then
echo "SKIP_TRANSFER=1: not posting 5B M1. Run with JOURNAL_MATRIX=.../omnl-journal-matrix-samama-5b.json and set OFFICE_ID_SAMAMA=$SAMAMA_OFFICE_ID, or run this script without SKIP_TRANSFER." >&2
echo "OFFICE_ID_SAMAMA=$SAMAMA_OFFICE_ID"
exit 0
fi
# Post 5B M1 transfer: HO leg (officeId=1) + Office leg (officeId=SAMAMA_OFFICE_ID)
# GL: 2100 (M1), 2410 (Due To), 1410 (Due From). Same pattern as T-004 (TAJ).
CURL_OPTS_HTTP=(-s -S -w "\n%{http_code}" -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
GL_ACCOUNTS=$(curl "${CURL_OPTS_HTTP[@]}" "${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
}
ID_2100=$(get_gl_id "2100")
ID_2410=$(get_gl_id "2410")
ID_1410=$(get_gl_id "1410")
TRANSACTION_DATE="${TRANSACTION_DATE:-$(date +%Y-%m-%d)}"
if [ -z "$ID_2100" ] || [ -z "$ID_2410" ] || [ -z "$ID_1410" ]; then
echo "Missing GL accounts (2100, 2410, 1410). Create them first (see omnl-gl-accounts-create.sh and Phase C interoffice docs)." >&2
exit 1
fi
post_entry() {
local office_id="$1"
local debit_id="$2"
local credit_id="$3"
local amount="$4"
local comments="$5"
local body
local ref="${6:-}"
[ -z "$ref" ] && ref="OMNL-JE-${office_id}-${TRANSACTION_DATE//-/}-0"
body=$(jq -n \
--argjson officeId "$office_id" \
--arg transactionDate "$TRANSACTION_DATE" \
--arg comments "$comments" \
--arg referenceNumber "$ref" \
--arg dateFormat "yyyy-MM-dd" \
--arg locale "en" \
--arg currencyCode "USD" \
--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 [ "$DRY_RUN" = "1" ]; then
echo "DRY_RUN: $comments (officeId=$office_id amount=$amount)" >&2
return 0
fi
local out
out=$(curl "${CURL_OPTS_HTTP[@]}" -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
}
AMOUNT_5B=5000000000
DATE_REF="${TRANSACTION_DATE//-/}"
post_entry 1 "$ID_2100" "$ID_2410" "$AMOUNT_5B" "T-Samama-HO — Due To Samama Group LLC — M1 5B" "SAMAMA-20-${DATE_REF}-HO"
post_entry "$SAMAMA_OFFICE_ID" "$ID_1410" "$ID_2100" "$AMOUNT_5B" "T-Samama-OF — Due From HO — Samama Group LLC M1 5B" "SAMAMA-20-${DATE_REF}-OF"
echo "Done: Samama officeId=$SAMAMA_OFFICE_ID; 5B USD M1 transfer posted." >&2
echo "OFFICE_ID_SAMAMA=$SAMAMA_OFFICE_ID"

View File

@@ -0,0 +1,139 @@
#!/usr/bin/env bash
# Security test: OMNL-2 (office 2) user must not access other offices' data or achieve
# path traversal / command injection. See docs/04-configuration/mifos-omnl-central-bank/OMNL_OFFICE_2_ACCESS_SECURITY_TEST.md
# Set STRICT_OFFICE_LIST=1 to fail when GET /offices returns other offices or GET /offices/20 returns 200.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
if [ -f "${REPO_ROOT}/omnl-fineract/.env" ]; then set +u; source "${REPO_ROOT}/omnl-fineract/.env" 2>/dev/null || true; set -u; fi
BASE_URL="${OMNL_FINERACT_BASE_URL:-}"
TENANT="${OMNL_FINERACT_TENANT:-omnl}"
# Office-2 user (do NOT use app.omnl admin)
OFFICE2_USER="${OMNL_OFFICE2_TEST_USER:-shamrayan.admin}"
OFFICE2_PASS="${OMNL_OFFICE2_TEST_PASSWORD:-${OMNL_SHAMRAYAN_ADMIN_PASSWORD:-}}"
STRICT="${STRICT_OFFICE_LIST:-0}"
FAILED=0
if [ -z "$BASE_URL" ] || [ -z "$OFFICE2_PASS" ]; then
echo "Set OMNL_FINERACT_BASE_URL and either OMNL_OFFICE2_TEST_PASSWORD or OMNL_SHAMRAYAN_ADMIN_PASSWORD (office-2 user only)." >&2
exit 2
fi
CURL_OFFICE2=(-s -S -w "\n%{http_code}" -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${OFFICE2_USER}:${OFFICE2_PASS}")
echo "=== OMNL-2 access security test ==="
echo "Base URL: $BASE_URL"
echo "User: $OFFICE2_USER"
echo ""
# --- 1. Data isolation: GET /offices; office 2 must be present ---
echo "[1] Data isolation: GET /offices (office 2 must be visible)..."
OFFICES_RESP=$(curl "${CURL_OFFICE2[@]}" "${BASE_URL}/offices" 2>/dev/null)
OFFICES_BODY=$(echo "$OFFICES_RESP" | sed '$d')
OFFICES_CODE=$(echo "$OFFICES_RESP" | tail -n1)
if [ "$OFFICES_CODE" = "401" ]; then
echo " ERROR: Invalid or missing credentials (HTTP 401). Set office-2 user and password." >&2
exit 2
fi
if [ "$OFFICES_CODE" != "200" ]; then
echo " FAIL: GET /offices returned HTTP $OFFICES_CODE" >&2
FAILED=1
else
OFFICE_IDS=$(echo "$OFFICES_BODY" | jq -r '.[].id // empty' 2>/dev/null || true)
HAS_OFFICE2=""
for id in $OFFICE_IDS; do
[ "$id" = "2" ] && HAS_OFFICE2=1
done
if [ -z "$HAS_OFFICE2" ]; then
echo " FAIL: Office 2 not in GET /offices response" >&2
FAILED=1
else
BAD_IDS=""
for id in $OFFICE_IDS; do
if [ "$id" != "1" ] && [ "$id" != "2" ]; then
BAD_IDS="${BAD_IDS} ${id}"
fi
done
if [ -n "$BAD_IDS" ] && [ "$STRICT" = "1" ]; then
echo " FAIL: Strict mode — office-2 user sees other offices:${BAD_IDS}" >&2
FAILED=1
elif [ -n "$BAD_IDS" ]; then
echo " OK: Office 2 visible (other offices also listed:${BAD_IDS}; set STRICT_OFFICE_LIST=1 to fail)"
else
echo " OK: Only offices 1 and 2 visible"
fi
fi
fi
# --- 2. Data isolation: GET /offices/20 (strict: must not return 200 with office 20) ---
echo "[2] Data isolation: GET /offices/20..."
OFF20_RESP=$(curl "${CURL_OFFICE2[@]}" "${BASE_URL}/offices/20" 2>/dev/null)
OFF20_CODE=$(echo "$OFF20_RESP" | tail -n1)
if [ "$OFF20_CODE" = "200" ]; then
OFF20_BODY=$(echo "$OFF20_RESP" | sed '$d')
if echo "$OFF20_BODY" | jq -e '.id == 20' >/dev/null 2>&1; then
if [ "$STRICT" = "1" ]; then
echo " FAIL: Strict mode — office-2 user can read office 20 by ID" >&2
FAILED=1
else
echo " OK: 200 with office 20 (set STRICT_OFFICE_LIST=1 to fail)"
fi
else
echo " OK: 200 but no office 20 data"
fi
else
echo " OK: HTTP $OFF20_CODE (access denied or not found)"
fi
# --- 2b. Data isolation: GET /clients?officeId=20 must not return other offices' clients ---
echo "[3] Data isolation: GET /clients?officeId=20 (must be 403 or empty)..."
CLIENTS_RESP=$(curl "${CURL_OFFICE2[@]}" "${BASE_URL}/clients?officeId=20" 2>/dev/null)
CLIENTS_CODE=$(echo "$CLIENTS_RESP" | tail -n1)
CLIENTS_BODY=$(echo "$CLIENTS_RESP" | sed '$d')
if [ "$CLIENTS_CODE" = "200" ]; then
# pageItems or top-level array
COUNT=$(echo "$CLIENTS_BODY" | jq -r 'if .pageItems then (.pageItems | length) else (if type == "array" then length else 0 end) end' 2>/dev/null || echo "0")
case "${COUNT:-0}" in
''|null) COUNT=0 ;;
esac
if [ "${COUNT:-0}" -gt 0 ] 2>/dev/null; then
echo " FAIL: Office-2 user can list clients for office 20 (count=$COUNT)" >&2
FAILED=1
else
echo " OK: No clients for office 20 returned"
fi
else
echo " OK: HTTP $CLIENTS_CODE (access denied or no data)"
fi
# --- 4. Command injection: response must not contain actual file/command output ---
echo "[4] Command injection: GET response (must not contain file/command output)..."
INJECT_RESP=$(curl "${CURL_OFFICE2[@]}" "${BASE_URL}/offices?locale=en" 2>/dev/null)
INJECT_BODY=$(echo "$INJECT_RESP" | sed '$d')
if echo "$INJECT_BODY" | grep -qE 'root:.*:0:0:|uid=[0-9]+\(.*\)\s+gid='; then
echo " FAIL: Response may contain command output or file content" >&2
FAILED=1
else
echo " OK: No command/file output in response"
fi
# --- 5. Path traversal: must not return server file content ---
echo "[5] Path traversal: GET with path-like param (must not return file content)..."
TRAVERSE_RESP=$(curl "${CURL_OFFICE2[@]}" -G --data-urlencode "dateFormat=../../../etc/passwd" "${BASE_URL}/offices" 2>/dev/null)
TRAVERSE_BODY=$(echo "$TRAVERSE_RESP" | sed '$d')
if echo "$TRAVERSE_BODY" | grep -qE 'root:.*:0:0:'; then
echo " FAIL: Response may contain file content (path traversal)" >&2
FAILED=1
else
echo " OK: No file content in response"
fi
echo ""
if [ $FAILED -eq 0 ]; then
echo "All OMNL-2 access security checks passed."
exit 0
else
echo "One or more checks FAILED. Do not treat office-2 access as safe until resolved." >&2
exit 1
fi

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# OMNL Fineract — Populate the 15 operating entities as Offices (Organization / Manage Offices).
# Updates office 1 name to entity 1; creates offices 215 as children of office 1 with entity names.
# Usage: run from repo root; sources omnl-fineract/.env or .env.
# ENTITY_DATA=<path> JSON entity data (default: docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json)
# DRY_RUN=1 print only, do not PUT/POST.
# OPENING_DATE yyyy-MM-dd for new offices (default: 2026-01-01)
# Requires: curl, jq.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
DRY_RUN="${DRY_RUN:-0}"
ENTITY_DATA="${ENTITY_DATA:-${REPO_ROOT}/docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json}"
OPENING_DATE="${OPENING_DATE:-2026-01-01}"
if [ ! -f "$ENTITY_DATA" ]; then
echo "Entity data file not found: $ENTITY_DATA" >&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. in omnl-fineract/.env)" >&2
exit 1
fi
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
offices_json=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/offices")
# Response is array of offices
if ! echo "$offices_json" | jq -e 'type == "array"' >/dev/null 2>&1; then
echo "Unexpected offices response." >&2
exit 1
fi
# 1. Update office 1 name to entity 1
entity1_name=$(jq -r '.entities[] | select(.clientNumber == 1) | .entityName' "$ENTITY_DATA")
if [ -z "$entity1_name" ] || [ "$entity1_name" = "null" ]; then
echo "Entity 1 name not found in $ENTITY_DATA" >&2
exit 1
fi
echo "=== Office 1 (Head Office) ===" >&2
payload1=$(jq -n \
--arg name "$entity1_name" \
--arg openingDate "$OPENING_DATE" \
'{ name: $name, openingDate: $openingDate, dateFormat: "yyyy-MM-dd", locale: "en" }')
if [ "$DRY_RUN" = "1" ]; then
echo " [DRY RUN] PUT offices/1 name=$entity1_name" >&2
else
res=$(curl "${CURL_OPTS[@]}" -X PUT -d "$payload1" "${BASE_URL}/offices/1" 2>/dev/null) || true
if echo "$res" | jq -e '.resourceId // .officeId' >/dev/null 2>&1; then
echo " Updated office 1 name to: $entity1_name" >&2
else
echo " PUT office 1 response: $res" >&2
fi
fi
# 2. Create offices 2N as children of office 1 (N = max clientNumber in entity data)
created=0
skipped=0
for num in $(jq -r '.entities[] | select(.clientNumber >= 2) | .clientNumber' "$ENTITY_DATA" 2>/dev/null | sort -n | uniq); do
entity_name=$(jq -r --argjson n "$num" '.entities[] | select(.clientNumber == $n) | .entityName' "$ENTITY_DATA")
if [ -z "$entity_name" ] || [ "$entity_name" = "null" ]; then
echo "Skip: no entity $num in data" >&2
continue
fi
ext_id="OMNL-$num"
existing=$(echo "$offices_json" | jq -r --arg e "$ext_id" '.[] | select(.externalId == $e) | .id' 2>/dev/null | head -1)
if [ -n "$existing" ] && [ "$existing" != "null" ]; then
echo "Skip office $num: already exists (externalId=$ext_id, id=$existing)" >&2
((skipped++)) || true
continue
fi
payload=$(jq -n \
--arg name "$entity_name" \
--arg openingDate "$OPENING_DATE" \
--arg externalId "$ext_id" \
'{ name: $name, parentId: 1, openingDate: $openingDate, externalId: $externalId, dateFormat: "yyyy-MM-dd", locale: "en" }')
echo "Create office $num: $entity_name (externalId=$ext_id)" >&2
if [ "$DRY_RUN" = "1" ]; then
echo " [DRY RUN] POST offices" >&2
((created++)) || true
continue
fi
res=$(curl "${CURL_OPTS[@]}" -X POST -d "$payload" "${BASE_URL}/offices" 2>/dev/null) || true
if echo "$res" | jq -e '.resourceId // .officeId' >/dev/null 2>&1; then
new_id=$(echo "$res" | jq -r '.resourceId // .officeId')
echo " Created officeId=$new_id" >&2
((created++)) || true
else
echo " Failed: $res" >&2
fi
done
echo "Done: office 1 updated, $created new offices created, $skipped skipped (already existed)." >&2

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env bash
# OMNL Fineract — Single operator rail: resolve IDs, post closures (if missing), verify, reconciliation snapshot, print safe A/B/C templates.
# Usage: from repo root. SKIP_CLOSURES=1 or SKIP_RECON=1 to skip those steps. See OPERATING_RAILS.md.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
SKIP_CLOSURES="${SKIP_CLOSURES:-0}"
SKIP_RECON="${SKIP_RECON:-0}"
CLOSING_DATE="${CLOSING_DATE:-2026-02-24}"
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 -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
echo "=== 1) Resolve IDs ===" >&2
bash "${REPO_ROOT}/scripts/omnl/resolve_ids.sh"
# ids.env is in current dir (repo root when run from there)
if [ -f "${REPO_ROOT}/ids.env" ]; then
set +u
source "${REPO_ROOT}/ids.env"
set -u
elif [ -f "ids.env" ]; then
set +u
source "ids.env"
set -u
else
echo "ids.env not found; run resolve_ids.sh from repo root" >&2
exit 1
fi
echo "=== 2) GL closures (Office 20 + HO) ===" >&2
if [ "$SKIP_CLOSURES" = "1" ]; then
echo "SKIP_CLOSURES=1: skipping" >&2
else
CLOSING_DATE="$CLOSING_DATE" bash "${REPO_ROOT}/scripts/omnl/omnl-gl-closures-post.sh" || true
fi
echo "=== 3) Verification ===" >&2
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
offices=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/offices" 2>/dev/null)
o20=$(echo "$offices" | jq -r '.[] | select(.id == 20) | .name' 2>/dev/null)
if [ -n "$o20" ]; then
echo " Office 20: $o20" >&2
else
echo " Office 20: not found (create via omnl-office-create-samama.sh)" >&2
fi
closures=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/glclosures" 2>/dev/null)
c20=$(echo "$closures" | jq -r '(if type == "array" then . else (.pageItems // .) end) | map(select(.officeId == 20 or .office.id == 20)) | length' 2>/dev/null)
echo " Closures for office 20: $c20" >&2
echo "=== 4) Reconciliation snapshot (Office 20) ===" >&2
if [ "$SKIP_RECON" = "1" ]; then
echo "SKIP_RECON=1: skipping" >&2
else
bash "${REPO_ROOT}/scripts/omnl/omnl-reconciliation-office20.sh" || true
fi
echo "=== 5) A/B/C readiness ===" >&2
savings=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/savingsproducts" 2>/dev/null)
savings_count=$(echo "$savings" | jq 'if type == "array" then length else (.pageItems // []) | length end' 2>/dev/null || echo "0")
loans=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/loanproducts" 2>/dev/null)
loans_count=$(echo "$loans" | jq 'if type == "array" then length else (.pageItems // []) | length end' 2>/dev/null || echo "0")
echo " Savings products: $savings_count (Path B ready if > 0 and payment type set)" >&2
echo " Loan products: $loans_count (Path C ready if > 0)" >&2
path_b_ready=0
path_c_ready=0
[ -n "${PAYMENT_TYPE_ID:-}" ] && [ "${PAYMENT_TYPE_ID:-0}" -gt 0 ] 2>/dev/null && [ "${savings_count:-0}" -gt 0 ] 2>/dev/null && path_b_ready=1
[ "${loans_count:-0}" -gt 0 ] 2>/dev/null && path_c_ready=1
echo "=== 6) Safe templates (use after: source ids.env) ===" >&2
echo ""
echo "# --- Path A: Treasury transfer out from Office 20 (Dr 2100, Cr 1410) ---"
echo "# POST /journalentries (officeId=20, unique referenceNumber required)"
echo "curl -s -X POST -H \"Fineract-Platform-TenantId: \$TENANT\" -H \"Content-Type: application/json\" -u \"\$USER:\$PASS\" \\"
echo " -d '{ \"officeId\": 20, \"transactionDate\": \"$(date +%Y-%m-%d)\", \"dateFormat\": \"yyyy-MM-dd\", \"locale\": \"en\", \"currencyCode\": \"USD\", \"referenceNumber\": \"SAMAMA-20-$(date +%Y%m%d)-001\", \"comments\": \"Treasury transfer from Office 20\", \"debits\": [ { \"glAccountId\": '\$ID_2100', \"amount\": AMOUNT } ], \"credits\": [ { \"glAccountId\": '\$ID_1410', \"amount\": AMOUNT } ] }' \\"
echo " \"\$BASE_URL/journalentries\""
echo ""
if [ "$path_b_ready" = "1" ]; then
echo "# --- Path B: Deposit to savings (replace SAVINGS_ACCOUNT_ID) ---"
echo "# POST /savingsaccounts/<ID>/transactions?command=deposit"
echo "curl -s -X POST -H \"Fineract-Platform-TenantId: \$TENANT\" -H \"Content-Type: application/json\" -u \"\$USER:\$PASS\" \\"
echo " -d \"{ \\\"transactionDate\\\": \\\"$(date +%Y-%m-%d)\\\", \\\"transactionAmount\\\": AMOUNT, \\\"paymentTypeId\\\": \$PAYMENT_TYPE_ID, \\\"note\\\": \\\"Allocation from Samama Office\\\", \\\"dateFormat\\\": \\\"yyyy-MM-dd\\\", \\\"locale\\\": \\\"en\\\" }\" \\"
echo " \"\$BASE_URL/savingsaccounts/SAVINGS_ACCOUNT_ID/transactions?command=deposit\""
else
echo "# --- Path B: NOT READY — need payment type and at least one savings product with accounting enabled" >&2
fi
echo ""
if [ "$path_c_ready" = "1" ]; then
echo "# --- Path C: Loan disburse (replace LOAN_ID) ---"
echo "# POST /loans/<ID>?command=disburse"
echo "curl -s -X POST -H \"Fineract-Platform-TenantId: \$TENANT\" -H \"Content-Type: application/json\" -u \"\$USER:\$PASS\" \\"
echo " -d \"{ \\\"actualDisbursementDate\\\": \\\"$(date +%Y-%m-%d)\\\", \\\"transactionAmount\\\": AMOUNT, \\\"paymentTypeId\\\": \$PAYMENT_TYPE_ID, \\\"dateFormat\\\": \\\"yyyy-MM-dd\\\", \\\"locale\\\": \\\"en\\\" }\" \\"
echo " \"\$BASE_URL/loans/LOAN_ID?command=disburse\""
else
echo "# --- Path C: NOT READY — need at least one loan product with accounting enabled" >&2
fi
echo ""
echo "Resolved IDs: ID_1410=$ID_1410 ID_2100=$ID_2100 ID_2410=$ID_2410 PAYMENT_TYPE_ID=${PAYMENT_TYPE_ID:-n/a}"
echo ""
echo "Audit packet (recommended): bash scripts/omnl/omnl-audit-packet-office20.sh" >&2

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# OMNL Fineract — Minimum viable reconciliation snapshot for Office 20 (Samama): offices, GL balances, and optional trial balance.
# Writes a timestamped file with hash and metadata. Run daily (or after material postings).
# Usage: from repo root. OUT_DIR=./reconciliation (default). See OPERATING_RAILS.md.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
OUT_DIR="${OUT_DIR:-${REPO_ROOT}/reconciliation}"
OFFICE_ID="${OFFICE_ID:-20}"
TIMESTAMP="${TIMESTAMP:-$(date -u +%Y%m%d-%H%M%S)}"
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 "$OUT_DIR"
REPORT_FILE="${OUT_DIR}/office${OFFICE_ID}-${TIMESTAMP}.json"
META_FILE="${OUT_DIR}/office${OFFICE_ID}-${TIMESTAMP}.meta"
# 1) Office 20 (and HO) summary
offices=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/offices" 2>/dev/null)
echo "$offices" | jq --argjson o "$OFFICE_ID" '[.[] | select(.id == $o or .id == 1)]' > "${REPORT_FILE}.offices" 2>/dev/null || echo "$offices" > "${REPORT_FILE}.offices"
# 2) GL accounts (1410, 2100, 2410) — balances if API returns them; otherwise list
glaccounts=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/glaccounts" 2>/dev/null)
echo "$glaccounts" | jq '[.[] | select(.glCode == "1410" or .glCode == "2100" or .glCode == "2410") | {glCode, id, name, organizationRunningBalance: (.organizationRunningBalance // "n/a")}]' > "${REPORT_FILE}.gl" 2>/dev/null || echo "$glaccounts" > "${REPORT_FILE}.gl"
# 3) Trial balance for office (parameter names vary by deployment)
tb_params="R_officeId=${OFFICE_ID}"
curl "${CURL_OPTS[@]}" "${BASE_URL}/runreports/Trial%20Balance?${tb_params}&output-type=json" 2>/dev/null > "${REPORT_FILE}.trialbalance" || true
# 4) Combined snapshot (for hash and audit)
jq -n \
--arg ts "$(date -u -Iseconds)" \
--argjson oid "$OFFICE_ID" \
--arg op "${OPERATOR_ID:-manual}" \
--slurpfile ofc "${REPORT_FILE}.offices" \
--slurpfile gl "${REPORT_FILE}.gl" \
'{ timestamp: $ts, officeId: $oid, operator: $op, offices: ($ofc[0] // []), glRelevant: ($gl[0] // []) }' \
> "$REPORT_FILE" 2>/dev/null || echo "{\"timestamp\": \"$(date -u -Iseconds)\", \"officeId\": $OFFICE_ID}" > "$REPORT_FILE"
# 5) Hash and metadata
HASH=$(sha256sum "$REPORT_FILE" 2>/dev/null | awk '{print $1}')
echo "timestamp=$TIMESTAMP
file=$REPORT_FILE
sha256=$HASH
officeId=$OFFICE_ID" > "$META_FILE"
echo "Snapshot: $REPORT_FILE (sha256=$HASH)" >&2
echo "Meta: $META_FILE" >&2
echo "Office 20 acceptance: 1410 net Dr 5,000,000,000; 2100 net Cr 5,000,000,000 (verify in UI or trial balance)." >&2

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Create Staff for office 2 (Shamrayan) and User with full admin to that office only.
# Usage: set OMNL_SHAMRAYAN_ADMIN_PASSWORD and run from repo root with omnl-fineract/.env.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
OFFICE_ID_SHAMRAYAN=2
USERNAME="shamrayan.admin"
STAFF_FIRSTNAME="Shamrayan"
STAFF_LASTNAME="Office Admin"
if [ -f "${REPO_ROOT}/omnl-fineract/.env" ]; then set +u; source "${REPO_ROOT}/omnl-fineract/.env" 2>/dev/null || true; set -u; fi
BASE_URL="${OMNL_FINERACT_BASE_URL:-}"
TENANT="${OMNL_FINERACT_TENANT:-omnl}"
ADMIN_USER="${OMNL_FINERACT_USER:-app.omnl}"
ADMIN_PASS="${OMNL_FINERACT_PASSWORD:-}"
SHAMRAYAN_PASS="${OMNL_SHAMRAYAN_ADMIN_PASSWORD:-}"
[ -z "$BASE_URL" ] || [ -z "$ADMIN_PASS" ] && { echo "Set OMNL_FINERACT_BASE_URL and OMNL_FINERACT_PASSWORD" >&2; exit 1; }
[ -z "$SHAMRAYAN_PASS" ] && { echo "Set OMNL_SHAMRAYAN_ADMIN_PASSWORD" >&2; exit 1; }
CURL_OPTS=(-s -S -w "\n%{http_code}" -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${ADMIN_USER}:${ADMIN_PASS}")
# Use existing staff for office 2 if any; otherwise create
EXISTING_STAFF=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/staff?officeId=${OFFICE_ID_SHAMRAYAN}" 2>/dev/null | sed '$d')
STAFF_ID=$(echo "$EXISTING_STAFF" | jq -r 'if type == "array" then (.[0].id // empty) else empty end' 2>/dev/null)
if [ -n "$STAFF_ID" ]; then
echo "Using existing staff id=$STAFF_ID for office $OFFICE_ID_SHAMRAYAN" >&2
else
JOINING_DATE="${JOINING_DATE:-$(date +%Y-%m-%d)}"
STAFF_JSON=$(jq -n --argjson officeId "$OFFICE_ID_SHAMRAYAN" --arg fn "$STAFF_FIRSTNAME" --arg ln "$STAFF_LASTNAME" --arg jd "$JOINING_DATE" '{ officeId: $officeId, firstname: $fn, lastname: $ln, joiningDate: $jd, dateFormat: "yyyy-MM-dd", locale: "en", isActive: true }')
STAFF_OUT=$(curl "${CURL_OPTS[@]}" -X POST -d "$STAFF_JSON" "${BASE_URL}/staff" 2>/dev/null)
STAFF_CODE=$(echo "$STAFF_OUT" | tail -n1)
STAFF_RESP=$(echo "$STAFF_OUT" | sed '$d')
[ "$STAFF_CODE" = "200" ] || [ "${STAFF_CODE:0:1}" = "2" ] || { echo "Staff failed $STAFF_CODE: $STAFF_RESP" >&2; exit 1; }
STAFF_ID=$(echo "$STAFF_RESP" | jq -r '.resourceId // empty')
[ -n "$STAFF_ID" ] || { echo "No staff resourceId" >&2; exit 1; }
echo "Staff created id=$STAFF_ID" >&2
fi
ROLES_JSON=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/roles" 2>/dev/null | sed '$d')
ROLE_ID=$(echo "$ROLES_JSON" | jq -r '(.[] | select(.name == "Office Admin") | .id) // (.[] | select(.name != "Super user" and .name != "System") | .id) // .[0].id // 2' 2>/dev/null | head -n1)
ROLE_ID=${ROLE_ID:-3}
USER_JSON=$(jq -n --arg u "$USERNAME" --arg p "$SHAMRAYAN_PASS" --argjson sid "$STAFF_ID" --argjson oid "$OFFICE_ID_SHAMRAYAN" --arg fn "$STAFF_FIRSTNAME" --arg ln "$STAFF_LASTNAME" --argjson roleId "$ROLE_ID" '{ username: $u, password: $p, repeatPassword: $p, staffId: $sid, officeId: $oid, firstname: $fn, lastname: $ln, roles: [$roleId], passwordNeverExpires: true }')
USER_OUT=$(curl "${CURL_OPTS[@]}" -X POST -d "$USER_JSON" "${BASE_URL}/users" 2>/dev/null)
USER_CODE=$(echo "$USER_OUT" | tail -n1)
[ "$USER_CODE" = "200" ] || [ "${USER_CODE:0:1}" = "2" ] || { echo "User failed $USER_CODE: $(echo "$USER_OUT" | sed '$d')" >&2; exit 1; }
echo "User $USERNAME created for office $OFFICE_ID_SHAMRAYAN only" >&2

85
scripts/omnl/resolve_ids.sh Executable file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# OMNL Fineract — Resolve GL account IDs (1410, 2100, 2410) and payment type ID; write ids.env for downstream scripts.
# Usage: from repo root, source omnl-fineract/.env then run: bash scripts/omnl/resolve_ids.sh
# Output: ids.env in current directory (export ID_1410, ID_2100, ID_2410, PAYMENT_TYPE_ID). Exits non-zero if any missing.
# See docs/04-configuration/mifos-omnl-central-bank/OPERATING_RAILS.md
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
OUT_FILE="${OMNL_IDS_FILE:-ids.env}"
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
: "${OMNL_FINERACT_BASE_URL:?Set OMNL_FINERACT_BASE_URL (e.g. in omnl-fineract/.env)}"
: "${OMNL_FINERACT_TENANT:=omnl}"
: "${OMNL_FINERACT_USER:=app.omnl}"
: "${OMNL_FINERACT_PASSWORD:?Set OMNL_FINERACT_PASSWORD}"
BASE="${OMNL_FINERACT_BASE_URL}"
TENANT="${OMNL_FINERACT_TENANT}"
AUTH="-u ${OMNL_FINERACT_USER}:${OMNL_FINERACT_PASSWORD}"
HDR=(-H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json")
GL_JSON="$(curl -s "${HDR[@]}" $AUTH "$BASE/glaccounts")"
# Normalize to array (Fineract may return { pageItems: [...] } or [...])
GL_JSON="$(echo "$GL_JSON" | jq -c 'if type == "array" then . else (.pageItems // .) end' 2>/dev/null || echo "$GL_JSON")"
PT_JSON="$(curl -s "${HDR[@]}" $AUTH "$BASE/paymenttypes")"
PT_JSON="$(echo "$PT_JSON" | jq -c 'if type == "array" then . else (.pageItems // []) end' 2>/dev/null || echo "[]")"
id_for_glcode() {
local code="$1"
echo "$GL_JSON" | jq -r --arg c "$code" '.[]? | select(.glCode == $c) | .id' 2>/dev/null | head -n1
}
ID_1410="$(id_for_glcode "1410")"
ID_2100="$(id_for_glcode "2100")"
ID_2410="$(id_for_glcode "2410")"
# Prefer: onledger/cash/bank transfer; fallback first payment type
PAYMENT_TYPE_ID="$(
echo "$PT_JSON" | jq -r '
(.[]? | select((.code // "") | ascii_downcase == "onledger") | .id) // empty
' 2>/dev/null | head -n1
)"
if [ -z "$PAYMENT_TYPE_ID" ] || [ "$PAYMENT_TYPE_ID" = "null" ]; then
PAYMENT_TYPE_ID="$(
echo "$PT_JSON" | jq -r '
(.[]? | select((.name // "") | ascii_downcase | test("cash|bank|wire|transfer")) | .id) // empty
' 2>/dev/null | head -n1
)"
fi
if [ -z "$PAYMENT_TYPE_ID" ] || [ "$PAYMENT_TYPE_ID" = "null" ]; then
PAYMENT_TYPE_ID="$(echo "$PT_JSON" | jq -r '.[0].id // empty' 2>/dev/null)"
fi
for v in ID_1410 ID_2100 ID_2410; do
val="${!v}"
if [ -z "$val" ] || [ "$val" = "null" ]; then
echo "Missing required id: $v (create GL accounts first, e.g. omnl-gl-accounts-create.sh)" >&2
exit 1
fi
done
if [ -z "$PAYMENT_TYPE_ID" ] || [ "$PAYMENT_TYPE_ID" = "null" ]; then
echo "Warning: no payment type found; PAYMENT_TYPE_ID will be empty (deposits/disbursements may need it)" >&2
PAYMENT_TYPE_ID=""
fi
cat > "$OUT_FILE" <<EOF
# OMNL Fineract resolved IDs — do not commit if it contains env-specific values
# Generated by scripts/omnl/resolve_ids.sh
export ID_1410=$ID_1410
export ID_2100=$ID_2100
export ID_2410=$ID_2410
export PAYMENT_TYPE_ID=$PAYMENT_TYPE_ID
EOF
echo "Wrote $OUT_FILE:" >&2
cat "$OUT_FILE" >&2

40
scripts/omnl/validate-rail.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# OMNL operator rail CI: .gitignore check, shellcheck on scripts (if available), resolve_ids parse check.
# Usage: from repo root. Exit 0 if all pass.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
cd "$REPO_ROOT"
fail=0
# 1) .gitignore must include ids.env and reconciliation/
if ! grep -q 'ids\.env' .gitignore 2>/dev/null; then
echo "FAIL: .gitignore missing ids.env" >&2
fail=1
fi
if ! grep -q 'reconciliation' .gitignore 2>/dev/null; then
echo "FAIL: .gitignore missing reconciliation/" >&2
fail=1
fi
[ $fail -eq 0 ] && echo "PASS: .gitignore has ids.env and reconciliation/" >&2
# 2) resolve_ids.sh must handle both array and pageItems (grep for pattern)
if ! grep -q 'pageItems' scripts/omnl/resolve_ids.sh 2>/dev/null; then
echo "WARN: resolve_ids.sh may not handle pageItems response" >&2
fi
if ! grep -q 'if type == "array"' scripts/omnl/resolve_ids.sh 2>/dev/null; then
echo "WARN: resolve_ids.sh may not normalize array" >&2
fi
# 3) Shellcheck (optional)
if command -v shellcheck >/dev/null 2>&1; then
for f in scripts/omnl/*.sh; do
[ -f "$f" ] && shellcheck -x "$f" 2>/dev/null || true
done
echo "PASS: shellcheck completed" >&2
else
echo "SKIP: shellcheck not installed" >&2
fi
exit $fail