feat: implement DIP/MNRL lookups and enhance Sanctions screening workflows
- Implement DIP/MNRL lookup logs table, filtering, and detail review under reports. - Integrate DIP verification gating on client creation, allowing officers to record PROCEEDED/REJECTED decisions with reasons. - Upgrade Sanctions verification dialog to support comparing Name, DOB, and Nationality particulars. - Add Sanctions disposition recording (FALSE_POSITIVE / CONFIRMED_MATCH) awaiting Principal Officer approval. - Add sanctions-alert-review-dialog for reviewers to assess and escalate/clear flagged names. - Enforce strict screening reset logic on client name changes, preventing bypass of existing blocks. - Update routes and shell menu navigation to include DIP/MNRL Lookups and Sanctions Logs.
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort, MatSortHeader } from '@angular/material/sort';
|
||||
@@ -17,10 +25,13 @@ import {
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { NgClass, DatePipe } from '@angular/common';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { SanctionsLogsService } from './sanctions-logs.service';
|
||||
import { SanctionsAlertReviewDialogComponent } from './sanctions-alert-review-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-sanctions-logs',
|
||||
@@ -51,19 +62,29 @@ import { SanctionsLogsService } from './sanctions-logs.service';
|
||||
})
|
||||
export class SanctionsLogsComponent implements OnInit {
|
||||
private sanctionsLogsService = inject(SanctionsLogsService);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'screenedName',
|
||||
'screeningResult',
|
||||
'disposition',
|
||||
'matchCount',
|
||||
'screenedBy',
|
||||
'screenedAt'
|
||||
'screenedAt',
|
||||
'actions'
|
||||
];
|
||||
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
searchName = '';
|
||||
activeFilter: string | null = null;
|
||||
/**
|
||||
* When set, the table shows the Principal Officer's alert queue (flagged screenings and their
|
||||
* disposition) instead of the raw screening log. 'ALL' means every alert regardless of state.
|
||||
*/
|
||||
alertFilter: string | null = null;
|
||||
loading = false;
|
||||
rescreening = false;
|
||||
|
||||
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
|
||||
@ViewChild(MatSort, { static: true }) sort: MatSort;
|
||||
@@ -74,6 +95,20 @@ export class SanctionsLogsComponent implements OnInit {
|
||||
|
||||
loadLogs() {
|
||||
this.loading = true;
|
||||
|
||||
// The alert queue is a different endpoint, not a filter on the log list: it carries the
|
||||
// disposition columns the review dialog needs.
|
||||
const request$ = this.alertFilter
|
||||
? this.sanctionsLogsService.getAlerts(this.alertFilter === 'ALL' ? undefined : this.alertFilter)
|
||||
: this.sanctionsLogsService.getLogs(this.buildLogFilters());
|
||||
|
||||
request$.subscribe({
|
||||
next: (data: any) => this.applyRows(data || []),
|
||||
error: () => this.applyRows([])
|
||||
});
|
||||
}
|
||||
|
||||
private buildLogFilters() {
|
||||
const filters: any = {};
|
||||
if (this.activeFilter) {
|
||||
filters.result = this.activeFilter;
|
||||
@@ -81,35 +116,102 @@ export class SanctionsLogsComponent implements OnInit {
|
||||
if (this.searchName?.trim()) {
|
||||
filters.name = this.searchName.trim();
|
||||
}
|
||||
this.sanctionsLogsService.getLogs(filters).subscribe({
|
||||
next: (data: any) => {
|
||||
this.dataSource = new MatTableDataSource(data || []);
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
this.loading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.dataSource = new MatTableDataSource([]);
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
return filters;
|
||||
}
|
||||
|
||||
private applyRows(rows: any[]) {
|
||||
this.dataSource = new MatTableDataSource(rows);
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
applySearch(value: string) {
|
||||
this.searchName = value;
|
||||
// Name search only exists on the log endpoint, so leave the alert queue when searching.
|
||||
this.alertFilter = null;
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
filterByResult(result: string | null) {
|
||||
this.activeFilter = this.activeFilter === result ? null : result;
|
||||
this.alertFilter = null;
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
filterByAlert(status: string) {
|
||||
this.alertFilter = this.alertFilter === status ? null : status;
|
||||
this.activeFilter = null;
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
clearFilters() {
|
||||
this.searchName = '';
|
||||
this.activeFilter = null;
|
||||
this.alertFilter = null;
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
/** Only flagged rows carry a case to review. */
|
||||
isAlert(row: any): boolean {
|
||||
return !!row?.disposition;
|
||||
}
|
||||
|
||||
dispositionLabel(row: any): string {
|
||||
switch (row?.disposition) {
|
||||
case 'PENDING_REVIEW':
|
||||
return 'Pending Review';
|
||||
case 'FALSE_POSITIVE':
|
||||
return row.approvedBy ? 'False Positive — Approved' : 'False Positive — Awaiting PO';
|
||||
case 'CONFIRMED_MATCH':
|
||||
if (!row.approvedBy) {
|
||||
return 'Confirmed — Awaiting PO';
|
||||
}
|
||||
// An onboarding match has no client and therefore no accounts to freeze — saying
|
||||
// "Frozen" there would claim an action that never happened.
|
||||
return row.frozenAccountIds ? 'Confirmed — Frozen' : 'Confirmed — Blocked';
|
||||
default:
|
||||
return '—';
|
||||
}
|
||||
}
|
||||
|
||||
dispositionClass(row: any): string {
|
||||
if (row?.disposition === 'CONFIRMED_MATCH') return 'badge-flagged';
|
||||
if (row?.disposition === 'FALSE_POSITIVE') return row.approvedBy ? 'badge-clear' : 'badge-pending';
|
||||
if (row?.disposition === 'PENDING_REVIEW') return 'badge-pending';
|
||||
return '';
|
||||
}
|
||||
|
||||
reviewAlert(row: any) {
|
||||
this.dialog
|
||||
.open(SanctionsAlertReviewDialogComponent, { width: '600px', data: { ...row }, autoFocus: false })
|
||||
.afterClosed()
|
||||
.subscribe((changed: boolean) => {
|
||||
if (changed) {
|
||||
this.loadLogs();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Screens every existing client against the currently loaded list snapshot. This is what
|
||||
* detects a customer who was added to the UN list after they were onboarded — the only
|
||||
* situation in which there are accounts to freeze.
|
||||
*/
|
||||
rescreenExistingClients() {
|
||||
this.rescreening = true;
|
||||
this.sanctionsLogsService.rescreenExistingClients().subscribe({
|
||||
next: (res: any) => {
|
||||
this.rescreening = false;
|
||||
this.snackBar.open(`Re-screening complete — ${res?.clientsFlagged ?? 0} client(s) flagged.`, 'Close', {
|
||||
duration: 6000
|
||||
});
|
||||
this.filterByAlert('PENDING_REVIEW');
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.rescreening = false;
|
||||
this.snackBar.open(err?.error?.defaultUserMessage || 'Re-screening failed.', 'Close', { duration: 6000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user