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
@@ -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>