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
@@ -0,0 +1,61 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container lec-container">
<mat-card>
<mat-card-content>
<h2 class="lec-title">RBI Loan Eligibility Checklist</h2>
<p class="lec-subtitle">
Run the bank's RBI-circular-based eligibility checks for any client, independently of the loan application
itself.
</p>
@if (!selectedClient) {
<div class="lec-search">
<mat-form-field class="lec-search-field">
<mat-label>Search Client (name, account no., mobile...)</mat-label>
<input matInput [(ngModel)]="searchText" (ngModelChange)="onSearch($event)" />
</mat-form-field>
@if (searching) {
<p class="lec-hint">Searching…</p>
} @else if (searchText.length >= 2 && searchResults.length === 0) {
<p class="lec-hint">No clients found.</p>
}
@if (searchResults.length > 0) {
<div class="lec-results">
@for (client of searchResults; track client.id) {
<button type="button" class="lec-result-row" (click)="selectClient(client)">
<span class="lec-result-name">{{ client.displayName }}</span>
<span class="lec-result-meta">{{ client.accountNo ?? client.officeName ?? '' }}</span>
</button>
}
</div>
}
</div>
} @else {
<div class="lec-selected-client">
<div>
<span class="lec-selected-label">Client</span>
<span class="lec-selected-name">{{ selectedClient.displayName }}</span>
</div>
<button type="button" mat-button (click)="changeClient()">Change Client</button>
</div>
<mat-form-field class="lec-amount-field">
<mat-label>Proposed Loan Amount (₹)</mat-label>
<input matInput type="number" min="0" [(ngModel)]="loanAmount" />
</mat-form-field>
<mifosx-rbi-compliance-step [clientId]="selectedClient.id" [loanAmount]="loanAmount">
</mifosx-rbi-compliance-step>
}
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,104 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.lec-container {
max-width: 900px;
}
.lec-title {
margin: 0 0 4px;
}
.lec-subtitle {
color: #64748b;
font-size: 13.5px;
margin: 0 0 20px;
}
.lec-search {
max-width: 480px;
}
.lec-search-field {
width: 100%;
}
.lec-hint {
color: #94a3b8;
font-size: 12.5px;
margin: -8px 0 0;
}
.lec-results {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
}
.lec-result-row {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 10px 14px;
background: #fff;
border: none;
border-bottom: 1px solid #f1f5f9;
text-align: left;
cursor: pointer;
font: inherit;
&:last-child {
border-bottom: none;
}
&:hover {
background: #f8fafc;
}
}
.lec-result-name {
font-weight: 600;
color: #0f172a;
}
.lec-result-meta {
color: #94a3b8;
font-size: 12.5px;
}
.lec-selected-client {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #f8fafc;
border-radius: 8px;
margin-bottom: 16px;
}
.lec-selected-label {
display: block;
font-size: 11.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.4px;
color: #64748b;
}
.lec-selected-name {
font-size: 15px;
font-weight: 700;
color: #0f172a;
}
.lec-amount-field {
max-width: 320px;
display: block;
margin-bottom: 16px;
}
@@ -0,0 +1,76 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { ClientsService } from 'app/clients/clients.service';
import { RbiComplianceStepComponent } from '../rbi-compliance-step/rbi-compliance-step.component';
/**
* Standalone RBI eligibility checklist -- a loan officer picks any client and runs the same
* RBI-circular-based checks (age, area of operation, PSL classification, director/related-party,
* wilful defaulter, exposure norms, LTV/margin, fair practices code) independently of the loan
* account creation wizard, before or while a loan is being considered for that client.
*/
@Component({
selector: 'mifosx-loan-eligibility-checklist',
templateUrl: './loan-eligibility-checklist.component.html',
styleUrls: ['./loan-eligibility-checklist.component.scss'],
standalone: true,
imports: [
...STANDALONE_SHARED_IMPORTS,
FormsModule,
RbiComplianceStepComponent
]
})
export class LoanEligibilityChecklistComponent {
private clientsService = inject(ClientsService);
searchText = '';
searchResults: any[] = [];
searching = false;
private searchTimer: any;
selectedClient: { id: number; displayName: string } | null = null;
loanAmount: number | null = null;
onSearch(text: string): void {
this.searchText = text;
clearTimeout(this.searchTimer);
if (!text || text.length < 2) {
this.searchResults = [];
return;
}
this.searching = true;
this.searchTimer = setTimeout(() => {
this.clientsService.searchByText(text, 0, 8, '', '', true, 'INDIVIDUAL').subscribe({
next: (data: any) => {
this.searchResults = data?.content || [];
this.searching = false;
},
error: () => {
this.searchResults = [];
this.searching = false;
}
});
}, 400);
}
selectClient(client: any): void {
this.selectedClient = { id: client.id, displayName: client.displayName };
this.searchResults = [];
this.searchText = '';
this.loanAmount = null;
}
changeClient(): void {
this.selectedClient = null;
this.loanAmount = null;
}
}