Files
cbsnexora/src/app/rbi-circulars/aml-rules/add-aml-rule-dialog/add-aml-rule-dialog.component.ts
T
keshav 7b895a0e2b
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
feat: add KYC document renewal alerts, Aadhaar/PAN exceptions, and AML recurring alerts UI; remove old journal AML marking
2026-08-11 11:11:18 +05:30

196 lines
5.6 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, inject } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { CommonModule } from '@angular/common';
/** Custom Services */
import { ClientsService } from '../../../clients/clients.service';
/**
* Authoring form for a new AML catalog rule.
*
* The screen the officer came from renders one flat table, but a rule is stored across two
* tables: the profile rule in m_client_risk_rule and, when a transaction band or observation
* window is given, an evaluable counterpart in m_aml_transaction_rule. That split is the
* backend's concern — this form stays shaped like the table it was opened from.
*/
@Component({
selector: 'mifosx-add-aml-rule-dialog',
templateUrl: './add-aml-rule-dialog.component.html',
styleUrls: ['./add-aml-rule-dialog.component.scss'],
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatButtonModule,
MatIconModule,
MatProgressSpinnerModule
]
})
export class AddAmlRuleDialogComponent {
private formBuilder = inject(FormBuilder);
private clientsService = inject(ClientsService);
private dialogRef = inject(MatDialogRef<AddAmlRuleDialogComponent>);
submitting = false;
/** Server-side rejection, surfaced in the dialog so the officer keeps their input. */
errorMessage = '';
readonly riskCategories = [
{ value: 3, label: 'High Risk' },
{ value: 2, label: 'Medium Risk' },
{ value: 1, label: 'Low Risk' }
];
/** Mirrors the "Applies Over" column: window in days, per-transaction being no window. */
readonly windowOptions = [
{ value: 0, label: 'Per transaction' },
{ value: 1, label: 'Daily' },
{ value: 7, label: 'Weekly' },
{ value: 30, label: 'Monthly' }
];
readonly ruleTypes = [
'THRESHOLD',
'STRUCTURING',
'VELOCITY'
];
readonly appliesToOptions = [
'ALL',
'SAVINGS',
'LOAN',
'TRANSFER'
];
readonly amlCheckStatuses = [
'CLEAR',
'FLAGGED',
'PENDING'
];
readonly yesNo = [
'NO',
'YES'
];
ruleForm = this.formBuilder.group({
ruleName: [
'',
[
Validators.required,
Validators.maxLength(100)
]
],
reason: [
'',
[
Validators.required,
Validators.maxLength(500)
]
],
riskCategory: [
3,
Validators.required
],
txnWindowDays: [0],
txnRuleType: ['THRESHOLD'],
txnAppliesTo: ['ALL'],
txnMinLimitAmount: [
null as
| number
| null,
Validators.min(0)
],
txnMaxLimitAmount: [
null as
| number
| null,
Validators.min(0)
],
amlCheckStatus: ['FLAGGED'],
bannedEntityMatch: ['NO'],
willfulDefaulter: ['NO'],
ecgcSalFlag: ['NO'],
dormantFlag: ['ACTIVE']
});
/** Mirrors the backend band check so the officer is told before the round trip. */
get bandInverted(): boolean {
const min = this.ruleForm.value.txnMinLimitAmount;
const max = this.ruleForm.value.txnMaxLimitAmount;
return min != null && max != null && Number(max) < Number(min);
}
submit(): void {
if (this.ruleForm.invalid || this.bandInverted) {
this.ruleForm.markAllAsTouched();
return;
}
this.submitting = true;
this.errorMessage = '';
this.clientsService.createClientRiskRule(this.buildPayload()).subscribe({
next: (rule) => {
this.submitting = false;
this.dialogRef.close(rule);
},
error: (err) => {
this.submitting = false;
this.errorMessage = this.readError(err);
}
});
}
cancel(): void {
this.dialogRef.close();
}
private buildPayload(): any {
const v = this.ruleForm.value;
const amount = (n: any) => (n === null || n === undefined || n === '' ? null : Number(n));
return {
ruleName: (v.ruleName || '').trim(),
reason: (v.reason || '').trim(),
riskCategory: v.riskCategory,
amlCheckStatus: v.amlCheckStatus,
bannedEntityMatch: v.bannedEntityMatch,
willfulDefaulter: v.willfulDefaulter,
ecgcSalFlag: v.ecgcSalFlag,
dormantFlag: v.dormantFlag,
// A window of 0 means per-transaction, which the backend stores as a null window rather
// than a zero-day one.
txnWindowDays: v.txnWindowDays ? v.txnWindowDays : null,
txnRuleType: v.txnRuleType,
txnAppliesTo: v.txnAppliesTo,
txnMinLimitAmount: amount(v.txnMinLimitAmount),
txnMaxLimitAmount: amount(v.txnMaxLimitAmount)
};
}
/** Fineract returns field-level failures under errors[]; fall back to the envelope message. */
private readError(err: any): string {
const body = err?.error;
const first = body?.errors?.[0];
return (
first?.defaultUserMessage || first?.developerMessage || body?.defaultUserMessage || 'Could not save the rule.'
);
}
}