feat: add KYC document renewal alerts, Aadhaar/PAN exceptions, and AML recurring alerts UI; remove old journal AML marking
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

This commit is contained in:
2026-08-11 11:11:18 +05:30
parent 01e86173c1
commit 7b895a0e2b
77 changed files with 10046 additions and 1130 deletions
+47 -6
View File
@@ -539,10 +539,14 @@ export class FiuComponent implements OnInit {
: 'Suspicious — STR filed with FIU-IND and account escalated.';
}
/**
* Always fetches every status. The triage table and the cleared register are two views of the
* same set, so filtering server-side would empty one whenever the other was narrowed — and the
* CTR tab reads this list too, which a status filter chosen on the STR tab should not touch.
* amlAlertStatusFilter is applied client-side in {@link amlSuspiciousAlerts} instead.
*/
loadAmlAlerts(): void {
this.amlService
.getAlerts(this.amlAlertStatusFilter || undefined)
.subscribe((data) => (this.amlAlerts = data || []));
this.amlService.getAlerts().subscribe((data) => (this.amlAlerts = data || []));
}
loadAmlCtrReports(): void {
@@ -558,8 +562,25 @@ export class FiuComponent implements OnInit {
return (err: any) => this.toast(err?.error?.defaultUserMessage || `${action} failed — please retry`);
}
/**
* Clearing or escalating an alert has to carry a reason: review_notes is the only audit record of
* why a flagged transaction was disposed of, and an empty string leaves no trace of the decision.
*/
private reviewNotesOf(alert: any): string | null {
const notes = (this.amlReviewNotes[alert.id] || '').trim();
if (!notes) {
this.toast(`Enter review notes before disposing of alert #${alert.id}`);
return null;
}
return notes;
}
whitelistAlert(alert: any): void {
this.amlService.reviewAlert(alert.id, 'WHITELIST', this.amlReviewNotes[alert.id] || '').subscribe({
const notes = this.reviewNotesOf(alert);
if (!notes) {
return;
}
this.amlService.reviewAlert(alert.id, 'WHITELIST', notes).subscribe({
next: () => {
this.toast(`Alert #${alert.id} whitelisted — no STR raised`);
this.loadAmlAlerts();
@@ -569,7 +590,11 @@ export class FiuComponent implements OnInit {
}
escalateAlert(alert: any): void {
this.amlService.reviewAlert(alert.id, 'ESCALATE', this.amlReviewNotes[alert.id] || '').subscribe({
const notes = this.reviewNotesOf(alert);
if (!notes) {
return;
}
this.amlService.reviewAlert(alert.id, 'ESCALATE', notes).subscribe({
next: () => {
this.toast(`Alert #${alert.id} escalated — now an STR case for the Principal Officer`);
this.loadAmlAlerts();
@@ -780,7 +805,8 @@ export class FiuComponent implements OnInit {
return this.amlAlerts.filter((a) => ctrRuleCodes.includes(a.ruleCode));
}
get amlSuspiciousAlerts(): any[] {
/** Behavioural (non-CTR) alerts. */
private get behaviouralAlerts(): any[] {
const ctrRuleCodes = [
'CTR_THRESHOLD_10L',
'STRUCTURED_CTR_10L_MONTHLY',
@@ -789,6 +815,21 @@ export class FiuComponent implements OnInit {
return this.amlAlerts.filter((a) => !ctrRuleCodes.includes(a.ruleCode));
}
/**
* The STR triage queue: still to decide, or already escalated. Whitelisted alerts are closed
* with no report, so they are not STR business — they live in {@link amlWhitelistedAlerts}.
*/
get amlSuspiciousAlerts(): any[] {
return this.behaviouralAlerts.filter(
(a) => a.status !== 'WHITELISTED' && (!this.amlAlertStatusFilter || a.status === this.amlAlertStatusFilter)
);
}
/** Cleared register: alerts closed as false positives, kept for audit with their evidence. */
get amlWhitelistedAlerts(): any[] {
return this.behaviouralAlerts.filter((a) => a.status === 'WHITELISTED');
}
// -------------------- Head Office vs Branch scope --------------------
/** Maps a FINnet-style internal branch code to the app-wide canonical branch code. */
private canonicalBranchOf(branchCode: string): string | undefined {