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