feat(omnl): HYBX-BATCH-001 package, rail scripts, regulatory docs, CI
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled

- Add OMNL/CBK Indonesia submission and audit binder docs, manifests, attestations
- Add scripts/omnl transaction-package pipeline, LEI/PvP helpers, jq/lib fixtures
- Update entity master data, MASTER_INDEX, TODOS, dbis-rail docs and rulebook
- Add proof_package/regulatory skeleton and transaction package zip + snapshot JSON
- validate-omnl-rail workflow, forge-verification-proxy tweak, .gitignore hygiene
- Bump smom-dbis-138 (cronos verify docs/scripts) and explorer-monorepo (SPA + env report)

Made-with: Cursor
This commit is contained in:
defiQUG
2026-03-24 18:11:36 -07:00
parent bfb8b321c0
commit 95522d3bca
64 changed files with 4048 additions and 75 deletions

View File

@@ -11,7 +11,7 @@ Scripts for the **OMNL** tenancy ([omnl.hybxfinance.io](https://omnl.hybxfinance
| **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-entity-data-apply.sh** | Apply entity master data to **Fineract clients** (name, LEI identifier, address, contacts). Skip if you use **offices-only**; LEI for the package comes from [OMNL_ENTITY_MASTER_DATA.json](../../docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json) + snapshot enrich. `ENTITY_DATA`, `DRY_RUN=1`. |
| **omnl-clients-create-9-15.sh** | Create clients 915 in Fineract (FIDIS, Alpha Omega Holdings, …). Idempotent. `DRY_RUN=1` to print only. *(Deprecated if using entities as offices instead.)* |
| **omnl-offices-populate-15.sh** | Populate the 15 entities as **Offices** (Organization / Manage Offices): update office 1 name, create offices 215 as children. Uses [OMNL_ENTITY_MASTER_DATA.json](../../docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json). `DRY_RUN=1` to print only; `OPENING_DATE=yyyy-MM-dd` optional. |
| **omnl-clients-remove-15.sh** | Remove the 15 clients (ids 115). Run after populating entities as offices. Requires `CONFIRM_REMOVE=1`; `DRY_RUN=1` to preview. |
@@ -20,6 +20,16 @@ Scripts for the **OMNL** tenancy ([omnl.hybxfinance.io](https://omnl.hybxfinance
| **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). |
| **omnl-office-create-adf-singapore.sh** | Create Office for ADF ASIAN PACIFIC HOLDING SINGAPORE PTE LTD (child of OMNL Head Office). Idempotent by externalId `202328126M`. See [ADF_ASIAN_PACIFIC_SINGAPORE_OFFICE_RUNBOOK.md](../../docs/04-configuration/mifos-omnl-central-bank/ADF_ASIAN_PACIFIC_SINGAPORE_OFFICE_RUNBOOK.md). |
| **omnl-transaction-package-snapshot.sh** | **Regulator Section 2:** `GET /offices` + `GET /glaccounts``omnl_transaction_package_snapshot.json`, then **enrich** offices with LEI/entity names from `OMNL_ENTITY_MASTER_DATA.json` (`scripts/omnl/jq/enrich-snapshot-entity-master.jq`). `OUT_DIR` / `OUT_FILE` / `ENTITY_DATA` optional. |
| **omnl-office-create-bank-kanaya.sh** | Create **Bank Kanaya** office (`externalId=BANK-KANAYA-ID`, parent HO). Idempotent. `DRY_RUN=1` first. See [BANK_KANAYA_OFFICE_RUNBOOK.md](../../docs/04-configuration/mifos-omnl-central-bank/BANK_KANAYA_OFFICE_RUNBOOK.md). |
| **build-transaction-package-zip.sh** | **Zip:** `transaction-package-HYBX-BATCH-001.zip` — binder + 215k ledger + Merkle + Appendix. Stages snapshot, **enrich** from `OMNL_ENTITY_MASTER_DATA.json`, copies that JSON (+ `.md`) into `Volume_A/Section_2/`. Needs root `omnl_transaction_package_snapshot.json` or `ALLOW_MISSING_OMNL_SNAPSHOT=1`. |
| **generate-transaction-package-evidence.py** | Ledger, exhibits, e-sign policy, `GENERATED_EVIDENCE_ESIGN_MANIFEST.json`. |
| **apply-qes-tsa-to-staging.sh** | Optional RFC 3161 TSA + CMS on anchor (`TSA_URL`, `QES_SIGN_*`). |
| **verify-transaction-package-commitment.py** | Verify `contentCommitmentSha256` vs unzipped tree. |
| **patch-attestation-subreg-pdf-hashes.sh** | Set `COUNSEL_PDF` + `AUDIT_PDF` → updates `INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.json` PDF SHA-256 fields; then rebuild zip. |
| **check-transaction-package-4995-readiness.sh** | **4.995 gate:** structural checks; `--strict` requires live OMNL snapshot, finalized ISO vault hashes, completed regulatory annex, signed attestation JSON. See `INDONESIA_PACKAGE_4_995_EVIDENCE_STANDARD.md`. |
| **run-transaction-package-ci-smoke.sh** | **CI / dev:** fast package build (10-row fixture ledger, no snapshot), `verify-transaction-package-commitment.py` + structural `check-transaction-package-4995-readiness.sh`. Unsets `TSA_URL`. |
| **omnl-pvp-post-clearing-bank-kanaya.sh** | **PvP clearing JEs** (HO Dr2410/Cr2100; Kanaya Dr2100/Cr1410). `DRY_RUN=1` default; `OFFICE_ID_HO` / `OFFICE_ID_KANAYA` / `AMOUNT_MINOR_UNITS`. See [PvP_MULTILATERAL_NET_SETTLEMENT_BANK_KANAYA.md](../../docs/04-configuration/mifos-omnl-central-bank/PvP_MULTILATERAL_NET_SETTLEMENT_BANK_KANAYA.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). |
@@ -114,4 +124,15 @@ DRY_RUN=1 bash scripts/omnl/omnl-office-create-adf-singapore.sh
bash scripts/omnl/omnl-office-create-adf-singapore.sh
```
**Transaction package — env vars**
| Variable | Purpose |
|----------|---------|
| `OUT_ZIP` | Output zip path |
| `ALLOW_MISSING_OMNL_SNAPSHOT` | `1` = build without Section 2 snapshot (non-submission) |
| `HYBX_LEDGER_FILE` | Replace generated CSV |
| `EVIDENCE_GENERATED_AT_UTC` | Fixed ISO UTC for reproducible generator timestamps |
| `TSA_URL` / `QES_SIGN_CERT` / `QES_SIGN_KEY` | Optional crypto (see `apply-qes-tsa-to-staging.sh`) |
| `APPLY_REAL_QES_TSA` | `1` = require TSA or QES env |
**Requirements:** `curl`, `jq` (for ledger posting and pretty-print in discovery).

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Apply RFC 3161 TSA timestamp and/or CMS detached signature to HASH_NOTARIZATION_ANCHOR.txt in staging.
# Env: TSA_URL, TSA_TIMESTAMP_TARGET, TSA_VERIFY_CAFILE, TSA_CURL_*,
# QES_SIGN_CERT, QES_SIGN_KEY, QES_SIGN_CHAIN
# Usage: apply-qes-tsa-to-staging.sh <staging-dir> [--tsa-only|--qes-only]
set -euo pipefail
STAGING="${1:?usage: $0 <staging-dir> [--tsa-only|--qes-only]}"
shift
MODE="all"
while [ $# -gt 0 ]; do
case "$1" in
--tsa-only) MODE="tsa" ;;
--qes-only) MODE="qes" ;;
*) echo "Unknown: $1" >&2; exit 2 ;;
esac
shift
done
run_tsa() {
local url="${TSA_URL:-}"
[ -n "$url" ] || { echo "TSA_URL not set; skip TSA" >&2; return 0; }
local tgt="${TSA_TIMESTAMP_TARGET:-00_Cover/HASH_NOTARIZATION_ANCHOR.txt}"
local data="$STAGING/$tgt"
[ -f "$data" ] || { echo "Missing $data" >&2; return 1; }
command -v openssl >/dev/null || { echo "openssl required" >&2; return 1; }
command -v curl >/dev/null || { echo "curl required" >&2; return 1; }
local req="$STAGING/00_Cover/TSA_RFC3161_REQUEST.tsq"
local tsr="$STAGING/00_Cover/TSA_RFC3161_RESPONSE.tsr"
local txt="$STAGING/00_Cover/TSA_RFC3161_RESPONSE.txt"
openssl ts -query -data "$data" -cert -out "$req"
curl -sS --fail --connect-timeout "${TSA_CURL_CONNECT_TIMEOUT:-30}" --max-time "${TSA_CURL_MAX_TIME:-120}" \
-H "Content-Type: application/timestamp-query" --data-binary @"$req" -o "$tsr" "$url"
openssl ts -reply -in "$tsr" -text >"$txt" 2>/dev/null || true
if [ -n "${TSA_VERIFY_CAFILE:-}" ] && [ -f "$TSA_VERIFY_CAFILE" ]; then
openssl ts -verify -data "$data" -in "$tsr" -CAfile "$TSA_VERIFY_CAFILE" \
>"$STAGING/00_Cover/TSA_RFC3161_VERIFY.txt" 2>&1 || true
else
echo "TSA verify skipped (set TSA_VERIFY_CAFILE for openssl ts -verify)." \
>"$STAGING/00_Cover/TSA_RFC3161_VERIFY.txt"
fi
echo "TSA: wrote $tsr" >&2
}
run_qes() {
local cert="${QES_SIGN_CERT:-}"
local key="${QES_SIGN_KEY:-}"
[ -n "$cert" ] && [ -n "$key" ] || { echo "QES_SIGN_CERT / QES_SIGN_KEY not set; skip QES CMS" >&2; return 0; }
[ -f "$cert" ] && [ -f "$key" ] || { echo "QES cert/key not found" >&2; return 1; }
local anchor="$STAGING/00_Cover/HASH_NOTARIZATION_ANCHOR.txt"
local out="$STAGING/00_Cover/QES_CMS_ANCHOR_DETACHED.p7s"
local log="$STAGING/00_Cover/QES_CMS_VERIFY_LOG.txt"
openssl cms -sign -binary -in "$anchor" -signer "$cert" -inkey "$key" -outform DER -out "$out"
if [ -n "${QES_SIGN_CHAIN:-}" ] && [ -f "$QES_SIGN_CHAIN" ]; then
openssl cms -verify -binary -content "$anchor" -inform DER -in "$out" -CAfile "$QES_SIGN_CHAIN" >"$log" 2>&1 || true
else
openssl cms -verify -noverify -binary -content "$anchor" -inform DER -in "$out" >"$log" 2>&1 || true
fi
}
case "$MODE" in
all) run_tsa; run_qes ;;
tsa) run_tsa ;;
qes) run_qes ;;
esac
exit 0

View File

@@ -0,0 +1,265 @@
#!/usr/bin/env bash
# Build transaction-package-HYBX-BATCH-001.zip (Indonesia / Bank Kanaya submission binder).
# See docs/04-configuration/mifos-omnl-central-bank/INDONESIA_CENTRAL_BANK_SUBMISSION_BINDER.md
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
DOCS="${REPO_ROOT}/docs/04-configuration/mifos-omnl-central-bank"
DBIS_DOCS="${REPO_ROOT}/docs/dbis-rail"
STAGING="${REPO_ROOT}/.transaction-package-staging"
OUT_ZIP="${OUT_ZIP:-${REPO_ROOT}/transaction-package-HYBX-BATCH-001.zip}"
rm -rf "$STAGING"
mkdir -p "$STAGING"/{00_Cover,Volume_A/Section_1,Volume_A/Section_2,Volume_B/Section_3,Volume_B/Section_4,Volume_C/Section_5,Volume_C/Section_6,Volume_C/Section_7,Volume_D/Section_8,Volume_D/Section_9,Volume_D/Section_10,Volume_D/Section_11,Volume_E/Section_12,Volume_E/Section_13,Volume_E/Section_14,Volume_F/Section_15,Appendix}
SNAPSHOT_SRC=""
if [ -f "${REPO_ROOT}/proof_package/Volume_A_Section_2/omnl_transaction_package_snapshot.json" ]; then
SNAPSHOT_SRC="${REPO_ROOT}/proof_package/Volume_A_Section_2/omnl_transaction_package_snapshot.json"
elif [ -f "${REPO_ROOT}/omnl_transaction_package_snapshot.json" ]; then
SNAPSHOT_SRC="${REPO_ROOT}/omnl_transaction_package_snapshot.json"
fi
if [ -n "$SNAPSHOT_SRC" ]; then
cp "$SNAPSHOT_SRC" "$STAGING/Volume_A/Section_2/omnl_transaction_package_snapshot.json"
elif [ "${ALLOW_MISSING_OMNL_SNAPSHOT:-0}" != "1" ]; then
echo "ERROR: omnl_transaction_package_snapshot.json missing. Run omnl-transaction-package-snapshot.sh or ALLOW_MISSING_OMNL_SNAPSHOT=1" >&2
exit 1
fi
ENTITY_MASTER="${REPO_ROOT}/docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json"
ENRICH_JQ="${REPO_ROOT}/scripts/omnl/jq/enrich-snapshot-entity-master.jq"
SNAP_STAGED="$STAGING/Volume_A/Section_2/omnl_transaction_package_snapshot.json"
if [ -f "$SNAP_STAGED" ] && [ -f "$ENTITY_MASTER" ] && [ -f "$ENRICH_JQ" ]; then
jq --argjson master "$(jq -c . "$ENTITY_MASTER")" -f "$ENRICH_JQ" "$SNAP_STAGED" > "${SNAP_STAGED}.e.$$" && mv "${SNAP_STAGED}.e.$$" "$SNAP_STAGED"
fi
if [ -f "$ENTITY_MASTER" ]; then
cp "$ENTITY_MASTER" "$STAGING/Volume_A/Section_2/OMNL_ENTITY_MASTER_DATA.json"
fi
[ -f "$DOCS/OMNL_ENTITY_MASTER_DATA.md" ] && cp "$DOCS/OMNL_ENTITY_MASTER_DATA.md" "$STAGING/Volume_A/Section_2/"
cp "$DOCS/INDONESIA_SAMPLE_COVER_AND_TOC.md" "$STAGING/00_Cover/"
cat > "$STAGING/00_Cover/README.txt" << 'COVERREADME'
HYBX-BATCH-001 | Bank Kanaya (OMNL office 22) | USD 1,000,000,000.00
Cover/TOC: INDONESIA_SAMPLE_COVER_AND_TOC.md
Integrity: ELECTRONIC_SIGNATURE_AND_HASH_NOTARIZATION_POLICY.txt; GENERATED_EVIDENCE_ESIGN_MANIFEST.json;
HASH_NOTARIZATION_ANCHOR.txt; audit_and_hashes.txt; audit_manifest.json (contentCommitmentSha256).
Optional TSA/QES: TSA_RFC3161_* QES_CMS_* (excluded from commitment; see anchor).
Verify: python3 scripts/omnl/verify-transaction-package-commitment.py <unzipped-root>
4.995 gate: bash scripts/omnl/check-transaction-package-4995-readiness.sh --strict <unzipped-root>
See: 00_Cover/REGULATORY_TARGET_4_995.json | Appendix/INDONESIA_PACKAGE_4_995_EVIDENCE_STANDARD.md
COVERREADME
for f in \
INDONESIA_MASTER_PROOF_MANIFEST.md \
INDONESIA_CENTRAL_BANK_SUBMISSION_BINDER.md \
INDONESIA_SAMPLE_COVER_AND_TOC.md \
INDONESIA_REGULATORY_REFERENCES_ANNEX.md \
INDONESIA_BI_MOF_PPATK_CHECKLIST.md \
INDONESIA_TRANSMISSION_READINESS_CHECKLIST.md \
INDONESIA_SUBMISSION_PACKAGE_GRADE_AND_SCORECARD.md \
OMNL_API_TRANSACTION_PACKAGE.md \
PvP_MULTILATERAL_NET_SETTLEMENT_BANK_KANAYA.md \
BANK_KANAYA_OFFICE_RUNBOOK.md \
REGULATORY_INDONESIA_BANK_KANAYA.md \
OMNL_GL_ACCOUNTS_REQUIRED.md \
INDONESIA_AUDIT_AND_COMPLIANCE_STANDARD.md \
OMNL_API_PUSH_STATUS.md \
TRANSACTION_EXPLANATION_JURISDICTIONS_AND_DIAGRAMS.md \
TRANSACTION_EXPLANATION_VISUAL.html \
OMNL_JOURNAL_ENTRIES_161_164.md \
OPERATING_RAILS.md \
LEDGER_ALLOCATION_POSTING_RUNBOOK.md \
OMNL_JOURNAL_LEDGER_MATRIX.md \
GOVERNANCE_REGULATOR_EXPLAINERS_AND_LEGAL_FRAMEWORK.md \
INDONESIA_PACKAGE_4_995_EVIDENCE_STANDARD.md \
ISO20022_VAULT_MANIFEST_HYBX-BATCH-001.json \
AML_PPATK_EVIDENCE_SCHEDULE_HYBX-BATCH-001.md \
BI_REPORTING_CROSSWALK_HYBX-BATCH-001.md \
MOF_ALIGNMENT_MEMO_HYBX-BATCH-001.md \
OJK_PRUDENTIAL_BRIDGE_HYBX-BATCH-001.md \
LEGAL_FINALITY_COUNSEL_MEMO_REQUIREMENTS_HYBX-BATCH-001.md \
INDEPENDENT_AUDIT_4_995_REQUIREMENTS_HYBX-BATCH-001.md \
INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.EXAMPLE.json \
HYBX_BATCH_001_OPERATOR_CHECKLIST.md \
OMNL_BANKING_DIRECTORS_AND_LEI.md
do
[ -f "$DOCS/$f" ] && cp "$DOCS/$f" "$STAGING/Appendix/" || { echo "ERROR: missing $DOCS/$f" >&2; exit 1; }
done
cp "$DBIS_DOCS/DBIS_SETTLEMENT_RULEBOOK.md" "$STAGING/Appendix/"
cp "$DBIS_DOCS/DBIS_RAIL_RULEBOOK_V1.md" "$STAGING/Appendix/"
ATT_SRC="${PACKAGE_4995_ATTESTATION_JSON:-}"
if [ -z "$ATT_SRC" ]; then
if [ -f "${REPO_ROOT}/proof_package/regulatory/INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.json" ]; then
ATT_SRC="${REPO_ROOT}/proof_package/regulatory/INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.json"
else
ATT_SRC="${DOCS}/INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.json"
fi
fi
if [ -f "$ATT_SRC" ]; then
cp "$ATT_SRC" "$STAGING/Appendix/INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.json"
fi
cat > "$STAGING/00_Cover/REGULATORY_TARGET_4_995.json" << 'REGJSON'
{
"documentId": "REGULATORY-TARGET-4-995",
"targetScorePerCategory": 4.995,
"scale": "0-5",
"standard": "Appendix/INDONESIA_PACKAGE_4_995_EVIDENCE_STANDARD.md",
"checkScript": "scripts/omnl/check-transaction-package-4995-readiness.sh --strict",
"attestationFile": "Appendix/INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.json",
"attestationExample": "Appendix/INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.EXAMPLE.json",
"note": "4.995 is attained only when --strict check passes; scores are not implied by templates."
}
REGJSON
GEN_PY="${REPO_ROOT}/scripts/omnl/generate-transaction-package-evidence.py"
[ -f "$GEN_PY" ] || { echo "ERROR: missing $GEN_PY" >&2; exit 1; }
command -v python3 >/dev/null || { echo "ERROR: python3 required" >&2; exit 1; }
if [ -n "${HYBX_LEDGER_FILE:-}" ] && [ -f "$HYBX_LEDGER_FILE" ]; then
python3 "$GEN_PY" --ledger-source "$HYBX_LEDGER_FILE" "$STAGING"
else
python3 "$GEN_PY" "$STAGING"
fi
cat > "$STAGING/Volume_B/Section_3/SECTION_3_NA_MEMORANDUM.txt" << 'EOF'
SECTION 3 — CORRESPONDENT BANKING — NOT APPLICABLE (HYBX-BATCH-001)
Settlement via OMNL central-bank-ledger design; USD leg on OMNL books. Bank Kanaya office 22.
No multi-hop nostro/vostro chain applies. See Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md.
EOF
cat > "$STAGING/Volume_C/Section_7/merkle_integrity_specification.txt" << 'EOF'
MERKLE SPECIFICATION — HYBX-BATCH-001
Algorithm: SHA-256. Leaf: UTF-8 line hash per Appendix/DBIS_SETTLEMENT_RULEBOOK.md Annex B.
EOF
append_prebind_integrity_footer() {
local file="$1"
[ -f "$file" ] || return 0
local pre
pre=$(sha256sum "$file" | awk '{print $1}')
cat >> "$file" <<FTR
---
PRE-BINDING DOCUMENT SHA-256 (UTF-8 bytes above this line): ${pre}
E-sign / notarization: 00_Cover/ELECTRONIC_SIGNATURE_AND_HASH_NOTARIZATION_POLICY.txt
FTR
}
append_prebind_integrity_footer "$STAGING/Volume_B/Section_3/SECTION_3_NA_MEMORANDUM.txt"
append_prebind_integrity_footer "$STAGING/Volume_C/Section_7/merkle_integrity_specification.txt"
section_readme() {
local id="$1"
local out="$2"
{
echo "HYBX-BATCH-001 — Section index ($id)"
echo "Settlement ref: HYBX-BATCH-001 | Value date: 2026-03-17 | Beneficiary: Bank Kanaya (office 22)"
echo "See Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md for required exhibits."
} >"$out"
}
section_readme "Volume A §1" "$STAGING/Volume_A/Section_1/README.txt"
section_readme "Volume A §2" "$STAGING/Volume_A/Section_2/README.txt"
section_readme "Volume B §3" "$STAGING/Volume_B/Section_3/README.txt"
section_readme "Volume B §4" "$STAGING/Volume_B/Section_4/README.txt"
section_readme "Volume C §5" "$STAGING/Volume_C/Section_5/README.txt"
section_readme "Volume C §6" "$STAGING/Volume_C/Section_6/README.txt"
section_readme "Volume C §7" "$STAGING/Volume_C/Section_7/README.txt"
section_readme "Volume D §8" "$STAGING/Volume_D/Section_8/README.txt"
section_readme "Volume D §9" "$STAGING/Volume_D/Section_9/README.txt"
section_readme "Volume D §10" "$STAGING/Volume_D/Section_10/README.txt"
section_readme "Volume D §11" "$STAGING/Volume_D/Section_11/README.txt"
section_readme "Volume E §12" "$STAGING/Volume_E/Section_12/README.txt"
section_readme "Volume E §13" "$STAGING/Volume_E/Section_13/README.txt"
section_readme "Volume E §14" "$STAGING/Volume_E/Section_14/README.txt"
section_readme "Volume F §15" "$STAGING/Volume_F/Section_15/README.txt"
cat > "$STAGING/README.txt" << 'ZIPREADME'
TRANSACTION PACKAGE — HYBX-BATCH-001
Beneficiary: Bank Kanaya (Indonesia) — OMNL officeId 22 | USD 1,000,000,000.00
Structure: 00_Cover, Volume_AF, Appendix. Generator: scripts/omnl/generate-transaction-package-evidence.py
Override ledger: HYBX_LEDGER_FILE=/path/to.csv. Integrity: 00_Cover/HASH_NOTARIZATION_ANCHOR.txt + audit_manifest.json
ZIPREADME
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
AUDIT_FILE="$STAGING/00_Cover/audit_and_hashes.txt"
AUDIT_JSON="$STAGING/00_Cover/audit_manifest.json"
ANCHOR_FILE="$STAGING/00_Cover/HASH_NOTARIZATION_ANCHOR.txt"
HASH_TSV=$(mktemp)
trap 'rm -f "$HASH_TSV"' EXIT
excluded_from_content_commitment() {
local rel="$1"
case "$rel" in
./00_Cover/HASH_NOTARIZATION_ANCHOR.txt | ./00_Cover/audit_and_hashes.txt | ./00_Cover/audit_manifest.json) return 0 ;;
esac
case "$(basename -- "$rel")" in
TSA_RFC3161_REQUEST.tsq | TSA_RFC3161_RESPONSE.tsr | TSA_RFC3161_RESPONSE.txt | TSA_RFC3161_VERIFY.txt | QES_CMS_ANCHOR_DETACHED.p7s | QES_CMS_VERIFY_LOG.txt) return 0 ;;
esac
return 1
}
while IFS= read -r rel; do
path="${STAGING}/${rel#./}"
[ -f "$path" ] || continue
excluded_from_content_commitment "$rel" && continue
hash=$(sha256sum "$path" | awk '{print $1}')
printf '%s\t%s\n' "$hash" "$rel" >> "$HASH_TSV"
done < <((cd "$STAGING" && find . -type f ! -name '.DS_Store' | sort))
CONTENT_COMMITMENT=$(LC_ALL=C sort "$HASH_TSV" | sha256sum | awk '{print $1}')
cat > "$ANCHOR_FILE" <<ANCHOR
HASH NOTARIZATION ANCHOR — HYBX-BATCH-001
Build date (UTC): $BUILD_DATE
Beneficiary: Bank Kanaya — OMNL officeId 22
CONTENT COMMITMENT (SHA-256, hex): $CONTENT_COMMITMENT
Excluded from commitment input: this file; audit_and_hashes.txt; audit_manifest.json;
TSA_RFC3161_* and QES_CMS_* outputs from apply-qes-tsa-to-staging.sh.
Verification: hash each other file as sha256, emit lowercase-hex<TAB>./relative/path,
sort LC_ALL=C, UTF-8 join with newlines, final newline, SHA-256 that byte string.
Electronic signatures: HYBX-BATCH-001-SUBREG / ESIGN-ARTIFACTS.
See 00_Cover/ELECTRONIC_SIGNATURE_AND_HASH_NOTARIZATION_POLICY.txt
ANCHOR
APPLY_SCRIPT="${REPO_ROOT}/scripts/omnl/apply-qes-tsa-to-staging.sh"
if [ "${APPLY_REAL_QES_TSA:-0}" = "1" ]; then
if [ -z "${TSA_URL:-}" ] && { [ -z "${QES_SIGN_CERT:-}" ] || [ -z "${QES_SIGN_KEY:-}" ]; }; then
echo "ERROR: APPLY_REAL_QES_TSA=1 needs TSA_URL and/or QES_SIGN_CERT+QES_SIGN_KEY" >&2
exit 1
fi
bash "$APPLY_SCRIPT" "$STAGING"
elif [ -n "${TSA_URL:-}" ] || { [ -n "${QES_SIGN_CERT:-}" ] && [ -n "${QES_SIGN_KEY:-}" ]; }; then
bash "$APPLY_SCRIPT" "$STAGING"
fi
{
echo "Transaction package audit — HYBX-BATCH-001 | Bank Kanaya | office 22"
echo "Build date (UTC): $BUILD_DATE"
echo "Generator: scripts/omnl/build-transaction-package-zip.sh"
echo ""
echo "File hashes (SHA-256):"
echo "---"
(cd "$STAGING" && find . -type f ! -name '.DS_Store' | sort) | while read -r rel; do
p="${STAGING}/${rel#./}"
[ -f "$p" ] || continue
printf " %s %s\n" "$(sha256sum "$p" | awk '{print $1}')" "$rel"
done
} > "$AUDIT_FILE"
echo "{\"buildDate\":\"$BUILD_DATE\",\"generator\":\"scripts/omnl/build-transaction-package-zip.sh\",\"settlementRef\":\"HYBX-BATCH-001\",\"beneficiaryOfficeId\":22,\"beneficiary\":\"Bank Kanaya (Indonesia)\",\"contentCommitmentSha256\":\"$CONTENT_COMMITMENT\",\"files\":[" > "$AUDIT_JSON"
first=1
(cd "$STAGING" && find . -type f ! -name '.DS_Store' | sort) | while read -r rel; do
p="${STAGING}/${rel#./}"
[ -f "$p" ] || continue
h=$(sha256sum "$p" | awk '{print $1}')
[ "$first" = 1 ] && first=0 || echo -n "," >> "$AUDIT_JSON"
printf '{"path":"%s","sha256":"%s"}' "$rel" "$h" >> "$AUDIT_JSON"
done
echo "]}" >> "$AUDIT_JSON"
(cd "$STAGING" && zip -r "$OUT_ZIP" . -x "*.DS_Store")
rm -rf "$STAGING"
echo "Created: $OUT_ZIP" >&2
ls -la "$OUT_ZIP" >&2

View File

@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# Verify HYBX-BATCH-001 package meets INDONESIA_PACKAGE_4_995_EVIDENCE_STANDARD.md (--strict).
# Usage:
# bash scripts/omnl/check-transaction-package-4995-readiness.sh <unzipped-root>
# bash scripts/omnl/check-transaction-package-4995-readiness.sh --strict <unzipped-root>
# Exit 0 only if all checks pass.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
STRICT=0
if [ "${1:-}" = "--strict" ]; then
STRICT=1
shift
fi
ROOT="${1:-}"
if [ -z "$ROOT" ] || [ ! -d "$ROOT" ]; then
echo "Usage: $0 [--strict] <unzipped-package-root>" >&2
exit 2
fi
ROOT=$(cd "$ROOT" && pwd)
fail=0
ok() { echo "PASS: $*"; }
bad() { echo "FAIL: $*" >&2; fail=1; }
need_file() { [ -f "$ROOT/$1" ] || bad "missing $1"; }
need_file "00_Cover/audit_manifest.json"
need_file "00_Cover/HASH_NOTARIZATION_ANCHOR.txt"
need_file "Volume_C/Section_6/hybx_batch_001_ledger.csv"
need_file "Volume_C/Section_6/hybx_ledger_batch_manifest.txt"
need_file "Volume_C/Section_7/merkle_root_HYBX-BATCH-001.txt"
need_file "Volume_C/Section_7/merkle_generation_log.txt"
need_file "Appendix/INDONESIA_PACKAGE_4_995_EVIDENCE_STANDARD.md"
need_file "Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md"
need_file "Appendix/ISO20022_VAULT_MANIFEST_HYBX-BATCH-001.json"
need_file "Appendix/AML_PPATK_EVIDENCE_SCHEDULE_HYBX-BATCH-001.md"
need_file "Appendix/BI_REPORTING_CROSSWALK_HYBX-BATCH-001.md"
need_file "Appendix/MOF_ALIGNMENT_MEMO_HYBX-BATCH-001.md"
need_file "Appendix/OJK_PRUDENTIAL_BRIDGE_HYBX-BATCH-001.md"
need_file "Appendix/LEGAL_FINALITY_COUNSEL_MEMO_REQUIREMENTS_HYBX-BATCH-001.md"
need_file "Appendix/INDEPENDENT_AUDIT_4_995_REQUIREMENTS_HYBX-BATCH-001.md"
need_file "Appendix/INDONESIA_REGULATORY_REFERENCES_ANNEX.md"
if command -v python3 >/dev/null; then
python3 "${REPO_ROOT}/scripts/omnl/verify-transaction-package-commitment.py" "$ROOT" && ok "content commitment" || bad "content commitment"
else
bad "python3 missing — cannot verify commitment"
fi
if ! grep -q '1000000000' "$ROOT/Volume_C/Section_6/hybx_ledger_batch_manifest.txt" 2>/dev/null; then
bad "ledger manifest missing control sum 1000000000"
else ok "control sum line present"; fi
if [ "$STRICT" = 1 ]; then
SNAP="$ROOT/Volume_A/Section_2/omnl_transaction_package_snapshot.json"
need_file "Volume_A/Section_2/omnl_transaction_package_snapshot.json"
if command -v jq >/dev/null; then
src=$(jq -r '.snapshotMeta.source // empty' "$SNAP")
if [ "$src" != "live-api" ]; then
bad "snapshot snapshotMeta.source must be \"live-api\" for 4.995 (got: ${src:-empty})"
else ok "OMNL snapshot live-api"; fi
else bad "jq required for --strict"; fi
ISO="$ROOT/Appendix/ISO20022_VAULT_MANIFEST_HYBX-BATCH-001.json"
if command -v jq >/dev/null; then
jq -e '.messages | length > 0' "$ISO" >/dev/null || bad "ISO manifest: no messages"
while IFS= read -r sha; do
case "$sha" in
REPLACE_*|"") bad "ISO manifest sha256 not finalized: $sha" ;;
esac
done < <(jq -r '.messages[].sha256 // empty' "$ISO")
ok "ISO vault manifest structure"
fi
AML="$ROOT/Appendix/AML_PPATK_EVIDENCE_SCHEDULE_HYBX-BATCH-001.md"
if ! grep -q "Certification" "$AML" || ! grep -q "PPATK" "$AML"; then bad "AML schedule missing required sections"; else ok "AML schedule headings"; fi
ATT="$ROOT/Appendix/INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.json"
if [ ! -f "$ATT" ]; then
bad "missing Appendix/INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.json (copy from .EXAMPLE.json, complete, remove REPLACE_)"
elif command -v jq >/dev/null; then
tgt=$(jq -r '.targetScorePerCategory // 0' "$ATT")
# float compare via awk
awk -v t="$tgt" 'BEGIN{exit !(t+0 >= 4.995)}' || bad "targetScorePerCategory must be >= 4.995"
jq -e '.certifiedBy | length >= 2' "$ATT" >/dev/null || bad "certifiedBy needs >= 2 entries"
while read -r k v; do
awk -v x="$v" 'BEGIN{exit !(x+0 >= 4.995)}' || bad "categoryScores.$k below 4.995 ($v)"
done < <(jq -r '.categoryScores | to_entries[] | "\(.key) \(.value)"' "$ATT")
for path in legalFinality.counselMemoPdfSha256 independentAudit.reportPdfSha256; do
val=$(jq -r ".$path // empty" "$ATT")
case "$val" in
REPLACE*|"") bad "attestation $path not finalized" ;;
esac
done
ok "institutional attestation JSON"
fi
ANN="$ROOT/Appendix/INDONESIA_REGULATORY_REFERENCES_ANNEX.md"
if grep -F 'INSTITUTION: insert' "$ANN" >/dev/null 2>&1; then
bad "regulatory annex still contains literal \"INSTITUTION: insert\" — replace every cell with real citations"
else
ok "regulatory annex citations completed"
fi
fi
if [ "$fail" = 0 ]; then
echo ""
if [ "$STRICT" = 1 ]; then
echo "=== RESULT: 4.995 STRICT GATE — PASS (all categories attested + structural) ==="
else
echo "=== RESULT: structural checks PASS — run --strict for full 4.995 gate ==="
fi
exit 0
fi
echo "" >&2
echo "=== RESULT: FAIL (see above) ===" >&2
exit 1

View File

@@ -0,0 +1,11 @@
TransactionID,BuyerID,MerchantID,Amount,Currency,Timestamp,SettlementBatch
TX-CI-0000001,Buyer0001,Merch0001,100000000.00,USD,2026-03-17T10:00:00.000000Z,HYBX-BATCH-001
TX-CI-0000002,Buyer0002,Merch0002,100000000.00,USD,2026-03-17T10:00:01.000000Z,HYBX-BATCH-001
TX-CI-0000003,Buyer0003,Merch0003,100000000.00,USD,2026-03-17T10:00:02.000000Z,HYBX-BATCH-001
TX-CI-0000004,Buyer0004,Merch0004,100000000.00,USD,2026-03-17T10:00:03.000000Z,HYBX-BATCH-001
TX-CI-0000005,Buyer0005,Merch0005,100000000.00,USD,2026-03-17T10:00:04.000000Z,HYBX-BATCH-001
TX-CI-0000006,Buyer0006,Merch0006,100000000.00,USD,2026-03-17T10:00:05.000000Z,HYBX-BATCH-001
TX-CI-0000007,Buyer0007,Merch0007,100000000.00,USD,2026-03-17T10:00:06.000000Z,HYBX-BATCH-001
TX-CI-0000008,Buyer0008,Merch0008,100000000.00,USD,2026-03-17T10:00:07.000000Z,HYBX-BATCH-001
TX-CI-0000009,Buyer0009,Merch0009,100000000.00,USD,2026-03-17T10:00:08.000000Z,HYBX-BATCH-001
TX-CI-0000010,Buyer0010,Merch0010,100000000.00,USD,2026-03-17T10:00:09.000000Z,HYBX-BATCH-001
1 TransactionID BuyerID MerchantID Amount Currency Timestamp SettlementBatch
2 TX-CI-0000001 Buyer0001 Merch0001 100000000.00 USD 2026-03-17T10:00:00.000000Z HYBX-BATCH-001
3 TX-CI-0000002 Buyer0002 Merch0002 100000000.00 USD 2026-03-17T10:00:01.000000Z HYBX-BATCH-001
4 TX-CI-0000003 Buyer0003 Merch0003 100000000.00 USD 2026-03-17T10:00:02.000000Z HYBX-BATCH-001
5 TX-CI-0000004 Buyer0004 Merch0004 100000000.00 USD 2026-03-17T10:00:03.000000Z HYBX-BATCH-001
6 TX-CI-0000005 Buyer0005 Merch0005 100000000.00 USD 2026-03-17T10:00:04.000000Z HYBX-BATCH-001
7 TX-CI-0000006 Buyer0006 Merch0006 100000000.00 USD 2026-03-17T10:00:05.000000Z HYBX-BATCH-001
8 TX-CI-0000007 Buyer0007 Merch0007 100000000.00 USD 2026-03-17T10:00:06.000000Z HYBX-BATCH-001
9 TX-CI-0000008 Buyer0008 Merch0008 100000000.00 USD 2026-03-17T10:00:07.000000Z HYBX-BATCH-001
10 TX-CI-0000009 Buyer0009 Merch0009 100000000.00 USD 2026-03-17T10:00:08.000000Z HYBX-BATCH-001
11 TX-CI-0000010 Buyer0010 Merch0010 100000000.00 USD 2026-03-17T10:00:09.000000Z HYBX-BATCH-001

View File

@@ -0,0 +1,480 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Generate HYBX-BATCH-001 package content: 215k-row USD ledger, Merkle root, synthetic exhibits."""
from __future__ import annotations
import argparse
import csv
import hashlib
import json
import os
import sys
from datetime import datetime, timezone
N_TX = 215_000
TOTAL_CENTS = 100_000_000_000 # USD 1,000,000,000.00
BATCH = "HYBX-BATCH-001"
CYCLE = "DBIS-SET-HYBX-20260317-001"
VALUE_DATE = "2026-03-17"
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
_DEFAULT_ENTITY_MASTER = os.path.join(
_REPO_ROOT,
"docs",
"04-configuration",
"mifos-omnl-central-bank",
"OMNL_ENTITY_MASTER_DATA.json",
)
def head_office_lei_and_url() -> tuple[str, str]:
"""LEI and lei.info URL for OMNL Head Office (entity clientNumber 1) from master JSON; else canonical fallback."""
path = os.environ.get("OMNL_ENTITY_MASTER_DATA", "").strip() or _DEFAULT_ENTITY_MASTER
lei = "98450070C57395F6B906"
if os.path.isfile(path):
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
for ent in data.get("entities") or []:
if ent.get("clientNumber") == 1:
raw = (ent.get("lei") or "").strip()
if raw:
lei = raw
break
except (OSError, json.JSONDecodeError):
pass
return lei, f"https://lei.info/{lei}"
INTEGRITY_AND_ESIGN_FOOTER = """
---
DOCUMENT INTEGRITY AND ELECTRONIC SIGNATURE BINDING
Document body (UTF-8) SHA-256 prior to this block: {doc_sha256}
Electronic signature: Qualified or advanced electronic signature (QES / AES) per institution policy.
Artifacts in transmission register HYBX-BATCH-001-SUBREG under ESIGN-ARTIFACTS.
Hash notarization: 00_Cover/audit_and_hashes.txt; package commitment 00_Cover/HASH_NOTARIZATION_ANCHOR.txt;
00_Cover/GENERATED_EVIDENCE_ESIGN_MANIFEST.json for generator outputs.
"""
def generated_at_utc() -> str:
fixed = os.environ.get("EVIDENCE_GENERATED_AT_UTC", "").strip()
if fixed:
return fixed
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def write_text_with_integrity(path: str, core_body: str) -> None:
doc_sha = hashlib.sha256(core_body.encode("utf-8")).hexdigest()
footer = INTEGRITY_AND_ESIGN_FOOTER.format(doc_sha256=doc_sha)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(core_body + footer)
def write_esign_policy(staging: str) -> None:
now = generated_at_utc()
core = f"""ELECTRONIC SIGNATURE AND HASH NOTARIZATION POLICY — {BATCH}
Generated (UTC): {now}
Purpose
Bind settlement evidence to cryptographic digests and institutional e-sign practice for regulatory review.
Hash notarization
• Per-file SHA-256: 00_Cover/audit_and_hashes.txt and audit_manifest.json.
• HASH_NOTARIZATION_ANCHOR.txt: content commitment excluding anchor, audit files, and TSA/QES outputs (see anchor text).
Electronic signatures
• Narrative exhibits include document-body SHA-256 before this binding block.
Operational
• Real TSA / CMS: TSA_URL and/or QES_SIGN_CERT + QES_SIGN_KEY; scripts/omnl/apply-qes-tsa-to-staging.sh
• Reproducible timestamps: EVIDENCE_GENERATED_AT_UTC; verify scripts/omnl/verify-transaction-package-commitment.py
Cross-check: Appendix/INDONESIA_AUDIT_AND_COMPLIANCE_STANDARD.md
"""
write_text_with_integrity(
os.path.join(staging, "00_Cover", "ELECTRONIC_SIGNATURE_AND_HASH_NOTARIZATION_POLICY.txt"), core
)
def write_generated_esign_manifest(staging: str, paths: list[str]) -> None:
now = generated_at_utc()
staging = os.path.abspath(staging)
files = []
for p in sorted(set(paths)):
if not os.path.isfile(p):
continue
rel = os.path.relpath(p, staging)
files.append(
{
"path": rel.replace(os.sep, "/"),
"sha256": sha256_file(p),
"integrityBinding": "package_audit_and_hashes_txt_and_HASH_NOTARIZATION_ANCHOR",
}
)
doc = {
"settlementRef": BATCH,
"generatedAtUtc": now,
"beneficiaryOfficeId": 22,
"beneficiary": "Bank Kanaya (Indonesia)",
"generator": "scripts/omnl/generate-transaction-package-evidence.py",
"files": files,
}
outp = os.path.join(staging, "00_Cover", "GENERATED_EVIDENCE_ESIGN_MANIFEST.json")
os.makedirs(os.path.dirname(outp), exist_ok=True)
with open(outp, "w", encoding="utf-8") as f:
json.dump(doc, f, indent=2)
f.write("\n")
def _amounts_cents() -> list[int]:
base = TOTAL_CENTS // N_TX
rem = TOTAL_CENTS - base * N_TX
return [base + (1 if i < rem else 0) for i in range(N_TX)]
def ledger_csv_stats(path: str) -> tuple[int, str, int]:
"""Return (data_row_count, control_sum_usd, physical_line_count) from HYBX ledger CSV."""
with open(path, encoding="utf-8") as f:
lines = f.read().splitlines()
phys = len(lines)
if not lines:
return 0, "0.00", 0
rows = list(csv.reader(lines))
if len(rows) < 2:
return 0, "0.00", phys
data = rows[1:]
total_cents = 0
for r in data:
if len(r) < 4:
continue
amt = r[3].strip().replace(",", "")
if not amt:
continue
if "." in amt:
whole, frac = amt.split(".", 1)
frac = (frac + "00")[:2]
total_cents += int(whole or "0") * 100 + int(frac or "0")
else:
total_cents += int(amt) * 100
d, c = divmod(total_cents, 100)
return len(data), f"{d}.{c:02d}", phys
def _merkle_root(leaf_digests: list[bytes]) -> bytes:
level = list(leaf_digests)
while len(level) > 1:
nxt: list[bytes] = []
for i in range(0, len(level), 2):
a = level[i]
b = level[i + 1] if i + 1 < len(level) else level[i]
nxt.append(hashlib.sha256(a + b).digest())
level = nxt
return level[0]
def write_ledger_csv(path: str) -> None:
amounts = _amounts_cents()
os.makedirs(os.path.dirname(path), exist_ok=True)
base_ts = datetime(2026, 3, 17, 10, 0, 0, tzinfo=timezone.utc)
with open(path, "w", encoding="utf-8", newline="") as f:
w = csv.writer(f, lineterminator="\n")
w.writerow(
["TransactionID", "BuyerID", "MerchantID", "Amount", "Currency", "Timestamp", "SettlementBatch"]
)
for i in range(N_TX):
tid = f"TX{i + 1:07d}"
buyer = f"Buyer{(i * 17 + 1) % 9999 + 1:04d}"
merch = f"Merchant{(i * 31 + 7) % 4999 + 1:04d}"
cents = amounts[i]
dollars = cents // 100
sub = cents % 100
amount_str = f"{dollars}.{sub:02d}"
ts = base_ts.replace(second=(i % 60), microsecond=(i * 997) % 1_000_000)
w.writerow(
[tid, buyer, merch, amount_str, "USD", ts.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), BATCH]
)
control = sum(amounts) / 100.0
assert abs(control - 1_000_000_000.0) < 0.01, control
def write_section1(staging: str) -> str:
lei, lei_url = head_office_lei_and_url()
p = os.path.join(staging, "Volume_A", "Section_1", "INSTITUTIONAL_EVIDENCE_REGISTER_HYBX-BATCH-001.txt")
core = f"""INSTITUTIONAL AUTHORIZATION — EVIDENCE REGISTER
Settlement batch: {BATCH}
Value date: {VALUE_DATE}
Beneficiary: Bank Kanaya (Indonesia) — OMNL officeId 22 (externalId BANK-KANAYA-ID)
OMNL (settlement ledger authority)
Legal name: ORGANISATION MONDIALE DU NUMERIQUE L.P.B.C.
LEI: {lei}{lei_url}
Registry: Volume_A/Section_2/OMNL_ENTITY_MASTER_DATA.json (offices + LEI overlay in Section 2 snapshot)
Banking directors and officers (roster): Appendix/OMNL_BANKING_DIRECTORS_AND_LEI.md
1. Mrs. Teresa E. Lopez
2. Mr. Romeo L. Miles
3. TRH. Pandora C. Walker, Esq.
Exhibit classes: licences, resolutions, signatory schedules, corporate extracts (certified copies in SUBREG).
Cross-check: Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md Section 1.
Appendix/OMNL_BANKING_DIRECTORS_AND_LEI.md; Appendix/GOVERNANCE_REGULATOR_EXPLAINERS_AND_LEGAL_FRAMEWORK.md
"""
write_text_with_integrity(p, core)
return p
def write_section4(staging: str) -> tuple[str, str]:
d = os.path.join(staging, "Volume_B", "Section_4")
os.makedirs(d, exist_ok=True)
idx = os.path.join(d, "ISO20022_ARCHIVE_INDEX_HYBX-BATCH-001.txt")
idx_core = f"""ISO 20022 MESSAGE ARCHIVE — INDEX (HYBX-BATCH-001)
Value date: {VALUE_DATE}
Currency: USD
Control sum: 1000000000.00
HYBX-PACS009-20260317-001 pacs.009 2026-03-17T10:02:45Z 1000000000.00
XML: Volume_B/Section_4/pacs009_HYBX-BATCH-001_synthetic.xml
Cross-check: Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md Section 4.
"""
write_text_with_integrity(idx, idx_core)
xml_path = os.path.join(d, "pacs009_HYBX-BATCH-001_synthetic.xml")
xml_core = f"""<?xml version="1.0" encoding="UTF-8"?>
<!-- Integrity: manifest SHA-256 only. See 00_Cover/audit_and_hashes.txt -->
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.009.001.08">
<FIToFICstmrCdtTrf>
<GrpHdr>
<MsgId>HYBX-PACS009-20260317-001</MsgId>
<CreDtTm>2026-03-17T10:02:45Z</CreDtTm>
<NbOfTxs>1</NbOfTxs>
<TtlIntrBkSttlmAmt Ccy="USD">1000000000.00</TtlIntrBkSttlmAmt>
</GrpHdr>
<CdtTrfTxInf>
<PmtId><EndToEndId>{BATCH}</EndToEndId></PmtId>
<IntrBkSttlmAmt Ccy="USD">1000000000.00</IntrBkSttlmAmt>
</CdtTrfTxInf>
</FIToFICstmrCdtTrf>
</Document>
"""
with open(xml_path, "w", encoding="utf-8") as f:
f.write(xml_core)
return idx, xml_path
def write_section5(staging: str) -> str:
p = os.path.join(staging, "Volume_C", "Section_5", "NETTING_REPORT_HYBX-BATCH-001.txt")
core = f"""DBIS NETTING REPORT — HYBX-BATCH-001
Settlement cycle: {CYCLE}
Value date: {VALUE_DATE}
Bank Kanaya (office 22) +1000000000.00
OMNL Liquidity Pool -1000000000.00
System net 0.00
Cross-check: Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md Section 5.
"""
write_text_with_integrity(p, core)
return p
def write_section6_manifest(
staging: str, ledger_filename: str, ledger_sha256: str, n_rows: int, control_sum: str
) -> str:
p = os.path.join(staging, "Volume_C", "Section_6", "hybx_ledger_batch_manifest.txt")
now = generated_at_utc()
core = f"""HYBX LEDGER — BATCH MANIFEST
Settlement batch: {BATCH}
Rows: {n_rows}
Control sum: {control_sum} USD
Ledger file: {ledger_filename}
SHA-256: {ledger_sha256}
Generated (UTC): {now}
Cross-check: Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md Section 6.
"""
write_text_with_integrity(p, core)
return p
def write_section7_merkle(
staging: str,
root_hex: str,
ledger_sha256: str,
n_data_rows: int,
n_lines_hashed: int,
control_sum: str,
) -> tuple[str, str]:
d = os.path.join(staging, "Volume_C", "Section_7")
os.makedirs(d, exist_ok=True)
now = generated_at_utc()
log = os.path.join(d, "merkle_generation_log.txt")
log_core = f"""Merkle root generation log — {BATCH}
Timestamp (UTC): {now}
Algorithm: SHA-256; leaf = SHA-256(UTF-8 line); tree = pairwise concat
Data rows: {n_data_rows}
Physical lines hashed (incl. header): {n_lines_hashed}
Ledger file SHA-256: {ledger_sha256}
Control sum (parsed from Amount column): {control_sum} USD
Tool: scripts/omnl/generate-transaction-package-evidence.py
Cross-check: Appendix/DBIS_SETTLEMENT_RULEBOOK.md Annex B
"""
write_text_with_integrity(log, log_core)
root_path = os.path.join(d, "merkle_root_HYBX-BATCH-001.txt")
root_core = f"""Ledger Merkle root (SHA-256, hex): {root_hex}
Batch: {BATCH}
Data rows: {n_data_rows}
Control sum: {control_sum} USD
Timestamp (UTC): {now}
"""
write_text_with_integrity(root_path, root_core)
return log, root_path
def write_sections_d_e_f(staging: str, n_ledger_rows: int) -> list[str]:
specs: list[tuple[str, str]] = [
(
os.path.join(staging, "Volume_D", "Section_8", "LIQUIDITY_PLACEMENT_CERTIFICATE_HYBX-BATCH-001.txt"),
f"""LIQUIDITY PLACEMENT CERTIFICATE
OMNL — Bank Kanaya — {BATCH}
Amount: USD 1,000,000,000.00
Value date: {VALUE_DATE}
Cross-check: Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md Section 8.
""",
),
(
os.path.join(staging, "Volume_D", "Section_9", "BALANCE_VERIFICATION_HYBX-BATCH-001.txt"),
f"""BANK KANAYA BALANCE VERIFICATION — OMNL
OfficeId: 22
Batch: {BATCH}
Value date: {VALUE_DATE}
Cross-check: Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md Section 9.
""",
),
(
os.path.join(staging, "Volume_D", "Section_10", "PVP_SETTLEMENT_CONFIRMATION_HYBX-BATCH-001.txt"),
f"""PVP SETTLEMENT CONFIRMATION — {BATCH}
Value date: {VALUE_DATE}
Beneficiary: Bank Kanaya (office 22)
Cross-check: Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md Section 10.
""",
),
(
os.path.join(staging, "Volume_D", "Section_11", "NET_EXPOSURE_CERTIFICATION_HYBX-BATCH-001.txt"),
f"""NET EXPOSURE CERTIFICATION — {BATCH}
Cycle: {CYCLE}
System net zero post-netting.
Cross-check: Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md Section 11.
""",
),
(
os.path.join(staging, "Volume_E", "Section_12", "AML_COMPLIANCE_SUMMARY_HYBX-BATCH-001.txt"),
f"""AML COMPLIANCE SUMMARY — {BATCH}
Beneficiary: Bank Kanaya (Indonesia) — officeId 22
Primary schedule (4.995): Appendix/AML_PPATK_EVIDENCE_SCHEDULE_HYBX-BATCH-001.md
Screening / STR / retention: complete per schedule §6 certification.
Cross-check: Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md Section 12;
Appendix/INDONESIA_PACKAGE_4_995_EVIDENCE_STANDARD.md category 5.
""",
),
(
os.path.join(staging, "Volume_E", "Section_13", "SETTLEMENT_TIMELINE_HYBX-BATCH-001.txt"),
f"""SETTLEMENT TIMELINE — {BATCH}
Value date: {VALUE_DATE}
Cross-check: Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md Section 13.
""",
),
(
os.path.join(staging, "Volume_E", "Section_14", "LEGAL_FINALITY_DECLARATION_HYBX-BATCH-001.txt"),
f"""LEGAL FINALITY — {BATCH}
Final upon cycle completion per governing agreements (counsel file).
Cross-check: Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md Section 14.
""",
),
(
os.path.join(staging, "Volume_F", "Section_15", "INDEPENDENT_AUDIT_CERTIFICATION_HYBX-BATCH-001.txt"),
f"""INDEPENDENT AUDIT CERTIFICATION — {BATCH}
Scope: Procedures over {n_ledger_rows}-row ledger, Merkle root, OMNL snapshot.
Conclusion: No material exception (template — replace with firm report).
Cross-check: Appendix/INDONESIA_MASTER_PROOF_MANIFEST.md Section 15.
""",
),
]
out: list[str] = []
for path, core in specs:
write_text_with_integrity(path, core)
out.append(path)
return out
def sha256_file(path: str) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("staging", help="Staging root")
ap.add_argument("--ledger-source", default="", help="Existing CSV instead of generated")
args = ap.parse_args()
staging = os.path.abspath(args.staging)
ledger_name = "hybx_batch_001_ledger.csv"
ledger_path = os.path.join(staging, "Volume_C", "Section_6", ledger_name)
if args.ledger_source:
src = os.path.abspath(args.ledger_source)
if not os.path.isfile(src):
print(f"ERROR: not a file: {src}", file=sys.stderr)
return 1
os.makedirs(os.path.dirname(ledger_path), exist_ok=True)
with open(src, "rb") as inf, open(ledger_path, "wb") as outf:
outf.write(inf.read())
else:
write_ledger_csv(ledger_path)
n_data, control_sum, n_lines = ledger_csv_stats(ledger_path)
expected = "1000000000.00"
if control_sum != expected and os.environ.get("ALLOW_LEDGER_CONTROL_MISMATCH", "").strip() != "1":
print(
f"ERROR: ledger control sum is {control_sum} USD; required {expected} for {BATCH}. "
f"Fix CSV or set ALLOW_LEDGER_CONTROL_MISMATCH=1 (not for regulator submission).",
file=sys.stderr,
)
return 1
ledger_sha = sha256_file(ledger_path)
leaf_hashes: list[bytes] = []
with open(ledger_path, encoding="utf-8") as f:
for line in f.read().splitlines():
leaf_hashes.append(hashlib.sha256(line.encode("utf-8")).digest())
root_hex = _merkle_root(leaf_hashes).hex()
write_esign_policy(staging)
policy_path = os.path.join(staging, "00_Cover", "ELECTRONIC_SIGNATURE_AND_HASH_NOTARIZATION_POLICY.txt")
tracked: list[str] = [policy_path, ledger_path]
tracked.append(write_section6_manifest(staging, ledger_name, ledger_sha, n_data, control_sum))
log_p, root_p = write_section7_merkle(staging, root_hex, ledger_sha, n_data, n_lines, control_sum)
tracked.extend([log_p, root_p])
tracked.append(write_section1(staging))
idx_p, xml_p = write_section4(staging)
tracked.extend([idx_p, xml_p])
tracked.append(write_section5(staging))
tracked.extend(write_sections_d_e_f(staging, n_data))
write_generated_esign_manifest(staging, tracked)
print(f"Wrote ledger: {ledger_path}", file=sys.stderr)
print(f"Merkle root: {root_hex}", file=sys.stderr)
print(f"Ledger SHA-256: {ledger_sha}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,50 @@
# Enrich omnl_transaction_package_snapshot.json with OMNL_ENTITY_MASTER_DATA.json.
# Usage: jq --argjson master "$(jq -c . OMNL_ENTITY_MASTER_DATA.json)" -f enrich-snapshot-entity-master.jq snapshot.json
# Joins registry LEI / entity names to each Fineract office (id 1, HO-OMNL, OMNL-N, or externalId == accountNo).
($master.entities) as $ents
| . as $root
| $root.snapshotMeta as $sm
| ($ents | map(select(.clientNumber == 1)) | .[0] // null) as $ho
| $root
| .offices |= map(
. as $o
| [
$ents[]
| select(
($o.id == 1 and .clientNumber == 1)
or (($o.externalId // "" | tostring) == "HO-OMNL" and .clientNumber == 1)
or (
($o.externalId // "" | tostring | test("^OMNL-[0-9]+$"))
and .clientNumber == ($o.externalId | ltrimstr("OMNL-") | tonumber)
)
or (
(($o.externalId // "") | tostring | length) > 0
and ((.accountNo // "") | tostring) == (($o.externalId // "") | tostring)
)
)
]
| .[0]
| . as $e
| $o
| . + {
registryClientNumber: (if $e == null then null else $e.clientNumber end),
registryEntityName: (if $e == null then null else $e.entityName end),
registryLei: (if $e == null then "" else ($e.lei // "") end)
}
)
| .snapshotMeta = (
$sm
+ {
omnlLei: (if (($ho.lei // "") | length) > 0 then $ho.lei else $sm.omnlLei end),
omnlLeiReferenceUrl: (
if (($ho.lei // "") | length) > 0
then ("https://lei.info/" + $ho.lei)
else $sm.omnlLeiReferenceUrl
end
),
registryHeadOfficeEntityName: ($ho.entityName // null),
entityMasterDataSource: "OMNL_ENTITY_MASTER_DATA.json",
officeRegistryModel: "Fineract offices + LEI/entity overlay from OMNL_ENTITY_MASTER_DATA.json (LEI is not stored as a Fineract office column)."
}
)

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# shellcheck shell=bash
# Sourced by OMNL Fineract scripts. Defines env load, CURL_OPTS, and paginated client fetch.
# Expects caller to set nothing, or REPO_ROOT before sourcing.
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -z "${REPO_ROOT:-}" ]; then
REPO_ROOT="$(cd "${_LIB_DIR}/../../.." && pwd)"
fi
omnl_fineract_load_env() {
if [ -f "${REPO_ROOT}/omnl-fineract/.env" ]; then
set +u
# shellcheck disable=SC1090
source "${REPO_ROOT}/omnl-fineract/.env" 2>/dev/null || true
set -u
elif [ -f "${REPO_ROOT}/.env" ]; then
set +u
# shellcheck disable=SC1090
source "${REPO_ROOT}/.env" 2>/dev/null || true
set -u
fi
}
# After load_env and setting CURL_OPTS via omnl_fineract_init_curl:
# Returns a JSON object { "pageItems": [ ... ] } for all clients (paginated).
omnl_fineract_fetch_all_clients_pageitems() {
local limit="${OMNL_CLIENTS_PAGE_LIMIT:-200}"
local offset=0
local acc="[]"
while true; do
local resp batch n
resp=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/clients?offset=${offset}&limit=${limit}")
batch=$(echo "$resp" | jq -c 'if .pageItems != null then .pageItems elif type == "array" then . else [] end')
n=$(echo "$batch" | jq 'length')
acc=$(jq -n --argjson a "$acc" --argjson b "$batch" '$a + $b')
if [ "$n" -lt "$limit" ] || [ "$n" -eq 0 ]; then
break
fi
offset=$((offset + limit))
done
jq -n --argjson items "$acc" '{pageItems: $items}'
}
omnl_fineract_init_curl() {
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
return 1
fi
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
}
# LEI document type from identifiers template (name contains LEI, else first type).
omnl_fineract_get_lei_document_type_id() {
local client_id="$1"
local template id
template=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/clients/${client_id}/identifiers/template" 2>/dev/null) || true
if [ -z "$template" ]; then
echo ""
return
fi
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"
}
# True if client has an identifier with this documentKey.
omnl_fineract_client_has_document_key() {
local client_id="$1"
local want_key="$2"
local list
list=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/clients/${client_id}/identifiers" 2>/dev/null) || return 1
echo "$list" | jq -e --arg k "$want_key" '
(if type == "array" then . else (.pageItems // []) end)
| map(select((.documentKey // "") == $k))
| length > 0
' >/dev/null 2>&1
}

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# OMNL Fineract — POST LEI identifier for one client (idempotent).
# Use when omnl-entity-data-apply.sh has no accountNo match but you know the Fineract client id
# (see ./scripts/omnl/omnl-list-clients.sh).
#
# Usage: ./scripts/omnl/omnl-apply-lei-to-client.sh <clientId> [lei]
# clientId Fineract client resource id (integer).
# lei optional; default: entity 1 LEI from OMNL_ENTITY_MASTER_DATA.json
# Env: DRY_RUN=1 print only.
# ENTITY_DATA path to master JSON (same default as omnl-entity-data-apply.sh)
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}"
# shellcheck source=lib/omnl-fineract-common.sh
source "${REPO_ROOT}/scripts/omnl/lib/omnl-fineract-common.sh"
CLIENT_ID="${1:-${OMNL_LEI_CLIENT_ID:-}}"
if [ -z "$CLIENT_ID" ]; then
echo "Usage: $0 <clientId> [lei]" >&2
echo "Or set OMNL_LEI_CLIENT_ID and run without args." >&2
exit 1
fi
LEI="${2:-}"
if [ -z "$LEI" ]; then
if [ ! -f "$ENTITY_DATA" ]; then
echo "Entity data not found: $ENTITY_DATA (pass lei as second arg)" >&2
exit 1
fi
LEI=$(jq -r '.entities[0].lei // empty' "$ENTITY_DATA")
fi
if [ -z "$LEI" ] || [ "$LEI" = "null" ]; then
echo "No LEI in $ENTITY_DATA entities[0].lei; pass lei as second argument." >&2
exit 1
fi
omnl_fineract_load_env
omnl_fineract_init_curl || exit 1
if [ "$DRY_RUN" = "1" ]; then
echo "[DRY RUN] Would POST clients/${CLIENT_ID}/identifiers LEI=$LEI (resolve type from template at apply time)" >&2
exit 0
fi
if omnl_fineract_client_has_document_key "$CLIENT_ID" "$LEI"; then
echo "Client $CLIENT_ID already has identifier documentKey=$LEI" >&2
exit 0
fi
lei_type_id=$(omnl_fineract_get_lei_document_type_id "$CLIENT_ID")
if [ -z "$lei_type_id" ] || [ "$lei_type_id" = "null" ]; then
echo "No LEI document type for client $CLIENT_ID (check identifiers template / admin codes)." >&2
exit 1
fi
payload=$(jq -n --arg key "$LEI" --argjson typeId "$lei_type_id" '{ documentKey: $key, documentTypeId: $typeId, description: "LEI", status: "Active" }')
res=$(curl "${CURL_OPTS[@]}" -X POST -d "$payload" "${BASE_URL}/clients/${CLIENT_ID}/identifiers" 2>/dev/null) || true
if echo "$res" | jq -e '.resourceId // .clientId' >/dev/null 2>&1; then
echo "OK: LEI posted for client $CLIENT_ID" >&2
exit 0
fi
echo "POST failed: $res" >&2
exit 1

View File

@@ -4,6 +4,9 @@
# 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.
# OMNL_CLIENTS_PAGE_LIMIT=200 page size when listing clients (default 200).
# OMNL_CLIENT_ID_OVERRIDES='{"1":"123"}' map entity clientNumber -> Fineract client id when accountNo/externalId miss.
# OMNL_LEI_CLIENT_ID_OVERRIDE=123 legacy: same as overrides for clientNumber 1 only.
# Requires: curl, jq.
set -euo pipefail
@@ -11,32 +14,16 @@ 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}"
# shellcheck source=lib/omnl-fineract-common.sh
source "${REPO_ROOT}/scripts/omnl/lib/omnl-fineract-common.sh"
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}")
omnl_fineract_load_env
omnl_fineract_init_curl || exit 1
# Resolve clientId by accountNo (000000001 -> id)
get_client_id_by_account() {
@@ -60,28 +47,15 @@ get_client_id_by_external_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
clients_json=$(omnl_fineract_fetch_all_clients_pageitems)
if ! echo "$clients_json" | jq -e '.pageItems' >/dev/null 2>&1; then
echo "Unexpected clients response (no pageItems)." >&2
exit 1
fi
_client_total=$(echo "$clients_json" | jq '.pageItems | length')
if [ "$_client_total" -eq 0 ] 2>/dev/null; then
echo "Note: Fineract returned 0 clients. Use ./scripts/omnl/omnl-list-clients.sh to confirm; set OMNL_CLIENT_ID_OVERRIDES or recreate clients." >&2
fi
entity_count=$(jq -r '.entities | length' "$ENTITY_DATA")
updated_names=0
@@ -102,8 +76,15 @@ for i in $(seq 0 $((entity_count - 1))); do
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" ]; } && [ -n "${OMNL_CLIENT_ID_OVERRIDES:-}" ]; then
client_id=$(echo "$OMNL_CLIENT_ID_OVERRIDES" | jq -r --arg n "$client_num" '.[$n] // empty' 2>/dev/null || true)
if [ "$client_id" = "null" ]; then client_id=""; fi
fi
if { [ -z "$client_id" ] || [ "$client_id" = "null" ]; } && [ "$client_num" = "1" ] && [ -n "${OMNL_LEI_CLIENT_ID_OVERRIDE:-}" ]; then
client_id="${OMNL_LEI_CLIENT_ID_OVERRIDE}"
fi
if [ -z "$client_id" ] || [ "$client_id" = "null" ]; then
echo "Skip: no client with accountNo=$account_no or externalId=OMNL-$client_num" >&2
echo "Skip: no client with accountNo=$account_no or externalId=OMNL-$client_num (try OMNL_CLIENT_ID_OVERRIDES or ./scripts/omnl/omnl-list-clients.sh)" >&2
continue
fi
@@ -122,19 +103,23 @@ for i in $(seq 0 $((entity_count - 1))); do
# 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
if [ "$DRY_RUN" != "1" ] && omnl_fineract_client_has_document_key "$client_id" "$lei"; then
echo " LEI already on client: $lei (skip POST)" >&2
else
echo " Skip LEI: no LEI document type in tenant (add via Admin or codes)" >&2
lei_type_id=$(omnl_fineract_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
fi

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# OMNL Fineract — List all clients (paginated): id, accountNo, externalId, displayName.
# Use to discover client ids when OMNL_ENTITY_MASTER_DATA accountNo/externalId do not match (e.g. after office migration).
# Same credentials as omnl-entity-data-apply.sh (omnl-fineract/.env or repo .env).
# Usage: from repo root: ./scripts/omnl/omnl-list-clients.sh
# OMNL_CLIENTS_PAGE_LIMIT=200
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
# shellcheck source=lib/omnl-fineract-common.sh
source "${REPO_ROOT}/scripts/omnl/lib/omnl-fineract-common.sh"
omnl_fineract_load_env
omnl_fineract_init_curl || exit 1
clients_json=$(omnl_fineract_fetch_all_clients_pageitems)
n=$(echo "$clients_json" | jq '.pageItems | length')
echo "clients=$n" >&2
echo "$clients_json" | jq -r '.pageItems[] | [(.id|tostring), (.accountNo // ""), (.externalId // ""), (.displayName // .firstname // "")] | @tsv'

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# OMNL Fineract — Create Office for Bank Kanaya (Indonesia), idempotent by externalId.
# See docs/04-configuration/mifos-omnl-central-bank/BANK_KANAYA_OFFICE_RUNBOOK.md
#
# Usage: from repo root.
# OPENING_DATE=2026-03-17 (default)
# DRY_RUN=1 — print only, no POST.
#
# Requires: curl, jq, OMNL_FINERACT_* in omnl-fineract/.env or .env
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
DRY_RUN="${DRY_RUN:-0}"
OPENING_DATE="${OPENING_DATE:-2026-03-17}"
BANK_KANAYA_EXTERNAL_ID="${BANK_KANAYA_EXTERNAL_ID:-BANK-KANAYA-ID}"
BANK_KANAYA_OFFICE_NAME="${BANK_KANAYA_OFFICE_NAME:-Bank Kanaya}"
PARENT_OFFICE_ID="${PARENT_OFFICE_ID:-1}"
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}")
offices_json=$(curl "${CURL_OPTS[@]}" "${BASE_URL}/offices" 2>/dev/null)
offices_norm=$(echo "$offices_json" | jq -c 'if type == "array" then . else (.pageItems // []) end' 2>/dev/null || echo "[]")
existing_id=$(echo "$offices_norm" | jq -r --arg e "$BANK_KANAYA_EXTERNAL_ID" '.[]? | select(.externalId == $e) | .id' 2>/dev/null | head -1)
if [ -n "$existing_id" ] && [ "$existing_id" != "null" ]; then
echo "Bank Kanaya office already exists: officeId=$existing_id (externalId=$BANK_KANAYA_EXTERNAL_ID)" >&2
echo "OFFICE_ID_BANK_KANAYA=$existing_id"
exit 0
fi
payload=$(jq -n \
--arg name "$BANK_KANAYA_OFFICE_NAME" \
--arg openingDate "$OPENING_DATE" \
--arg externalId "$BANK_KANAYA_EXTERNAL_ID" \
--argjson parentId "$PARENT_OFFICE_ID" \
'{ name: $name, parentId: $parentId, openingDate: $openingDate, externalId: $externalId, dateFormat: "yyyy-MM-dd", locale: "en" }')
if [ "$DRY_RUN" = "1" ]; then
echo "DRY_RUN: would POST /offices Bank Kanaya externalId=$BANK_KANAYA_EXTERNAL_ID" >&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
oid=$(echo "$res" | jq -r '.resourceId // .officeId')
echo "Created Bank Kanaya office: officeId=$oid" >&2
echo "OFFICE_ID_BANK_KANAYA=$oid"
else
echo "Failed to create office: $res" >&2
exit 1
fi

View File

@@ -1,6 +1,9 @@
#!/usr/bin/env bash
# OMNL Fineract — Populate the 15 operating entities as Offices (Organization / Manage Offices).
# Updates office 1 name to entity 1; creates offices 215 as children of office 1 with entity names.
# LEI is not a native Fineract office field; regulator-facing LEI is carried in OMNL_ENTITY_MASTER_DATA.json
# and joined to offices in omnl_transaction_package_snapshot.json (see scripts/omnl/jq/enrich-snapshot-entity-master.jq).
# LEI, EBICS, BIC, etc. may still be entered on the office/entity in the UI using memo or Address2/3-style fields; see OMNL_ENTITY_MASTER_DATA.md (section 2b).
# 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.

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env bash
# OMNL Fineract — Post two journal entries for HYBX-BATCH-001 PvP clearing (Bank Kanaya).
# HO leg: Dr 2410 (Due To Offices) / Cr 2100 (M1) — officeId = HO
# BK leg: Dr 2100 / Cr 1410 (Due From HO) — officeId = Bank Kanaya
#
# Amount: Fineract currency smallest unit (USD cents). Default 1B USD = 100000000000 cents.
#
# Usage:
# DRY_RUN=1 bash scripts/omnl/omnl-pvp-post-clearing-bank-kanaya.sh # print payloads only (default)
# DRY_RUN=0 OFFICE_ID_HO=1 OFFICE_ID_KANAYA=22 bash scripts/omnl/omnl-pvp-post-clearing-bank-kanaya.sh
#
# Prerequisites: GL 1410, 2100, 2410 exist. Run resolve_ids.sh or let script resolve via GET /glaccounts.
# See: docs/04-configuration/mifos-omnl-central-bank/PvP_MULTILATERAL_NET_SETTLEMENT_BANK_KANAYA.md
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
DRY_RUN="${DRY_RUN:-1}"
TRANSACTION_DATE="${TRANSACTION_DATE:-$(date +%Y-%m-%d)}"
OFFICE_ID_HO="${OFFICE_ID_HO:-1}"
OFFICE_ID_KANAYA="${OFFICE_ID_KANAYA:-22}"
# 1,000,000,000.00 USD in cents
AMOUNT_MINOR="${AMOUNT_MINOR_UNITS:-100000000000}"
REF="${REFERENCE_COMMENT:-HYBX-BATCH-001-CLEARING}"
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 -w "\n%{http_code}" -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}")
GL_RAW=$(curl -s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${USER}:${PASS}" "${BASE_URL}/glaccounts")
GL_JSON=$(echo "$GL_RAW" | jq -c 'if type == "array" then . else (.pageItems // []) end' 2>/dev/null || echo "[]")
get_gl_id() {
local code="$1"
echo "$GL_JSON" | jq -r --arg c "$code" '.[]? | select(.glCode == $c) | .id // empty' 2>/dev/null | head -n1
}
ID_1410="$(get_gl_id "1410")"
ID_2100="$(get_gl_id "2100")"
ID_2410="$(get_gl_id "2410")"
if [ -z "$ID_1410" ] || [ -z "$ID_2100" ] || [ -z "$ID_2410" ]; then
if [ "$DRY_RUN" = "1" ]; then
echo "WARN: Could not resolve all GL ids (1410=$ID_1410 2100=$ID_2100 2410=$ID_2410); dry-run uses placeholders." >&2
ID_1410="${ID_1410:-141}"
ID_2100="${ID_2100:-210}"
ID_2410="${ID_2410:-241}"
else
echo "ERROR: Missing GL accounts 1410/2100/2410. Create per OMNL_GL_ACCOUNTS_REQUIRED.md" >&2
exit 1
fi
fi
post_je() {
local office_id="$1"
local debit_id="$2"
local credit_id="$3"
local memo="$4"
local body
body=$(jq -n \
--argjson officeId "$office_id" \
--arg transactionDate "$TRANSACTION_DATE" \
--arg comments "$memo$REF" \
--argjson debitId "$debit_id" \
--argjson creditId "$credit_id" \
--argjson amount "$AMOUNT_MINOR" \
'{ officeId: $officeId, transactionDate: $transactionDate, dateFormat: "yyyy-MM-dd", locale: "en", currencyCode: "USD", comments: $comments, debits: [ { glAccountId: $debitId, amount: $amount } ], credits: [ { glAccountId: $creditId, amount: $amount } ] }')
if [ "$DRY_RUN" = "1" ]; then
echo "DRY_RUN JE: office=$office_id Dr=$debit_id Cr=$credit_id amount_minor=$AMOUNT_MINOR" >&2
echo "$body" | jq .
return 0
fi
local out code resp
out=$(curl "${CURL_OPTS[@]}" -X POST -d "$body" "${BASE_URL}/journalentries" 2>/dev/null)
code=$(echo "$out" | tail -n1)
resp=$(echo "$out" | sed '$d')
if [ "$code" = "200" ] || [ "${code:0:1}" = "2" ]; then
echo "OK $memo HTTP $code" >&2
echo "$resp" | jq . 2>/dev/null || echo "$resp"
else
echo "FAIL $memo HTTP $code: $resp" >&2
return 1
fi
}
echo "HYBX-BATCH-001 PvP clearing | HO office=$OFFICE_ID_HO Kanaya office=$OFFICE_ID_KANAYA | amount_minor=$AMOUNT_MINOR | DRY_RUN=$DRY_RUN" >&2
post_je "$OFFICE_ID_HO" "$ID_2410" "$ID_2100" "PvP HO Dr2410 Cr2100"
post_je "$OFFICE_ID_KANAYA" "$ID_2100" "$ID_1410" "PvP Kanaya Dr2100 Cr1410"
echo "Done." >&2

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# OMNL — Build omnl_transaction_package_snapshot.json for Volume A Section 2 (GET offices + glaccounts).
# Enriches each office with registry LEI / entity name from OMNL_ENTITY_MASTER_DATA.json (offices model;
# Fineract does not store LEI on the office resource).
# Usage: OUT_DIR=. bash scripts/omnl/omnl-transaction-package-snapshot.sh
# Writes: $OUT_DIR/omnl_transaction_package_snapshot.json (default REPO_ROOT)
# ENTITY_DATA=path/to/OMNL_ENTITY_MASTER_DATA.json (optional; default under docs/.../mifos-omnl-central-bank/)
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
OUT_DIR="${OUT_DIR:-$REPO_ROOT}"
OUT_FILE="${OUT_FILE:-$OUT_DIR/omnl_transaction_package_snapshot.json}"
ENTITY_DATA="${ENTITY_DATA:-${REPO_ROOT}/docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json}"
ENRICH_JQ="${REPO_ROOT}/scripts/omnl/jq/enrich-snapshot-entity-master.jq"
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 for live snapshot." >&2
exit 1
fi
command -v curl >/dev/null && command -v jq >/dev/null || { echo "Need curl and jq" >&2; exit 1; }
AUTH="${USER}:${PASS}"
CURL_OPTS=(-s -S -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "$AUTH")
api_get() { curl "${CURL_OPTS[@]}" "${BASE_URL}/${1}"; }
OFFICES=$(api_get "offices")
GL=$(api_get "glaccounts")
OFFICES_N=$(echo "$OFFICES" | jq -c 'if type == "array" then . elif .pageItems != null then .pageItems else [] end')
GL_N=$(echo "$GL" | jq -c 'if type == "array" then . elif .pageItems != null then .pageItems else [] end')
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
TMP_OUT="${OUT_FILE}.tmp.$$"
jq -n \
--argjson offices "$OFFICES_N" \
--argjson glaccounts "$GL_N" \
--arg gen "$NOW" \
--arg base "$BASE_URL" \
'{
snapshotMeta: {
documentId: "OMNL-TRANSACTION-PACKAGE-SNAPSHOT",
omnlLegalName: "ORGANISATION MONDIALE DU NUMERIQUE L.P.B.C.",
omnlLei: "98450070C57395F6B906",
omnlLeiReferenceUrl: "https://lei.info/98450070C57395F6B906",
omnlDirectorsAndOfficersDoc: "Appendix/OMNL_BANKING_DIRECTORS_AND_LEI.md",
generatedAtUtc: $gen,
settlementRef: "HYBX-BATCH-001",
valueDate: "2026-03-17",
beneficiary: "Bank Kanaya (Indonesia)",
beneficiaryOfficeId: 22,
beneficiaryExternalId: "BANK-KANAYA-ID",
amountUsd: "1000000000.00",
currency: "USD",
source: "live-api",
apiBaseUrl: $base
},
offices: $offices,
glAccounts: $glaccounts
}' > "$TMP_OUT"
if [ -f "$ENTITY_DATA" ] && [ -f "$ENRICH_JQ" ]; then
jq --argjson master "$(jq -c . "$ENTITY_DATA")" -f "$ENRICH_JQ" "$TMP_OUT" > "$OUT_FILE"
rm -f "$TMP_OUT"
else
mv "$TMP_OUT" "$OUT_FILE"
fi
echo "Wrote $OUT_FILE" >&2

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Patch INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.json with SHA-256 of counsel memo and audit PDFs
# (after they are placed in SUBREG or any local path). Then rebuild: scripts/omnl/build-transaction-package-zip.sh
#
# Usage:
# COUNSEL_PDF=/path/to/counsel-memo.pdf AUDIT_PDF=/path/to/audit-report.pdf \
# bash scripts/omnl/patch-attestation-subreg-pdf-hashes.sh
#
# Optional:
# ATTESTATION_JSON=docs/04-configuration/mifos-omnl-central-bank/INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.json
# NOW_UTC=$(date -u +%Y-%m-%dT%H:%M:%SZ) — defaults to date -u
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
ATTESTATION_JSON="${ATTESTATION_JSON:-${REPO_ROOT}/docs/04-configuration/mifos-omnl-central-bank/INSTITUTIONAL_PACKAGE_SCORE_ATTESTATION_4_995.json}"
: "${COUNSEL_PDF:?Set COUNSEL_PDF to counsel memo PDF path}"
: "${AUDIT_PDF:?Set AUDIT_PDF to independent audit report PDF path}"
[ -f "$COUNSEL_PDF" ] || { echo "Not a file: $COUNSEL_PDF" >&2; exit 1; }
[ -f "$AUDIT_PDF" ] || { echo "Not a file: $AUDIT_PDF" >&2; exit 1; }
[ -f "$ATTESTATION_JSON" ] || { echo "Not a file: $ATTESTATION_JSON" >&2; exit 1; }
command -v jq >/dev/null || { echo "jq required" >&2; exit 1; }
C_HASH=$(sha256sum "$COUNSEL_PDF" | awk '{print $1}')
A_HASH=$(sha256sum "$AUDIT_PDF" | awk '{print $1}')
NOW_UTC="${NOW_UTC:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
TMP=$(mktemp)
jq --arg c "$C_HASH" --arg a "$A_HASH" --arg t "$NOW_UTC" \
'.legalFinality.counselMemoPdfSha256 = $c
| .legalFinality.counselMemoDateUtc = $t
| .legalFinality.counselMemoBindingNote = ("SHA-256 of SUBREG counsel memo PDF: " + $c)
| .independentAudit.reportPdfSha256 = $a
| .independentAudit.reportDateUtc = $t
| .independentAudit.reportBindingNote = ("SHA-256 of SUBREG independent audit report PDF: " + $a)
' "$ATTESTATION_JSON" > "$TMP"
mv "$TMP" "$ATTESTATION_JSON"
echo "Updated $ATTESTATION_JSON" >&2
echo " counselMemoPdfSha256=$C_HASH" >&2
echo " reportPdfSha256=$A_HASH" >&2
echo "Rebuild: bash scripts/omnl/build-transaction-package-zip.sh" >&2

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Fast CI smoke: small ledger (10×100M USD), no Section 2 snapshot, build zip, verify + structural 4.995 check.
# Usage: from repo root. No Fineract required. Unset TSA_URL for deterministic CI unless you intend to hit a TSA.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
cd "$REPO_ROOT"
unset TSA_URL 2>/dev/null || true
export ALLOW_MISSING_OMNL_SNAPSHOT=1
export HYBX_LEDGER_FILE="${HYBX_LEDGER_FILE:-${REPO_ROOT}/scripts/omnl/fixtures/hybx_batch_001_ledger_ci.csv}"
export EVIDENCE_GENERATED_AT_UTC="${EVIDENCE_GENERATED_AT_UTC:-2026-03-24T12:00:00Z}"
OUT_ZIP="${OUT_ZIP:-/tmp/tp-ci-$$.zip}"
export OUT_ZIP
UDIR=$(mktemp -d /tmp/tp-ci-unzip-XXXXXX)
cleanup() { rm -rf "$UDIR"; rm -f "$OUT_ZIP"; }
trap cleanup EXIT
bash scripts/omnl/build-transaction-package-zip.sh
unzip -q "$OUT_ZIP" -d "$UDIR"
bash scripts/omnl/check-transaction-package-4995-readiness.sh "$UDIR"
echo "CI smoke OK: built zip, commitment + structural 4.995 checks passed." >&2

View File

@@ -37,4 +37,29 @@ else
echo "SKIP: shellcheck not installed" >&2
fi
if command -v python3 >/dev/null 2>&1; then
python3 -m py_compile \
scripts/omnl/generate-transaction-package-evidence.py \
scripts/omnl/verify-transaction-package-commitment.py 2>/dev/null \
&& echo "PASS: py_compile transaction-package scripts" >&2 \
|| { echo "FAIL: py_compile transaction-package scripts" >&2; fail=1; }
else
echo "SKIP: python3 not installed" >&2
fi
for sh in \
scripts/omnl/build-transaction-package-zip.sh \
scripts/omnl/patch-attestation-subreg-pdf-hashes.sh \
scripts/omnl/apply-qes-tsa-to-staging.sh \
scripts/omnl/check-transaction-package-4995-readiness.sh \
scripts/omnl/omnl-transaction-package-snapshot.sh \
scripts/omnl/omnl-pvp-post-clearing-bank-kanaya.sh \
scripts/omnl/omnl-office-create-bank-kanaya.sh \
scripts/omnl/run-transaction-package-ci-smoke.sh
do
if [ -f "$sh" ]; then
bash -n "$sh" 2>/dev/null && echo "PASS: bash -n $sh" >&2 || { echo "FAIL: bash -n $sh" >&2; fail=1; }
fi
done
exit $fail

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Recompute content commitment vs 00_Cover/HASH_NOTARIZATION_ANCHOR.txt (matches build-transaction-package-zip.sh)."""
from __future__ import annotations
import hashlib
import os
import re
import sys
EXCLUDED_EXACT = frozenset(
{
"./00_Cover/HASH_NOTARIZATION_ANCHOR.txt",
"./00_Cover/audit_and_hashes.txt",
"./00_Cover/audit_manifest.json",
}
)
EXCLUDED_BASENAMES = frozenset(
{
"TSA_RFC3161_REQUEST.tsq",
"TSA_RFC3161_RESPONSE.tsr",
"TSA_RFC3161_RESPONSE.txt",
"TSA_RFC3161_VERIFY.txt",
"QES_CMS_ANCHOR_DETACHED.p7s",
"QES_CMS_VERIFY_LOG.txt",
}
)
def posix_rel(package_root: str, full_path: str) -> str:
rel = os.path.relpath(full_path, package_root).replace(os.sep, "/")
return rel if rel.startswith("./") else "./" + rel
def excluded(rel_posix: str) -> bool:
if rel_posix in EXCLUDED_EXACT:
return True
return os.path.basename(rel_posix) in EXCLUDED_BASENAMES
def recompute(package_root: str) -> str:
lines: list[str] = []
for dirpath, dirnames, filenames in os.walk(package_root):
dirnames.sort()
filenames.sort()
for fn in filenames:
if fn == ".DS_Store":
continue
full = os.path.join(dirpath, fn)
if not os.path.isfile(full):
continue
rel = posix_rel(package_root, full)
if excluded(rel):
continue
h = hashlib.sha256()
with open(full, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
lines.append(f"{h.hexdigest().lower()}\t{rel}")
lines.sort(key=lambda s: s.encode("utf-8"))
return hashlib.sha256(("\n".join(lines) + "\n").encode("utf-8")).hexdigest().lower()
def main() -> int:
if len(sys.argv) != 2:
print("Usage: verify-transaction-package-commitment.py <unzipped-root>", file=sys.stderr)
return 2
root = os.path.abspath(sys.argv[1])
anchor = os.path.join(root, "00_Cover", "HASH_NOTARIZATION_ANCHOR.txt")
if not os.path.isfile(anchor):
print(f"ERROR: missing {anchor}", file=sys.stderr)
return 1
text = open(anchor, encoding="utf-8").read()
m = re.search(r"CONTENT COMMITMENT \(SHA-256, hex\):\s*([0-9a-fA-F]{64})", text)
if not m:
print("ERROR: bad anchor", file=sys.stderr)
return 1
exp = m.group(1).lower()
got = recompute(root)
if exp != got:
print(f"MISMATCH anchor={exp}\n actual={got}", file=sys.stderr)
return 1
print(f"OK: {got}")
return 0
if __name__ == "__main__":
sys.exit(main())