feat(loan-origination): RBI compliance checklist, real client integration, and CIC consent/charge flows
- 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:
@@ -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: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user