feat: add database export, welcome kit dialog, and DIP/sanctions updates
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -283,26 +283,61 @@
|
||||
</div>
|
||||
<div class="pd-section__body">
|
||||
<div class="pd-grid">
|
||||
<!-- Primary Mobile DIP -->
|
||||
<!--
|
||||
Primary Mobile DIP. Driven by dipLiveStatus — the latest lookup — not by dipVerified,
|
||||
which is frozen at onboarding and would keep reporting "Verified" long after the number
|
||||
was reported for fraud or reassigned to someone else.
|
||||
-->
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Mobile DIP Status</div>
|
||||
<div class="pd-field__value">
|
||||
@if (
|
||||
clientViewData.mobileVerified === true ||
|
||||
clientViewData.mobileVerified === 'true' ||
|
||||
clientViewData.mobileVerified === 1 ||
|
||||
clientViewData.dipVerified === true ||
|
||||
clientViewData.dipVerified === 'true' ||
|
||||
clientViewData.dipVerified === 1
|
||||
) {
|
||||
@if (isDipFlagged(clientViewData.dipLiveStatus)) {
|
||||
<span class="pd-verify-badge pd-verify-badge--flagged">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ dipStatusLabel(clientViewData) }}
|
||||
</span>
|
||||
} @else if (isDipClear(clientViewData)) {
|
||||
<span class="pd-verify-badge pd-verify-badge--verified">
|
||||
<i class="fa fa-check-circle"></i> Verified
|
||||
</span>
|
||||
} @else {
|
||||
<span class="pd-verify-badge pd-verify-badge--pending"> <i class="fa fa-clock-o"></i> Pending </span>
|
||||
<span class="pd-verify-badge pd-verify-badge--pending">
|
||||
<i class="fa fa-clock-o"></i> {{ dipStatusLabel(clientViewData) }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (isDipFlagged(clientViewData.dipLiveStatus)) {
|
||||
<div class="pd-field pd-field--full">
|
||||
<div class="pd-dip-alert">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<div>
|
||||
<strong>This mobile number is flagged on the DoT Digital Intelligence Platform.</strong>
|
||||
<div class="pd-dip-alert__detail">
|
||||
OTPs and SMS alerts to this number are blocked. Contact the customer through an alternate channel and
|
||||
obtain a new number. Account operations are not restricted.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!--
|
||||
A sanctions alert raised after onboarding is far more serious than a DIP hit and must be
|
||||
impossible to miss on the customer's own page, not only in the Principal Officer's queue.
|
||||
-->
|
||||
@if (clientViewData.sanctionsAlertStatus) {
|
||||
<div class="pd-field pd-field--full">
|
||||
<div
|
||||
class="pd-dip-alert"
|
||||
[class.pd-dip-alert--review]="clientViewData.sanctionsAlertStatus === 'PENDING_REVIEW'"
|
||||
>
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<div>
|
||||
<strong>{{ sanctionsAlertHeadline(clientViewData) }}</strong>
|
||||
<div class="pd-dip-alert__detail">{{ sanctionsAlertDetail(clientViewData) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.dipTimestamp) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">DIP Verified On</div>
|
||||
@@ -314,7 +349,12 @@
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Alternate Mobile DIP Status</div>
|
||||
<div class="pd-field__value">
|
||||
@if (
|
||||
@if (isDipFlagged(clientViewData.alternateDipLiveStatus)) {
|
||||
<span class="pd-verify-badge pd-verify-badge--flagged">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ alternateDipStatusLabel(clientViewData) }}
|
||||
</span>
|
||||
} @else if (
|
||||
clientViewData.alternateDipLiveStatus === 'CLEAN' ||
|
||||
clientViewData.alternateDipVerified === true ||
|
||||
clientViewData.alternateDipVerified === 'true' ||
|
||||
clientViewData.alternateDipVerified === 1
|
||||
|
||||
@@ -248,9 +248,59 @@ $teal: #00897b;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
&--danger,
|
||||
&--flagged {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
}
|
||||
|
||||
// A DIP hit on an existing customer restricts their NUMBER, not their account, so it reads as a
|
||||
// standing advisory across the full width rather than as a blocked-account state.
|
||||
.pd-field--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.pd-dip-alert {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-left: 4px solid #dc2626;
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
color: #7f1d1d;
|
||||
line-height: 1.5;
|
||||
|
||||
i {
|
||||
color: #dc2626;
|
||||
font-size: 15px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__detail {
|
||||
margin-top: 3px;
|
||||
font-weight: 400;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
// An unreviewed hit is not yet a finding, so it reads as amber rather than red.
|
||||
&--review {
|
||||
background: #fffbeb;
|
||||
border-color: #fde68a;
|
||||
border-left-color: #d97706;
|
||||
color: #92400e;
|
||||
|
||||
i {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.pd-dip-alert__detail {
|
||||
color: #b45309;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,14 @@ interface ClientViewData {
|
||||
displayName?: string;
|
||||
mobileNo?: string;
|
||||
emailAddress?: string;
|
||||
/**
|
||||
* Current DIP/MNRL status resolved from the latest lookup, as opposed to the `dipVerified`
|
||||
* flag captured when the client was onboarded.
|
||||
*/
|
||||
dipLiveStatus?: string;
|
||||
alternateDipLiveStatus?: string;
|
||||
/** Disposition of the latest flagged sanctions alert, or absent when the client carries none. */
|
||||
sanctionsAlertStatus?: string;
|
||||
dateOfBirth?: number[];
|
||||
gender?: { id: number; name: string; active?: boolean };
|
||||
clientType?: { id: number; name: string; active?: boolean };
|
||||
@@ -67,6 +75,91 @@ export class PersonalDataTabComponent {
|
||||
/** Client View Data */
|
||||
clientViewData!: ClientViewData;
|
||||
|
||||
/**
|
||||
* Whether the number's CURRENT DIP/MNRL status is a hit.
|
||||
*
|
||||
* The onboarding-time `dipVerified` flag never changes, so a customer cleared when they joined
|
||||
* kept reading "Verified" even after their number was reported for fraud or reassigned. These
|
||||
* helpers read `dipLiveStatus`, which the API resolves from the most recent lookup.
|
||||
*/
|
||||
isDipFlagged(liveStatus: string | null | undefined): boolean {
|
||||
return !!liveStatus && liveStatus !== 'CLEAN' && liveStatus !== 'ERROR';
|
||||
}
|
||||
|
||||
/** True only when the number is affirmatively clean — not merely "not flagged". */
|
||||
isDipClear(client: any): boolean {
|
||||
if (client?.dipLiveStatus) {
|
||||
return client.dipLiveStatus === 'CLEAN';
|
||||
}
|
||||
return (
|
||||
client?.mobileVerified === true ||
|
||||
client?.mobileVerified === 'true' ||
|
||||
client?.mobileVerified === 1 ||
|
||||
client?.dipVerified === true ||
|
||||
client?.dipVerified === 'true' ||
|
||||
client?.dipVerified === 1
|
||||
);
|
||||
}
|
||||
|
||||
dipStatusLabel(client: any): string {
|
||||
return this.labelFor(client?.dipLiveStatus, this.isDipClear(client));
|
||||
}
|
||||
|
||||
/** Headline for the sanctions banner, driven by how far the alert has progressed. */
|
||||
sanctionsAlertHeadline(client: any): string {
|
||||
switch (client?.sanctionsAlertStatus) {
|
||||
case 'CONFIRMED_MATCH':
|
||||
return 'Confirmed UN Security Council sanctions match.';
|
||||
case 'FALSE_POSITIVE':
|
||||
return 'Sanctions hit reviewed as a false positive.';
|
||||
default:
|
||||
return 'Sanctions match awaiting Principal Officer review.';
|
||||
}
|
||||
}
|
||||
|
||||
sanctionsAlertDetail(client: any): string {
|
||||
switch (client?.sanctionsAlertStatus) {
|
||||
case 'CONFIRMED_MATCH':
|
||||
return (
|
||||
'Debits are frozen on this client’s deposit accounts and the case has been escalated. ' +
|
||||
'Report to FIU-IND and the nodal officer within 24 hours if not already done.'
|
||||
);
|
||||
case 'FALSE_POSITIVE':
|
||||
return 'Recorded as a different person from the listed individual. See the Sanctions Logs for the reasoning.';
|
||||
default:
|
||||
return (
|
||||
'This client matched the UN Security Council Consolidated List on a later screening. ' +
|
||||
'Review the alert under Reports → Sanctions Screening & Alerts before transacting.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
alternateDipStatusLabel(client: any): string {
|
||||
const clear =
|
||||
client?.alternateDipLiveStatus === 'CLEAN' ||
|
||||
client?.alternateDipVerified === true ||
|
||||
client?.alternateDipVerified === 'true' ||
|
||||
client?.alternateDipVerified === 1;
|
||||
return this.labelFor(client?.alternateDipLiveStatus, clear);
|
||||
}
|
||||
|
||||
private labelFor(liveStatus: string | null | undefined, clear: boolean): string {
|
||||
switch (liveStatus) {
|
||||
case 'SUSPECTED':
|
||||
return 'Flagged — Suspected';
|
||||
case 'DISCONNECTED':
|
||||
return 'Flagged — Disconnected';
|
||||
case 'SUSPECTED_AND_DISCONNECTED':
|
||||
return 'Flagged — Suspected & Disconnected';
|
||||
case 'SERVICE_UNAVAILABLE':
|
||||
return 'Not Verified';
|
||||
case 'CLEAN':
|
||||
return 'Verified';
|
||||
default:
|
||||
return clear ? 'Verified' : 'Pending';
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.route.parent.data.pipe(takeUntilDestroyed()).subscribe((data: { clientViewData: ClientViewData }) => {
|
||||
this.clientViewData = data.clientViewData;
|
||||
|
||||
Reference in New Issue
Block a user