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
@@ -1750,6 +1750,38 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
return salutation ? `${salutation} ${full}` : full;
}
/**
* The name PAN and Aadhaar are actually matched against.
*
* Deliberately not {@link fullNamePreview}: that prefixes the salutation, while
* ClientKycWritePlatformServiceJpaRepositoryImpl builds the verification name from firstname +
* middlename + lastname only. Values are passed through untouched rather than capitalised, so what
* the KYC step displays is exactly what goes to the provider.
*/
get verificationName(): string {
return [
this.createClientForm?.get('firstname')?.value,
this.createClientForm?.get('middlename')?.value,
this.createClientForm?.get('lastname')?.value
]
.map((part: any) => (part || '').toString().trim())
.filter(Boolean)
.join(' ');
}
/** Date of birth as captured, sent to the provider formatted dd/MM/yyyy. */
get verificationDateOfBirth(): Date | null {
const value = this.createClientForm?.get('dateOfBirth')?.value;
if (!value) {
return null;
}
if (value instanceof Date) {
return value;
}
const parsed = new Date(value);
return isNaN(parsed.getTime()) ? null : parsed;
}
getSelectedGender(): any {
const val = this.createClientForm.get('genderId')?.value;
return this.genderOptions?.find((g: any) => g.id === val) || null;
@@ -38,10 +38,17 @@ import { ClientsService } from 'app/clients/clients.service';
}
</div>
<p class="sanctions-msg">
Screening <strong>{{ clientName }}</strong> against<br />
<strong>UN Security Council</strong> &amp;
<strong>Financial Intelligence Unit - India (FIU-IND under UNSC Resolution 2231)</strong>
Screening <strong>{{ clientName }}</strong> against the<br />
<strong>UN Security Council Consolidated List</strong><br />
<span class="sanctions-lists"
>ISIL (Da&rsquo;esh) &amp; Al-Qaida (1267/1989/2253) and Taliban (1988) sanctions lists</span
>
</p>
@if (listDataAsOf) {
<p class="list-as-of">
Sanctions list data as of <strong>{{ listDataAsOf }}</strong>
</p>
}
<p class="regulatory-note">{{ regulatoryBasis }}</p>
</div>
}
@@ -54,8 +61,8 @@ import { ClientsService } from 'app/clients/clients.service';
<h3 class="approve-title">No Sanctions Match Found</h3>
<p class="approve-detail">
<strong>{{ clientName }}</strong> has been screened against the
<strong>UN Security Council Consolidated List</strong> and <strong>FIU-IND</strong> records. No confirmed
match was found.
<strong>UN Security Council Consolidated List</strong> (ISIL &amp; Al-Qaida and Taliban sanctions lists). No
confirmed match was found.
</p>
<div class="approve-meta">
<div class="meta-row">
@@ -222,6 +229,20 @@ import { ClientsService } from 'app/clients/clients.service';
text-align: center;
}
.list-as-of {
font-size: 12px;
color: #374151;
text-align: center;
margin: 4px 0 0;
}
.sanctions-lists {
display: inline-block;
margin-top: 4px;
font-size: 12px;
color: #6b7280;
}
.regulatory-note {
font-size: 11px;
color: #9ca3af;
@@ -550,16 +571,24 @@ export class SanctionsVerifyDialogComponent implements OnInit, OnDestroy {
screeningLogId: number | null = null;
listDataAsOf: string | null = null;
/**
* Cites the direction actually in force. Chapter IX ("Requirements/obligations under International
* Agreements") is the clause that puts the UNSC lists in scope, and the 2025 Directions superseded
* the older Master Direction this screen used to name.
*/
regulatoryBasis =
'Screened as per Section 51-A of the Unlawful Activities (Prevention) Act, 1967 and the RBI Master Direction on KYC.';
'Screened under Section 51-A of the Unlawful Activities (Prevention) Act, 1967 and Chapter IX of the ' +
'RBI Know Your Customer Directions, 2025 (amended as on December 29, 2025).';
screenedDate = new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
progressValue = 0;
currentStep = 0;
// Steps describe the screening that actually runs against the UNSC consolidated list; there is no
// FIU-IND lookup behind this call, so it is no longer claimed here.
progressSteps = [
{ label: 'Connecting to UN Security Council database...', icon: 'globe' as any },
{ label: 'Screening name against consolidated list...', icon: 'search' as any },
{ label: 'Cross-checking FIU-IND records...', icon: 'database' as any },
{ label: 'Cross-checking listed aliases and entities...', icon: 'database' as any },
{ label: 'Analyzing results...', icon: 'chart-bar' as any }
];
@@ -577,6 +606,9 @@ export class SanctionsVerifyDialogComponent implements OnInit, OnDestroy {
next: (result: any) => {
this.apiResult = result;
this.apiDone = true;
// Surfaced while the screening is still running, so the operator can see which snapshot of
// the UNSC list the check is being made against rather than only learning it afterwards.
this.listDataAsOf = this.formatListDate(result?.listDataAsOf);
},
error: (err: any) => {
this.apiError = err;
@@ -652,13 +684,7 @@ export class SanctionsVerifyDialogComponent implements OnInit, OnDestroy {
'Unable to connect to sanctions screening service.';
} else {
this.screeningLogId = this.apiResult?.screeningLogId || null;
this.listDataAsOf = this.apiResult?.listDataAsOf
? new Date(this.apiResult.listDataAsOf).toLocaleDateString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric'
})
: null;
this.listDataAsOf = this.formatListDate(this.apiResult?.listDataAsOf);
if (this.apiResult?.matchFound) {
this.matchFound = true;
this.matchCount = this.apiResult.matchCount || 0;
@@ -668,6 +694,18 @@ export class SanctionsVerifyDialogComponent implements OnInit, OnDestroy {
}, 400);
}
/** Formats the date the UNSC snapshot was loaded; null when the backend did not report one. */
private formatListDate(value: string | null | undefined): string | null {
if (!value) {
return null;
}
const parsed = new Date(value);
if (isNaN(parsed.getTime())) {
return null;
}
return parsed.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
}
ngOnDestroy() {
clearInterval(this.progressTimer);
}