feat: add KYC document renewal alerts, Aadhaar/PAN exceptions, and AML recurring alerts UI; remove old journal AML marking
This commit is contained in:
@@ -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 ·
|
||||
<span class="report-pill report-cleared">Cleared</span> whitelisted — no report filed ·
|
||||
<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 }} — {{ alert.ruleDescription }}</td>
|
||||
<td class="ta-r mono">₹{{ 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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user