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:
+87
-10
@@ -334,6 +334,16 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
alternateDipLookupLogId: number | null = null;
|
||||
/** Alternate DIP lookup timestamp from API */
|
||||
alternateDipTimestamp: string | null = null;
|
||||
/**
|
||||
* DIP/MNRL status the number actually returned (CLEAN / SUSPECTED / SERVICE_UNAVAILABLE).
|
||||
* Kept separately from `mobileVerified` so an unverifiable number is not indistinguishable
|
||||
* from one that genuinely passed.
|
||||
*/
|
||||
dipStatus: string | null = null;
|
||||
alternateDipStatus: string | null = null;
|
||||
/** Officer rejected the number outright — it must be replaced before saving. */
|
||||
dipRejected = false;
|
||||
alternateDipRejected = false;
|
||||
|
||||
/**
|
||||
* CLIENT CREATION contact-OTP verification state (mobileNo / emailAddress). Unlike the
|
||||
@@ -530,8 +540,37 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
sanctionsVerified = false;
|
||||
/** Sanctions match was found — blocks account opening */
|
||||
sanctionsBlocked = false;
|
||||
/**
|
||||
* Outcome recorded against a match: 'FALSE_POSITIVE' (awaiting Principal Officer approval)
|
||||
* or 'CONFIRMED_MATCH'. Null when the hit has not been reviewed yet. Both keep the client
|
||||
* blocked — only the Principal Officer's approval, applied server-side, releases a name.
|
||||
*/
|
||||
sanctionsDisposition: string | null = null;
|
||||
/**
|
||||
* The name was edited after a block, so the applicant may no longer be the flagged one.
|
||||
* Re-exposes the verify button; the block itself is only lifted by a CLEAR from the API.
|
||||
*/
|
||||
sanctionsNeedsRescreen = false;
|
||||
/** Screening log ID from sanctions API */
|
||||
screeningLogId: number | null = null;
|
||||
|
||||
/** Cites the direction actually in force — kept in step with the verification dialog. */
|
||||
readonly sanctionsCitation = 'UAPA 1967 s.51-A & RBI KYC Directions, 2025 — Chapter IX';
|
||||
readonly sanctionsTooltip =
|
||||
'All customer names must be verified against the UN Security Council Consolidated List before account ' +
|
||||
'opening and periodically thereafter. A match is not a rejection: compare the identifiers, record a ' +
|
||||
'false positive with reasons, or confirm the match and escalate to the Principal Officer immediately.';
|
||||
|
||||
/** Badge text reflecting how far the flagged alert has progressed. */
|
||||
get sanctionsStatusLabel(): string {
|
||||
if (this.sanctionsDisposition === 'FALSE_POSITIVE') {
|
||||
return 'Awaiting PO Approval';
|
||||
}
|
||||
if (this.sanctionsDisposition === 'CONFIRMED_MATCH') {
|
||||
return 'Confirmed Match';
|
||||
}
|
||||
return 'Sanctions Match';
|
||||
}
|
||||
/** Snapshot of the name that was last verified — used to invalidate verification when name changes */
|
||||
private verifiedName: string = '';
|
||||
/** True while prefillForEdit() is patching historical monthly/annual values — see linkMonthlyAnnual(). */
|
||||
@@ -665,13 +704,19 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.mobileVerified = false;
|
||||
this.dipLookupLogId = null;
|
||||
this.dipTimestamp = null;
|
||||
this.dipStatus = null;
|
||||
this.dipRejected = false;
|
||||
this.alternateMobileVerified = false;
|
||||
this.alternateDipLookupLogId = null;
|
||||
this.alternateDipTimestamp = null;
|
||||
this.alternateDipStatus = null;
|
||||
this.alternateDipRejected = false;
|
||||
this.mobileNewOtpId = null;
|
||||
this.emailNewOtpId = null;
|
||||
this.sanctionsVerified = false;
|
||||
this.sanctionsBlocked = false;
|
||||
this.sanctionsDisposition = null;
|
||||
this.sanctionsNeedsRescreen = false;
|
||||
this.screeningLogId = null;
|
||||
this.verifiedName = '';
|
||||
}
|
||||
@@ -1268,6 +1313,11 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
.valueChanges.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.mobileVerified = false;
|
||||
// The rejection applied to the previous number, not this one. Leaving it set would
|
||||
// keep Save & Next dead with a message about a number no longer in the field.
|
||||
this.dipRejected = false;
|
||||
this.dipStatus = null;
|
||||
this.dipLookupLogId = null;
|
||||
// Also reset create-time contact-OTP verification — an OTP verified against a
|
||||
// previous mobile number no longer applies once the value is edited. Belt-and-braces
|
||||
// alongside ContactOtpBlockComponent's own destination-change reset: this fires even
|
||||
@@ -1282,6 +1332,9 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
.valueChanges.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.alternateMobileVerified = false;
|
||||
this.alternateDipRejected = false;
|
||||
this.alternateDipStatus = null;
|
||||
this.alternateDipLookupLogId = null;
|
||||
});
|
||||
|
||||
// Reset create-time contact-OTP verification when email address changes — see the
|
||||
@@ -1322,9 +1375,11 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((result: any) => {
|
||||
if (!result) return;
|
||||
if (result.verified) {
|
||||
this.mobileVerified = true;
|
||||
}
|
||||
// A flagged number the officer chose to proceed with counts as verified — the decision and
|
||||
// its reason are already stored against the lookup, which is what the save gate checks.
|
||||
this.mobileVerified = result.verified === true || result.decision === 'PROCEEDED';
|
||||
this.dipRejected = result.decision === 'REJECTED';
|
||||
this.dipStatus = result.status || null;
|
||||
this.dipLookupLogId = result.lookupLogId || null;
|
||||
this.dipTimestamp = result.timestamp || null;
|
||||
});
|
||||
@@ -1342,9 +1397,9 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((result: any) => {
|
||||
if (!result) return;
|
||||
if (result.verified) {
|
||||
this.alternateMobileVerified = true;
|
||||
}
|
||||
this.alternateMobileVerified = result.verified === true || result.decision === 'PROCEEDED';
|
||||
this.alternateDipRejected = result.decision === 'REJECTED';
|
||||
this.alternateDipStatus = result.status || null;
|
||||
this.alternateDipLookupLogId = result.lookupLogId || null;
|
||||
this.alternateDipTimestamp = result.timestamp || null;
|
||||
});
|
||||
@@ -1387,10 +1442,17 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
*/
|
||||
verifySanctions() {
|
||||
const name = this.currentNameSnapshot();
|
||||
const dob = this.createClientForm.get('dateOfBirth')?.value;
|
||||
|
||||
const dialogRef = this.dialog.open(SanctionsVerifyDialogComponent, {
|
||||
width: '550px',
|
||||
data: { name }
|
||||
width: '620px',
|
||||
data: {
|
||||
name,
|
||||
// The dialog compares these against the listed particulars — a name-only hit cannot be
|
||||
// judged without the applicant's own identifiers next to it.
|
||||
dateOfBirth: dob ? new Date(dob).toLocaleDateString('en-IN') : '',
|
||||
nationality: this.nationalityLabel()
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((result: any) => {
|
||||
if (!result) return;
|
||||
@@ -1399,16 +1461,26 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
// CLEAR — no match found
|
||||
this.sanctionsVerified = true;
|
||||
this.sanctionsBlocked = false;
|
||||
this.sanctionsDisposition = null;
|
||||
this.sanctionsNeedsRescreen = false;
|
||||
this.verifiedName = name;
|
||||
} else if (result.verified === false) {
|
||||
// FLAGGED — match found, block account opening
|
||||
// FLAGGED — blocked either way. A FALSE_POSITIVE is a claim awaiting the Principal
|
||||
// Officer, not a clearance; only their approval lets this name through.
|
||||
this.sanctionsVerified = false;
|
||||
this.sanctionsBlocked = true;
|
||||
this.sanctionsDisposition = result.disposition || null;
|
||||
this.sanctionsNeedsRescreen = false;
|
||||
this.verifiedName = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Human-readable nationality for the sanctions comparison panel. */
|
||||
private nationalityLabel(): string {
|
||||
return this.getSelectedNationality()?.label || '';
|
||||
}
|
||||
|
||||
/** Concatenated current name from the form (handles individual + entity) */
|
||||
private currentNameSnapshot(): string {
|
||||
const firstname = this.createClientForm.get('firstname')?.value || '';
|
||||
@@ -1448,9 +1520,14 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
// Only invalidate if a verification was previously recorded
|
||||
if (!this.sanctionsVerified && !this.sanctionsBlocked) return;
|
||||
if (this.currentNameSnapshot() !== this.verifiedName) {
|
||||
// Editing the name forces a fresh screening, but must NOT silently clear an existing
|
||||
// block: otherwise a flagged applicant is waved through by nudging one character. The
|
||||
// block is only lifted by re-running the check and getting a CLEAR back from the API.
|
||||
this.sanctionsVerified = false;
|
||||
this.sanctionsBlocked = false;
|
||||
this.screeningLogId = null;
|
||||
if (this.sanctionsBlocked) {
|
||||
this.sanctionsNeedsRescreen = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user