feat: add RBI circulars, CKYC, KYC, Superset analytics, passbook modules and UI updates
Add new feature modules (RBI feeds, CKYC, KYC tab, Form60, Superset analytics, passbook), update client family members, dashboard, login page, sidenav, images, translations, environment config, and fix RBI feeds API auth by using relative URLs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
07f701f654
commit
73aa69ee02
@@ -0,0 +1,621 @@
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="kyc-tab-container">
|
||||
<!-- Loading State -->
|
||||
@if (loading) {
|
||||
<div class="kyc-loading">
|
||||
<mat-progress-spinner mode="indeterminate" diameter="48"></mat-progress-spinner>
|
||||
<p>{{ 'labels.inputs.KYC Verification' | translate }}...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading) {
|
||||
<!-- Header -->
|
||||
<div class="kyc-tab-header">
|
||||
<div class="kyc-tab-title">
|
||||
<h3>{{ 'labels.inputs.KYC Verification' | translate }}</h3>
|
||||
<span class="kyc-badge">KYC</span>
|
||||
@if (kycData?.kycStatusCode) {
|
||||
<span class="kyc-overall-status" [class]="'status-' + kycData.kycStatusCode.toLowerCase()">
|
||||
{{ kycData.kycStatusCode }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<button mat-raised-button color="primary" (click)="toggleEditMode()" type="button">
|
||||
<fa-icon [icon]="isEditMode ? 'times' : 'edit'" class="m-r-10"></fa-icon>
|
||||
{{ isEditMode ? ('labels.buttons.Cancel' | translate) : ('labels.buttons.Edit' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- KYC Summary (read-only when not in edit mode) -->
|
||||
@if (!isEditMode && kycData) {
|
||||
<div class="kyc-summary-grid">
|
||||
<!-- PAN Card -->
|
||||
<mat-card class="kyc-card">
|
||||
<mat-card-content>
|
||||
<div class="kyc-card-header">
|
||||
<div class="kyc-card-label">
|
||||
<mat-icon class="kyc-card-icon">credit_card</mat-icon>
|
||||
<span>{{ 'labels.inputs.PAN Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
@if (kycData.panVerified) {
|
||||
<span class="status-chip verified">
|
||||
<mat-icon class="status-chip-icon">check_circle</mat-icon>
|
||||
{{ 'labels.inputs.Verified' | translate }}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="status-chip not-verified">
|
||||
<mat-icon class="status-chip-icon">radio_button_unchecked</mat-icon>
|
||||
{{ 'labels.inputs.Not Verified' | translate }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<p class="kyc-value">{{ kycData.panNumber || '—' }}</p>
|
||||
@if (kycData.panVerifiedOn) {
|
||||
<p class="kyc-meta">
|
||||
{{ 'labels.inputs.Last Verified On' | translate }}: {{ formatDate(kycData.panVerifiedOn) }}
|
||||
</p>
|
||||
}
|
||||
@if (verificationError.pan) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.pan }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (verificationStatus.pan === 'VERIFIED' && verificationDetails.pan?.name) {
|
||||
<p class="kyc-meta verification-detail-text">{{ verificationDetails.pan.name }}</p>
|
||||
}
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
class="re-verify-btn"
|
||||
type="button"
|
||||
[disabled]="verifying.pan || !kycData.panNumber"
|
||||
(click)="verifyPAN()"
|
||||
>
|
||||
@if (verifying.pan) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.inputs.Re-Verify' | translate }}
|
||||
}
|
||||
</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- Aadhaar Card -->
|
||||
<mat-card class="kyc-card">
|
||||
<mat-card-content>
|
||||
<div class="kyc-card-header">
|
||||
<div class="kyc-card-label">
|
||||
<mat-icon class="kyc-card-icon">fingerprint</mat-icon>
|
||||
<span>{{ 'labels.inputs.Aadhaar Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
@if (kycData.aadhaarVerified) {
|
||||
<span class="status-chip verified">
|
||||
<mat-icon class="status-chip-icon">check_circle</mat-icon>
|
||||
{{ 'labels.inputs.Verified' | translate }}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="status-chip not-verified">
|
||||
<mat-icon class="status-chip-icon">radio_button_unchecked</mat-icon>
|
||||
{{ 'labels.inputs.Not Verified' | translate }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<p class="kyc-value">{{ kycData.aadhaarNumberMasked || '—' }}</p>
|
||||
@if (kycData.aadhaarVerifiedOn) {
|
||||
<p class="kyc-meta">
|
||||
{{ 'labels.inputs.Last Verified On' | translate }}: {{ formatDate(kycData.aadhaarVerifiedOn) }}
|
||||
</p>
|
||||
}
|
||||
@if (verificationError.aadhaar) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.aadhaar }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (verificationStatus.aadhaar === 'VERIFIED' && verificationDetails.aadhaar?.name) {
|
||||
<p class="kyc-meta verification-detail-text">{{ verificationDetails.aadhaar.name }}</p>
|
||||
}
|
||||
|
||||
<!-- Step 1: Re-Verify button -->
|
||||
@if (!showAadhaarReVerify && !aadhaarOtpSent) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
class="re-verify-btn"
|
||||
type="button"
|
||||
[disabled]="verifying.aadhaar || !kycData.aadhaarNumberMasked"
|
||||
(click)="startAadhaarReVerify()"
|
||||
>
|
||||
{{ 'labels.inputs.Re-Verify' | translate }}
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Step 2: Enter full Aadhaar + Send OTP -->
|
||||
@if (showAadhaarReVerify && !aadhaarOtpSent) {
|
||||
<div class="otp-section">
|
||||
<div class="otp-inline">
|
||||
<mat-form-field appearance="outline" class="otp-field">
|
||||
<mat-label>{{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="aadhaarFullNumber"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
maxlength="12"
|
||||
type="password"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
class="otp-verify-btn"
|
||||
type="button"
|
||||
[disabled]="!aadhaarFullNumber || aadhaarFullNumber.length !== 12 || verifying.aadhaar"
|
||||
(click)="sendAadhaarOtp()"
|
||||
>
|
||||
@if (verifying.aadhaar) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Send OTP
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Step 3: Enter OTP + Verify -->
|
||||
@if (aadhaarOtpSent) {
|
||||
<div class="otp-section">
|
||||
<p class="otp-info">
|
||||
<mat-icon class="otp-info-icon">sms</mat-icon>
|
||||
OTP sent to Aadhaar-linked mobile
|
||||
</p>
|
||||
<div class="otp-inline">
|
||||
<mat-form-field appearance="outline" class="otp-field">
|
||||
<mat-label>Enter OTP</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="aadhaarOtp"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
class="otp-verify-btn"
|
||||
type="button"
|
||||
[disabled]="!aadhaarOtp || aadhaarOtp.length < 4 || aadhaarOtpVerifying"
|
||||
(click)="verifyAadhaarOtp()"
|
||||
>
|
||||
@if (aadhaarOtpVerifying) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Verify OTP
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="otp-resend-btn"
|
||||
[disabled]="verifying.aadhaar"
|
||||
(click)="sendAadhaarOtp()"
|
||||
>
|
||||
Resend
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- GSTIN Card -->
|
||||
<mat-card class="kyc-card">
|
||||
<mat-card-content>
|
||||
<div class="kyc-card-header">
|
||||
<div class="kyc-card-label">
|
||||
<mat-icon class="kyc-card-icon">business</mat-icon>
|
||||
<span>{{ 'labels.inputs.GSTIN Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
@if (kycData.gstinVerified) {
|
||||
<span class="status-chip verified">
|
||||
<mat-icon class="status-chip-icon">check_circle</mat-icon>
|
||||
{{ 'labels.inputs.Verified' | translate }}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="status-chip not-verified">
|
||||
<mat-icon class="status-chip-icon">radio_button_unchecked</mat-icon>
|
||||
{{ 'labels.inputs.Not Verified' | translate }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<p class="kyc-value">{{ kycData.gstinNumber || '—' }}</p>
|
||||
@if (kycData.gstinVerifiedOn) {
|
||||
<p class="kyc-meta">
|
||||
{{ 'labels.inputs.Last Verified On' | translate }}: {{ formatDate(kycData.gstinVerifiedOn) }}
|
||||
</p>
|
||||
}
|
||||
@if (verificationError.gstin) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.gstin }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (verificationStatus.gstin === 'VERIFIED' && verificationDetails.gstin?.businessName) {
|
||||
<p class="kyc-meta verification-detail-text">{{ verificationDetails.gstin.businessName }}</p>
|
||||
}
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
class="re-verify-btn"
|
||||
type="button"
|
||||
[disabled]="verifying.gstin || !kycData.gstinNumber"
|
||||
(click)="verifyGSTIN()"
|
||||
>
|
||||
@if (verifying.gstin) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.inputs.Re-Verify' | translate }}
|
||||
}
|
||||
</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<!-- Additional Info -->
|
||||
<mat-card class="kyc-info-card">
|
||||
<mat-card-content>
|
||||
<div class="kyc-info-grid">
|
||||
<div class="kyc-info-item">
|
||||
<span class="kyc-info-label">{{ 'labels.inputs.KYC Type' | translate }}</span>
|
||||
<span class="kyc-info-value">{{
|
||||
kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="kyc-info-item">
|
||||
<span class="kyc-info-label">{{ 'labels.inputs.Risk Category' | translate }}</span>
|
||||
<span class="kyc-info-value">{{
|
||||
kycData.riskCategory === 1
|
||||
? 'Low'
|
||||
: kycData.riskCategory === 2
|
||||
? 'Medium'
|
||||
: kycData.riskCategory === 3
|
||||
? 'High'
|
||||
: '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="kyc-info-item">
|
||||
<span class="kyc-info-label">{{ 'labels.inputs.Initiated On' | translate }}</span>
|
||||
<span class="kyc-info-value">{{ kycData.initiatedOn || '—' }}</span>
|
||||
</div>
|
||||
<div class="kyc-info-item">
|
||||
<span class="kyc-info-label">{{ 'labels.inputs.Last Verified On' | translate }}</span>
|
||||
<span class="kyc-info-value">{{ formatDate(kycData.verifiedOn) }}</span>
|
||||
</div>
|
||||
<div class="kyc-info-item">
|
||||
<span class="kyc-info-label">{{ 'labels.inputs.Expiry Date' | translate }}</span>
|
||||
<span class="kyc-info-value">{{ kycData.expiryDate || '—' }}</span>
|
||||
</div>
|
||||
@if (kycData.remarks) {
|
||||
<div class="kyc-info-item full-span">
|
||||
<span class="kyc-info-label">{{ 'labels.inputs.Remarks' | translate }}</span>
|
||||
<span class="kyc-info-value">{{ kycData.remarks }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
|
||||
<!-- No KYC Data State -->
|
||||
@if (!isEditMode && !kycData) {
|
||||
<div class="kyc-empty">
|
||||
<mat-icon class="kyc-empty-icon">verified_user</mat-icon>
|
||||
<p>{{ 'labels.inputs.KYC Verification' | translate }} not initiated yet.</p>
|
||||
<button mat-raised-button color="primary" (click)="toggleEditMode()" type="button">
|
||||
{{ 'labels.inputs.KYC verification initiated' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Edit Form -->
|
||||
@if (isEditMode) {
|
||||
<mat-card class="kyc-edit-card">
|
||||
<mat-card-content>
|
||||
<form [formGroup]="kycForm" (ngSubmit)="saveKyc()">
|
||||
<div class="kyc-edit-grid">
|
||||
<!-- PAN -->
|
||||
<div class="kyc-edit-field">
|
||||
<div class="kyc-field-label-row">
|
||||
<span class="kyc-field-title">{{ 'labels.inputs.PAN Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
<div class="kyc-input-row">
|
||||
<mat-form-field appearance="outline" class="kyc-input">
|
||||
<mat-label>{{ 'labels.inputs.PAN Number' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="panNumber"
|
||||
maxlength="10"
|
||||
style="text-transform: uppercase"
|
||||
(input)="
|
||||
kycForm
|
||||
.get('panNumber')
|
||||
?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||
"
|
||||
/>
|
||||
@if (kycForm.get('panNumber')?.hasError('pattern') && kycForm.get('panNumber')?.touched) {
|
||||
<mat-error>{{ 'labels.inputs.Enter 10-character PAN' | translate }}</mat-error>
|
||||
}
|
||||
@if (verificationStatus.pan === 'VERIFIED') {
|
||||
<mat-icon matSuffix class="status-icon verified">check_circle</mat-icon>
|
||||
}
|
||||
@if (verificationStatus.pan === 'FAILED') {
|
||||
<mat-icon matSuffix class="status-icon failed">cancel</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
type="button"
|
||||
class="verify-btn"
|
||||
[disabled]="
|
||||
!kycId || !kycForm.get('panNumber')?.value || kycForm.get('panNumber')?.invalid || verifying.pan
|
||||
"
|
||||
(click)="verifyPAN()"
|
||||
[matTooltip]="!kycId ? 'Save KYC details first' : ''"
|
||||
>
|
||||
@if (verifying.pan) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.inputs.Verify' | translate }}
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@if (verificationError.pan) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.pan }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (verificationStatus.pan === 'VERIFIED' && verificationDetails.pan?.name) {
|
||||
<span class="verification-detail-text">{{ verificationDetails.pan.name }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Aadhaar -->
|
||||
<div class="kyc-edit-field">
|
||||
<div class="kyc-field-label-row">
|
||||
<span class="kyc-field-title">{{ 'labels.inputs.Aadhaar Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
<div class="kyc-input-row">
|
||||
<mat-form-field appearance="outline" class="kyc-input">
|
||||
<mat-label>{{ 'labels.inputs.Aadhaar Number' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="aadhaarNumber"
|
||||
maxlength="12"
|
||||
type="password"
|
||||
[readonly]="aadhaarOtpSent"
|
||||
/>
|
||||
@if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) {
|
||||
<mat-error>{{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }}</mat-error>
|
||||
}
|
||||
@if (verificationStatus.aadhaar === 'VERIFIED') {
|
||||
<mat-icon matSuffix class="status-icon verified">check_circle</mat-icon>
|
||||
}
|
||||
@if (verificationStatus.aadhaar === 'FAILED') {
|
||||
<mat-icon matSuffix class="status-icon failed">cancel</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
@if (!aadhaarOtpSent) {
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
type="button"
|
||||
class="verify-btn"
|
||||
[disabled]="
|
||||
!kycId ||
|
||||
!kycForm.get('aadhaarNumber')?.value ||
|
||||
kycForm.get('aadhaarNumber')?.invalid ||
|
||||
verifying.aadhaar
|
||||
"
|
||||
(click)="sendAadhaarOtp()"
|
||||
[matTooltip]="!kycId ? 'Save KYC details first' : ''"
|
||||
>
|
||||
@if (verifying.aadhaar) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Send OTP
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- OTP Section in Edit Mode -->
|
||||
@if (aadhaarOtpSent) {
|
||||
<div class="otp-section">
|
||||
<p class="otp-info">
|
||||
<mat-icon class="otp-info-icon">sms</mat-icon>
|
||||
OTP sent to Aadhaar-linked mobile
|
||||
</p>
|
||||
<div class="otp-inline">
|
||||
<mat-form-field appearance="outline" class="otp-field">
|
||||
<mat-label>Enter OTP</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="aadhaarOtp"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
class="otp-verify-btn"
|
||||
type="button"
|
||||
[disabled]="!aadhaarOtp || aadhaarOtp.length < 4 || aadhaarOtpVerifying"
|
||||
(click)="verifyAadhaarOtp()"
|
||||
>
|
||||
@if (aadhaarOtpVerifying) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Verify OTP
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="otp-resend-btn"
|
||||
[disabled]="verifying.aadhaar"
|
||||
(click)="sendAadhaarOtp()"
|
||||
>
|
||||
Resend
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (verificationError.aadhaar) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.aadhaar }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- GSTIN -->
|
||||
<div class="kyc-edit-field">
|
||||
<div class="kyc-field-label-row">
|
||||
<span class="kyc-field-title">{{ 'labels.inputs.GSTIN Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
<div class="kyc-input-row">
|
||||
<mat-form-field appearance="outline" class="kyc-input">
|
||||
<mat-label>{{ 'labels.inputs.GSTIN Number' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="gstinNumber"
|
||||
maxlength="15"
|
||||
style="text-transform: uppercase"
|
||||
(input)="
|
||||
kycForm
|
||||
.get('gstinNumber')
|
||||
?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||
"
|
||||
/>
|
||||
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
|
||||
<mat-error>{{ 'labels.inputs.Enter 15-character GSTIN' | translate }}</mat-error>
|
||||
}
|
||||
@if (verificationStatus.gstin === 'VERIFIED') {
|
||||
<mat-icon matSuffix class="status-icon verified">check_circle</mat-icon>
|
||||
}
|
||||
@if (verificationStatus.gstin === 'FAILED') {
|
||||
<mat-icon matSuffix class="status-icon failed">cancel</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
type="button"
|
||||
class="verify-btn"
|
||||
[disabled]="
|
||||
!kycId ||
|
||||
!kycForm.get('gstinNumber')?.value ||
|
||||
kycForm.get('gstinNumber')?.invalid ||
|
||||
verifying.gstin
|
||||
"
|
||||
(click)="verifyGSTIN()"
|
||||
[matTooltip]="!kycId ? 'Save KYC details first' : ''"
|
||||
>
|
||||
@if (verifying.gstin) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.inputs.Verify' | translate }}
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@if (verificationError.gstin) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.gstin }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (verificationStatus.gstin === 'VERIFIED' && verificationDetails.gstin?.businessName) {
|
||||
<span class="verification-detail-text">{{ verificationDetails.gstin.businessName }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- KYC Type -->
|
||||
@if (!kycId) {
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.KYC Type' | translate }}</mat-label>
|
||||
<mat-select formControlName="kycType">
|
||||
<mat-option [value]="1">Individual</mat-option>
|
||||
<mat-option [value]="2">Non-Individual</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<!-- Risk Category -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.Risk Category' | translate }}</mat-label>
|
||||
<mat-select formControlName="riskCategory">
|
||||
<mat-option [value]="1">Low</mat-option>
|
||||
<mat-option [value]="2">Medium</mat-option>
|
||||
<mat-option [value]="3">High</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Remarks -->
|
||||
<mat-form-field appearance="outline" class="full-span">
|
||||
<mat-label>{{ 'labels.inputs.Remarks' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="remarks" rows="3"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@if (errorMessage) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!kycId) {
|
||||
<div class="kyc-info-hint">
|
||||
<mat-icon class="kyc-info-hint-icon">info</mat-icon>
|
||||
<span>Save KYC details first to enable verification buttons.</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="kyc-form-actions">
|
||||
<button mat-raised-button type="submit" color="primary" [disabled]="kycForm.invalid || saving">
|
||||
@if (saving) {
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
}
|
||||
</button>
|
||||
<button mat-stroked-button type="button" (click)="toggleEditMode()">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
.kyc-tab-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.kyc-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
gap: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.kyc-tab-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.kyc-tab-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-badge {
|
||||
background-color: #1976d2;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.kyc-tag {
|
||||
background-color: #e3f2fd;
|
||||
color: #1565c0;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 7px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #90caf9;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.kyc-overall-status {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
|
||||
&.status-verified,
|
||||
&.status-complete {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
border: 1px solid #a5d6a7;
|
||||
}
|
||||
|
||||
&.status-pending,
|
||||
&.status-initiated {
|
||||
background-color: #fff8e1;
|
||||
color: #f57f17;
|
||||
border: 1px solid #ffe082;
|
||||
}
|
||||
|
||||
&.status-rejected,
|
||||
&.status-failed {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
border: 1px solid #ef9a9a;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.kyc-card {
|
||||
.mat-mdc-card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.kyc-card-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.kyc-card-icon {
|
||||
font-size: 18px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.kyc-value {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
margin: 8px 0 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.kyc-meta {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.re-verify-btn {
|
||||
font-size: 12px;
|
||||
padding: 0 12px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 3px 8px;
|
||||
border-radius: 14px;
|
||||
|
||||
&.verified {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
border: 1px solid #a5d6a7;
|
||||
}
|
||||
|
||||
&.not-verified {
|
||||
background-color: #f5f5f5;
|
||||
color: #757575;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-chip-icon {
|
||||
font-size: 14px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.kyc-info-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.kyc-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.kyc-info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
&.full-span {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-info-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.kyc-info-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.kyc-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 64px 16px;
|
||||
gap: 16px;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kyc-empty-icon {
|
||||
font-size: 64px;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
color: #bdbdbd;
|
||||
}
|
||||
|
||||
.kyc-edit-card {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.kyc-edit-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
|
||||
@media (width <= 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-edit-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.kyc-field-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.kyc-field-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.kyc-input-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.kyc-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.verify-btn {
|
||||
margin-top: 4px;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.full-span {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.kyc-form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* OTP Section */
|
||||
.otp-section {
|
||||
margin-top: 8px;
|
||||
padding: 10px 14px;
|
||||
background-color: #f3f8ff;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.otp-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #1565c0;
|
||||
margin: 0 0 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.otp-info-icon {
|
||||
font-size: 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.otp-inline {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.otp-field {
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.otp-verify-btn {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.otp-resend-btn {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
/* Error message */
|
||||
.kyc-error-msg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: #c62828;
|
||||
margin-top: 6px;
|
||||
padding: 5px 8px;
|
||||
background-color: #ffebee;
|
||||
border: 1px solid #ef9a9a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.kyc-error-icon {
|
||||
font-size: 14px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.verification-detail-text {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #2e7d32;
|
||||
font-weight: 500;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Status icons in edit mode */
|
||||
.status-icon {
|
||||
font-size: 20px;
|
||||
|
||||
&.verified {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
&.failed {
|
||||
color: #c62828;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators, FormsModule } from '@angular/forms';
|
||||
import { catchError, of, Subject, takeUntil } from 'rxjs';
|
||||
|
||||
/** Custom Services */
|
||||
import { ClientsService } from '../../clients.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* KYC Tab Component.
|
||||
* Displays and manages KYC (Know Your Customer) details for an existing client.
|
||||
* API: GET/POST /clients/{clientId}/kyc
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-kyc-tab',
|
||||
templateUrl: './kyc-tab.component.html',
|
||||
styleUrls: ['./kyc-tab.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FormsModule,
|
||||
MatIcon,
|
||||
MatProgressSpinner,
|
||||
MatTooltip,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class KycTabComponent implements OnInit, OnDestroy {
|
||||
private route = inject(ActivatedRoute);
|
||||
private clientsService = inject(ClientsService);
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
private dateUtils = inject(Dates);
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
clientId: string;
|
||||
kycId: string | null = null;
|
||||
kycData: any = null;
|
||||
clientDateOfBirth: string | null = null;
|
||||
loading = true;
|
||||
saving = false;
|
||||
isEditMode = false;
|
||||
errorMessage: string | null = null;
|
||||
|
||||
verifying: { pan: boolean; aadhaar: boolean; gstin: boolean } = {
|
||||
pan: false,
|
||||
aadhaar: false,
|
||||
gstin: false
|
||||
};
|
||||
|
||||
/** Verification status tracking for re-verify */
|
||||
verificationStatus: { pan: string | null; aadhaar: string | null; gstin: string | null } = {
|
||||
pan: null,
|
||||
aadhaar: null,
|
||||
gstin: null
|
||||
};
|
||||
|
||||
verificationError: { pan: string | null; aadhaar: string | null; gstin: string | null } = {
|
||||
pan: null,
|
||||
aadhaar: null,
|
||||
gstin: null
|
||||
};
|
||||
|
||||
verificationDetails: { pan: any; aadhaar: any; gstin: any } = {
|
||||
pan: null,
|
||||
aadhaar: null,
|
||||
gstin: null
|
||||
};
|
||||
|
||||
/** Aadhaar OTP flow state */
|
||||
aadhaarOtpSent = false;
|
||||
aadhaarOtpReferenceId: string | null = null;
|
||||
aadhaarOtp = '';
|
||||
aadhaarOtpVerifying = false;
|
||||
showAadhaarReVerify = false;
|
||||
aadhaarFullNumber = '';
|
||||
|
||||
kycForm: UntypedFormGroup = this.formBuilder.group({
|
||||
kycType: [
|
||||
1,
|
||||
[Validators.required]
|
||||
],
|
||||
riskCategory: [
|
||||
1,
|
||||
[Validators.required]
|
||||
],
|
||||
panNumber: [
|
||||
'',
|
||||
[Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
|
||||
],
|
||||
aadhaarNumber: [
|
||||
'',
|
||||
[Validators.pattern(/^\d{12}$/)]
|
||||
],
|
||||
gstinNumber: [
|
||||
'',
|
||||
[Validators.pattern(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/)]
|
||||
],
|
||||
remarks: ['']
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.parent.params.subscribe((params: any) => {
|
||||
this.clientId = params['clientId'];
|
||||
this.loadKycData();
|
||||
this.loadClientDateOfBirth();
|
||||
});
|
||||
}
|
||||
|
||||
loadKycData(): void {
|
||||
this.loading = true;
|
||||
this.errorMessage = null;
|
||||
this.clientsService
|
||||
.getClientKyc(this.clientId)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
this.loading = false;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((data: any) => {
|
||||
this.loading = false;
|
||||
this.kycData = data;
|
||||
if (data) {
|
||||
this.kycId = data.id?.toString() || null;
|
||||
this.kycForm.patchValue({
|
||||
kycType: data.kycType || 1,
|
||||
riskCategory: data.riskCategory || 1,
|
||||
panNumber: data.panNumber || '',
|
||||
aadhaarNumber: data.aadhaarNumberMasked || '',
|
||||
gstinNumber: data.gstinNumber || '',
|
||||
remarks: data.remarks || ''
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Load client's date of birth from parent route resolved data */
|
||||
loadClientDateOfBirth(): void {
|
||||
this.route.parent.data.subscribe((data: any) => {
|
||||
if (data?.clientViewData?.dateOfBirth) {
|
||||
const dob = data.clientViewData.dateOfBirth;
|
||||
if (Array.isArray(dob) && dob.length >= 3) {
|
||||
this.clientDateOfBirth = `${dob[0]}-${String(dob[1]).padStart(2, '0')}-${String(dob[2]).padStart(2, '0')}`;
|
||||
} else if (typeof dob === 'string') {
|
||||
this.clientDateOfBirth = dob;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleEditMode(): void {
|
||||
this.isEditMode = !this.isEditMode;
|
||||
this.resetOtpState();
|
||||
}
|
||||
|
||||
saveKyc(): void {
|
||||
if (this.kycForm.invalid) {
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
this.errorMessage = null;
|
||||
|
||||
const aadhaarRaw = this.kycForm.value.aadhaarNumber || '';
|
||||
const aadhaarMasked = aadhaarRaw.length === 12 ? 'XXXXXXXX' + aadhaarRaw.slice(-4) : aadhaarRaw || null;
|
||||
|
||||
const payload: any = {
|
||||
locale: 'en',
|
||||
panNumber: this.kycForm.value.panNumber || null,
|
||||
aadhaarNumberMasked: aadhaarMasked,
|
||||
gstinNumber: this.kycForm.value.gstinNumber || null,
|
||||
remarks: this.kycForm.value.remarks || null
|
||||
};
|
||||
|
||||
if (!this.kycId) {
|
||||
payload.kycType = this.kycForm.value.kycType || 1;
|
||||
payload.riskCategory = this.kycForm.value.riskCategory || 1;
|
||||
} else {
|
||||
payload.riskCategory = this.kycForm.value.riskCategory || 1;
|
||||
}
|
||||
|
||||
const save$ = this.kycId
|
||||
? this.clientsService.updateClientKyc(this.clientId, this.kycId, payload)
|
||||
: this.clientsService.saveClientKyc(this.clientId, payload);
|
||||
|
||||
save$.subscribe({
|
||||
next: (response: any) => {
|
||||
this.saving = false;
|
||||
this.isEditMode = false;
|
||||
if (!this.kycId && response?.resourceId) {
|
||||
this.kycId = response.resourceId.toString();
|
||||
}
|
||||
this.loadKycData();
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.saving = false;
|
||||
this.errorMessage =
|
||||
err?.error?.developerMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
err?.message ||
|
||||
'Failed to save KYC details.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
verifyPAN(): void {
|
||||
if (!this.kycId) {
|
||||
this.verificationError.pan = 'KYC record not found. Save KYC details first.';
|
||||
return;
|
||||
}
|
||||
this.verifying.pan = true;
|
||||
this.verificationStatus.pan = 'PENDING';
|
||||
this.verificationError.pan = null;
|
||||
this.verificationDetails.pan = null;
|
||||
|
||||
this.clientsService
|
||||
.verifyPAN(this.clientId, this.kycId)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (response: any) => {
|
||||
this.verifying.pan = false;
|
||||
if (response?.changes?.panVerified) {
|
||||
this.verificationStatus.pan = 'VERIFIED';
|
||||
this.verificationDetails.pan = response.changes;
|
||||
this.loadKycData();
|
||||
} else {
|
||||
this.verificationStatus.pan = 'FAILED';
|
||||
this.verificationError.pan = 'PAN verification failed.';
|
||||
}
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.verifying.pan = false;
|
||||
this.verificationStatus.pan = 'FAILED';
|
||||
this.verificationError.pan =
|
||||
err?.error?.developerMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
err?.message ||
|
||||
'PAN verification failed. Please try again.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Show Aadhaar input in summary view for re-verification */
|
||||
startAadhaarReVerify(): void {
|
||||
this.showAadhaarReVerify = true;
|
||||
this.aadhaarFullNumber = '';
|
||||
this.verificationError.aadhaar = null;
|
||||
}
|
||||
|
||||
/** Step 1: Send OTP to Aadhaar-linked mobile */
|
||||
sendAadhaarOtp(): void {
|
||||
const aadhaar = this.isEditMode ? this.kycForm?.get('aadhaarNumber')?.value : this.aadhaarFullNumber;
|
||||
if (!aadhaar || aadhaar.length !== 12) {
|
||||
this.verificationError.aadhaar = 'Please enter the full 12-digit Aadhaar number to generate OTP.';
|
||||
return;
|
||||
}
|
||||
if (!this.kycId) {
|
||||
this.verificationError.aadhaar = 'KYC record not found. Save KYC details first.';
|
||||
return;
|
||||
}
|
||||
this.verifying.aadhaar = true;
|
||||
this.verificationStatus.aadhaar = 'PENDING';
|
||||
this.verificationError.aadhaar = null;
|
||||
this.aadhaarOtpSent = false;
|
||||
this.aadhaarOtp = '';
|
||||
|
||||
this.clientsService
|
||||
.sendAadhaarOtp(this.clientId, this.kycId, aadhaar)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (response: any) => {
|
||||
this.verifying.aadhaar = false;
|
||||
const refNumber =
|
||||
response?.changes?.aadhaarRefNumber ||
|
||||
response?.aadhaarRefNumber ||
|
||||
response?.changes?.referenceNumber ||
|
||||
response?.resourceId;
|
||||
this.aadhaarOtpSent = true;
|
||||
this.aadhaarOtpReferenceId = refNumber || null;
|
||||
this.verificationStatus.aadhaar = 'PENDING';
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.verifying.aadhaar = false;
|
||||
this.verificationStatus.aadhaar = 'FAILED';
|
||||
this.verificationError.aadhaar =
|
||||
err?.error?.developerMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
err?.message ||
|
||||
'Failed to send OTP. Please try again.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Step 2: Verify OTP entered by user */
|
||||
verifyAadhaarOtp(): void {
|
||||
if (!this.kycId || !this.aadhaarOtp || this.aadhaarOtp.length < 4) {
|
||||
return;
|
||||
}
|
||||
this.aadhaarOtpVerifying = true;
|
||||
this.verificationError.aadhaar = null;
|
||||
|
||||
this.clientsService
|
||||
.verifyAadhaarOtp(this.clientId, this.kycId, this.aadhaarOtp, this.aadhaarOtpReferenceId || '')
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (response: any) => {
|
||||
this.aadhaarOtpVerifying = false;
|
||||
if (response?.changes?.aadhaarVerified) {
|
||||
this.verificationStatus.aadhaar = 'VERIFIED';
|
||||
this.verificationDetails.aadhaar = response.changes;
|
||||
this.resetOtpState();
|
||||
this.loadKycData();
|
||||
} else {
|
||||
this.verificationStatus.aadhaar = 'FAILED';
|
||||
this.verificationError.aadhaar = 'OTP verification failed.';
|
||||
this.resetOtpState();
|
||||
}
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.aadhaarOtpVerifying = false;
|
||||
this.verificationStatus.aadhaar = 'FAILED';
|
||||
this.verificationError.aadhaar =
|
||||
err?.error?.developerMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
err?.message ||
|
||||
'OTP verification failed. Please try again.';
|
||||
this.resetOtpState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
verifyGSTIN(): void {
|
||||
if (!this.kycId) {
|
||||
this.verificationError.gstin = 'KYC record not found. Save KYC details first.';
|
||||
return;
|
||||
}
|
||||
this.verifying.gstin = true;
|
||||
this.verificationStatus.gstin = 'PENDING';
|
||||
this.verificationError.gstin = null;
|
||||
this.verificationDetails.gstin = null;
|
||||
|
||||
this.clientsService
|
||||
.verifyGSTIN(this.clientId, this.kycId)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (response: any) => {
|
||||
this.verifying.gstin = false;
|
||||
if (response?.changes?.gstinVerified) {
|
||||
this.verificationStatus.gstin = 'VERIFIED';
|
||||
this.verificationDetails.gstin = response.changes;
|
||||
this.loadKycData();
|
||||
} else {
|
||||
this.verificationStatus.gstin = 'FAILED';
|
||||
this.verificationError.gstin = 'GSTIN verification failed.';
|
||||
}
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.verifying.gstin = false;
|
||||
this.verificationStatus.gstin = 'FAILED';
|
||||
this.verificationError.gstin =
|
||||
err?.error?.developerMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
err?.message ||
|
||||
'GSTIN verification failed. Please try again.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formatDate(date: any): string {
|
||||
if (!date) return '—';
|
||||
let year: number, month: number, day: number;
|
||||
if (Array.isArray(date) && date.length >= 3) {
|
||||
[
|
||||
year,
|
||||
month,
|
||||
day
|
||||
] = date;
|
||||
} else {
|
||||
return date;
|
||||
}
|
||||
const months = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
];
|
||||
return `${day} ${months[month - 1]} ${year}`;
|
||||
}
|
||||
|
||||
private resetOtpState(): void {
|
||||
this.aadhaarOtpSent = false;
|
||||
this.aadhaarOtp = '';
|
||||
this.aadhaarOtpReferenceId = null;
|
||||
this.aadhaarOtpVerifying = false;
|
||||
this.showAadhaarReVerify = false;
|
||||
this.aadhaarFullNumber = '';
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user