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
+55 -4
View File
@@ -354,11 +354,12 @@
<h3>Live AML Alerts (Awaiting Triage)</h3>
<p>Flagged by the monitoring engine for suspicious behavioral patterns — triage and whitelist or escalate</p>
</div>
<select class="fiu-inp" [(ngModel)]="amlAlertStatusFilter" (ngModelChange)="loadAmlAlerts()">
<!-- Whitelisted is deliberately absent: cleared alerts are not STR business and have
their own register below. -->
<select class="fiu-inp" [(ngModel)]="amlAlertStatusFilter">
<option value="">All statuses</option>
<option value="RAISED">Raised</option>
<option value="UNDER_REVIEW">Under Review</option>
<option value="WHITELISTED">Whitelisted</option>
<option value="ESCALATED">Escalated</option>
</select>
</div>
@@ -416,7 +417,7 @@
</tr>
} @empty {
<tr>
<td colspan="10" class="fiu-empty">No suspicious behavioral alerts.</td>
<td colspan="10" class="fiu-empty">No suspicious behavioral alerts awaiting triage.</td>
</tr>
}
</tbody>
@@ -424,12 +425,62 @@
</div>
<p class="fiu-tbl-legend">
<span class="report-pill report-pending">Pending</span> awaiting Whitelist/Escalate decision &middot;
<span class="report-pill report-cleared">Cleared</span> whitelisted — no report filed &middot;
<span class="report-pill report-str">→ STR</span> escalated to Principal Officer as an STR case
</p>
<hr style="border: 0; border-top: 1px solid var(--f-g200); margin: 8px 0" />
<!-- Cleared register: whitelisted alerts, kept out of the STR queue but retained with the
evidence recorded at review time. -->
<div class="fiu-form-meta">
<div class="fiu-form-title">
<h3>Cleared Alerts (Whitelisted)</h3>
<p>Reviewed and closed as false positives — no STR filed. Retained with the evidence relied on.</p>
</div>
</div>
<div class="fiu-tbl-wrap">
<table class="fiu-tbl">
<thead>
<tr>
<th>ID</th>
<th>Client</th>
<th>Type</th>
<th>Rule</th>
<th class="ta-r">Amount</th>
<th>Raised On</th>
<th>Cleared By</th>
<th>Evidence</th>
</tr>
</thead>
<tbody>
@for (alert of amlWhitelistedAlerts; track alert.id) {
<tr>
<td>{{ alert.id }}</td>
<td>{{ alert.clientDisplayName }}</td>
<td>{{ alert.transactionType }}</td>
<td>{{ alert.ruleCode }} &mdash; {{ alert.ruleDescription }}</td>
<td class="ta-r mono">&#8377;{{ alert.amount | number: '1.0-0' }}</td>
<td>{{ alert.raisedOn | date: 'medium' }}</td>
<td>{{ alert.reviewedByUsername || '—' }}</td>
<td>
<span class="muted">{{ alert.reviewNotes || '—' }}</span>
</td>
</tr>
} @empty {
<tr>
<td colspan="8" class="fiu-empty">No alerts have been cleared.</td>
</tr>
}
</tbody>
</table>
</div>
<p class="fiu-tbl-legend">
<span class="report-pill report-cleared">Cleared</span> whitelisted — no report filed, evidence retained for
audit
</p>
<hr style="border: 0; border-top: 1px solid var(--f-g200); margin: 8px 0" />
<!-- LIVE: STR cases with full Principal Officer actions -->
<div class="fiu-form-meta">
<div class="fiu-form-title">
+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 {