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
@@ -15,6 +15,8 @@
import { jsPDF } from 'jspdf';
import { BankUserDsc } from 'app/users/dsc/dsc.model';
import { FyInterestBreakup, LoanInterestRecord } from './loan-interest/loan-interest.component';
import { ShareCertificateRecord } from './share-certificate/share-certificate.component';
import { BalanceCertificateRecord } from './balance-certificate/balance-certificate.component';
@@ -394,6 +396,73 @@ function newCtx(accent: [number, number, number]): PdfCtx {
};
}
/**
* Paint a "Digitally Signed" stamp + auditor footer line on the final page of
* the given doc. Stamp = green-bordered rounded box on the bottom-right with
* a ✓ icon, the signer's common name, masked DSC serial, and signing time.
* Audit line = thin teal strip under the existing footer with the same info
* in monospace, so the certificate also reads cleanly when printed in B/W.
*
* Call this just before `doc.save(...)` — it doesn't add a new page.
*/
export function paintDscSignatureStamp(doc: jsPDF, dsc: BankUserDsc): void {
const pageW = doc.internal.pageSize.getWidth();
const pageH = doc.internal.pageSize.getHeight();
const margin = 20;
const signedAt = new Date();
const pad = (n: number) => String(n).padStart(2, '0');
const ts = `${signedAt.getFullYear()}-${pad(signedAt.getMonth() + 1)}-${pad(signedAt.getDate())} ${pad(signedAt.getHours())}:${pad(signedAt.getMinutes())}:${pad(signedAt.getSeconds())}`;
// ── Bottom-right rounded green stamp ──
const sW = 68;
const sH = 22;
const sX = pageW - margin - sW;
const sY = pageH - margin - sH - 4;
// Tint background + green border (slight rounding gives it a "stamped" feel)
doc.setFillColor(220, 252, 231); // green-100
doc.setDrawColor(21, 128, 61); // green-700
doc.setLineWidth(0.6);
doc.roundedRect(sX, sY, sW, sH, 2, 2, 'FD');
// ✓ icon + heading
doc.setFont('helvetica', 'bold');
doc.setFontSize(9);
doc.setTextColor(21, 128, 61);
doc.text('✓ DIGITALLY SIGNED', sX + 3, sY + 5);
// Signer name (Common Name)
doc.setFont('helvetica', 'bold');
doc.setFontSize(8.5);
doc.setTextColor(15, 23, 42);
doc.text(dsc.commonName, sX + 3, sY + 10);
// DSC serial (masked) + type
doc.setFont('courier', 'normal');
doc.setFontSize(7);
doc.setTextColor(71, 85, 105);
doc.text(`DSC ${dsc.dscSerialNoMasked}`, sX + 3, sY + 14);
doc.setFont('helvetica', 'normal');
doc.setFontSize(6.5);
doc.setTextColor(100, 116, 139);
doc.text(dsc.dscType, sX + 3, sY + 17.5);
doc.text(`Signed at ${ts}`, sX + 3, sY + 20.5);
// ── Audit footer line (thin teal strip just below the existing footer) ──
const fY = pageH - margin + 2;
doc.setFillColor(204, 251, 241); // teal-100
doc.rect(margin, fY, pageW - margin * 2, 4, 'F');
doc.setFont('courier', 'normal');
doc.setFontSize(7);
doc.setTextColor(13, 148, 136); // teal-600
doc.text(
`[SIGNED] ${dsc.commonName} · DSC ${dsc.dscSerialNoMasked} · Issued by ${dsc.issuedBy.slice(0, 38)} · ${ts}`,
margin + 2,
fY + 2.8
);
}
// ───────────────────── Loan Interest Certificate ─────────────────────
export function buildLoanInterestPdf(record: LoanInterestRecord, fy: FyInterestBreakup): jsPDF {
const c = newCtx(ACCENT_BLUE);
@@ -467,8 +536,9 @@ export function buildLoanInterestPdf(record: LoanInterestRecord, fy: FyInterestB
return c.doc;
}
export function downloadLoanInterestPdf(record: LoanInterestRecord, fy: FyInterestBreakup): void {
export function downloadLoanInterestPdf(record: LoanInterestRecord, fy: FyInterestBreakup, dsc?: BankUserDsc): void {
const doc = buildLoanInterestPdf(record, fy);
if (dsc) paintDscSignatureStamp(doc, dsc);
doc.save(`Interest-Certificate-${record.accountNo}-${fy.fy.replace(/\s+/g, '')}.pdf`);
}
@@ -552,8 +622,9 @@ export function buildShareCertificatePdf(r: ShareCertificateRecord): jsPDF {
return c.doc;
}
export function downloadShareCertificatePdf(r: ShareCertificateRecord): void {
export function downloadShareCertificatePdf(r: ShareCertificateRecord, dsc?: BankUserDsc): void {
const doc = buildShareCertificatePdf(r);
if (dsc) paintDscSignatureStamp(doc, dsc);
doc.save(`Share-Certificate-${r.certificateNo}.pdf`);
}
@@ -645,8 +716,9 @@ export function buildBalanceCertificatePdf(r: BalanceCertificateRecord, asOnDate
return c.doc;
}
export function downloadBalanceCertificatePdf(r: BalanceCertificateRecord, asOnDate: string): void {
export function downloadBalanceCertificatePdf(r: BalanceCertificateRecord, asOnDate: string, dsc?: BankUserDsc): void {
const doc = buildBalanceCertificatePdf(r, asOnDate);
if (dsc) paintDscSignatureStamp(doc, dsc);
doc.save(`Balance-Certificate-${r.accountNo}-${asOnDate}.pdf`);
}
@@ -736,8 +808,9 @@ export function buildForm60Pdf(r: Form60Record): jsPDF {
return c.doc;
}
export function downloadForm60Pdf(r: Form60Record): void {
export function downloadForm60Pdf(r: Form60Record, dsc?: BankUserDsc): void {
const doc = buildForm60Pdf(r);
if (dsc) paintDscSignatureStamp(doc, dsc);
doc.save(`Form-60-${r.customerId}.pdf`);
}
@@ -830,8 +903,9 @@ export function buildForm15ghPdf(r: Form15ghRecord, kind: Form15Kind): jsPDF {
return c.doc;
}
export function downloadForm15ghPdf(r: Form15ghRecord, kind: Form15Kind): void {
export function downloadForm15ghPdf(r: Form15ghRecord, kind: Form15Kind, dsc?: BankUserDsc): void {
const doc = buildForm15ghPdf(r, kind);
if (dsc) paintDscSignatureStamp(doc, dsc);
doc.save(`Form-${kind}-${r.customerId}-${r.fy.replace(/\s+/g, '')}.pdf`);
}
@@ -922,8 +996,9 @@ export function buildHypothecationPdf(r: HypothecationRecord): jsPDF {
return c.doc;
}
export function downloadHypothecationPdf(r: HypothecationRecord): void {
export function downloadHypothecationPdf(r: HypothecationRecord, dsc?: BankUserDsc): void {
const doc = buildHypothecationPdf(r);
if (dsc) paintDscSignatureStamp(doc, dsc);
doc.save(`Hypothecation-${r.certificateNo}.pdf`);
}
@@ -1052,7 +1127,8 @@ export function buildNocPdf(r: NocRecord): jsPDF {
return c.doc;
}
export function downloadNocPdf(r: NocRecord): void {
export function downloadNocPdf(r: NocRecord, dsc?: BankUserDsc): void {
const doc = buildNocPdf(r);
if (dsc) paintDscSignatureStamp(doc, dsc);
doc.save(`NOC-${r.nocNo}.pdf`);
}