Files
cbsnexora/src/app/rbi-circulars/aml-rules/aml-rules.component.ts
T

250 lines
6.9 KiB
TypeScript

/**
* 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<any>([]);
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 '';
}
}
}