+
Office ID
{profile?.officeId ?? 24}
-
Default currency
-
{profile?.settlement?.defaultCurrency ?? 'USD'}
+
M1 circulating (GL 2100)
+
+ {formatUsd(moneySupply?.M1?.circulating)}
+
+
+
+
Due from HO (GL 1410)
+
+ {formatUsd(moneySupply?.interoffice?.dueFromHeadOffice)}
+
M2 tradable tokens
@@ -49,6 +58,12 @@ export default function Office24Dashboard() {
+ {moneySupply?.fineractLive === false && (
+
+ No Fineract journals for Office 24 yet. Post opening M1: Dr 1410 / Cr 2100 via{' '}
+ scripts/deployment/seed-office-24-opening-journal.mjs
+
+ )}
Settlement rails
diff --git a/frontend-dapp/src/features/office24/useOffice24.ts b/frontend-dapp/src/features/office24/useOffice24.ts
index 2011711..b99ee72 100644
--- a/frontend-dapp/src/features/office24/useOffice24.ts
+++ b/frontend-dapp/src/features/office24/useOffice24.ts
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { DBIS_EXCHANGE_URL, SETTLEMENT_MIDDLEWARE_URL, type DexToken } from '../../config/dex';
+import type { MoneySupply } from '../central-bank/useCentralBank';
export type M2Token = DexToken & {
moneyLayer?: string;
@@ -13,6 +14,7 @@ export function useOffice24() {
const [office, setOffice] = useState | null>(null);
const [tokens, setTokens] = useState([]);
const [health, setHealth] = useState | null>(null);
+ const [moneySupply, setMoneySupply] = useState(null);
const [loading, setLoading] = useState(true);
const settlement = SETTLEMENT_MIDDLEWARE_URL.replace(/\/$/, '');
@@ -21,14 +23,18 @@ export function useOffice24() {
const refresh = useCallback(async () => {
setLoading(true);
try {
- const [o, t, h] = await Promise.all([
+ const [o, t, h, ms] = await Promise.all([
fetch(`${settlement}/api/v1/settlement/office`).then((r) => r.json()),
fetch(`${settlement}/api/v1/settlement/tokens/m2`).then((r) => r.json()),
fetch(`${exchange}/api/v1/exchange/health`).then((r) => r.json()),
+ fetch(`${settlement}/api/v1/settlement/money-supply/public`).then((r) =>
+ r.ok ? r.json() : null,
+ ),
]);
setOffice(o);
setTokens(Array.isArray(t.tokens) ? t.tokens : []);
setHealth(h);
+ setMoneySupply(ms);
} finally {
setLoading(false);
}
@@ -38,5 +44,5 @@ export function useOffice24() {
refresh();
}, [refresh]);
- return { office, tokens, health, loading, refresh };
+ return { office, tokens, health, moneySupply, loading, refresh };
}
diff --git a/frontend-dapp/src/utils/formatMoney.ts b/frontend-dapp/src/utils/formatMoney.ts
new file mode 100644
index 0000000..748133e
--- /dev/null
+++ b/frontend-dapp/src/utils/formatMoney.ts
@@ -0,0 +1,11 @@
+/** Format Fineract USD balance strings for dashboard display */
+export function formatUsd(value: string | number | undefined | null): string {
+ if (value === undefined || value === null || value === '') return '—';
+ const n = typeof value === 'number' ? value : parseFloat(String(value));
+ if (!Number.isFinite(n)) return '—';
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ maximumFractionDigits: 2,
+ }).format(n);
+}
diff --git a/scripts/deployment/deploy-omnl-bank-production.sh b/scripts/deployment/deploy-omnl-bank-production.sh
index ad1bf4f..8bd9c13 100644
--- a/scripts/deployment/deploy-omnl-bank-production.sh
+++ b/scripts/deployment/deploy-omnl-bank-production.sh
@@ -73,6 +73,12 @@ start_svc() {
DBIS_EXCHANGE_CONFIG="$DBIS_EXCHANGE_CONFIG" \
OMNL_SUPPORTED_CHAINS_CONFIG="$OMNL_SUPPORTED_CHAINS_CONFIG" \
OMNL_M2_TOKEN_REGISTRY="$OMNL_M2_TOKEN_REGISTRY" \
+ OMNL_API_KEY="${OMNL_API_KEY:-}" \
+ OMNL_FINERACT_BASE_URL="${OMNL_FINERACT_BASE_URL:-${FINERACT_BASE_URL:-}}" \
+ OMNL_FINERACT_TENANT="${OMNL_FINERACT_TENANT:-${FINERACT_TENANT:-omnl}}" \
+ OMNL_FINERACT_USERNAME="${OMNL_FINERACT_USERNAME:-}" \
+ OMNL_FINERACT_PASSWORD="${OMNL_FINERACT_PASSWORD:-}" \
+ OMNL_PUBLIC_MONEY_SUPPLY="${OMNL_PUBLIC_MONEY_SUPPLY:-1}" \
bash -c "$cmd" >"$LOG_DIR/$name.log" 2>&1 &
echo $! >"$LOG_DIR/$name.pid"
}
@@ -94,6 +100,8 @@ sleep 5
log "Health checks..."
curl -sf "http://127.0.0.1:3011/api/v1/settlement/health" | head -c 200 || true
echo
+curl -sf "http://127.0.0.1:3011/api/v1/settlement/money-supply/public" | head -c 300 || true
+echo
curl -sf "http://127.0.0.1:3012/api/v1/exchange/health" | head -c 200 || true
echo
curl -sf -o /dev/null -w "frontend=%{http_code}\n" "http://127.0.0.1:$FRONTEND_PORT/central-bank"
diff --git a/scripts/deployment/seed-office-24-opening-journal.mjs b/scripts/deployment/seed-office-24-opening-journal.mjs
new file mode 100644
index 0000000..cd368f3
--- /dev/null
+++ b/scripts/deployment/seed-office-24-opening-journal.mjs
@@ -0,0 +1,73 @@
+#!/usr/bin/env node
+/**
+ * Post Office 24 opening M1 journal to Fineract:
+ * Dr 1410 Due From Head Office
+ * Cr 2100 M1 Central Liabilities
+ *
+ * Requires: OMNL_FINERACT_BASE_URL, OMNL_FINERACT_PASSWORD
+ * Amount: OFFICE24_OPENING_M1_USD (required unless --dry-run)
+ */
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const repoRoot = path.resolve(__dirname, '../..');
+const configPath = path.join(repoRoot, 'config/offices/office-24-opening-journal.v1.json');
+
+const dryRun = process.argv.includes('--dry-run');
+const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
+
+const base = (process.env.OMNL_FINERACT_BASE_URL || process.env.FINERACT_BASE_URL || '').replace(/\/$/, '');
+const tenant = process.env.OMNL_FINERACT_TENANT || process.env.FINERACT_TENANT || 'omnl';
+const user =
+ process.env.OMNL_FINERACT_USER?.trim() ||
+ process.env.OMNL_FINERACT_USERNAME?.trim() ||
+ 'ali_hospitallers_tenant';
+const pass = process.env.OMNL_FINERACT_PASSWORD || '';
+const amountRaw = process.env[cfg.amountEnv] || process.env.OFFICE24_OPENING_M1_USD || '';
+const amount = parseFloat(amountRaw);
+
+if (!base || !pass) {
+ console.error('Set OMNL_FINERACT_BASE_URL and OMNL_FINERACT_PASSWORD');
+ process.exit(1);
+}
+if (!Number.isFinite(amount) || amount <= 0) {
+ console.error(`Set ${cfg.amountEnv} to a positive USD amount (e.g. export OFFICE24_OPENING_M1_USD=1000000)`);
+ process.exit(1);
+}
+
+const entry = {
+ officeId: cfg.officeId,
+ transactionDate: new Date().toISOString().slice(0, 10),
+ referenceNumber: cfg.referenceNumber,
+ comments: cfg.comments,
+ debits: [{ glAccountId: parseInt(cfg.debitGlCode, 10), amount }],
+ credits: [{ glAccountId: parseInt(cfg.creditGlCode, 10), amount }],
+};
+
+console.log(JSON.stringify({ dryRun, officeId: cfg.officeId, amount, entry }, null, 2));
+
+if (dryRun) {
+ console.log('Dry run — no journal posted.');
+ process.exit(0);
+}
+
+const auth = Buffer.from(`${user}:${pass}`).toString('base64');
+const res = await fetch(`${base}/journalentries`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Basic ${auth}`,
+ 'Fineract-Platform-TenantId': tenant,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(entry),
+});
+
+const body = await res.text();
+if (!res.ok) {
+ console.error(`Fineract POST failed (${res.status}): ${body}`);
+ process.exit(1);
+}
+
+console.log('Office 24 opening journal posted:', body);
diff --git a/scripts/deployment/start-omnl-banking-portal.sh b/scripts/deployment/start-omnl-banking-portal.sh
index fb0db45..9dd318d 100644
--- a/scripts/deployment/start-omnl-banking-portal.sh
+++ b/scripts/deployment/start-omnl-banking-portal.sh
@@ -20,6 +20,12 @@ start() {
DBIS_EXCHANGE_CONFIG="$DBIS_EXCHANGE_CONFIG" \
OMNL_SUPPORTED_CHAINS_CONFIG="$OMNL_SUPPORTED_CHAINS_CONFIG" \
OMNL_M2_TOKEN_REGISTRY="$OMNL_M2_TOKEN_REGISTRY" \
+ OMNL_API_KEY="${OMNL_API_KEY:-}" \
+ OMNL_FINERACT_BASE_URL="${OMNL_FINERACT_BASE_URL:-}" \
+ OMNL_FINERACT_TENANT="${OMNL_FINERACT_TENANT:-omnl}" \
+ OMNL_FINERACT_USERNAME="${OMNL_FINERACT_USERNAME:-}" \
+ OMNL_FINERACT_PASSWORD="${OMNL_FINERACT_PASSWORD:-}" \
+ OMNL_PUBLIC_MONEY_SUPPLY="${OMNL_PUBLIC_MONEY_SUPPLY:-1}" \
bash -c "$cmd" >"${LOG_DIR}/${name}.log" 2>&1 &
echo $! >"${LOG_DIR}/${name}.pid"
}
diff --git a/services/settlement-middleware/dist/api/routes/settlement.js b/services/settlement-middleware/dist/api/routes/settlement.js
index afae8be..cb4a656 100644
--- a/services/settlement-middleware/dist/api/routes/settlement.js
+++ b/services/settlement-middleware/dist/api/routes/settlement.js
@@ -10,6 +10,7 @@ const transfer_1 = require("../../workflows/transfer");
const office_scope_1 = require("../../workflows/office-scope");
const settlement_store_1 = require("../../store/settlement-store");
const swift_inbound_1 = require("../../workflows/swift-inbound");
+const fineract_1 = require("../../adapters/fineract");
function requireApiKey(req, res) {
const cfg = (0, config_1.loadConfig)();
if (!cfg.production.requireApiKey)
@@ -54,6 +55,29 @@ function createSettlementRouter() {
storePath: `data/settlement-store/office-${officeId}`,
});
});
+ router.get('/money-supply/public', async (_req, res) => {
+ const cfg = (0, config_1.loadConfig)();
+ const allow = cfg.production.onlineBankMode || process.env.OMNL_PUBLIC_MONEY_SUPPLY === '1';
+ if (!allow) {
+ res.status(404).json({ error: 'Public money supply disabled' });
+ return;
+ }
+ try {
+ const balances = await (0, fineract_1.fetchGlBalances)(officeId);
+ const snapshot = (0, settlement_core_1.buildMoneySupplySnapshot)(officeId, balances);
+ res.json({
+ ...snapshot,
+ fineractLive: Object.keys(balances).length > 0,
+ interoffice: {
+ dueFromHeadOffice: balances['1410'] ?? '0',
+ gl1410: balances['1410'] ?? '0',
+ },
+ });
+ }
+ catch (e) {
+ res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
+ }
+ });
router.get('/money-supply', async (req, res) => {
if (!requireApiKey(req, res))
return;
diff --git a/services/settlement-middleware/src/api/routes/settlement.ts b/services/settlement-middleware/src/api/routes/settlement.ts
index 3937ab2..336352e 100644
--- a/services/settlement-middleware/src/api/routes/settlement.ts
+++ b/services/settlement-middleware/src/api/routes/settlement.ts
@@ -1,6 +1,6 @@
import { Router, type Request, type Response } from 'express';
import type { ExternalTransferRequest, TradeFinanceInstrument, TokenLoadRequest, TransferRequest } from '@dbis/settlement-core';
-import { loadOmnlChainRegistry, getActiveChains } from '@dbis/settlement-core';
+import { buildMoneySupplySnapshot, loadOmnlChainRegistry, getActiveChains } from '@dbis/settlement-core';
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../../config';
import {
getMoneySupply,
@@ -8,9 +8,11 @@ import {
processExternalTransfer,
} from '../../workflows/external-transfer';
import { processTokenLoad } from '../../workflows/token-load';
-import { processTransfer } from '../../workflows/transfer';import { bindOffice24 } from '../../workflows/office-scope';
+import { processTransfer } from '../../workflows/transfer';
+import { bindOffice24 } from '../../workflows/office-scope';
import { settlementStore } from '../../store/settlement-store';
import { processSwiftInbound } from '../../workflows/swift-inbound';
+import { fetchGlBalances } from '../../adapters/fineract';
function requireApiKey(req: Request, res: Response): boolean {
const cfg = loadConfig();
@@ -57,6 +59,30 @@ export function createSettlementRouter(): Router {
});
});
+ router.get('/money-supply/public', async (_req, res) => {
+ const cfg = loadConfig();
+ const allow =
+ cfg.production.onlineBankMode || process.env.OMNL_PUBLIC_MONEY_SUPPLY === '1';
+ if (!allow) {
+ res.status(404).json({ error: 'Public money supply disabled' });
+ return;
+ }
+ try {
+ const balances = await fetchGlBalances(officeId);
+ const snapshot = buildMoneySupplySnapshot(officeId, balances);
+ res.json({
+ ...snapshot,
+ fineractLive: Object.keys(balances).length > 0,
+ interoffice: {
+ dueFromHeadOffice: balances['1410'] ?? '0',
+ gl1410: balances['1410'] ?? '0',
+ },
+ });
+ } catch (e) {
+ res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
+ }
+ });
+
router.get('/money-supply', async (req, res) => {
if (!requireApiKey(req, res)) return;
try {