feat(clients, alm): add CIN/MCA verification & authorized signatories tab; refactor ALM report print and export
This commit is contained in:
@@ -29,6 +29,7 @@ import { ClientsService } from '../../clients.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { ProofType, normalizeProofType, groupProofsByCategory } from '../../shared/kyc-proof-types';
|
||||
import { ENTITY_KYC_DOCUMENT_TYPES, KycDocumentType, withEntityKycDocuments } from '../../shared/entity-kyc-documents';
|
||||
import { deriveKycExceptionFields } from '../../kyc-exception.util';
|
||||
import { KycUploadHelperService } from '../../shared/kyc-upload-helper.service';
|
||||
import { CaptureImageDialogComponent } from '../../clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component';
|
||||
@@ -110,6 +111,27 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
} | null = null;
|
||||
|
||||
today = new Date();
|
||||
/**
|
||||
* ClientKycType on the backend: 1 = INDIVIDUAL, 2 = NON_INDIVIDUAL. Every payload must carry the
|
||||
* value matching the client's legal form — the backend rejects the entity-only fields
|
||||
* (enterprisePan / gstinNumber / cinNumber / incorporationDate) on a KYC record typed INDIVIDUAL.
|
||||
*/
|
||||
get kycTypeValue(): number {
|
||||
return this.isNonIndividual ? 2 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Risk category as it will actually be saved.
|
||||
*
|
||||
* Read via getRawValue() because the riskCategory control is disabled whenever the override
|
||||
* checkbox is off — which is the normal case — and `kycForm.value` omits disabled controls.
|
||||
* Reading `.value` there yields undefined and quietly falls back to 1 (Low), so a verification
|
||||
* would stamp Low onto the record and the next full save would change it to the auto-calculated
|
||||
* category, showing up as an edit of a verified record that the officer never made.
|
||||
*/
|
||||
private get currentRiskCategory(): number {
|
||||
return this.kycForm.getRawValue().riskCategory || 1;
|
||||
}
|
||||
kycId: string = '';
|
||||
/** Whether existing KYC data has been loaded (edit mode) */
|
||||
kycLoaded = false;
|
||||
@@ -121,12 +143,17 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
/** KYC Document Types — populated from API template, falls back to defaults */
|
||||
kycDocumentTypes: { name: string; code: string; mandatory: boolean }[] = [];
|
||||
kycDocumentTypes: KycDocumentType[] = [];
|
||||
|
||||
/** Chip-group state for KYC document picker (mirrors identity proof in identifier dialog) */
|
||||
kycDocProofs: ProofType[] = [];
|
||||
kycDocGroups: { label: string; items: ProofType[] }[] = [];
|
||||
selectedKycDoc: ProofType | null = null;
|
||||
/**
|
||||
* Constitution whose document list is currently loaded. Distinguishes the first arrival of a
|
||||
* constitution from the officer switching to a different one — see updateDocTypesForConstitution.
|
||||
*/
|
||||
private appliedConstitutionValue: string | null = null;
|
||||
/** Front/back files for the chosen KYC document */
|
||||
kycDocFiles: { front: File | null; back: File | null } = { front: null, back: null };
|
||||
/**
|
||||
@@ -185,6 +212,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
/** Photo + signature are mandatory before leaving the KYC tab */
|
||||
get photoSigComplete(): boolean {
|
||||
if (this.isNonIndividual) {
|
||||
return true;
|
||||
}
|
||||
return this.hasPhoto && this.hasSignature;
|
||||
}
|
||||
|
||||
@@ -225,7 +255,10 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
const needsCin = !!this.kycForm?.get('cinNumber')?.value;
|
||||
const panOk = this.entPanVerifyStatus === 'VERIFIED';
|
||||
const gstinOk = !needsGstin || this.gstinVerifyStatus === 'VERIFIED';
|
||||
const cinOk = !needsCin || this.cinVerifyStatus === 'VERIFIED';
|
||||
// A CIN the provider cannot confirm still counts as done. MCA lookups return no match for
|
||||
// CINs that are perfectly valid, and PAN and GSTIN already establish the entity's identity —
|
||||
// holding the whole KYC open on a check that cannot pass would strand the client instead.
|
||||
const cinOk = !needsCin || this.cinVerifyStatus === 'VERIFIED' || this.cinVerifyStatus === 'UNCONFIRMED';
|
||||
return panOk && gstinOk && cinOk;
|
||||
} else {
|
||||
const needsPan = !this.isMinor && this.kycForm?.get('hasPan')?.value;
|
||||
@@ -250,18 +283,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
{ name: 'Birth Certificate', code: 'BIRTH_CERTIFICATE' },
|
||||
{ name: 'School ID Card', code: 'SCHOOL_ID_CARD' }
|
||||
];
|
||||
/** Generic RBI-prescribed KYC documents for legal entities (Master Direction on KYC,
|
||||
* Annexure I) — shown for non-individual clients before a specific constitution type is
|
||||
* picked. Individual OVDs (Aadhaar/Passport/DL/Voter ID/NREGA/NPR) don't apply to entities. */
|
||||
private nonIndividualDefaultDocTypes = [
|
||||
{ name: 'Certificate of Incorporation / Registration Certificate', code: 'CERT_INCORPORATION' },
|
||||
{ name: 'PAN Card of Entity', code: 'ENTITY_PAN' },
|
||||
{ name: 'Memorandum & Articles of Association / Partnership Deed / Trust Deed', code: 'CONSTITUTIVE_DOC' },
|
||||
{ name: 'Board Resolution / Authorization Letter', code: 'BOARD_RESOLUTION' },
|
||||
{ name: 'KYC of Authorized Signatory / Power of Attorney Holder', code: 'AUTH_SIGNATORY_KYC' },
|
||||
{ name: 'GST Registration Certificate', code: 'GST_CERTIFICATE' }
|
||||
];
|
||||
/** Constitution-based KYC document types for non-individual clients */
|
||||
/** Constitution-based KYC document types for non-individual clients. Shown in addition to
|
||||
* the RBI entity documents (ENTITY_KYC_DOCUMENT_TYPES), never instead of them — see
|
||||
* withEntityKycDocuments. */
|
||||
private constitutionDocTypes: Record<string, { name: string; mandatory: boolean }[]> = {
|
||||
HUF: [
|
||||
{ name: 'PAN Card of HUF', mandatory: true },
|
||||
@@ -513,7 +537,14 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
gstinVerifyState = '';
|
||||
|
||||
/** CIN verification (dummy) */
|
||||
cinVerifyStatus: 'NONE' | 'VERIFIED' | 'FAILED' = 'NONE';
|
||||
/**
|
||||
* UNCONFIRMED means the CIN is saved and correctly formatted, but MCA (via LincPay) would not
|
||||
* confirm it. Kept distinct from VERIFIED, which is only ever set by a positive answer from the
|
||||
* provider, and from FAILED, which is reserved for the provider being unreachable.
|
||||
*/
|
||||
cinVerifyStatus: 'NONE' | 'VERIFIED' | 'UNCONFIRMED' | 'FAILED' = 'NONE';
|
||||
/** Explanation shown beside the CIN when the provider could not confirm it. */
|
||||
cinUnconfirmedMessage = '';
|
||||
cinVerifying = false;
|
||||
cinVerifiedOn = '';
|
||||
cinCompanyName = '';
|
||||
@@ -648,9 +679,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Individual-only OVDs vs. entity-only RBI docs — never both, and minor filtering only
|
||||
* ever applies to individuals. */
|
||||
private getDefaultDocTypes(): { name: string; code: string }[] {
|
||||
return this.isNonIndividual
|
||||
? this.nonIndividualDefaultDocTypes
|
||||
: this.filterDocTypesForMinor(this.defaultDocumentTypes);
|
||||
return this.isNonIndividual ? ENTITY_KYC_DOCUMENT_TYPES : this.filterDocTypesForMinor(this.defaultDocumentTypes);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -673,9 +702,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (this.calculatedRisk) {
|
||||
riskControl?.setValue(this.calculatedRisk.category);
|
||||
}
|
||||
if (!this.isKycVerified) {
|
||||
this.kycForm.get('remarks')?.clearValidators();
|
||||
}
|
||||
// Overriding the risk category is the only thing this form demands a justification for.
|
||||
// A verified record does not, by itself, need one — see loadExistingKycIfNeeded.
|
||||
this.kycForm.get('remarks')?.clearValidators();
|
||||
}
|
||||
this.kycForm.get('remarks')?.updateValueAndValidity();
|
||||
});
|
||||
@@ -797,6 +826,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.gstinVerifiedOn = '';
|
||||
this.gstinVerifyState = '';
|
||||
this.cinVerifyStatus = 'NONE';
|
||||
this.cinUnconfirmedMessage = '';
|
||||
this.cinVerifying = false;
|
||||
this.cinVerifiedOn = '';
|
||||
this.cinCompanyName = '';
|
||||
@@ -898,14 +928,26 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
private updateDocTypesForConstitution(): void {
|
||||
const constitutionValue = this.constitutionMeta?.value;
|
||||
if (constitutionValue && this.constitutionDocTypes[constitutionValue]) {
|
||||
this.kycDocumentTypes = this.constitutionDocTypes[constitutionValue].map((doc) => ({
|
||||
name: doc.name,
|
||||
code: doc.name,
|
||||
mandatory: doc.mandatory
|
||||
}));
|
||||
// Reset selected document type since options changed
|
||||
this.kycForm.get('kycDocumentName')?.setValue('');
|
||||
this.kycForm.get('kycDocumentName')?.updateValueAndValidity();
|
||||
// The RBI entity documents stay at the head of the list — picking a constitution adds that
|
||||
// constitution's own requirements, it does not swap out the set the officer was just shown.
|
||||
this.kycDocumentTypes = withEntityKycDocuments(
|
||||
this.constitutionDocTypes[constitutionValue],
|
||||
this.constitutionMeta?.label || null
|
||||
);
|
||||
|
||||
// Clearing the chosen document type only makes sense when the officer switches constitution,
|
||||
// which genuinely invalidates it. It must NOT happen the first time a constitution arrives:
|
||||
// on edit that happens after the saved KYC has been restored, so wiping the value here would
|
||||
// discard the restored document type — and with it the upload slots that display the file
|
||||
// already held against the client, since they render only once a type is selected.
|
||||
const constitutionChanged =
|
||||
this.appliedConstitutionValue !== null && this.appliedConstitutionValue !== constitutionValue;
|
||||
this.appliedConstitutionValue = constitutionValue;
|
||||
if (constitutionChanged) {
|
||||
this.kycForm.get('kycDocumentName')?.setValue('');
|
||||
this.kycForm.get('kycDocumentName')?.updateValueAndValidity();
|
||||
}
|
||||
// Re-resolves the selected chip against the new list from whatever name is on the form.
|
||||
this.rebuildKycDocPicker();
|
||||
}
|
||||
}
|
||||
@@ -913,18 +955,51 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Rebuild chip-group picker model from kycDocumentTypes */
|
||||
private rebuildKycDocPicker(): void {
|
||||
this.kycDocProofs = (this.kycDocumentTypes || []).map((d, i) =>
|
||||
normalizeProofType({ id: d.code || d.name || i, name: d.name, code: d.code, description: d.code || d.name })
|
||||
normalizeProofType({
|
||||
id: d.code || d.name || i,
|
||||
name: d.name,
|
||||
code: d.code,
|
||||
description: d.code || d.name,
|
||||
// Constitution-specific documents carry no code PROOF_META knows, so their group label
|
||||
// has to be handed over explicitly or they all collect under "Other".
|
||||
category: d.category
|
||||
})
|
||||
);
|
||||
this.kycDocGroups = groupProofsByCategory(this.kycDocProofs);
|
||||
// Sync selected proof with current form value (e.g. on edit-mode prefill)
|
||||
const currentName = this.kycForm?.get('kycDocumentName')?.value;
|
||||
if (currentName) {
|
||||
const cleanStr = (s: string) => (s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const targetName = cleanStr(currentName);
|
||||
this.selectedKycDoc = this.kycDocProofs.find((p) => cleanStr(p.name) === targetName) || null;
|
||||
} else {
|
||||
this.selectedKycDoc = null;
|
||||
this.selectedKycDoc = currentName ? this.findKycDocProofByName(currentName) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a stored document-type name back to a chip in the picker.
|
||||
*
|
||||
* A tolerant match is required because the two names are not written by the same thing: an
|
||||
* identifier row carries the CodeValue label the backend holds ("Certificate of Incorporation")
|
||||
* while the picker shows the fuller, constitution-specific label ("Certificate of Incorporation /
|
||||
* Registration Certificate"), so one is usually contained in the other rather than equal to it.
|
||||
* An exact comparison leaves the chip unselected on edit, which in turn hides the file-upload
|
||||
* slots and therefore the document the officer already uploaded.
|
||||
*/
|
||||
private findKycDocProofByName(name: string | null | undefined): ProofType | null {
|
||||
const clean = (s: string) => (s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const target = clean(name || '');
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
const proofs = this.kycDocProofs || [];
|
||||
const exact = proofs.find((p) => clean(p.name) === target);
|
||||
if (exact) {
|
||||
return exact;
|
||||
}
|
||||
// Closest containment wins, so a short label cannot claim a chip that a nearer one also matches.
|
||||
const contained = proofs
|
||||
.filter((p) => {
|
||||
const candidate = clean(p.name);
|
||||
return candidate.includes(target) || target.includes(candidate);
|
||||
})
|
||||
.sort((a, b) => Math.abs(clean(a.name).length - target.length) - Math.abs(clean(b.name).length - target.length));
|
||||
return contained[0] || null;
|
||||
}
|
||||
|
||||
selectKycDoc(proof: ProofType): void {
|
||||
@@ -1102,19 +1177,17 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
isException: data.isException || false,
|
||||
exceptionReason: data.exceptionReason ? data.exceptionReason.split(', ') : []
|
||||
});
|
||||
// Set remarks as required if KYC is verified
|
||||
if (this.isKycVerified) {
|
||||
this.kycForm.get('remarks')?.setValidators(Validators.required);
|
||||
this.kycForm.get('remarks')?.updateValueAndValidity();
|
||||
this.kycForm.get('remarks')?.markAsTouched(); // Force red underline so user knows it's blocking
|
||||
}
|
||||
// Remarks are NOT forced just because the record is verified. The server asks for them
|
||||
// only when a save actually overwrites data the verified record already held
|
||||
// (ClientKycWritePlatformServiceJpaRepositoryImpl.updateKyc), and it says so in the error
|
||||
// it returns. Demanding them up front made every non-individual client open with a red
|
||||
// "Reason for Re-edit" the moment PAN and GSTIN auto-verified — before the officer had
|
||||
// changed anything, and while they still had the KYC document fields left to fill in.
|
||||
this.persistedIsException = data.isException === true;
|
||||
this.persistedExceptionReason = data.exceptionReason || null;
|
||||
// Sync chip-picker selection with the prefilled document name
|
||||
if (data.kycDocumentName) {
|
||||
const cleanStr = (s: string) => (s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const targetName = cleanStr(data.kycDocumentName);
|
||||
this.selectedKycDoc = this.kycDocProofs.find((p) => cleanStr(p.name) === targetName) || null;
|
||||
this.selectedKycDoc = this.findKycDocProofByName(data.kycDocumentName);
|
||||
}
|
||||
// Set verification statuses from existing data
|
||||
if (data.panVerified) {
|
||||
@@ -1175,6 +1248,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
if (data.cinVerified) {
|
||||
this.cinVerifyStatus = 'VERIFIED';
|
||||
// The verified card shows this; without it a reopened client renders the panel blank.
|
||||
this.cinCompanyName = data.cinCompanyName || 'Verified via MCA';
|
||||
const rawCinDate = data.cinVerifiedOn;
|
||||
if (Array.isArray(rawCinDate) && rawCinDate.length >= 3) {
|
||||
this.cinVerifiedOn = `${rawCinDate[2]?.toString().padStart(2, '0')}/${rawCinDate[1]?.toString().padStart(2, '0')}/${rawCinDate[0]}`;
|
||||
@@ -1243,11 +1318,14 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// Backfill document type + number on the form in case the KYC record didn't have
|
||||
// them (older clients were created via the identifier flow only).
|
||||
if (!this.kycForm.get('kycDocumentName')?.value && identifier?.documentType?.name) {
|
||||
const proof = this.findKycDocProofByName(identifier.documentType.name);
|
||||
this.selectedKycDoc = proof;
|
||||
// Store the picker's own label rather than the identifier's when one matched, so the
|
||||
// selected chip and the form value agree and a later save round-trips the same name.
|
||||
this.kycForm.patchValue({
|
||||
kycDocumentName: identifier.documentType.name,
|
||||
kycDocumentName: proof?.name || identifier.documentType.name,
|
||||
kycDocumentNumber: identifier.documentKey || ''
|
||||
});
|
||||
this.selectedKycDoc = this.kycDocProofs.find((p) => p.name === identifier.documentType.name) || null;
|
||||
}
|
||||
|
||||
this.clientsService.getClientIdentificationDocuments(this.existingKycIdentifierId).subscribe({
|
||||
@@ -1453,13 +1531,18 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.kycTemplate = template;
|
||||
this.kycTemplateLoaded = true;
|
||||
if (template?.requiredDocuments?.length > 0) {
|
||||
this.kycDocumentTypes = this.filterDocTypesForMinor(
|
||||
template.requiredDocuments.map((doc: any) => ({
|
||||
name: doc.name,
|
||||
code: doc.code || doc.name,
|
||||
mandatory: doc.mandatory ?? false
|
||||
}))
|
||||
);
|
||||
const templateDocs = template.requiredDocuments.map((doc: any) => ({
|
||||
name: doc.name,
|
||||
code: doc.code || doc.name,
|
||||
mandatory: doc.mandatory ?? false
|
||||
}));
|
||||
// The backend template names the same entity documents differently ("Certificate of
|
||||
// Incorporation" / "COI", "PAN Card of Company" / "PAN"), so for an entity it is merged
|
||||
// onto the RBI list rather than replacing it — otherwise reopening a non-individual
|
||||
// client for edit shows a different set of chips than the one used to create it.
|
||||
this.kycDocumentTypes = this.isNonIndividual
|
||||
? withEntityKycDocuments(templateDocs, template?.clientTypeName || this.constitutionMeta?.label || null)
|
||||
: this.filterDocTypesForMinor(templateDocs);
|
||||
// Update kycDocumentName validator based on mandatory docs
|
||||
const hasMandatory = this.kycDocumentTypes.some((d) => d.mandatory);
|
||||
if (hasMandatory) {
|
||||
@@ -1472,11 +1555,18 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
error: () => {
|
||||
// Fallback to defaults if template API unavailable
|
||||
this.kycTemplateLoaded = true;
|
||||
this.kycDocumentTypes = this.getDefaultDocTypes().map((d) => ({
|
||||
name: d.name,
|
||||
code: d.code,
|
||||
mandatory: false
|
||||
}));
|
||||
// An entity keeps whatever constitution-specific documents it already has alongside the
|
||||
// RBI list; a failed template call must not quietly shorten the picker.
|
||||
const constitutionDocs = this.isNonIndividual
|
||||
? this.constitutionDocTypes[this.constitutionMeta?.value] || []
|
||||
: [];
|
||||
this.kycDocumentTypes = this.isNonIndividual
|
||||
? withEntityKycDocuments(constitutionDocs, this.constitutionMeta?.label || null)
|
||||
: this.getDefaultDocTypes().map((d) => ({
|
||||
name: d.name,
|
||||
code: d.code,
|
||||
mandatory: false
|
||||
}));
|
||||
this.rebuildKycDocPicker();
|
||||
}
|
||||
});
|
||||
@@ -1485,17 +1575,25 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Save or update KYC, then execute callback */
|
||||
private saveKycAndDo(payload: any, callback: () => void, onError: (msg: string) => void) {
|
||||
if (this.kycId) {
|
||||
const { kycType, ...updatePayload } = payload;
|
||||
// riskCategory is dropped alongside kycType on an update. The verify payloads only carry it to
|
||||
// satisfy the initiate validator, which requires it when the record is first created; once the
|
||||
// row exists, re-asserting it on every PAN/GSTIN/CIN check means a verification silently
|
||||
// rewrites the risk category. On a record that has auto-verified that reads as an edit of
|
||||
// verified data and is refused, blocking the verification itself. The officer's risk choice is
|
||||
// persisted by the KYC step's own save, which does not come through here.
|
||||
const { kycType, riskCategory, ...updatePayload } = payload;
|
||||
|
||||
// If KYC is VERIFIED, remarks are mandatory — use re-edit command
|
||||
// If KYC is VERIFIED, changing data already on the record needs remarks — use re-edit command.
|
||||
// Whether this particular save actually replaces stored data is something only the server can
|
||||
// tell (it holds the current values), so it decides: a non-individual record auto-verifies as
|
||||
// soon as PAN and GSTIN pass, and the officer must still be able to fill in the KYC document
|
||||
// fields that are empty at that point. Remarks are forwarded when present and the server
|
||||
// rejects the save if they were required.
|
||||
if (this.isKycVerified) {
|
||||
const remarks = this.kycForm.get('remarks')?.value?.trim();
|
||||
if (!remarks) {
|
||||
onError('Remarks are mandatory when updating a verified KYC record.');
|
||||
this.kycForm.get('remarks')?.markAsTouched();
|
||||
return;
|
||||
if (remarks) {
|
||||
updatePayload.remarks = remarks;
|
||||
}
|
||||
updatePayload.remarks = remarks;
|
||||
this.clientsService.reEditClientKyc(this.clientId, this.kycId, updatePayload).subscribe({
|
||||
next: () => {
|
||||
// Status transitions to IN_PROGRESS, PAN verification is invalidated
|
||||
@@ -1571,12 +1669,30 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
const aadhaarRaw = this.kycForm.value.aadhaarNumber || '';
|
||||
const panPayload: any = {
|
||||
locale: 'en',
|
||||
kycType: 1,
|
||||
riskCategory: this.kycForm.value.riskCategory || 1,
|
||||
kycType: this.kycTypeValue,
|
||||
riskCategory: this.currentRiskCategory,
|
||||
panNumber: pan,
|
||||
aadhaarNumberMasked: aadhaarRaw.length === 12 ? 'XXXXXXXX' + aadhaarRaw.slice(-4) : 'XXXXXXXX0000'
|
||||
};
|
||||
|
||||
// A proprietorship reaches this method (it verifies the proprietor's individual PAN) but is
|
||||
// still legalForm=ENTITY to the backend, which matches an entity on name + date of
|
||||
// incorporation. Persist that date with this save so verifyPAN() can read it off the record.
|
||||
// Never sent for individuals — the backend rejects entity-only fields on an individual KYC.
|
||||
if (this.isNonIndividual) {
|
||||
const incorpDate = this.kycForm.value.incorporationDate;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
panPayload.dateFormat = dateFormat;
|
||||
panPayload.incorporationDate =
|
||||
incorpDate instanceof Date ? this.dateUtils.formatDate(incorpDate, dateFormat) : incorpDate || null;
|
||||
if (!panPayload.incorporationDate) {
|
||||
this.panVerifying = false;
|
||||
this.panVerifyError = 'Enter the Date of Incorporation before verifying the PAN.';
|
||||
this.kycForm.get('incorporationDate')?.markAsTouched();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.saveKycAndDo(
|
||||
panPayload,
|
||||
() => {
|
||||
@@ -1834,16 +1950,27 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const incorpDate = this.kycForm.value.incorporationDate;
|
||||
|
||||
// Same payload structure as individual — backend uses panNumber + client name/DOB for verification
|
||||
// Same payload structure as individual — backend uses panNumber + client name/DOB for verification.
|
||||
// For an entity the provider's "DOB" is the date of incorporation, so it must be persisted on the
|
||||
// KYC record by this same save — verifyPAN() below reads it back off the record, not off the form.
|
||||
const payload: any = {
|
||||
locale: 'en',
|
||||
kycType: 1,
|
||||
riskCategory: this.kycForm.value.riskCategory || 1,
|
||||
kycType: this.kycTypeValue,
|
||||
riskCategory: this.currentRiskCategory,
|
||||
panNumber: pan, // Company PAN → panNumber
|
||||
enterprisePan: pan,
|
||||
incorporationDate: incorpDate instanceof Date ? this.dateUtils.formatDate(incorpDate, dateFormat) : incorpDate,
|
||||
dateFormat,
|
||||
aadhaarNumberMasked: 'XXXXXXXX0000' // Placeholder — not applicable for entities
|
||||
};
|
||||
|
||||
if (!payload.incorporationDate) {
|
||||
this.entPanVerifying = false;
|
||||
this.showPanErrorAlert('Enter the Date of Incorporation before verifying the Enterprise PAN.');
|
||||
this.kycForm.get('incorporationDate')?.markAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.saveKycAndDo(
|
||||
payload,
|
||||
() => {
|
||||
@@ -1862,6 +1989,11 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
rawDate ||
|
||||
`${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
|
||||
}
|
||||
// Seeding status — mirrors verifyPan(), so the value the provider returned is held
|
||||
// here too and the next save round-trips it instead of leaving this screen blank.
|
||||
if (res.changes?.panAadhaarSeedingStatus) {
|
||||
this.panAadhaarSeedingStatus = res.changes.panAadhaarSeedingStatus;
|
||||
}
|
||||
} else {
|
||||
this.entPanVerifyStatus = 'FAILED';
|
||||
this.showPanErrorAlert(
|
||||
@@ -1907,8 +2039,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
const payload: any = {
|
||||
locale: 'en',
|
||||
kycType: 1,
|
||||
riskCategory: this.kycForm.value.riskCategory || 1,
|
||||
kycType: this.kycTypeValue,
|
||||
riskCategory: this.currentRiskCategory,
|
||||
gstinNumber: gstin,
|
||||
dateFormat,
|
||||
panNumber: this.kycForm.value.enterprisePan || this.kycForm.value.panNumber || null,
|
||||
@@ -1976,7 +2108,105 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.clientId) {
|
||||
this.showPanErrorAlert('Client must be created first. Complete General tab and click Next.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.cinVerifying = true;
|
||||
this.startCinProgress();
|
||||
|
||||
// The CIN has to be on the record before the command runs: the backend reads it off the KYC row
|
||||
// (and rejects with "CIN number is not set on this KYC record" otherwise) rather than taking it
|
||||
// from the request. Same save-then-verify order as PAN and GSTIN.
|
||||
const payload: any = {
|
||||
locale: 'en',
|
||||
kycType: this.kycTypeValue,
|
||||
riskCategory: this.currentRiskCategory,
|
||||
cinNumber: cin,
|
||||
dateFormat: this.settingsService.dateFormat,
|
||||
panNumber: this.kycForm.value.enterprisePan || this.kycForm.value.panNumber || null,
|
||||
aadhaarNumberMasked: 'XXXXXXXX0000'
|
||||
};
|
||||
|
||||
this.saveKycAndDo(
|
||||
payload,
|
||||
() => {
|
||||
this.clientsService.verifyCIN(this.clientId, this.kycId).subscribe({
|
||||
next: (res: any) => {
|
||||
this.stopCinProgress();
|
||||
const changes = res?.changes || res;
|
||||
if (changes?.cinVerified === true) {
|
||||
this.cinVerifyStatus = 'VERIFIED';
|
||||
// The label distinguishes a CIN the registry actually matched from one accepted on
|
||||
// its structure alone, so the card never claims an MCA confirmation that never came.
|
||||
this.cinCompanyName =
|
||||
changes?.cinCompanyName ||
|
||||
(changes?.cinConfirmedByRegistry === false ? 'Format validated' : 'Verified via MCA');
|
||||
this.cinUnconfirmedMessage =
|
||||
changes?.cinConfirmedByRegistry === false ? changes?.cinVerificationMessage || '' : '';
|
||||
const rawDate = changes?.cinVerifiedOn;
|
||||
if (Array.isArray(rawDate)) {
|
||||
this.cinVerifiedOn = `${rawDate[2]?.toString().padStart(2, '0')}/${rawDate[1]?.toString().padStart(2, '0')}/${rawDate[0]}`;
|
||||
} else {
|
||||
const now = new Date();
|
||||
this.cinVerifiedOn =
|
||||
rawDate ||
|
||||
`${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
|
||||
}
|
||||
} else {
|
||||
this.markCinUnconfirmed(changes?.cinVerificationMessage);
|
||||
}
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.stopCinProgress();
|
||||
const message =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
'CIN verification failed.';
|
||||
// A 503 means the provider could not be reached, which is worth interrupting the officer
|
||||
// for — it is likely to succeed on a retry. Anything else is the provider having
|
||||
// answered without confirming the company, which is not something a retry fixes and not
|
||||
// something the officer can correct, so it is reported in place instead of as an alert.
|
||||
if (err?.status === 503) {
|
||||
this.cinVerifyStatus = 'FAILED';
|
||||
this.showPanErrorAlert(message);
|
||||
} else {
|
||||
this.markCinUnconfirmed(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
(msg) => {
|
||||
this.stopCinProgress();
|
||||
this.showPanErrorAlert(msg);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The CIN is stored and well-formed, but MCA did not confirm it.
|
||||
*
|
||||
* Reported in place rather than as an error dialog because there is nothing for the officer to
|
||||
* correct: the number is already saved and the provider simply has no record to match it against.
|
||||
* Deliberately NOT recorded as verified — `cinVerified` stays false on the KYC record, so the
|
||||
* distinction between a confirmed and an unconfirmed CIN survives into the client's history.
|
||||
*/
|
||||
private markCinUnconfirmed(message?: string): void {
|
||||
this.cinVerifyStatus = 'UNCONFIRMED';
|
||||
this.cinUnconfirmedMessage =
|
||||
message || 'MCA could not confirm this CIN. The number has been saved against the client.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Drives the stepped MCA progress display while the verification is in flight. Deliberately stops
|
||||
* short of the last step and of 100%: the real outcome comes from the server, and letting the bar
|
||||
* finish on its own is what used to make an unverified CIN look verified.
|
||||
*/
|
||||
private startCinProgress(): void {
|
||||
if (this.cinProgressTimer) {
|
||||
clearInterval(this.cinProgressTimer);
|
||||
}
|
||||
this.cinProgressValue = 0;
|
||||
this.cinCurrentStep = 0;
|
||||
|
||||
@@ -2000,22 +2230,23 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.cinProgressValue = Math.min(Math.round((elapsed / totalDuration) * 100), 100);
|
||||
|
||||
this.cinProgressValue = Math.min(Math.round((elapsed / totalDuration) * 100), 90);
|
||||
if (elapsed >= totalDuration) {
|
||||
clearInterval(this.cinProgressTimer);
|
||||
this.cinCurrentStep = stepDurations.length;
|
||||
setTimeout(() => {
|
||||
this.cinVerifying = false;
|
||||
this.cinVerifyStatus = 'VERIFIED';
|
||||
this.cinCompanyName = 'Verified via MCA';
|
||||
const now = new Date();
|
||||
this.cinVerifiedOn = `${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
|
||||
}, 300);
|
||||
this.cinProgressTimer = null;
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
|
||||
private stopCinProgress(): void {
|
||||
if (this.cinProgressTimer) {
|
||||
clearInterval(this.cinProgressTimer);
|
||||
this.cinProgressTimer = null;
|
||||
}
|
||||
this.cinProgressValue = 100;
|
||||
this.cinVerifying = false;
|
||||
}
|
||||
|
||||
/** Send Aadhaar OTP — saves Aadhaar-only KYC data, then sends OTP */
|
||||
sendAadhaarOtp() {
|
||||
if (!this.clientId) {
|
||||
@@ -2041,8 +2272,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
const aadhaarMasked = 'XXXXXXXX' + aadhaar.slice(-4);
|
||||
const aadhaarPayload: any = {
|
||||
locale: 'en',
|
||||
kycType: 1,
|
||||
riskCategory: this.kycForm.value.riskCategory || 1,
|
||||
kycType: this.kycTypeValue,
|
||||
riskCategory: this.currentRiskCategory,
|
||||
aadhaarNumberMasked: aadhaarMasked
|
||||
};
|
||||
const panVal = this.kycForm.value.panNumber;
|
||||
@@ -2203,7 +2434,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
getRenewalYears(): number {
|
||||
switch (this.kycForm.value.riskCategory) {
|
||||
switch (this.currentRiskCategory) {
|
||||
case 1:
|
||||
return 10;
|
||||
case 2:
|
||||
@@ -2387,12 +2618,16 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return {
|
||||
locale: 'en',
|
||||
dateFormat,
|
||||
kycType: 1,
|
||||
kycType: this.kycTypeValue,
|
||||
riskCategory: v.riskCategory || 1,
|
||||
// For non-individual: Company PAN is used as panNumber for verification
|
||||
panNumber: this.isNonIndividual ? enterprisePanNorm : v.hasPan ? panNorm : null,
|
||||
aadhaarNumberMasked: this.isNonIndividual ? 'XXXXXXXX0000' : aadhaarMasked,
|
||||
panAadhaarSeedingStatus: this.panAadhaarSeedingStatus || null,
|
||||
// Only ever sent when this screen actually holds a value. The seeding status is derived by
|
||||
// the provider during PAN verification and written straight to the record, so sending an
|
||||
// empty one back would blank a field the server filled in — which reads as an officer
|
||||
// editing a verified record rather than the no-op it is.
|
||||
...(this.panAadhaarSeedingStatus ? { panAadhaarSeedingStatus: this.panAadhaarSeedingStatus } : {}),
|
||||
// PAN declaration — stored so the waived exception can be justified later, and so a reload
|
||||
// does not silently drop the paperwork that is holding the exception off.
|
||||
panDeclarationTaken: this.panDeclarationTaken,
|
||||
@@ -2470,6 +2705,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
v.gstinNumber ||
|
||||
v.enterprisePan ||
|
||||
v.cinNumber ||
|
||||
v.incorporationDate ||
|
||||
v.kycDocumentName ||
|
||||
v.form15Applicable ||
|
||||
v.isException
|
||||
|
||||
Reference in New Issue
Block a user