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:
Aditya
2026-05-20 12:40:58 +05:30
co-authored by Claude Opus 4.7
parent 325228c514
commit 83ea57ccf0
84 changed files with 10281 additions and 1194 deletions
@@ -44,6 +44,8 @@ import { TranslateService } from '@ngx-translate/core';
import { ClientsService } from '../../clients.service';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
/**
* Identities Tab Component
@@ -206,15 +208,57 @@ export class IdentitiesTabComponent implements OnDestroy {
/**
* Create one identifier from a KYC section payload, then upload each file
* as a document attached to that identifier.
*
* `section.proofId` may arrive either as a numeric CodeValue id (when sourced from
* /kyc/proof-types — the address-proof chip-picker is built from this list and works
* straight away) or as a string CODE like "AADHAAR" / "PAN" (when sourced from the
* KYC step's constitution-driven document list — that list isn't backed by
* CodeValues). The backend's ClientIdentifierRequest.documentTypeId is a Long, so a
* string proofId triggers Spring's HttpMessageNotReadableException → generic 400
* "String instead of Number". Route through resolveDocumentTypeId so we always post
* a numeric documentTypeId.
*/
private createIdentifierWithDocs(section: any, description: string, kind: 'id' | 'address'): void {
const identifierData = {
documentTypeId: section.proofId,
status: 'Active',
documentKey: kind === 'id' ? section.documentKey : `${section.proof?.name || 'Address Proof'} - ${this.clientId}`,
description: kind === 'address' && section.address ? section.address : description
};
const proofName = section.proof?.name || (kind === 'address' ? 'Address Proof' : 'KYC Document');
this.resolveDocumentTypeId(section.proofId, proofName).subscribe((documentTypeId: number | null) => {
if (documentTypeId == null) {
// No CodeValue match (e.g. a constitution-specific non-individual doc like
// "Trust Deed" that hasn't been seeded). Skip silently rather than firing the
// 400 — the parent already captured the file separately if needed.
return;
}
const identifierData = {
documentTypeId,
status: 'Active',
documentKey: kind === 'id' ? section.documentKey : `${proofName} - ${this.clientId}`,
description: kind === 'address' && section.address ? section.address : description
};
this.postIdentifierAndDocs(identifierData, section, proofName, description);
});
}
/**
* Pass-through when proofId is already numeric (address proof, identities tab address
* picker). 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.clientService.getKycProofTypes('id').pipe(
map((res: any) => {
const list: any[] = res?.idProofs || [];
const target = (proofName || '').trim().toLowerCase();
const match = list.find((p: any) => (p?.name || '').trim().toLowerCase() === target);
return (match?.id ?? null) as number | null;
})
);
}
/** Extracted post-and-upload-files step so the resolver wrapper stays readable. */
private postIdentifierAndDocs(identifierData: any, section: any, proofName: string, description: string): void {
this.clientService.addClientIdentifier(this.clientId, identifierData).subscribe({
next: (res: any) => {
const newIdentifierId = res.resourceId;