chore: sync all changes to Gitea
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
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:
@@ -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-002A–T-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-001–T-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 9–15 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 2–15 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 1–15). 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 2–15)
|
||||
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 (1–15) 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 9–15 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-001–T-008)
|
||||
# 2. Post ledger entries (T-001–T-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 9–15 (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).
|
||||
|
||||
128
scripts/omnl/create-office-and-fund.sh
Executable file
128
scripts/omnl/create-office-and-fund.sh
Executable 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
|
||||
268
scripts/omnl/office2-5b-full-execution.sh
Executable file
268
scripts/omnl/office2-5b-full-execution.sh
Executable 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"
|
||||
177
scripts/omnl/office2-shamrayan-dryrun.sh
Executable file
177
scripts/omnl/office2-shamrayan-dryrun.sh
Executable 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 "---"
|
||||
128
scripts/omnl/omnl-audit-packet-office20.sh
Executable file
128
scripts/omnl/omnl-audit-packet-office20.sh
Executable 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
|
||||
29
scripts/omnl/omnl-baseline-create.sh
Executable file
29
scripts/omnl/omnl-baseline-create.sh
Executable 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"
|
||||
121
scripts/omnl/omnl-client-names-fix.sh
Normal file
121
scripts/omnl/omnl-client-names-fix.sh
Normal 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 2–15 = operating entities (by account no 000000001–000000015)
|
||||
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
|
||||
117
scripts/omnl/omnl-clients-create-9-15.sh
Normal file
117
scripts/omnl/omnl-clients-create-9-15.sh
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
# OMNL Fineract — Create clients 9–15 (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
|
||||
64
scripts/omnl/omnl-clients-remove-15.sh
Normal file
64
scripts/omnl/omnl-clients-remove-15.sh
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# OMNL Fineract — Remove the 15 clients (ids 1–15) 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
|
||||
34
scripts/omnl/omnl-config-hash.sh
Executable file
34
scripts/omnl/omnl-config-hash.sh
Executable 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)\"}"
|
||||
183
scripts/omnl/omnl-entity-data-apply.sh
Normal file
183
scripts/omnl/omnl-entity-data-apply.sh
Normal 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
|
||||
@@ -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-001–T-008."
|
||||
|
||||
134
scripts/omnl/omnl-gl-accounts-fx-gru-create.sh
Normal file
134
scripts/omnl/omnl-gl-accounts-fx-gru-create.sh
Normal 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."
|
||||
85
scripts/omnl/omnl-gl-closures-post.sh
Executable file
85
scripts/omnl/omnl-gl-closures-post.sh
Executable 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
85
scripts/omnl/omnl-je-checker.sh
Executable 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
56
scripts/omnl/omnl-je-maker.sh
Executable 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
|
||||
90
scripts/omnl/omnl-je-reverse-by-reference.sh
Executable file
90
scripts/omnl/omnl-je-reverse-by-reference.sh
Executable 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
|
||||
179
scripts/omnl/omnl-ledger-post-from-matrix.sh
Normal file
179
scripts/omnl/omnl-ledger-post-from-matrix.sh
Normal 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
|
||||
45
scripts/omnl/omnl-monitor-office20-movement.sh
Executable file
45
scripts/omnl/omnl-monitor-office20-movement.sh
Executable 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
|
||||
75
scripts/omnl/omnl-office-create-pelican.sh
Normal file
75
scripts/omnl/omnl-office-create-pelican.sh
Normal 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
|
||||
135
scripts/omnl/omnl-office-create-samama.sh
Executable file
135
scripts/omnl/omnl-office-create-samama.sh
Executable 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"
|
||||
139
scripts/omnl/omnl-office2-access-security-test.sh
Executable file
139
scripts/omnl/omnl-office2-access-security-test.sh
Executable 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
|
||||
113
scripts/omnl/omnl-offices-populate-15.sh
Normal file
113
scripts/omnl/omnl-offices-populate-15.sh
Normal 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 2–15 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 2–N 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
|
||||
119
scripts/omnl/omnl-operator-rail.sh
Executable file
119
scripts/omnl/omnl-operator-rail.sh
Executable 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
|
||||
69
scripts/omnl/omnl-reconciliation-office20.sh
Executable file
69
scripts/omnl/omnl-reconciliation-office20.sh
Executable 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
|
||||
42
scripts/omnl/omnl-user-shamrayan-office-create.sh
Normal file
42
scripts/omnl/omnl-user-shamrayan-office-create.sh
Normal 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
85
scripts/omnl/resolve_ids.sh
Executable 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
40
scripts/omnl/validate-rail.sh
Executable 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
|
||||
Reference in New Issue
Block a user