/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { Component, OnInit, inject } from '@angular/core'; import { DecimalPipe } from '@angular/common'; import { RouterLink } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { I4cService, i4cErrorMessage } from '../i4c.service'; /** * Integration health at a glance, and the API performance figures the RBI letter (05 Dec 2025, para 4) asks to be * reviewed periodically: calls, failures and response times in each direction. */ @Component({ selector: 'mifosx-i4c-dashboard', templateUrl: './i4c-dashboard.component.html', styleUrls: ['./i4c-dashboard.component.scss'], imports: [ DecimalPipe, RouterLink, MatButtonModule, MatProgressBarModule, FaIconComponent ] }) export class I4cDashboardComponent implements OnInit { private i4c = inject(I4cService); data: any = null; loading = false; error = ''; showConfigModal = false; copiedKey = ''; ngOnInit(): void { this.load(); } load(): void { this.loading = true; this.error = ''; this.i4c.dashboard().subscribe({ next: (d) => { this.data = d; this.loading = false; }, error: (e) => { this.error = i4cErrorMessage(e); this.loading = false; } }); } copyText(text: string, key: string): void { if (!text) { return; } navigator.clipboard.writeText(text); this.copiedKey = key; setTimeout(() => { if (this.copiedKey === key) { this.copiedKey = ''; } }, 2000); } statusClass(status: string): string { switch (status) { case 'PROCESSED': return 'i4c-badge--green'; case 'RETRY': return 'i4c-badge--red'; case 'RECEIVED': case 'PROCESSING': return 'i4c-badge--amber'; default: return ''; } } /** Success rate of calls in one direction, as a percentage; null when there were none. */ successRate(row: any): number | null { const calls = Number(row?.calls || 0); return calls === 0 ? null : (Number(row?.succeeded || 0) * 100) / calls; } /** What still stands between this bank and live traffic, worked out from the configuration. */ get setupGaps(): string[] { const c = this.data?.configuration; if (!c) { return []; } const gaps: string[] = []; if (!c.enabled) { gaps.push('Integration is switched off (fineract.i4c.enabled=false): I4C calls are refused with 503.'); } if (!c.inboundCredentialsConfigured) { gaps.push('No inbound username/password set for I4C to obtain a token.'); } else if (!c.inboundPasswordHashed) { gaps.push('Inbound password is configured in plain text; set the bcrypt hash for production.'); } if (!c.allowedIpCount) { gaps.push('No I4C source IP allowlist: any address may call (UAT only).'); } if (c.cryptoMode !== 'NONE' && !c.cryptoPassphraseConfigured) { gaps.push('Encryption mode is ' + c.cryptoMode + ' but no passphrase is configured.'); } if (!c.outboundConfigured) { gaps.push('I4C response URL is not configured: replies wait in the outbox.'); } if (!c.bankCode) { gaps.push("This bank's I4C bank code is not set (fineract.i4c.bank-code); root_bankid is sent as 0."); } if (c.simulatorEnabled) { gaps.push('The local I4C simulator is enabled; switch it off in production.'); } return gaps; } }