Client-stepper: pre-fill existing KYC/address proofs in edit, fix identifier saves
Edit-mode now shows the previously selected KYC document type + uploaded file thumbnail
(loads from /clients/{id}/identifiers + /client_identifiers/{id}/documents). Same for
address proof on the India address step. Closes the gap where editing a client showed
an empty file picker even when proof had been uploaded on create.
Identifier-save hardened across all three callsites (create-client, edit-client,
identities-tab.createIdentifierWithDocs) - the KYC chip picker proofId is a CODE
string ('AADHAAR'), but backend ClientIdentifierRequest.documentTypeId is Long, which
Spring rejects with HttpMessageNotReadableException -> 400. resolveDocumentTypeId
helper looks up the code against /kyc/proof-types?type=id and substitutes the numeric
CodeValue id before posting. Skips silently for constitution-specific non-individual
docs that have no CodeValue mapping.
KYC payload normalises panNumber / enterprisePan / gstinNumber / cinNumber via
toUpperCase + whitespace strip so user-typed lowercase or trailing spaces don't trip
the backend regex (validation.msg.CLIENTKYC.panNumber.does.not.match.regexp).
State name match in client-address save now normalises both sides (lowercase, collapse
underscores + spaces) so legacy MADHYA_PRADESH resolves to 'Madhya Pradesh' returned
by api.postalpincode.in. Backed by tenant migration 0297.
Identities-tab resolver flattened - was returning identifiers before the per-row
forkJoin(getClientIdentificationDocuments) completed, so the view rendered empty
documents arrays. Now uses switchMap + forkJoin so the resolver awaits completion.
Also bundles concurrent work-in-progress on letter-mgmt, organization, users,
bank-calendar, shell, nominee/notes/identifier dialogs, and theming.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
325228c514
commit
83ea57ccf0
@@ -9,6 +9,8 @@
|
||||
/** Angular Imports */
|
||||
import { Component, QueryList, ViewChild, ViewChildren, inject } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
/** Custom Services */
|
||||
import { ClientsService } from '../clients.service';
|
||||
@@ -133,6 +135,76 @@ export class CreateClientComponent {
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-tab form-validity gate for the Save & Next button.
|
||||
* Returns false when the current tab has unfilled / invalid required fields
|
||||
* so the user can't move forward (and the button is visibly disabled).
|
||||
* The mapping follows the tab order in create-client.component.html:
|
||||
* 0 General
|
||||
* 1 KYC & Documents
|
||||
* 2 (non-individual) Scheme & Sector | (individual) Address
|
||||
* 3 (non-individual) Address | (individual) Family
|
||||
* 4 Application Summary (handled by Submit, not Save & Next)
|
||||
*/
|
||||
get currentStepValid(): boolean {
|
||||
if (this.savingClient) return false;
|
||||
const idx = this.selectedTabIndex;
|
||||
|
||||
if (idx === 0) {
|
||||
if (!this.clientGeneralStep || !this.clientGeneralForm) return false;
|
||||
if (this.clientGeneralStep.sanctionsBlocked) return false;
|
||||
return this.clientGeneralForm.valid;
|
||||
}
|
||||
if (idx === 1) {
|
||||
// KYC tab — block when the kyc form has unfilled required fields
|
||||
// (kycDocumentName / kycDocumentNumber / pattern errors on PAN / Aadhaar etc.)
|
||||
const kf = this.clientKycStep?.kycForm;
|
||||
return !kf || kf.valid;
|
||||
}
|
||||
if (this.legalFormType === 2) {
|
||||
if (idx === 2) {
|
||||
// Scheme & Sector (entity KYC) — categories are opt-in, so always allow.
|
||||
return true;
|
||||
}
|
||||
if (idx === 3) {
|
||||
// Address tab for non-individuals
|
||||
return this.isAddressStepValid();
|
||||
}
|
||||
} else {
|
||||
if (idx === 2) {
|
||||
// Address tab for individuals
|
||||
return this.isAddressStepValid();
|
||||
}
|
||||
if (idx === 3) {
|
||||
// Family tab — relations are optional, always allow.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Permanent address is the only mandatory address; current/office/correspondence are optional. */
|
||||
private isAddressStepValid(): boolean {
|
||||
const step = this.clientIndiaAddressStep;
|
||||
if (!step) return true; // step not yet mounted — don't block
|
||||
const perm = step.permanentForm;
|
||||
return !perm || perm.valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable explanation of why the Save & Next button is disabled.
|
||||
* Surfaced via matTooltip so the user knows what to fix.
|
||||
*/
|
||||
get currentStepBlockerMessage(): string {
|
||||
if (this.savingClient) return 'Saving — please wait…';
|
||||
if (this.currentStepValid) return '';
|
||||
const idx = this.selectedTabIndex;
|
||||
if (idx === 0 && this.clientGeneralStep?.sanctionsBlocked) {
|
||||
return 'Client failed sanctions screening — cannot proceed.';
|
||||
}
|
||||
return 'Please fill all mandatory fields on this tab to continue.';
|
||||
}
|
||||
|
||||
/** Get constitution type from general step for entity KYC */
|
||||
getConstitutionType(): string {
|
||||
return this.clientGeneralStep?.createClientForm?.get('clientNonPersonDetails.constitutionId')?.value || '';
|
||||
@@ -521,11 +593,17 @@ export class CreateClientComponent {
|
||||
isActive: tab === 'permanent'
|
||||
};
|
||||
|
||||
// Try to resolve state to stateProvinceId
|
||||
// Try to resolve state to stateProvinceId. We normalise both sides (lowercase,
|
||||
// collapse underscores + whitespace) so legacy UPPER_SNAKE rows still match the
|
||||
// postal-PIN API's "Madhya Pradesh" form and vice versa. See migration 0297.
|
||||
if ((addr as any).state && template?.stateProvinceIdOptions?.length) {
|
||||
const stateMatch = template.stateProvinceIdOptions.find(
|
||||
(s: any) => s.name?.toLowerCase() === (addr as any).state?.toLowerCase()
|
||||
);
|
||||
const norm = (s: string) =>
|
||||
(s || '')
|
||||
.toLowerCase()
|
||||
.replace(/[_\s]+/g, ' ')
|
||||
.trim();
|
||||
const target = norm((addr as any).state);
|
||||
const stateMatch = template.stateProvinceIdOptions.find((s: any) => norm(s.name) === target);
|
||||
if (stateMatch) payload.stateProvinceId = stateMatch.id;
|
||||
}
|
||||
|
||||
@@ -580,28 +658,60 @@ export class CreateClientComponent {
|
||||
if (!payload?.proofId || !payload?.files?.length) return;
|
||||
|
||||
const proofName = payload.proof?.name || 'KYC Document';
|
||||
const identifierData: any = {
|
||||
documentTypeId: payload.proofId,
|
||||
status: 'Active',
|
||||
documentKey: payload.documentKey || `${proofName} - ${clientId}`,
|
||||
description: 'KYC document'
|
||||
};
|
||||
// The KYC step's chip-picker carries `id` as a doc-type CODE string (e.g. "AADHAAR")
|
||||
// because its list is constitution-driven, not CodeValue-backed. The backend
|
||||
// ClientIdentifierRequest.documentTypeId is a Long, so a string proofId trips Jackson
|
||||
// with HttpMessageNotReadableException → 400. Resolve to the numeric CodeValue id by
|
||||
// matching on name against /kyc/proof-types?type=id before posting.
|
||||
this.resolveDocumentTypeId(payload.proofId, proofName).subscribe((documentTypeId: number | null) => {
|
||||
if (documentTypeId == null) {
|
||||
// No CodeValue match (e.g. non-individual constitution-specific docs). Skip the
|
||||
// identifier save rather than firing a 400; the form's other KYC fields still get
|
||||
// persisted via the KYC entity.
|
||||
return;
|
||||
}
|
||||
const identifierData: any = {
|
||||
documentTypeId,
|
||||
status: 'Active',
|
||||
documentKey: payload.documentKey || `${proofName} - ${clientId}`,
|
||||
description: 'KYC document'
|
||||
};
|
||||
|
||||
this.clientsService.addClientIdentifier(clientId, identifierData).subscribe({
|
||||
next: (res: any) => {
|
||||
const identifierId = res?.resourceId;
|
||||
if (!identifierId) return;
|
||||
for (const entry of payload.files) {
|
||||
const formData = new FormData();
|
||||
formData.append('name', `${proofName} — ${entry.slot}`);
|
||||
formData.append('file', entry.file);
|
||||
this.clientsService.uploadClientIdentifierDocument(identifierId, formData).subscribe({ error: () => {} });
|
||||
}
|
||||
},
|
||||
error: () => {}
|
||||
this.clientsService.addClientIdentifier(clientId, identifierData).subscribe({
|
||||
next: (res: any) => {
|
||||
const identifierId = res?.resourceId;
|
||||
if (!identifierId) return;
|
||||
for (const entry of payload.files) {
|
||||
const formData = new FormData();
|
||||
formData.append('name', `${proofName} — ${entry.slot}`);
|
||||
formData.append('file', entry.file);
|
||||
this.clientsService.uploadClientIdentifierDocument(identifierId, formData).subscribe({ error: () => {} });
|
||||
}
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass-through when proofId is already numeric (address proof, identities tab). Otherwise
|
||||
* resolve the string code/name against the ID-proof CodeValues so the backend gets a
|
||||
* proper Long for documentTypeId.
|
||||
*/
|
||||
private resolveDocumentTypeId(proofId: any, proofName: string): Observable<number | null> {
|
||||
if (typeof proofId === 'number' && Number.isFinite(proofId)) {
|
||||
return of(proofId);
|
||||
}
|
||||
return this.clientsService.getKycProofTypes('id').pipe(
|
||||
map((res: any) => {
|
||||
const list: any[] = res?.idProofs || [];
|
||||
const targetName = (proofName || '').trim().toLowerCase();
|
||||
const match = list.find((p: any) => (p?.name || '').trim().toLowerCase() === targetName);
|
||||
return (match?.id ?? null) as number | null;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save family members via POST /clients/{id}/familymembers
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user