diff --git a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.scss b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.scss
index a9555ea7e..c2ac3c67c 100644
--- a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.scss
+++ b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.scss
@@ -36,6 +36,20 @@
letter-spacing: 0.4px;
text-transform: uppercase;
}
+
+ /* Same chip, muted — says "you may" where the red one says "you must". */
+ .section-optional-tag {
+ margin-left: 6px;
+ padding: 2px 9px;
+ border-radius: 999px;
+ background: #f1f5f9;
+ color: #475569;
+ border: 1px solid #e2e8f0;
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.4px;
+ text-transform: uppercase;
+ }
}
.kyc-badge {
@@ -312,6 +326,34 @@
color: #b45309;
border: 1px solid #fcd34d;
}
+
+ // Recorded but unconfirmed by the registry. Neutral blue rather than green or red: nothing failed
+ // and nothing passed, so it must not read as either.
+ &.unconfirmed {
+ background: #eff6ff;
+ color: #1d4ed8;
+ border: 1px solid #bfdbfe;
+ }
+}
+
+// ── CIN saved but not confirmed by MCA ─────────────────────────────────────
+.cin-unconfirmed-note {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ margin: -4px 0 12px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ background: #eff6ff;
+ border: 1px solid #bfdbfe;
+ color: #1e40af;
+ font-size: 12.5px;
+ line-height: 1.45;
+}
+
+.cin-unconfirmed-note__icon {
+ margin-top: 1px;
+ flex-shrink: 0;
}
// ── PAN declaration summary ────────────────────────────────────────────────
@@ -1225,6 +1267,12 @@
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
+
+ /* Entity clients get the photograph box only, so it keeps a card's width instead of
+ stretching across the whole step where the signature box used to sit beside it. */
+ &--single {
+ grid-template-columns: minmax(0, 420px);
+ }
}
.upload-box {
diff --git a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts
index 29d2f22f2..e2ed6cfb1 100644
--- a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts
+++ b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts
@@ -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 = {
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
diff --git a/src/app/clients/client-stepper/client-preview-step/client-preview-step.component.html b/src/app/clients/client-stepper/client-preview-step/client-preview-step.component.html
index 00857b355..564f7f25b 100644
--- a/src/app/clients/client-stepper/client-preview-step/client-preview-step.component.html
+++ b/src/app/clients/client-stepper/client-preview-step/client-preview-step.component.html
@@ -442,18 +442,18 @@
{{ kycData.enterprisePan }}
}
+ @if (kycData.incorporationDate) {
+
+ Date of Incorporation
+ {{ kycData.incorporationDate }}
+
- @if (kycData?.panAadhaarSeedingStatus === 'Y') {
- check_circle Linked (Seeded)
- } @else if (kycData?.panAadhaarSeedingStatus === 'N') {
- cancel Not Linked
- } @else if (kycData?.panAadhaarSeedingStatus === 'E') {
- info Exempt
- } @else {
- radio_button_unchecked Pending
- }
+ linking state is the first thing a reviewer sees.
+
+ Section 139AA seeding applies to a person's PAN and Aadhaar. An entity has no Aadhaar
+ to seed, so for a non-individual the card could only ever read "Pending" against a
+ requirement that does not apply to it. -->
+ @if (!isNonIndividual) {
+