+ @if (application; as a) {
+
+
+
+
+
+
- Branch
+ - {{ a.branchName }}
+
+
+
- Channel
+ -
+ {{ a.channel === 'BC' ? a.bcAgentName : a.channel === 'SELF_APPLY' ? 'Self-Apply (Digital)' : 'Branch' }}
+
+
+
+
- Customer Type
+ - {{ a.customerType === 'EXISTING' ? 'Existing Customer' : 'New Customer' }}
+
+
+
- Tenure / Rate
+ - {{ a.tenureMonths }} months @ {{ a.interestRate }}%
+
+
+
- Monthly Income / Eligible Amount
+ - ₹{{ a.monthlyIncome | number: '1.0-0' }} / ₹{{ a.eligibleLoanAmount | number: '1.0-0' }}
+
+
+
- Sanctioning Authority
+ - {{ sanctioningAuthorityOf(a) }}
+
+
+
- Share Member
+ -
+ @if (a.isShareMember) {
+ Yes — {{ a.shareCertNo }} (since {{ a.shareMemberSince }})
+ } @else {
+ Not a Member
+
+ }
+
+
+ @if (a.coBorrowerRequired) {
+
+
- Co-Borrower
+ - {{ a.coBorrowerName }}
+
+ }
+ @if (a.guarantorRequired) {
+
+
- Guarantor
+ - {{ a.guarantorName }}
+
+ }
+
+
+
+
+
Sanctioning Workflow Progress
+
+ @for (s of stageOrder; track s) {
+
+
+ {{ stepMarker(a, s) }}
+ {{ stageLabels[s] }}
+
+ }
+
+
+
+ @if (a.overallStatus === 'REJECTED') {
+
+
Application Rejected — at {{ stageLabels[a.currentStage] }}
+
See the approval summary below for the recorded reason.
+
+ } @else if (a.overallStatus === 'ON_HOLD') {
+
+
Personal Discussion — {{ discrepancyLabels[a.discrepancyType] }}
+
+
{{ a.personalDiscussionNotes }}
+
+
+
+
+
+
+ } @else if (a.overallStatus === 'DISBURSED') {
+
+
Disbursed
+
+
+
- Disbursement Date
+ - {{ a.disbursementDate }}
+
+
+
- Mode
+ - {{ a.disbursementMode }}
+
+
+
- ACH Confirmation
+ - {{ a.achConfirmationRef }}
+
+
+
- EMI Activation Confirmation
+ - {{ a.emiActivationConfirmed ? 'Confirmed on ' + a.emiActivationDate : 'Pending' }}
+
+
+
+
+
+
EMI Repayment Schedule
+ @if (isRevolving(a)) {
+
+ {{ a.productName }} is a revolving/renewable limit — repaid on demand and renewed annually, not on a fixed
+ EMI schedule.
+
+ } @else {
+
+
+ Loan Amount
+ ₹{{ a.requestedAmount | number: '1.0-0' }}
+ {{ a.tenureMonths }} months @ {{ a.interestRate }}%
+
+
+ Monthly EMI
+ ₹{{ emiAmountFor(a) | number: '1.0-0' }}
+ Reducing balance
+
+
+ Total Interest Payable
+ ₹{{ totalInterestFor(a) | number: '1.0-0' }}
+ over the full tenure
+
+
+ Total Amount Payable
+ ₹{{ totalPayableFor(a) | number: '1.0-0' }}
+ principal + interest
+
+
+
+ }
+
+ } @else {
+
+ @if (a.currentStage === 'APPLICATION') {
+
+
Application Received
+
+
+ Received via
+ {{ a.channel === 'BC' ? a.bcAgentName : a.channel === 'SELF_APPLY' ? 'Self-Apply (Digital)' : 'Branch' }}
+ on {{ a.applicationDate }}.
+
+
+
+
+
+
+ }
+
+
+ @if (a.currentStage === 'KYC_DOCUMENTS') {
+
+
KYC & Document Collection — OVD (RBI KYC Master Direction)
+
+
+ }
+
+
+ @if (a.currentStage === 'CIC_CHECK') {
+
+
CIC / Credit Bureau Check — {{ a.cicBureau }}
+
+ @if (a.cibilScore === 0) {
+
+
+
+
+ } @else {
+
+
+
- Bureau
+ - {{ a.cicBureau }}
+
+
+
- Credit Score
+ - {{ a.cibilScore }} ({{ cibilBandOf(a.cibilScore) }})
+
+
+
- Report
+ - {{ a.cicReportUploaded ? a.cicReportFileName : 'Not uploaded yet' }}
+
+
+
- Field Inquiry
+ -
+ {{
+ a.fiRequestPushed
+ ? 'Pushed to ' + a.fiAgencyAssigned + ' (' + a.fiRequestDate + ')'
+ : 'Not pushed yet'
+ }}
+
+
+
+ }
+ @if (a.cibilScore > 0) {
+ @if (!a.cicReportUploaded) {
+
+
+
+ } @else if (!a.fiRequestPushed) {
+
+
+
+ } @else {
+
+
+
+ }
+ }
+
+
+ }
+
+
+ @if (a.currentStage === 'FIELD_INVESTIGATION') {
+
+
Field Investigation (FI) — Agent Report
+
+
+
+
+ @if (a.secured) {
+
+
+ }
+
+
+
+
Agent Report
+ {{ a.agentReportUploaded ? a.agentReportFileName : 'Not uploaded yet' }}
+
+
+ @if (!a.agentReportUploaded) {
+
+
+
+ }
+
+
+
+
+
+ }
+
+
+ @if (a.currentStage === 'PERSONAL_DISCUSSION') {
+
+
Personal Discussion with Manager
+
+
+
+
+ @if (discrepancySelection === 'NONE') {
+
+ } @else {
+
+ }
+
+
+
+ }
+
+
+ @if (a.currentStage === 'CREDIT_COMMITTEE') {
+
+
Loan Committee Approval — {{ sanctioningAuthorityOf(a) }}
+
+
+ }
+
+
+ @if (a.currentStage === 'RISK_APPROVAL') {
+
+
Risk Approval — Verification Checklist
+
+
+ }
+
+
+ @if (a.currentStage === 'LEGAL_APPROVAL') {
+
+
Legal Approval — Fees & A/c Opened By
+
+
+ }
+
+
+ @if (a.currentStage === 'CUSTOMER_SIGNING') {
+
+
Customer Signing Slot — {{ a.agreementSigningSlot }}
+
+
Loan Agreement and Sanction Letter ready for the customer's acknowledgement and signature.
+
+
+
+
+
+ }
+
+
+ @if (a.currentStage === 'CHEQUE_ACH') {
+
+
Cheque Received / ACH Sign
+
+
+ }
+
+
+ @if (a.currentStage === 'POST_CHECK') {
+
+
Post-Disbursement Check — Final Compliance Gate
+
+
+ }
+
+
+ @if (a.currentStage === 'DISBURSED') {
+
+
Ready for Disbursement
+
+
+
+ Disbursing also confirms EMI Activation for this account in the same step.
+
+
+
+
+
+
+ }
+ }
+
+
+
Approval Summary
+
+
+ Loan Committee
+ {{ a.creditCommitteeApproval.decision }}
+ {{ a.creditCommitteeApproval.approverName }} · {{ a.creditCommitteeApproval.date }}
+ {{ a.creditCommitteeApproval.remarks }}
+
+
+ Risk Approval
+ {{ a.riskApproval.decision }}
+ {{ a.riskApproval.approverName }} · {{ a.riskApproval.date }}
+ {{ a.riskApproval.remarks }}
+
+
+ Legal Approval
+ {{ a.legalApproval.decision }}
+ {{ a.legalApproval.approverName }} · {{ a.legalApproval.date }}
+ {{ a.legalApproval.remarks }}
+
+
+ Post-Disbursement Check
+ {{ a.postCheckApproval.decision }}
+ {{ a.postCheckApproval.approverName }} · {{ a.postCheckApproval.date }}
+ {{ a.postCheckApproval.remarks }}
+
+
+
+
+
+
Application Journey — Date-wise Audit Trail
+
+ @for (h of a.stageHistory; track h.date + h.stage + h.outcome) {
+
+
{{ stepMarker(a, h.stage) }}
+
+
+ {{ stageLabels[h.stage] }} — {{ h.outcome }}
+ {{ h.date }}
+
+
+
{{ h.actionBy }}
+
+
+ } @empty {
+
No activity recorded yet.
+ }
+
+
+ } @else {
+
+
Application Not Found
+
+
+
+
+ }
+
diff --git a/src/app/loan-origination/application-detail/application-detail.component.ts b/src/app/loan-origination/application-detail/application-detail.component.ts
new file mode 100644
index 000000000..3066bf767
--- /dev/null
+++ b/src/app/loan-origination/application-detail/application-detail.component.ts
@@ -0,0 +1,418 @@
+/**
+ * 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, OnDestroy, OnInit, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { ActivatedRoute, Router, RouterModule } from '@angular/router';
+import { Subscription } from 'rxjs';
+import { AuthenticationService } from '../../core/authentication/authentication.service';
+import {
+ ApprovalField,
+ BRANCHES,
+ BranchMaster,
+ DiscrepancyType,
+ DISCREPANCY_LABELS,
+ EmiScheduleRow,
+ LoanApplication,
+ LOAN_APPLICATIONS,
+ OriginationStage,
+ STAGE_LABELS,
+ STAGE_ORDER,
+ STATUS_LABELS,
+ allotShareMembership,
+ buildEmiSchedule,
+ calcEligibleAmount,
+ calculateEmi,
+ checkCreditScore,
+ cibilBand,
+ CIC_BUREAUS,
+ clearPersonalDiscussionAndProceed,
+ confirmChequeAch,
+ decideApproval,
+ disburseApplication,
+ flagDiscrepancy,
+ isRevolvingProduct,
+ markCustomerSigned,
+ progressPct,
+ pushFieldInquiry,
+ resolveDiscrepancyAndProceed,
+ sanctioningAuthority,
+ setMonthlyIncome,
+ stageBadgeClass,
+ stageIndex,
+ proceedToNextStage,
+ recordVerification,
+ todayDDMMYYYY,
+ uploadAgentReport,
+ uploadCicReport
+} from '../loan-origination-data';
+
+/**
+ * A single application's full sanctioning journey — the stage stepper, the current stage's
+ * action card (checklist / CIC upload / Field Investigation / Committee-Risk-Legal approval /
+ * signing / disbursement), and the complete date-wise audit trail. Every action here mutates the
+ * SAME LoanApplication object the Pipeline list reads, so the two screens can never disagree.
+ */
+@Component({
+ selector: 'mifosx-application-detail',
+ templateUrl: './application-detail.component.html',
+ styleUrls: [
+ '../../cashier/cashier-shared.scss',
+ '../../investment/investment-shared.scss',
+ '../../npa/npa-shared.scss',
+ '../loan-origination-shared.scss'
+ ],
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ RouterModule
+ ]
+})
+export class ApplicationDetailComponent implements OnInit, OnDestroy {
+ readonly stageOrder = STAGE_ORDER;
+ readonly stageLabels = STAGE_LABELS;
+ readonly statusLabels = STATUS_LABELS;
+ readonly discrepancyLabels = DISCREPANCY_LABELS;
+ asOnDate = todayDDMMYYYY();
+
+ application: LoanApplication | undefined;
+
+ /** Shared remarks box for whichever action card is currently on screen. */
+ actionRemarks = '';
+ discrepancySelection: DiscrepancyType = 'NONE';
+ disbursementMode = 'Account Credit';
+ /** KYC stage's Monthly Income entry field — blank until the officer keys it in. */
+ monthlyIncomeInput: number | null = null;
+
+ currentUserName = 'Branch User';
+
+ /** True for a Head Office login, which can open any branch's application — a branch-level
+ * login is locked to its own branch, the same split every other report in the app uses. */
+ isHeadOffice = false;
+ private ownBranchCode: string | null = null;
+
+ private readonly route = inject(ActivatedRoute);
+ private readonly router = inject(Router);
+ private readonly auth = inject(AuthenticationService);
+ private sub?: Subscription;
+
+ ngOnInit(): void {
+ const creds = this.auth.getCredentials();
+ this.currentUserName = creds?.username || 'Branch User';
+ this.isHeadOffice = this.auth.isHeadOfficeUser();
+ if (!this.isHeadOffice) {
+ const officeName = creds?.officeName || '';
+ this.ownBranchCode =
+ BRANCHES.find((b: BranchMaster) => officeName.toLowerCase().includes(b.name.toLowerCase()))?.code || null;
+ }
+ this.sub = this.route.paramMap.subscribe((params) => {
+ const applicationNo = params.get('applicationNo');
+ const found = LOAN_APPLICATIONS.find((a) => a.applicationNo === applicationNo);
+ // A branch-level login can only open applications belonging to its own branch — treated
+ // exactly like "not found" rather than a distinct "access denied", so it doesn't reveal
+ // that a differently-branched application exists at all.
+ this.application = found && !this.isHeadOffice && found.branchCode !== this.ownBranchCode ? undefined : found;
+ this.actionRemarks = '';
+ this.discrepancySelection = 'NONE';
+ this.monthlyIncomeInput = this.application?.monthlyIncome || null;
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.sub?.unsubscribe();
+ }
+
+ goBack(): void {
+ this.router.navigate(['/loan-origination/pipeline']);
+ }
+
+ // -------------------- Stepper --------------------
+ stepStatusClass(a: LoanApplication, stage: OriginationStage): string {
+ const idx = stageIndex(stage);
+ const curIdx = stageIndex(a.currentStage);
+ if (stage === a.currentStage && a.overallStatus === 'REJECTED') {
+ return 'loan-step loan-step--rejected';
+ }
+ if (stage === a.currentStage && a.overallStatus === 'ON_HOLD') {
+ return 'loan-step loan-step--hold';
+ }
+ if (idx < curIdx || (idx === curIdx && a.overallStatus === 'DISBURSED')) {
+ return 'loan-step loan-step--done';
+ }
+ if (idx === curIdx) {
+ return 'loan-step loan-step--current';
+ }
+ return 'loan-step';
+ }
+
+ stepMarker(a: LoanApplication, stage: OriginationStage): string {
+ const idx = stageIndex(stage);
+ const curIdx = stageIndex(a.currentStage);
+ if (idx < curIdx || (idx === curIdx && a.overallStatus === 'DISBURSED')) {
+ return '✓';
+ }
+ return String(idx + 1);
+ }
+
+ progress(a: LoanApplication): number {
+ return progressPct(a);
+ }
+
+ /** Colours an audit-trail entry's icon badge by what actually happened — a rejection/hold
+ * reads as an alert, everything else (approvals, verifications, uploads, signings) reads
+ * as positive progress. */
+ historyToneClass(outcome: string): string {
+ if (/reject|hold/i.test(outcome)) {
+ return 'npa-timeline-item npa-timeline-item--alert';
+ }
+ return 'npa-timeline-item npa-timeline-item--positive';
+ }
+
+ badgeClass(a: LoanApplication): string {
+ return stageBadgeClass(a);
+ }
+
+ cibilBandOf(score: number): string {
+ return cibilBand(score);
+ }
+
+ readonly cicBureaus = CIC_BUREAUS;
+
+ checkCreditScoreNow(a: LoanApplication): void {
+ checkCreditScore(a, this.currentUserName);
+ }
+
+ sanctioningAuthorityOf(a: LoanApplication): string {
+ return sanctioningAuthority(a.requestedAmount);
+ }
+
+ // -------------------- Generic stage-advance actions --------------------
+ proceed(a: LoanApplication, remarks: string): void {
+ proceedToNextStage(a, this.currentUserName, remarks);
+ }
+
+ // -------------------- KYC & Documents --------------------
+ /** Live preview of what Eligible Loan Amount would be for whatever's currently typed into the
+ * Monthly Income box — before the officer actually commits it via "Assess Income". */
+ eligiblePreview(a: LoanApplication): number {
+ return calcEligibleAmount(this.monthlyIncomeInput || 0, a.productCode);
+ }
+
+ setIncomeNow(a: LoanApplication): void {
+ if (!this.monthlyIncomeInput || this.monthlyIncomeInput <= 0) {
+ return;
+ }
+ setMonthlyIncome(a, this.monthlyIncomeInput, this.currentUserName);
+ }
+
+ allKycDone(a: LoanApplication): boolean {
+ const base =
+ a.aadhaarVerified &&
+ a.panVerified &&
+ a.aadhaarFingerprintCaptured &&
+ a.incomeProofSubmitted &&
+ a.bankStatementSubmitted &&
+ a.electricityBillSubmitted &&
+ a.monthlyIncome > 0;
+ return a.secured ? base && a.propertyDetailsSubmitted && a.taxReceiptSubmitted : base;
+ }
+
+ // -------------------- Field Investigation --------------------
+ fiComplete(a: LoanApplication): boolean {
+ const addressOk = a.addressVerification.status === 'VERIFIED';
+ const officeOk = a.officeVerification.status === 'VERIFIED';
+ const propertyOk = !a.secured || a.propertyVerification.status === 'VERIFIED';
+ return addressOk && officeOk && propertyOk;
+ }
+
+ completeFi(a: LoanApplication): void {
+ const agent = 'Field Verification Team';
+ const keys: Array<'addressVerification' | 'officeVerification' | 'propertyVerification'> = [
+ 'addressVerification',
+ 'officeVerification',
+ 'propertyVerification'
+ ];
+ for (const key of keys) {
+ if (a[key].status !== 'PENDING') {
+ recordVerification(
+ a,
+ key,
+ a[key].status,
+ agent,
+ a[key].remarks === '—' ? `${key} recorded on FI visit.` : a[key].remarks
+ );
+ }
+ }
+ this.proceed(a, 'Field Investigation completed — Agent Report filed.');
+ }
+
+ // -------------------- Personal Discussion --------------------
+ clearPersonalDiscussion(a: LoanApplication): void {
+ clearPersonalDiscussionAndProceed(
+ a,
+ this.currentUserName,
+ this.actionRemarks.trim() || 'No discrepancy found — cleared to proceed to Loan Committee.'
+ );
+ this.actionRemarks = '';
+ }
+
+ flagPersonalDiscussion(a: LoanApplication): void {
+ const notes =
+ this.actionRemarks.trim() ||
+ `${this.discrepancyLabels[this.discrepancySelection]} flagged during personal discussion — held pending resolution.`;
+ flagDiscrepancy(a, this.discrepancySelection, this.currentUserName, notes);
+ this.actionRemarks = '';
+ this.discrepancySelection = 'NONE';
+ }
+
+ resolveDiscrepancy(a: LoanApplication): void {
+ resolveDiscrepancyAndProceed(
+ a,
+ this.currentUserName,
+ this.actionRemarks.trim() || 'Discrepancy resolved on further review — cleared to proceed.'
+ );
+ this.actionRemarks = '';
+ }
+
+ // -------------------- Share Membership --------------------
+ allotMembership(a: LoanApplication): void {
+ allotShareMembership(a, this.currentUserName);
+ }
+
+ // -------------------- Approvals (Credit Committee / Risk / Legal / Post-Check) --------------------
+ approveStage(a: LoanApplication, field: ApprovalField, approverRole: string): void {
+ decideApproval(
+ a,
+ field,
+ 'APPROVED',
+ this.currentUserName,
+ approverRole,
+ this.actionRemarks.trim() || 'Reviewed and approved.'
+ );
+ this.actionRemarks = '';
+ }
+
+ rejectStage(a: LoanApplication, field: ApprovalField, approverRole: string): void {
+ decideApproval(
+ a,
+ field,
+ 'REJECTED',
+ this.currentUserName,
+ approverRole,
+ this.actionRemarks.trim() || 'Declined on review.'
+ );
+ this.actionRemarks = '';
+ }
+
+ exceptionApprove(a: LoanApplication): void {
+ decideApproval(
+ a,
+ 'postCheckApproval',
+ 'EXCEPTION_APPROVED',
+ this.currentUserName,
+ 'Post-Disbursement Check',
+ this.actionRemarks.trim() || 'Approved on exception — minor gap noted and accepted.'
+ );
+ this.actionRemarks = '';
+ }
+
+ legalChecklistDone(a: LoanApplication): boolean {
+ return a.feesAndAccountOpened && (!a.secured || a.mortgageCreated) && a.legalStampDone && a.fileFilingDone;
+ }
+
+ committeeReportsReady(a: LoanApplication): boolean {
+ return (
+ a.cicReportUploaded &&
+ a.backgroundCheckDone &&
+ (!a.secured || a.cersaiSearchDone) &&
+ a.aadhaarVerified &&
+ a.panVerified &&
+ a.incomeProofSubmitted &&
+ this.fiComplete(a) &&
+ a.managerMeetingDone
+ );
+ }
+
+ riskChecklistReady(a: LoanApplication): boolean {
+ return (
+ a.addressVerification.status === 'VERIFIED' &&
+ a.incomeProofSubmitted &&
+ a.previousLoanDeclarationDone &&
+ (!a.secured || (a.propertySearchReportDone && a.valuationReportConfirmed)) &&
+ this.fiComplete(a) &&
+ a.backgroundCheckDone
+ );
+ }
+
+ postCheckSignaturesDone(a: LoanApplication): boolean {
+ return (
+ a.signedByAgent && a.signedByManager && a.signedByCommittee && a.signedByRiskApproval && a.signedByLegalApproval
+ );
+ }
+
+ // -------------------- CIC Upload --------------------
+ onCicFileSelected(event: Event, a: LoanApplication): void {
+ const input = event.target as HTMLInputElement;
+ const file = input.files?.[0];
+ if (!file) {
+ return;
+ }
+ uploadCicReport(a, file.name, this.currentUserName);
+ input.value = '';
+ }
+
+ pushFieldInquiryNow(a: LoanApplication): void {
+ pushFieldInquiry(a, 'Field Verification Team', this.currentUserName);
+ }
+
+ onAgentReportFileSelected(event: Event, a: LoanApplication): void {
+ const input = event.target as HTMLInputElement;
+ const file = input.files?.[0];
+ if (!file) {
+ return;
+ }
+ uploadAgentReport(a, file.name, this.currentUserName);
+ input.value = '';
+ }
+
+ // -------------------- Signing / Cheque-ACH / Disbursement --------------------
+ markSigned(a: LoanApplication): void {
+ markCustomerSigned(a, a.applicantName);
+ }
+
+ confirmChequeAndProceed(a: LoanApplication): void {
+ confirmChequeAch(a, this.currentUserName);
+ }
+
+ disburseNow(a: LoanApplication): void {
+ disburseApplication(a, this.currentUserName, this.disbursementMode);
+ }
+
+ // -------------------- EMI Repayment Schedule (once disbursed) --------------------
+ isRevolving(a: LoanApplication): boolean {
+ return isRevolvingProduct(a.productCode);
+ }
+
+ emiAmountFor(a: LoanApplication): number {
+ return calculateEmi(a.requestedAmount, a.interestRate, a.tenureMonths);
+ }
+
+ emiScheduleFor(a: LoanApplication): EmiScheduleRow[] {
+ return buildEmiSchedule(a.requestedAmount, a.interestRate, a.tenureMonths, a.disbursementDate);
+ }
+
+ totalInterestFor(a: LoanApplication): number {
+ return this.emiScheduleFor(a).reduce((s, r) => s + r.interestComponent, 0);
+ }
+
+ totalPayableFor(a: LoanApplication): number {
+ return a.requestedAmount + this.totalInterestFor(a);
+ }
+}
diff --git a/src/app/loan-origination/loan-origination-data.ts b/src/app/loan-origination/loan-origination-data.ts
new file mode 100644
index 000000000..dd5f90e45
--- /dev/null
+++ b/src/app/loan-origination/loan-origination-data.ts
@@ -0,0 +1,1510 @@
+/**
+ * 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/.
+ */
+
+// Dummy Loan Origination / Sanctioning workflow data layer — the pre-disbursement pipeline a
+// real-world cooperative-bank CBS runs every application through: Channel intake → KYC/Document
+// collection → CIC (Credit Information Company) check → Field Investigation → Personal Discussion
+// (discrepancy resolution) → Credit Committee → Risk Approval → Legal Approval → Customer Signing
+// → Cheque/ACH → Post-Disbursement Check → Disbursement. Modelled per RBI's KYC Master Direction
+// (OVD-based identity verification), the mandatory Credit Information Companies check before
+// sanction, and the Fair Practices Code for lenders (sanction letter, loan agreement, transparent
+// terms) — plus the co-operative-bank-specific rule that a borrower must hold membership shares.
+//
+// Everything downstream (progress %, sanctioning authority, CIBIL band, ageing) is *derived* from
+// the stored application fields via pure functions below — never duplicated by hand — and the book
+// is generated with a seeded RNG (not Math.random()) so it stays identical across reloads.
+
+import { BRANCHES, BranchMaster } from '../npa/npa-data';
+
+export { BRANCHES, BranchMaster };
+
+// -------------------------------------------------------------------------
+// Stage model
+// -------------------------------------------------------------------------
+export type OriginationStage =
+ | 'APPLICATION'
+ | 'KYC_DOCUMENTS'
+ | 'CIC_CHECK'
+ | 'FIELD_INVESTIGATION'
+ | 'PERSONAL_DISCUSSION'
+ | 'CREDIT_COMMITTEE'
+ | 'RISK_APPROVAL'
+ | 'LEGAL_APPROVAL'
+ | 'CUSTOMER_SIGNING'
+ | 'CHEQUE_ACH'
+ | 'POST_CHECK'
+ | 'DISBURSED';
+
+export const STAGE_ORDER: OriginationStage[] = [
+ 'APPLICATION',
+ 'KYC_DOCUMENTS',
+ 'CIC_CHECK',
+ 'FIELD_INVESTIGATION',
+ 'PERSONAL_DISCUSSION',
+ 'CREDIT_COMMITTEE',
+ 'RISK_APPROVAL',
+ 'LEGAL_APPROVAL',
+ 'CUSTOMER_SIGNING',
+ 'CHEQUE_ACH',
+ 'POST_CHECK',
+ 'DISBURSED'
+];
+
+export const STAGE_LABELS: Record