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
@@ -11,6 +11,7 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { AuthenticationService } from '../../core/authentication/authentication.service';
import { ClientsService } from '../../clients/clients.service';
import { ExportSheetSpec, exportWorkbook } from '../../cashier/cashier-export';
import {
BRANCHES,
@@ -134,12 +135,19 @@ export class LoanPipelineComponent implements OnInit {
bcAgentName: '',
customerType: 'NEW' as CustomerType,
applicantName: '',
clientId: null as number | null,
branchCode: this.branches[0].code,
productCode: this.products[0].code,
requestedAmount: null as number | null,
tenureMonths: null as number | null
};
/** Existing-customer search (real Fineract clients, unlike the rest of this mock pipeline). */
existingClientSearchText = '';
existingClientSearchResults: any[] = [];
searchingExistingClient = false;
private existingClientSearchTimer: any;
toastMsg = '';
toastVisible = false;
private toastTimer: ReturnType<typeof setTimeout> | undefined;
@@ -147,6 +155,7 @@ export class LoanPipelineComponent implements OnInit {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly auth = inject(AuthenticationService);
private readonly clientsService = inject(ClientsService);
ngOnInit(): void {
const requestedView = this.route.snapshot.queryParamMap.get('view') as ViewId | null;
@@ -428,11 +437,59 @@ export class LoanPipelineComponent implements OnInit {
return this.products.find((p) => p.code === code) || this.products[0];
}
/** Switching Customer Type away from "Existing" drops any client picked under the old mode --
* avoids storing a stale clientId against a now-"New Customer" application. */
onCustomerTypeChange(): void {
if (this.newApp.customerType === 'NEW') {
this.clearExistingClient();
}
}
onExistingClientSearch(text: string): void {
this.existingClientSearchText = text;
clearTimeout(this.existingClientSearchTimer);
if (!text || text.length < 2) {
this.existingClientSearchResults = [];
return;
}
this.searchingExistingClient = true;
this.existingClientSearchTimer = setTimeout(() => {
this.clientsService.searchByText(text, 0, 8, '', '', true, 'INDIVIDUAL').subscribe({
next: (data: any) => {
this.existingClientSearchResults = data?.content || [];
this.searchingExistingClient = false;
},
error: () => {
this.existingClientSearchResults = [];
this.searchingExistingClient = false;
}
});
}, 400);
}
selectExistingClient(client: any): void {
this.newApp.clientId = client.id;
this.newApp.applicantName = client.displayName;
this.existingClientSearchResults = [];
this.existingClientSearchText = '';
}
clearExistingClient(): void {
this.newApp.clientId = null;
this.newApp.applicantName = '';
this.existingClientSearchText = '';
this.existingClientSearchResults = [];
}
saveNewApplication(): void {
if (!this.newApp.applicantName.trim() || !this.newApp.requestedAmount || this.newApp.requestedAmount <= 0) {
this.toast('Enter the applicant name and a valid requested amount.');
return;
}
if (this.newApp.customerType === 'EXISTING' && !this.newApp.clientId) {
this.toast('Search and select the existing client for this application.');
return;
}
if (this.newApp.channel === 'BC' && !this.newApp.bcAgentName.trim()) {
this.toast('Enter the BC agent/point name for a Business Correspondent application.');
return;
@@ -451,6 +508,7 @@ export class LoanPipelineComponent implements OnInit {
bcAgentName: this.newApp.bcAgentName || '—',
customerType: this.newApp.customerType,
applicantName: this.newApp.applicantName.trim(),
clientId: this.newApp.clientId,
branchCode: branch.code,
branchName: branch.name,
productCode: product.code,
@@ -469,11 +527,14 @@ export class LoanPipelineComponent implements OnInit {
bcAgentName: '',
customerType: 'NEW',
applicantName: '',
clientId: null,
branchCode: this.isHeadOffice ? this.branches[0].code : this.ownBranchCode || this.branches[0].code,
productCode: this.products[0].code,
requestedAmount: null,
tenureMonths: null
};
this.existingClientSearchText = '';
this.existingClientSearchResults = [];
this.router.navigate([
'/loan-origination/application',
app.applicationNo