feat: add KYC document renewal alerts, Aadhaar/PAN exceptions, and AML recurring alerts UI; remove old journal AML marking
This commit is contained in:
@@ -68,6 +68,32 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!--
|
||||
The OVD captured at onboarding has gone stale, or is about to. Sits beside the exception
|
||||
banner because it is the same kind of thing: something staff must act on before the account
|
||||
drifts out of compliance.
|
||||
-->
|
||||
@if (showKycDocumentAlert) {
|
||||
<div class="kyc-doc-alert" [class.kyc-doc-alert--critical]="kycDocumentAlertSeverity === 'critical'">
|
||||
<mat-icon class="kyc-doc-alert__icon">{{
|
||||
kycDocumentAlertSeverity === 'critical' ? 'error' : 'schedule'
|
||||
}}</mat-icon>
|
||||
<div class="kyc-doc-alert__body">
|
||||
<strong class="kyc-doc-alert__title">{{ kycDocumentAlertTitle }}</strong>
|
||||
<p class="kyc-doc-alert__text">{{ kycDocumentAlertMessage }}</p>
|
||||
</div>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="button"
|
||||
class="kyc-doc-alert__action"
|
||||
(click)="toggleEditMode()"
|
||||
>
|
||||
Update Document
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Verification Table -->
|
||||
<div class="kv-table">
|
||||
<div class="kv-table__header">
|
||||
@@ -87,12 +113,21 @@
|
||||
<span class="kv-table__col kv-table__col--status">
|
||||
@if (kycData.panVerified) {
|
||||
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
||||
} @else if (panDeclarationTaken) {
|
||||
<!-- Deliberately not "Verified": the check never passed, paperwork covers it. -->
|
||||
<span class="kv-status kv-status--declared"><mat-icon>assignment_turned_in</mat-icon> Declaration</span>
|
||||
} @else {
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{
|
||||
kycData.panVerifiedOn ? formatDate(kycData.panVerifiedOn) : '—'
|
||||
kycData.panVerified
|
||||
? kycData.panVerifiedOn
|
||||
? formatDate(kycData.panVerifiedOn)
|
||||
: '—'
|
||||
: panDeclarationTaken && kycData.panDeclarationTakenOn
|
||||
? formatDate(kycData.panDeclarationTakenOn)
|
||||
: '—'
|
||||
}}</span>
|
||||
<span class="kv-table__col kv-table__col--action">
|
||||
<button
|
||||
@@ -108,9 +143,80 @@
|
||||
Re-Verify
|
||||
}
|
||||
</button>
|
||||
<!--
|
||||
A PAN that has never passed is not always fixable by another attempt. Offer the
|
||||
declaration here too, so an existing client sitting in the exception list can be
|
||||
cleared without the officer having to run a verification they know will fail.
|
||||
-->
|
||||
@if (!kycData.panVerified && !panDeclarationTaken && kycData.panNumber) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="kv-action-btn"
|
||||
type="button"
|
||||
[disabled]="!kycId || saving"
|
||||
(click)="takePanDeclaration()"
|
||||
>
|
||||
Declaration
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
PAN is unverified but a declaration is on file. Shown in full because it is the only
|
||||
reason PAN carries no KYC exception — a reviewer has to see what was declared.
|
||||
-->
|
||||
@if (!kycData.panVerified && panDeclarationTaken) {
|
||||
<div class="kv-decl">
|
||||
<div class="kv-decl__head">
|
||||
<mat-icon>assignment_turned_in</mat-icon>
|
||||
<span class="kv-decl__title">PAN Declaration</span>
|
||||
@if (!saving) {
|
||||
<span class="kv-decl__actions">
|
||||
<button mat-stroked-button type="button" class="kv-action-btn" (click)="takePanDeclaration()">
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="kv-action-btn danger"
|
||||
(click)="removePanDeclaration()"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="kv-decl__rows">
|
||||
<div class="kv-decl__row">
|
||||
<span class="k">Does not match</span>
|
||||
<span class="v strong">{{ panDeclarationMismatchLabel }}</span>
|
||||
</div>
|
||||
@if (kycData.panDeclarationNameOnPan) {
|
||||
<div class="kv-decl__row">
|
||||
<span class="k">Name on PAN</span>
|
||||
<span class="v">{{ kycData.panDeclarationNameOnPan }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (kycData.panDeclarationDobOnPan) {
|
||||
<div class="kv-decl__row">
|
||||
<span class="k">DOB on PAN</span>
|
||||
<span class="v">{{ formatDate(kycData.panDeclarationDobOnPan) }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (kycData.panDeclarationRemarks) {
|
||||
<div class="kv-decl__row">
|
||||
<span class="k">Declaration file</span>
|
||||
<span class="v">{{ kycData.panDeclarationRemarks }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="kv-decl__note">
|
||||
PAN remains unverified. No KYC exception is raised for PAN while this declaration is on file.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Aadhaar Row -->
|
||||
<div class="kv-table__row">
|
||||
<span class="kv-table__col kv-table__col--doc">
|
||||
@@ -120,12 +226,21 @@
|
||||
<span class="kv-table__col kv-table__col--status">
|
||||
@if (kycData.aadhaarVerified) {
|
||||
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
||||
} @else if (aadhaarReceiptTaken) {
|
||||
<!-- Deliberately not "Verified": no Aadhaar exists yet, the slip covers it. -->
|
||||
<span class="kv-status kv-status--receipt"><mat-icon>receipt_long</mat-icon> Receipt</span>
|
||||
} @else {
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{
|
||||
kycData.aadhaarVerifiedOn ? formatDate(kycData.aadhaarVerifiedOn) : '—'
|
||||
kycData.aadhaarVerified
|
||||
? kycData.aadhaarVerifiedOn
|
||||
? formatDate(kycData.aadhaarVerifiedOn)
|
||||
: '—'
|
||||
: aadhaarReceiptTaken && kycData.aadhaarReceiptTakenOn
|
||||
? formatDate(kycData.aadhaarReceiptTakenOn)
|
||||
: '—'
|
||||
}}</span>
|
||||
<span class="kv-table__col kv-table__col--action">
|
||||
@if (!showAadhaarReVerify && !aadhaarOtpSent) {
|
||||
@@ -139,9 +254,142 @@
|
||||
Re-Verify
|
||||
</button>
|
||||
}
|
||||
<!--
|
||||
No Aadhaar number means the OTP check has nothing to run against. Offer the enrolment
|
||||
slip here so an existing client sitting in the exception list can be cleared.
|
||||
-->
|
||||
@if (!kycData.aadhaarVerified && !aadhaarReceiptTaken) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="kv-action-btn"
|
||||
type="button"
|
||||
[disabled]="!kycId || saving"
|
||||
(click)="takeAadhaarReceipt()"
|
||||
>
|
||||
Receipt
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Aadhaar is unverified but an enrolment receipt is on file. Shown in full because it is the
|
||||
only reason Aadhaar carries no KYC exception.
|
||||
-->
|
||||
@if (!kycData.aadhaarVerified && aadhaarReceiptTaken) {
|
||||
<div class="kv-rcpt">
|
||||
<div class="kv-rcpt__head">
|
||||
<mat-icon>receipt_long</mat-icon>
|
||||
<span class="kv-rcpt__title">Aadhaar Enrolment Receipt</span>
|
||||
@if (!saving) {
|
||||
<span class="kv-rcpt__actions">
|
||||
<button mat-stroked-button type="button" class="kv-action-btn" (click)="takeAadhaarReceipt()">
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="kv-action-btn danger"
|
||||
(click)="removeAadhaarReceipt()"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="kv-rcpt__rows">
|
||||
<div class="kv-rcpt__row">
|
||||
<span class="k">Enrolment ID</span>
|
||||
<span class="v mono">{{ kycData.aadhaarEnrolmentId || '—' }}</span>
|
||||
</div>
|
||||
@if (kycData.aadhaarEnrolmentDate) {
|
||||
<div class="kv-rcpt__row">
|
||||
<span class="k">Enrolment date</span>
|
||||
<span class="v">{{ formatDate(kycData.aadhaarEnrolmentDate) }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (kycData.aadhaarReceiptRemarks) {
|
||||
<div class="kv-rcpt__row">
|
||||
<span class="k">Remarks</span>
|
||||
<span class="v">{{ kycData.aadhaarReceiptRemarks }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="kv-rcpt__note">
|
||||
Aadhaar remains unverified. No KYC exception is raised for Aadhaar while this receipt is on file.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!--
|
||||
The OVD collected at onboarding. It has never appeared here, so the dates driving the
|
||||
renewal alert were invisible — an officer told to update the document could not see which
|
||||
one, nor when it was issued.
|
||||
-->
|
||||
@if (kycData.kycDocumentName) {
|
||||
<div class="kv-table__row">
|
||||
<span class="kv-table__col kv-table__col--doc">
|
||||
<mat-icon class="kv-doc-icon">description</mat-icon> {{ kycData.kycDocumentName }}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--num kv-mono">{{ kycData.kycDocumentNumber || '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--status">
|
||||
@switch (kycDocumentRenewal.state) {
|
||||
@case ('EXPIRED') {
|
||||
<span class="kv-status kv-status--fail"><mat-icon>error</mat-icon> Expired</span>
|
||||
}
|
||||
@case ('EXPIRING_SOON') {
|
||||
<span class="kv-status kv-status--declared"><mat-icon>schedule</mat-icon> Expires Soon</span>
|
||||
}
|
||||
@case ('DUE') {
|
||||
<span class="kv-status kv-status--declared"><mat-icon>update</mat-icon> Update Due</span>
|
||||
}
|
||||
@case ('OK') {
|
||||
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Current</span>
|
||||
}
|
||||
@default {
|
||||
<span class="kv-status kv-status--pending"><mat-icon>help_outline</mat-icon> No dates</span>
|
||||
}
|
||||
}
|
||||
</span>
|
||||
<!--
|
||||
Issued-on, not verified-on: this document is sighted, never API-verified.
|
||||
|
||||
Every document shows an expiry, printed or not: one with no expiry on the card is
|
||||
still only good for five years from issue, so the derived date is shown rather than a
|
||||
blank. It is tagged so a calculated date is never mistaken for one on the document.
|
||||
-->
|
||||
<span class="kv-table__col kv-table__col--date">
|
||||
@if (kycData.kycDocIssueDate || kycData.kycDocExpiryDate) {
|
||||
<span class="kv-doc-dates">
|
||||
@if (kycData.kycDocIssueDate) {
|
||||
<span>Issued {{ formatDate(kycData.kycDocIssueDate) }}</span>
|
||||
}
|
||||
@if (kycDocumentExpiresOn) {
|
||||
<span
|
||||
class="kv-doc-dates__expiry"
|
||||
[class.kv-doc-dates__expiry--derived]="kycDocumentExpiryIsDerived"
|
||||
>
|
||||
Expires {{ kycDocumentExpiresOn }}
|
||||
@if (kycDocumentExpiryIsDerived) {
|
||||
<span
|
||||
class="kv-doc-dates__tag"
|
||||
matTooltip="No expiry printed on this document — calculated as 5 years from the issue date"
|
||||
>5-yr</span
|
||||
>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
} @else {
|
||||
—
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--action">
|
||||
<button mat-stroked-button class="kv-action-btn" type="button" (click)="toggleEditMode()">Update</button>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- GSTIN Row (Non-Individual only) -->
|
||||
@if (isNonIndividual) {
|
||||
<div class="kv-table__row">
|
||||
@@ -360,39 +608,72 @@
|
||||
<!-- KYC Meta Grid -->
|
||||
<div class="kv-meta-grid">
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">KYC Type</span>
|
||||
<span class="kv-meta-value">{{
|
||||
kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—'
|
||||
}}</span>
|
||||
<mat-icon class="kv-meta-icon icon-blue">assignment_ind</mat-icon>
|
||||
<div class="kv-meta-info">
|
||||
<span class="kv-meta-label">KYC Type</span>
|
||||
<span class="kv-meta-value">
|
||||
{{ kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">Risk Category</span>
|
||||
<span class="kv-meta-value">{{
|
||||
kycData.riskCategory === 1
|
||||
? 'Low'
|
||||
: kycData.riskCategory === 2
|
||||
? 'Medium'
|
||||
: kycData.riskCategory === 3
|
||||
? 'High'
|
||||
: '—'
|
||||
}}</span>
|
||||
<mat-icon
|
||||
class="kv-meta-icon"
|
||||
[ngClass]="{
|
||||
'icon-green': kycData.riskCategory === 1,
|
||||
'icon-orange': kycData.riskCategory === 2,
|
||||
'icon-red': kycData.riskCategory === 3
|
||||
}"
|
||||
>shield</mat-icon
|
||||
>
|
||||
<div class="kv-meta-info">
|
||||
<span class="kv-meta-label">Risk Category</span>
|
||||
<span class="kv-meta-value">
|
||||
{{
|
||||
kycData.riskCategory === 1
|
||||
? 'Low'
|
||||
: kycData.riskCategory === 2
|
||||
? 'Medium'
|
||||
: kycData.riskCategory === 3
|
||||
? 'High'
|
||||
: '—'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">Initiated On</span>
|
||||
<span class="kv-meta-value">{{ formatDate(kycData.initiatedOn) }}</span>
|
||||
<mat-icon class="kv-meta-icon icon-grey">calendar_today</mat-icon>
|
||||
<div class="kv-meta-info">
|
||||
<span class="kv-meta-label">Initiated On</span>
|
||||
<span class="kv-meta-value">{{ formatDate(kycData.initiatedOn) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">Last Verified</span>
|
||||
<span class="kv-meta-value">{{ formatDate(kycData.verifiedOn) }}</span>
|
||||
<mat-icon class="kv-meta-icon icon-green">check_circle</mat-icon>
|
||||
<div class="kv-meta-info">
|
||||
<span class="kv-meta-label">Last Verified</span>
|
||||
<span class="kv-meta-value">{{ formatDate(kycData.verifiedOn) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">Expiry Date</span>
|
||||
<span class="kv-meta-value">{{ formatDate(kycData.expiryDate) }}</span>
|
||||
<mat-icon class="kv-meta-icon icon-red">history_toggle_off</mat-icon>
|
||||
<div class="kv-meta-info">
|
||||
<span class="kv-meta-label">Expiry Date</span>
|
||||
<span class="kv-meta-value">{{ formatDate(kycData.expiryDate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (kycData.remarks) {
|
||||
<div class="kv-meta-item kv-meta-item--full">
|
||||
<span class="kv-meta-label">Remarks</span>
|
||||
<span class="kv-meta-value">{{ kycData.remarks }}</span>
|
||||
<mat-icon class="kv-meta-icon icon-grey">notes</mat-icon>
|
||||
<div class="kv-meta-info">
|
||||
<span class="kv-meta-label">Remarks</span>
|
||||
<span class="kv-meta-value">{{ kycData.remarks }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -567,6 +848,22 @@
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.pan }}</span>
|
||||
</div>
|
||||
<!--
|
||||
The check can fail for reasons no retry will fix — a PAN card that spells the
|
||||
name differently, or a vendor that is simply down. The declaration is the way
|
||||
out, so it is offered here rather than leaving the officer at a dead end.
|
||||
-->
|
||||
<div class="kyc-decl-offer">
|
||||
<span>Customer's PAN card genuinely carries different details, or PAN cannot be checked?</span>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
[disabled]="!kycId || saving"
|
||||
(click)="takePanDeclaration()"
|
||||
>
|
||||
{{ panDeclarationTaken ? 'Edit Declaration' : 'Take Declaration' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (verificationStatus.pan === 'VERIFIED' && verificationDetails.pan?.name) {
|
||||
<span class="verification-detail-text">{{ verificationDetails.pan.name }}</span>
|
||||
@@ -678,6 +975,22 @@
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.aadhaar }}</span>
|
||||
</div>
|
||||
<!--
|
||||
A failed OTP is not always something a retry can fix — a customer who has only
|
||||
applied for Aadhaar has no number to verify at all. The enrolment slip is the way
|
||||
out, so it is offered here rather than leaving the officer at a dead end.
|
||||
-->
|
||||
<div class="kyc-decl-offer">
|
||||
<span>Customer has applied for Aadhaar but not received it yet?</span>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
[disabled]="!kycId || saving"
|
||||
(click)="takeAadhaarReceipt()"
|
||||
>
|
||||
{{ aadhaarReceiptTaken ? 'Edit Receipt' : 'Record Receipt' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user