792 lines
26 KiB
TypeScript
792 lines
26 KiB
TypeScript
/**
|
|
* 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 { deriveKycExceptionFields } from '../../kyc-exception.util';
|
|
import { Dates } from 'app/core/utils/dates';
|
|
import { jsPDF } from 'jspdf';
|
|
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;
|
|
clientData: any = null;
|
|
clientDateOfBirth: string | null = null;
|
|
isNonIndividual = false;
|
|
loading = true;
|
|
saving = false;
|
|
isEditMode = false;
|
|
errorMessage: string | null = null;
|
|
|
|
/** Re-KYC flow state */
|
|
reKycUploading = false;
|
|
reKycCompleting = false;
|
|
reKycSuccess = false;
|
|
reKycError: string | null = null;
|
|
selectedDeclarationFile: File | 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: [''],
|
|
isException: [false],
|
|
exceptionReason: [[]]
|
|
});
|
|
|
|
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) {
|
|
const rawKycId = data.id ?? data.kycId ?? data.resourceId;
|
|
this.kycId = rawKycId != null ? rawKycId.toString() : null;
|
|
this.kycForm.patchValue({
|
|
kycType: data.kycType || 1,
|
|
riskCategory: data.riskCategory || 1,
|
|
panNumber: data.panNumber || '',
|
|
// Aadhaar is stored masked on the server; user must re-enter
|
|
// the full 12-digit number to re-verify, so don't pre-fill.
|
|
aadhaarNumber: '',
|
|
gstinNumber: data.gstinNumber || '',
|
|
remarks: data.remarks || '',
|
|
isException: data.isException || false,
|
|
exceptionReason: data.exceptionReason ? data.exceptionReason.split(', ') : []
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Load client's date of birth and legal form from parent route resolved data */
|
|
loadClientDateOfBirth(): void {
|
|
this.route.parent.data.subscribe((data: any) => {
|
|
this.clientData = data?.clientViewData || null;
|
|
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;
|
|
}
|
|
}
|
|
if (data?.clientViewData?.legalForm?.id === 2) {
|
|
this.isNonIndividual = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
toggleEditMode(): void {
|
|
this.isEditMode = !this.isEditMode;
|
|
this.resetOtpState();
|
|
if (this.isEditMode) {
|
|
// Clear Aadhaar so the user types a fresh 12-digit number for re-verify;
|
|
// server only returns the masked form which would fail validation.
|
|
this.kycForm.get('aadhaarNumber')?.reset('');
|
|
this.verificationStatus.aadhaar = null;
|
|
this.verificationStatus.pan = null;
|
|
this.verificationError.aadhaar = null;
|
|
this.verificationError.pan = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exception columns derived from verification status — see `deriveKycExceptionFields()`.
|
|
*
|
|
* A field counts as verified if it was verified in this session OR is already verified on the
|
|
* stored record; either way the customer has cleared that check. Verifying one of two
|
|
* outstanding items narrows the exception to the remaining one rather than closing it.
|
|
*/
|
|
private buildExceptionFields() {
|
|
const panVerified = this.verificationStatus.pan === 'VERIFIED' || this.kycData?.panVerified === true;
|
|
const aadhaarVerified = this.verificationStatus.aadhaar === 'VERIFIED' || this.kycData?.aadhaarVerified === true;
|
|
|
|
return deriveKycExceptionFields({
|
|
panVerified,
|
|
aadhaarVerified,
|
|
aadhaarApplicable: !this.isNonIndividual,
|
|
hadException: this.kycData?.isException === true,
|
|
previousReason: this.kycData?.exceptionReason || null,
|
|
manualReasons: Array.isArray(this.kycForm.value.exceptionReason) ? this.kycForm.value.exceptionReason : [],
|
|
todayFormatted: this.dateUtils.formatDate(new Date(), 'dd MMMM yyyy')
|
|
});
|
|
}
|
|
|
|
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,
|
|
// dateFormat is required so the backend can parse exceptionResolvedOn when an exception closes.
|
|
dateFormat: 'dd MMMM yyyy',
|
|
...this.buildExceptionFields()
|
|
};
|
|
|
|
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.';
|
|
}
|
|
});
|
|
}
|
|
|
|
private saveKycBeforeVerify(callback: () => void, field: 'pan' | 'aadhaar' | 'gstin'): void {
|
|
if (this.isEditMode) {
|
|
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,
|
|
riskCategory: this.kycForm.value.riskCategory || 1,
|
|
dateFormat: 'dd MMMM yyyy',
|
|
...this.buildExceptionFields()
|
|
};
|
|
|
|
this.clientsService.updateClientKyc(this.clientId, this.kycId, payload).subscribe({
|
|
next: () => {
|
|
this.loadKycData();
|
|
callback();
|
|
},
|
|
error: (err: any) => {
|
|
this.verifying[field] = false;
|
|
this.verificationStatus[field] = 'FAILED';
|
|
this.verificationError[field] = err?.error?.defaultUserMessage || `Failed to update ${field.toUpperCase()} number before verification.`;
|
|
}
|
|
});
|
|
} else {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
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.saveKycBeforeVerify(() => {
|
|
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.';
|
|
}
|
|
});
|
|
}, 'pan');
|
|
}
|
|
|
|
/** 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.saveKycBeforeVerify(() => {
|
|
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.';
|
|
}
|
|
});
|
|
}, 'aadhaar');
|
|
}
|
|
|
|
/** 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.saveKycBeforeVerify(() => {
|
|
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.';
|
|
}
|
|
});
|
|
}, 'gstin');
|
|
}
|
|
|
|
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 = '';
|
|
}
|
|
|
|
getStatusLabel(code: string): string {
|
|
if (!code) return '';
|
|
const part = code.includes('.') ? code.split('.').pop() : code;
|
|
return (part.charAt(0).toUpperCase() + part.slice(1)).replace(/_/g, ' ');
|
|
}
|
|
|
|
getStatusClass(code: string): string {
|
|
if (!code) return '';
|
|
const part = code.includes('.') ? code.split('.').pop() : code;
|
|
return part.toLowerCase();
|
|
}
|
|
|
|
getPanAadhaarStatusLabel(status: string): string {
|
|
switch (status) {
|
|
case 'Y':
|
|
return 'Linked (Seeded)';
|
|
case 'N':
|
|
return 'Not Linked';
|
|
case 'E':
|
|
return 'Exempt';
|
|
default:
|
|
return 'Pending Verification';
|
|
}
|
|
}
|
|
|
|
/** Generate Self Declaration PDF with KYC details */
|
|
printSelfDeclaration(): void {
|
|
const client = this.clientData || {};
|
|
const kyc = this.kycData || {};
|
|
const clientName = client.displayName || client.fullname || '';
|
|
const cifNumber = client.accountNo || client.cifNumber || this.clientId;
|
|
const dob = this.clientDateOfBirth ? this.formatDate(client.dateOfBirth) : '—';
|
|
const officeName = client.officeName || '';
|
|
const today = new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'long', year: 'numeric' });
|
|
|
|
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
|
|
const pageWidth = doc.internal.pageSize.getWidth();
|
|
const margin = 20;
|
|
const contentWidth = pageWidth - margin * 2;
|
|
let y = 20;
|
|
|
|
// Helper
|
|
const addLine = (label: string, value: string, yPos: number): number => {
|
|
doc.setFont('helvetica', 'bold');
|
|
doc.setFontSize(10);
|
|
doc.text(label, margin, yPos);
|
|
doc.setFont('helvetica', 'normal');
|
|
doc.text(value || '—', margin + 55, yPos);
|
|
return yPos + 7;
|
|
};
|
|
|
|
const drawHr = (yPos: number): number => {
|
|
doc.setDrawColor(200, 200, 200);
|
|
doc.setLineWidth(0.3);
|
|
doc.line(margin, yPos, pageWidth - margin, yPos);
|
|
return yPos + 5;
|
|
};
|
|
|
|
// ── Header ──
|
|
doc.setFillColor(15, 23, 42);
|
|
doc.rect(0, 0, pageWidth, 30, 'F');
|
|
doc.setTextColor(255, 255, 255);
|
|
doc.setFont('helvetica', 'bold');
|
|
doc.setFontSize(16);
|
|
doc.text('Self Declaration Form', pageWidth / 2, 13, { align: 'center' });
|
|
doc.setFontSize(9);
|
|
doc.setFont('helvetica', 'normal');
|
|
doc.text('KYC / Re-KYC Compliance — As per RBI Master Direction on KYC', pageWidth / 2, 21, { align: 'center' });
|
|
doc.setTextColor(0, 0, 0);
|
|
|
|
y = 40;
|
|
|
|
// ── Branch / Date ──
|
|
doc.setFontSize(10);
|
|
doc.setFont('helvetica', 'normal');
|
|
doc.text(`Branch: ${officeName}`, margin, y);
|
|
doc.text(`Date: ${today}`, pageWidth - margin, y, { align: 'right' });
|
|
y += 8;
|
|
y = drawHr(y);
|
|
|
|
// ── Section: Customer Details ──
|
|
doc.setFont('helvetica', 'bold');
|
|
doc.setFontSize(12);
|
|
doc.setTextColor(16, 116, 185);
|
|
doc.text('Customer Details', margin, y);
|
|
doc.setTextColor(0, 0, 0);
|
|
y += 8;
|
|
|
|
y = addLine('Customer Name:', clientName, y);
|
|
y = addLine('CIF Number:', cifNumber, y);
|
|
y = addLine('Date of Birth:', dob, y);
|
|
if (client.mobileNo) y = addLine('Mobile:', client.mobileNo, y);
|
|
if (client.emailAddress) y = addLine('Email:', client.emailAddress, y);
|
|
y += 3;
|
|
y = drawHr(y);
|
|
|
|
// ── Section: KYC Documents ──
|
|
doc.setFont('helvetica', 'bold');
|
|
doc.setFontSize(12);
|
|
doc.setTextColor(16, 116, 185);
|
|
doc.text('KYC Document Details', margin, y);
|
|
doc.setTextColor(0, 0, 0);
|
|
y += 8;
|
|
|
|
// PAN
|
|
y = addLine('PAN Number:', kyc.panNumber || '—', y);
|
|
y = addLine('PAN Status:', kyc.panVerified ? 'Verified' : 'Pending', y);
|
|
if (kyc.panVerifiedOn) y = addLine('PAN Verified On:', this.formatDate(kyc.panVerifiedOn), y);
|
|
|
|
// Aadhaar
|
|
y = addLine('Aadhaar Number:', kyc.aadhaarNumberMasked || '—', y);
|
|
y = addLine('Aadhaar Status:', kyc.aadhaarVerified ? 'Verified' : 'Pending', y);
|
|
if (kyc.aadhaarVerifiedOn) y = addLine('Aadhaar Verified On:', this.formatDate(kyc.aadhaarVerifiedOn), y);
|
|
|
|
// PAN-Aadhaar Link
|
|
const seedingStatus =
|
|
kyc.panAadhaarSeedingStatus === 'Y'
|
|
? 'Linked'
|
|
: kyc.panAadhaarSeedingStatus === 'N'
|
|
? 'Not Linked'
|
|
: kyc.panAadhaarSeedingStatus === 'E'
|
|
? 'Exempt'
|
|
: 'Pending';
|
|
y = addLine('PAN-Aadhaar Link:', seedingStatus, y);
|
|
|
|
// GSTIN (Non-Individual)
|
|
if (this.isNonIndividual && kyc.gstinNumber) {
|
|
y = addLine('GSTIN:', kyc.gstinNumber, y);
|
|
y = addLine('GSTIN Status:', kyc.gstinVerified ? 'Verified' : 'Pending', y);
|
|
}
|
|
|
|
// Enterprise PAN / CIN
|
|
if (this.isNonIndividual && kyc.enterprisePan) {
|
|
y = addLine('Enterprise PAN:', kyc.enterprisePan, y);
|
|
}
|
|
if (this.isNonIndividual && kyc.cinNumber) {
|
|
y = addLine('CIN:', kyc.cinNumber, y);
|
|
}
|
|
|
|
y += 3;
|
|
y = drawHr(y);
|
|
|
|
// ── Section: KYC Summary ──
|
|
doc.setFont('helvetica', 'bold');
|
|
doc.setFontSize(12);
|
|
doc.setTextColor(16, 116, 185);
|
|
doc.text('KYC Summary', margin, y);
|
|
doc.setTextColor(0, 0, 0);
|
|
y += 8;
|
|
|
|
y = addLine('KYC Type:', kyc.kycType === 1 ? 'Individual' : kyc.kycType === 2 ? 'Non-Individual' : '—', y);
|
|
y = addLine(
|
|
'Risk Category:',
|
|
kyc.riskCategory === 1 ? 'Low' : kyc.riskCategory === 2 ? 'Medium' : kyc.riskCategory === 3 ? 'High' : '—',
|
|
y
|
|
);
|
|
y = addLine('Initiated On:', this.formatDate(kyc.initiatedOn), y);
|
|
y = addLine('Last Verified:', this.formatDate(kyc.verifiedOn), y);
|
|
y = addLine('Expiry Date:', this.formatDate(kyc.expiryDate), y);
|
|
|
|
y += 3;
|
|
y = drawHr(y);
|
|
|
|
// ── Declaration Text ──
|
|
doc.setFont('helvetica', 'bold');
|
|
doc.setFontSize(12);
|
|
doc.setTextColor(16, 116, 185);
|
|
doc.text('Declaration', margin, y);
|
|
doc.setTextColor(0, 0, 0);
|
|
y += 8;
|
|
|
|
doc.setFont('helvetica', 'normal');
|
|
doc.setFontSize(9.5);
|
|
const declarationText = `I, ${clientName || '_______________'}, hereby declare that the information provided above is true, correct and complete to the best of my knowledge and belief. I understand that furnishing false information is punishable under applicable laws. I authorize the Bank to verify the above details through appropriate channels as per RBI Master Direction on KYC (DBR.AML.BC.No.81/14.01.001/2015-16) and its subsequent amendments.
|
|
|
|
I further confirm that:
|
|
1. I am not a Politically Exposed Person (PEP) / relative of a PEP (unless declared otherwise).
|
|
2. The source of funds in my account is from legitimate sources.
|
|
3. I shall promptly notify the Bank of any change in the above information.
|
|
4. I have read and understood the Bank's terms and conditions regarding KYC compliance.`;
|
|
|
|
const lines = doc.splitTextToSize(declarationText, contentWidth);
|
|
doc.text(lines, margin, y);
|
|
y += lines.length * 4.5 + 10;
|
|
|
|
// ── Signature section ──
|
|
y = drawHr(y);
|
|
y += 5;
|
|
|
|
// Left: Client signature
|
|
doc.setFont('helvetica', 'normal');
|
|
doc.setFontSize(9);
|
|
doc.text('________________________', margin, y + 15);
|
|
doc.text('Signature of Customer', margin, y + 21);
|
|
doc.text(`Name: ${clientName}`, margin, y + 27);
|
|
doc.text(`Date: _______________`, margin, y + 33);
|
|
doc.text(`Place: _______________`, margin, y + 39);
|
|
|
|
// Right: Bank official
|
|
doc.text('________________________', pageWidth - margin - 50, y + 15);
|
|
doc.text('Authorized Bank Official', pageWidth - margin - 50, y + 21);
|
|
doc.text('Name: _______________', pageWidth - margin - 50, y + 27);
|
|
doc.text('Designation: _______________', pageWidth - margin - 50, y + 33);
|
|
doc.text('Stamp & Seal', pageWidth - margin - 50, y + 39);
|
|
|
|
// ── Footer ──
|
|
const footerY = doc.internal.pageSize.getHeight() - 10;
|
|
doc.setFontSize(7);
|
|
doc.setTextColor(150, 150, 150);
|
|
doc.text('This is a system-generated document. Generated from CBS Nexora — KYC Module.', pageWidth / 2, footerY, {
|
|
align: 'center'
|
|
});
|
|
|
|
// Save
|
|
doc.save(`Self_Declaration_${cifNumber}_${clientName.replace(/\s/g, '_')}.pdf`);
|
|
}
|
|
|
|
/** Handle file selection for signed declaration upload */
|
|
onDeclarationFileSelected(event: Event): void {
|
|
const input = event.target as HTMLInputElement;
|
|
if (input.files?.length) {
|
|
this.selectedDeclarationFile = input.files[0];
|
|
this.reKycError = null;
|
|
}
|
|
}
|
|
|
|
/** Upload signed declaration and complete re-KYC */
|
|
completeReKyc(): void {
|
|
if (!this.selectedDeclarationFile || !this.clientId || !this.kycId) return;
|
|
|
|
this.reKycCompleting = true;
|
|
this.reKycError = null;
|
|
this.reKycSuccess = false;
|
|
|
|
// Step 1: Upload the signed declaration as a client document
|
|
const formData = new FormData();
|
|
formData.append('file', this.selectedDeclarationFile);
|
|
formData.append('name', 'KYC Self Declaration');
|
|
formData.append(
|
|
'description',
|
|
`Signed self-declaration form uploaded on ${new Date().toLocaleDateString('en-IN')}`
|
|
);
|
|
|
|
this.clientsService.uploadClientDocument(this.clientId, formData).subscribe({
|
|
next: () => {
|
|
// Step 2: Call re-KYC completion API to update verified/expiry dates
|
|
this.clientsService.completeReKyc(this.clientId, this.kycId!).subscribe({
|
|
next: () => {
|
|
this.reKycCompleting = false;
|
|
this.reKycSuccess = true;
|
|
this.selectedDeclarationFile = null;
|
|
this.loadKycData(); // Refresh to show updated dates
|
|
},
|
|
error: (err: any) => {
|
|
this.reKycCompleting = false;
|
|
this.reKycError =
|
|
err?.error?.defaultUserMessage ||
|
|
err?.error?.developerMessage ||
|
|
'Failed to complete re-KYC. Please try again.';
|
|
}
|
|
});
|
|
},
|
|
error: (err: any) => {
|
|
this.reKycCompleting = false;
|
|
this.reKycError =
|
|
err?.error?.defaultUserMessage ||
|
|
err?.error?.developerMessage ||
|
|
'Failed to upload declaration. Please try again.';
|
|
}
|
|
});
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
}
|
|
}
|