feat(loan-origination): RBI compliance checklist, real client integration, and CIC consent/charge flows
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s

- Add standalone RBI Eligibility Checklist and RBI Rules Reference pages (11 real RBI-circular
  based rules: age-at-maturity, area of operation, PSL, director/related-party, wilful defaulter,
  exposure norms, LTV/margin, fair practices code, prepayment charges, own-shares security,
  insurance bundling), wired into the Loan Origination menu.
- Bridge the mock Loan Origination Pipeline to real Fineract data wherever an application is
  raised against a real Existing Customer: client search in the pipeline intake form, real Share
  Member accounts (with a details popup), and real KYC (Aadhaar/PAN verified status + masked
  numbers + document image view, generic document lookup for Income Proof/Bank Statement/
  Electricity Bill) on the Application Detail page.
- Add local-only capture UI for New Customer / no-account applications: front/back document
  images with crop + simulated Aadhaar OTP / PAN verification, employment-type-aware income proof,
  and an expiry-validated electricity bill upload.
- Add an EMI Calculation Basis breakdown (principal, rate, tenure, total interest/payable, and the
  substituted reducing-balance formula) to the KYC stage's income assessment.
- Add a CIC/Credit Bureau Check consent flow: a generated declaration letter (print for Branch,
  share for BC/Self-Apply), gated on the signed copy being uploaded before the credit score can be
  checked, plus a CIC check charge collected either by debiting a real linked account or via a
  mock payment-link flow for customers with none.
- Let the stage stepper jump directly to any already-reached stage (forward or backward) via a new
  furthestStageIndex, without losing any data already recorded on the application.
This commit is contained in:
2026-08-13 10:15:28 +05:30
parent 5a6fb1f9b5
commit 75df8865dd
22 changed files with 3688 additions and 48 deletions
@@ -108,6 +108,15 @@ export const CIC_BUREAUS: CicBureau[] = [
'CRIF High Mark'
];
/** What the bank recovers from the customer for pulling a bureau report — a flat pass-through
* of the bureau's per-pull fee, same for every application regardless of loan size. */
export const CIC_CHECK_CHARGE = 50;
/** How the CIC check charge was collected from the customer -- debited from an account they
* already hold with the bank (Existing Customer), or paid externally via a payment link
* (no account exists yet for a New Customer). */
export type CicChargeMode = 'ACCOUNT_DEBIT' | 'PAYMENT_LINK';
export type DiscrepancyType =
| 'NONE'
| 'DOCUMENT_DISCREPANCY'
@@ -172,6 +181,10 @@ export interface LoanApplication {
/** '—' unless channel === 'BC'. */
bcAgentName: string;
customerType: CustomerType;
/** Real Fineract client id -- only set when this application was raised against a client picked
* via the Existing Customer search (see loan-pipeline.component.ts); null for every other
* (dummy-data-generated or New Customer) application, which have no real backing client. */
clientId?: number | null;
productCode: string;
productName: string;
@@ -201,6 +214,25 @@ export interface LoanApplication {
coBorrowerName: string;
// CIC / Credit Bureau check — mandatory before sanction under the CICRA, 2005 framework.
/** RBI's Fair Practices Code requires the customer's informed consent before a credit bureau
* pull -- true once the officer confirms the signed declaration letter has come back from
* the customer, which is what actually unlocks the "Check Credit Score" action below. */
cicConsentSigned: boolean;
/** True once the CIC_CHECK_CHARGE has actually been collected -- via account debit for an
* Existing Customer with a real linked account, or a confirmed payment-link reference
* otherwise. Also gates "Check Credit Score", same as the consent declaration. */
cicChargeCollected: boolean;
cicChargeMode: CicChargeMode | null;
/** Which of the client's real savings/current accounts the charge was debited from --
* e.g. "SB0012345 — Savings (Regular Savings)" -- kept as a display label since this mock
* pipeline never posts a real Fineract transaction. */
cicChargeAccountLabel: string;
/** A mock payment-gateway link generated for a customer with no bank account yet -- this
* pipeline is entirely dummy data, so nothing is actually charged; the link is illustrative
* of the real-world flow (generate link → share with customer → they pay → officer records
* the reference they get back). */
cicPaymentLinkUrl: string;
cicPaymentReference: string;
cicBureau: CicBureau;
cibilScore: number;
cicReportUploaded: boolean;
@@ -273,6 +305,10 @@ export interface LoanApplication {
emiActivationDate: string;
currentStage: OriginationStage;
/** The furthest stage this application has ever reached, as a STAGE_ORDER index -- kept
* separate from `currentStage` so jumping back to an earlier stage for review (or forward
* again afterwards) doesn't make the stepper forget how far the application actually got. */
furthestStageIndex: number;
overallStatus: OverallStatus;
stageHistory: StageHistoryEntry[];
}
@@ -697,6 +733,12 @@ function generateApplications(count: number): LoanApplication[] {
eligibleLoanAmount,
coBorrowerRequired: eligibleLoanAmount < requestedAmount,
coBorrowerName: '—',
cicConsentSigned: false,
cicChargeCollected: false,
cicChargeMode: null,
cicChargeAccountLabel: '—',
cicPaymentLinkUrl: '—',
cicPaymentReference: '—',
cicBureau: pick(CIC_BUREAUS),
cibilScore,
cicReportUploaded: false,
@@ -747,6 +789,7 @@ function generateApplications(count: number): LoanApplication[] {
emiActivationConfirmed: false,
emiActivationDate: '—',
currentStage: 'APPLICATION',
furthestStageIndex: 0,
overallStatus: 'IN_PROGRESS',
stageHistory: []
};
@@ -808,6 +851,11 @@ function generateApplications(count: number): LoanApplication[] {
break;
case 'CIC_CHECK':
app.cicConsentSigned = true;
app.cicChargeCollected = true;
app.cicChargeMode = app.customerType === 'EXISTING' ? 'ACCOUNT_DEBIT' : 'PAYMENT_LINK';
app.cicChargeAccountLabel = app.customerType === 'EXISTING' ? 'Existing account on file' : '—';
app.cicPaymentReference = app.customerType === 'EXISTING' ? '—' : `PAY-${app.applicationNo}`;
app.cicReportUploaded = true;
app.cicReportFileName = `${app.cicBureau.replace(/\s+/g, '')}_${app.applicationNo}.pdf`;
app.cicUploadDate = fmt(cursor);
@@ -1063,6 +1111,7 @@ function generateApplications(count: number): LoanApplication[] {
}
app.currentStage = STAGE_ORDER[bucket.atStage];
app.furthestStageIndex = bucket.atStage;
app.overallStatus = bucket.outcome;
apps.push(app);
}
@@ -1083,6 +1132,14 @@ function recordStageProgress(app: LoanApplication, actionBy: string, outcome: st
app.stageHistory.push({ stage: app.currentStage, date: todayDDMMYYYY(), actionBy, outcome, remarks });
}
/** Moves `currentStage` forward and keeps `furthestStageIndex` in sync with it -- the one place
* every genuine forward move goes through, so the stepper never forgets how far the application
* actually got even after `moveToStage` below sends it back for a correction. */
function advanceStage(app: LoanApplication, newStage: OriginationStage): void {
app.currentStage = newStage;
app.furthestStageIndex = Math.max(app.furthestStageIndex, stageIndex(newStage));
}
/** Generic "this stage is done, move to the next one" action — used by every stage that doesn't
* carry its own ApprovalRecord (Application, KYC, CIC, Field Investigation, Customer Signing,
* Cheque/ACH). */
@@ -1090,10 +1147,33 @@ export function proceedToNextStage(app: LoanApplication, actionBy: string, remar
recordStageProgress(app, actionBy, 'Completed', remarks);
const next = nextStage(app.currentStage);
if (next) {
app.currentStage = next;
advanceStage(app, next);
}
}
/** Lets an officer correct course -- jump the application straight to any stage it has already
* reached (clicking that stage's dot in the stepper), whether that stage is currently behind or
* ahead of `currentStage`. Only `currentStage` moves; every checkbox, upload, score and approval
* already recorded on the application is left exactly as it was, so nothing has to be re-entered
* after a correction, and jumping back then forward again doesn't lose the spot the application
* was actually at -- `furthestStageIndex` is deliberately left untouched here. Refuses to move
* to a stage the application has never actually reached, so this can never be used to skip
* ahead into unfinished territory. */
export function moveToStage(app: LoanApplication, stage: OriginationStage, actionBy: string): void {
const targetIdx = stageIndex(stage);
if (targetIdx < 0 || targetIdx > app.furthestStageIndex || stage === app.currentStage) {
return;
}
const goingBack = targetIdx < stageIndex(app.currentStage);
recordStageProgress(
app,
actionBy,
goingBack ? 'Sent Back' : 'Returned',
`${goingBack ? 'Sent back' : 'Returned'} to "${STAGE_LABELS[stage]}" for review/correction.`
);
app.currentStage = stage;
}
export function flagDiscrepancy(app: LoanApplication, type: DiscrepancyType, actionBy: string, notes: string): void {
app.discrepancyType = type;
app.personalDiscussionDate = todayDDMMYYYY();
@@ -1143,7 +1223,7 @@ export function decideApproval(
recordStageProgress(app, approverName, APPROVAL_LABELS[decision], remarks);
const next = nextStage(app.currentStage);
if (next) {
app.currentStage = next;
advanceStage(app, next);
}
}
@@ -1341,6 +1421,7 @@ export interface NewApplicationInput {
bcAgentName: string;
customerType: CustomerType;
applicantName: string;
clientId?: number | null;
branchCode: string;
branchName: string;
productCode: string;
@@ -1363,6 +1444,7 @@ export function createApplication(input: NewApplicationInput, seq: number): Loan
channel: input.channel,
bcAgentName: input.channel === 'BC' ? input.bcAgentName : '—',
customerType: input.customerType,
clientId: input.clientId ?? null,
productCode: input.productCode,
productName: input.productName,
secured: input.secured,
@@ -1384,6 +1466,12 @@ export function createApplication(input: NewApplicationInput, seq: number): Loan
eligibleLoanAmount: 0,
coBorrowerRequired: false,
coBorrowerName: '—',
cicConsentSigned: false,
cicChargeCollected: false,
cicChargeMode: null,
cicChargeAccountLabel: '—',
cicPaymentLinkUrl: '—',
cicPaymentReference: '—',
cicBureau: 'CIBIL',
cibilScore: 0,
cicReportUploaded: false,
@@ -1434,6 +1522,7 @@ export function createApplication(input: NewApplicationInput, seq: number): Loan
emiActivationConfirmed: false,
emiActivationDate: '—',
currentStage: 'APPLICATION',
furthestStageIndex: 0,
overallStatus: 'IN_PROGRESS',
stageHistory: [
{