/** * 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/. */ /** Angular Imports */ import { Component, OnInit, inject, ViewChild } from '@angular/core'; import { MatTableModule, MatTableDataSource } from '@angular/material/table'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatPaginatorModule, MatPaginator } from '@angular/material/paginator'; import { CommonModule } from '@angular/common'; /** Custom Services */ import { ClientsService } from '../../clients/clients.service'; @Component({ selector: 'mifosx-aml-rules', templateUrl: './aml-rules.component.html', styleUrls: ['./aml-rules.component.scss'], standalone: true, imports: [ CommonModule, MatTableModule, MatCardModule, MatIconModule, MatButtonModule, MatCheckboxModule, MatPaginatorModule ] }) export class AmlRulesComponent implements OnInit { private clientsService = inject(ClientsService); rules = new MatTableDataSource([]); displayedColumns: string[] = [ 'orderPosition', 'ruleName', 'riskCategory', 'limitBasis', 'minLimit', 'maxLimit', 'appliesOver', 'reason', 'amlCheckStatus', 'bannedEntityMatch' ]; /** Every rule as returned by the API, before the period filter is applied. */ private allRules: any[] = []; /** Period buckets the rules can be separated into. */ readonly periodFilters: { key: string; label: string }[] = [ { key: '', label: 'All rules' }, { key: 'TXN', label: 'Per transaction' }, { key: 'DAY', label: 'Daily' }, { key: 'WEEK', label: 'Weekly' }, { key: 'MONTH', label: 'Monthly' }, { key: 'OTHER', label: 'Other window' } ]; activePeriod = ''; @ViewChild(MatPaginator) paginator!: MatPaginator; ngOnInit(): void { this.clientsService.getClientRiskRules().subscribe({ next: (data) => { this.allRules = data || []; this.applyPeriodFilter(); this.rules.paginator = this.paginator; }, error: () => {} }); } setPeriod(key: string): void { this.activePeriod = key; this.applyPeriodFilter(); } private applyPeriodFilter(): void { this.rules.data = this.activePeriod ? this.allRules.filter((r) => this.periodKey(r) === this.activePeriod) : this.allRules; if (this.rules.paginator) { this.rules.paginator.firstPage(); } } /** How many rules fall in each period bucket — shown on the filter chips. */ periodCount(key: string): number { return key ? this.allRules.filter((r) => this.periodKey(r) === key).length : this.allRules.length; } /** * Buckets a rule by the observation window it is measured over. * A rule with no window is evaluated on the single transaction itself, so it is * in force every day rather than accumulating over a period. */ periodKey(rule: any): string { const days = rule.txnWindowDays; if (days == null || days === 0) { return 'TXN'; } if (days === 1) { return 'DAY'; } if (days === 7) { return 'WEEK'; } if (days >= 28 && days <= 31) { return 'MONTH'; } return 'OTHER'; } periodLabel(rule: any): string { const days = rule.txnWindowDays; switch (this.periodKey(rule)) { case 'TXN': return 'Per transaction'; case 'DAY': return 'Daily'; case 'WEEK': return 'Weekly'; case 'MONTH': return 'Monthly'; default: return `${days} days`; } } periodClass(rule: any): string { return 'period-badge period-' + this.periodKey(rule).toLowerCase(); } /** Sub-caption telling the reader what the min/max figures actually measure. */ getLimitBasis(rule: any): string { if (rule.minMonthlyIncome > 0 || rule.maxMonthlyIncome > 0) { return 'Monthly Income'; } if (rule.minAnnualIncome > 0 || rule.maxAnnualIncome > 0) { return 'Annual Income'; } if (rule.minAnnualTurnover > 0 || rule.maxAnnualTurnover > 0) { return 'Annual Turnover'; } if (rule.txnThresholdAmountMediumRisk != null || rule.txnThresholdAmountHighRisk != null) { return 'Txn Amount (tiered)'; } if (rule.txnThresholdAmount != null) { return 'Txn Amount'; } if (rule.occupationName) { return 'Occupation'; } if (rule.occupationCategoryName) { return 'Occupation Category'; } return 'Behavioural pattern'; } private inr(n: number): string { return '₹' + Number(n).toLocaleString('en-IN'); } /** Lower bound of the rule, or an em dash where the criterion is not numeric. */ getMinLimit(rule: any): string { if (rule.minMonthlyIncome > 0) { return this.inr(rule.minMonthlyIncome); } if (rule.minAnnualIncome > 0) { return this.inr(rule.minAnnualIncome); } if (rule.minAnnualTurnover > 0) { return this.inr(rule.minAnnualTurnover); } // Declarative band from m_aml_transaction_rule takes precedence over the raw threshold. if (rule.txnMinLimitAmount != null) { return this.inr(rule.txnMinLimitAmount); } if (rule.txnThresholdAmount != null) { return this.inr(rule.txnThresholdAmount); } return '—'; } /** Upper bound, or "No upper limit" where the rule is genuinely open-ended. */ getMaxLimit(rule: any): string { if (rule.maxMonthlyIncome > 0) { return this.inr(rule.maxMonthlyIncome); } if (rule.maxAnnualIncome > 0) { return this.inr(rule.maxAnnualIncome); } if (rule.maxAnnualTurnover > 0) { return this.inr(rule.maxAnnualTurnover); } if (rule.txnMaxLimitAmount != null) { return this.inr(rule.txnMaxLimitAmount); } // A floor with no configured ceiling means the rule has no upper bound. if ( rule.minMonthlyIncome > 0 || rule.minAnnualIncome > 0 || rule.minAnnualTurnover > 0 || rule.txnThresholdAmount != null ) { return 'No upper limit'; } return '—'; } /** Occupation-driven rules carry no amount; show the criterion instead. */ getNonNumericCriterion(rule: any): string { return rule.occupationName || rule.occupationCategoryName || ''; } getRiskCategoryLabel(category: number): string { switch (category) { case 1: return 'Low Risk'; case 2: return 'Medium Risk'; case 3: return 'High Risk'; default: return 'Unknown'; } } getRiskColorClass(category: number): string { switch (category) { case 1: return 'risk-low'; case 2: return 'risk-medium'; case 3: return 'risk-high'; default: return ''; } } }