feat: KYC document update, FIU & AML recurring alerts integration, Sponsor DD revoke workflow, and TDS reports

This commit is contained in:
2026-09-14 11:43:26 +05:30
parent 7afa9c7a50
commit 2124bb4b76
40 changed files with 3028 additions and 929 deletions
@@ -51,15 +51,6 @@
<span>Could not load the review queue.</span>
</div>
<!-- Named on screen, because sample rows must never be mistaken for engine output on an AML queue -->
<div *ngIf="usingDemoData && rows.data.length > 0" class="ra-demo">
<mat-icon>science</mat-icon>
<span>
Showing <strong>sample data</strong> — the AML engine returned no recurring alerts for this threshold and window.
These rows are for demonstration only and are not reportable.
</span>
</div>
<table mat-table [dataSource]="rows" class="ra-table mat-elevation-z1" multiTemplateDataRows>
<ng-container matColumnDef="customer">
<th mat-header-cell *matHeaderCellDef>Customer</th>
@@ -79,27 +79,6 @@
font-size: 13px;
}
.ra-demo {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
margin-bottom: 12px;
border: 1px solid #fde68a;
border-radius: 6px;
background: #fffbeb;
color: #92400e;
font-size: 12.5px;
line-height: 1.5;
mat-icon {
flex: 0 0 auto;
font-size: 18px;
width: 18px;
height: 18px;
}
}
.ra-table {
width: 100%;
border-collapse: separate;
@@ -25,7 +25,6 @@ import { AmlService } from '../../../aml/aml.service';
import { AlertService } from '../../../core/alert/alert.service';
import { HasPermissionDirective } from '../../../directives/has-permission/has-permission.directive';
import { AmlRecurringReviewDialogComponent } from './aml-recurring-review-dialog.component';
import { amlRecurringAlertsDemoData } from './aml-recurring-alerts.demo-data';
/**
* Customers whose transactions keep tripping the AML engine, queued for an employee to review why.
@@ -95,8 +94,6 @@ export class AmlRecurringAlertsComponent implements OnInit {
loading = false;
loadFailed = false;
/** Set when the rows on screen are the sample set rather than engine output. */
usingDemoData = false;
/** Which customer's alert detail is expanded inline. */
expandedClientId: number | null = null;
@@ -111,20 +108,20 @@ export class AmlRecurringAlertsComponent implements OnInit {
this.amlService.getRecurringAlerts(this.minAlerts, this.windowDays, this.onlyNeedingReview).subscribe({
next: (data) => {
this.loading = false;
const rows = data || [];
this.usingDemoData = rows.length === 0;
this.rows.data = this.mapRows(this.usingDemoData ? this.demoRows() : rows);
this.rows.data = this.mapRows(data || []);
},
error: () => {
this.loading = false;
this.loadFailed = true;
this.usingDemoData = true;
this.rows.data = this.mapRows(this.demoRows());
// An AML review queue shows what the engine found or nothing at all. Standing in sample
// customers when the call fails invents suspicious activity against names that are not
// the bank's, which is worse than an empty table saying the load failed.
this.rows.data = [];
}
});
}
/** Parses the date fields the template renders. Shared by engine rows and the sample set. */
/** Parses the date fields the template renders. */
private mapRows(rows: any[]): any[] {
return rows.map((r: any) => ({
...r,
@@ -135,36 +132,6 @@ export class AmlRecurringAlertsComponent implements OnInit {
}));
}
/**
* The sample set narrowed by the on-screen controls, so the threshold and window selectors behave
* the way they would against the engine instead of returning the same three rows regardless.
*/
private demoRows(): any[] {
const cutoff = Date.now() - this.windowDays * 24 * 60 * 60 * 1000;
return amlRecurringAlertsDemoData()
.map((row: any) => {
const alerts = (row.alerts || []).filter((a: any) => new Date(a.raisedOn).getTime() >= cutoff);
const ruleCodes = Array.from(new Set(alerts.map((a: any) => a.ruleCode)));
return {
...row,
alerts,
alertCount: alerts.length,
openCount: alerts.filter((a: any) => a.status === 'OPEN').length,
escalatedCount: alerts.filter((a: any) => a.status === 'ESCALATED').length,
// Severity sits on the alert server-side; the sample rows carry a row total, so it is
// capped at what survived the window rather than recomputed.
highRiskCount: Math.min(row.highRiskCount, alerts.length),
distinctRules: ruleCodes.length,
ruleCodes,
totalAmount: alerts.reduce((sum: number, a: any) => sum + (Number(a.amount) || 0), 0),
firstAlertOn: alerts.length ? alerts[0].raisedOn : null,
lastAlertOn: alerts.length ? alerts[alerts.length - 1].raisedOn : null
};
})
.filter((row: any) => row.alertCount >= this.minAlerts)
.filter((row: any) => !this.onlyNeedingReview || row.needsReview);
}
toggleExpanded(row: any): void {
this.expandedClientId = this.expandedClientId === row.clientId ? null : row.clientId;
}
@@ -1,248 +0,0 @@
/**
* 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/.
*/
/**
* Sample recurring-alert rows for the AML review queue, used only when the engine returns nothing
* for the selected window so the screen can be demonstrated and reviewed.
*
* Shaped exactly like the `/aml/recurring-alerts` response — dates as ISO strings, alerts nested —
* so the rows travel through the component's normal mapping rather than a second code path.
*
* Dates are generated relative to today rather than hard-coded, otherwise every row would fall
* outside the "last 90 days" window as soon as the file aged.
*/
/** ISO timestamp `days` before now, at the given hour and minute. */
function daysAgo(days: number, hour = 11, minute = 20): string {
const d = new Date();
d.setDate(d.getDate() - days);
d.setHours(hour, minute, 0, 0);
return d.toISOString();
}
/**
* Three customers covering the three review states the table renders: never reviewed, reviewed but
* flagged again since, and reviewed with an outcome recorded.
*/
export function amlRecurringAlertsDemoData(): any[] {
return [
// Never reviewed — the row the queue exists to surface.
{
clientId: 10078,
clientDisplayName: 'Ravi Kumar Sharma',
accountNo: '000000010078',
clientRiskCategory: 3,
alertCount: 5,
highRiskCount: 3,
openCount: 4,
escalatedCount: 1,
distinctRules: 3,
ruleCodes: [
'CASH_TXN_10L',
'STRUCTURING',
'RAPID_MOVEMENT'
],
totalAmount: 4785000,
firstAlertOn: daysAgo(62, 10, 5),
lastAlertOn: daysAgo(3, 15, 42),
needsReview: true,
lastReviewedOn: null,
alertCountAtLastReview: null,
lastReviewOutcome: null,
lastReviewedByUsername: null,
lastReviewNotes: null,
alerts: [
{
alertId: 90121,
raisedOn: daysAgo(62, 10, 5),
transactionType: 'Savings Deposit',
transactionId: 552301,
ruleCode: 'CASH_TXN_10L',
ruleDescription: 'Cash deposit at or above ₹10,00,000 in a single transaction',
amount: 1200000,
status: 'OPEN',
reviewNotes: null
},
{
alertId: 90144,
raisedOn: daysAgo(48, 12, 18),
transactionType: 'Savings Deposit',
transactionId: 553980,
ruleCode: 'STRUCTURING',
ruleDescription: 'Multiple cash deposits just below the reporting threshold within 48 hours',
amount: 890000,
status: 'OPEN',
reviewNotes: null
},
{
alertId: 90190,
raisedOn: daysAgo(29, 9, 55),
transactionType: 'Savings Withdrawal',
transactionId: 556114,
ruleCode: 'RAPID_MOVEMENT',
ruleDescription: 'Funds withdrawn within 24 hours of a large credit',
amount: 1150000,
status: 'ESCALATED',
reviewNotes: 'Escalated to Principal Officer — source of funds not established.'
},
{
alertId: 90233,
raisedOn: daysAgo(14, 16, 30),
transactionType: 'Savings Deposit',
transactionId: 558702,
ruleCode: 'CASH_TXN_10L',
ruleDescription: 'Cash deposit at or above ₹10,00,000 in a single transaction',
amount: 1045000,
status: 'OPEN',
reviewNotes: null
},
{
alertId: 90275,
raisedOn: daysAgo(3, 15, 42),
transactionType: 'Savings Withdrawal',
transactionId: 560488,
ruleCode: 'STRUCTURING',
ruleDescription: 'Multiple cash deposits just below the reporting threshold within 48 hours',
amount: 500000,
status: 'OPEN',
reviewNotes: null
}
]
},
// Reviewed at 2 alerts, flagged twice since — returns to the queue as "New activity".
{
clientId: 10079,
clientDisplayName: 'Priya Patel',
accountNo: '000000010079',
clientRiskCategory: 2,
alertCount: 4,
highRiskCount: 1,
openCount: 2,
escalatedCount: 0,
distinctRules: 2,
ruleCodes: [
'HIGH_VALUE_TRANSFER',
'ROUND_TRIPPING'
],
totalAmount: 2130000,
firstAlertOn: daysAgo(71, 14, 10),
lastAlertOn: daysAgo(6, 11, 5),
needsReview: true,
lastReviewedOn: daysAgo(34, 17, 0),
alertCountAtLastReview: 2,
lastReviewOutcome: 'MONITOR',
lastReviewedByUsername: 'anjali.mehta',
lastReviewNotes: 'Textile trading receipts, invoices seen. Keep under watch for one more quarter.',
alerts: [
{
alertId: 90102,
raisedOn: daysAgo(71, 14, 10),
transactionType: 'Account Transfer',
transactionId: 551002,
ruleCode: 'HIGH_VALUE_TRANSFER',
ruleDescription: 'Transfer above ₹5,00,000 to an unrelated account',
amount: 620000,
status: 'WHITELISTED',
reviewNotes: 'Supplier payment, invoice verified.'
},
{
alertId: 90158,
raisedOn: daysAgo(41, 10, 45),
transactionType: 'Account Transfer',
transactionId: 554330,
ruleCode: 'ROUND_TRIPPING',
ruleDescription: 'Funds returned to the originating account within seven days',
amount: 450000,
status: 'WHITELISTED',
reviewNotes: 'Advance refunded after order cancellation.'
},
{
alertId: 90241,
raisedOn: daysAgo(19, 13, 25),
transactionType: 'Account Transfer',
transactionId: 559011,
ruleCode: 'HIGH_VALUE_TRANSFER',
ruleDescription: 'Transfer above ₹5,00,000 to an unrelated account',
amount: 540000,
status: 'OPEN',
reviewNotes: null
},
{
alertId: 90268,
raisedOn: daysAgo(6, 11, 5),
transactionType: 'Account Transfer',
transactionId: 560190,
ruleCode: 'ROUND_TRIPPING',
ruleDescription: 'Funds returned to the originating account within seven days',
amount: 520000,
status: 'OPEN',
reviewNotes: null
}
]
},
// Reviewed with an outcome and no activity since — shows the settled state.
{
clientId: 10080,
clientDisplayName: 'Arjun Deshmukh',
accountNo: '000000010080',
clientRiskCategory: 1,
alertCount: 3,
highRiskCount: 0,
openCount: 0,
escalatedCount: 0,
distinctRules: 1,
ruleCodes: ['CASH_TXN_10L'],
totalAmount: 3420000,
firstAlertOn: daysAgo(80, 9, 30),
lastAlertOn: daysAgo(23, 12, 15),
needsReview: false,
lastReviewedOn: daysAgo(9, 15, 20),
alertCountAtLastReview: 3,
lastReviewOutcome: 'CLEARED',
lastReviewedByUsername: 'vikram.singh',
lastReviewNotes: 'Sugarcane crushing-season proceeds. Mill settlement statements filed with the branch.',
alerts: [
{
alertId: 90088,
raisedOn: daysAgo(80, 9, 30),
transactionType: 'Savings Deposit',
transactionId: 550210,
ruleCode: 'CASH_TXN_10L',
ruleDescription: 'Cash deposit at or above ₹10,00,000 in a single transaction',
amount: 1180000,
status: 'WHITELISTED',
reviewNotes: 'Mill settlement for the 2025-26 crushing season.'
},
{
alertId: 90165,
raisedOn: daysAgo(52, 11, 0),
transactionType: 'Savings Deposit',
transactionId: 554901,
ruleCode: 'CASH_TXN_10L',
ruleDescription: 'Cash deposit at or above ₹10,00,000 in a single transaction',
amount: 1090000,
status: 'WHITELISTED',
reviewNotes: 'Second instalment, statement on record.'
},
{
alertId: 90212,
raisedOn: daysAgo(23, 12, 15),
transactionType: 'Savings Deposit',
transactionId: 557640,
ruleCode: 'CASH_TXN_10L',
ruleDescription: 'Cash deposit at or above ₹10,00,000 in a single transaction',
amount: 1150000,
status: 'WHITELISTED',
reviewNotes: 'Final instalment for the season.'
}
]
}
];
}