feat(loan-origination): persist the sanctioning pipeline to the backend
Replaces the in-memory mock (LOAN_APPLICATIONS array + seeded RNG, reset on every refresh) with real Fineract-backed persistence: a new LoanOriginationService for CRUD plus one API call per stage transition, a loan-fineract-bridge that creates a real loan account (create + approve + disburse) at disbursement for Existing Customers, and file uploads through Fineract's generic document store keyed by a stable checklist slot so "On file"/View controls survive a reload. Also fixes several gaps found while verifying full-cycle persistence: - CIC charge-account selection never saved (missing persist() call) - Co-borrower relationship/monthly income lived in local-only component state instead of on the application, so they reset on refresh - Field Investigation "View Full Details" showed local-session photos only; stored photos are now downloaded and shown as real thumbnails - "Assign Field Officer" was free text; now a dropdown of real CBS loan officers pulled from /staff - site photos and cheque/signature images now open in the same zoomable, pannable lightgallery viewer used elsewhere in the app Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -181,6 +181,9 @@ export interface StageHistoryEntry {
|
||||
}
|
||||
|
||||
export interface LoanApplication {
|
||||
/** Database row id, assigned on save. Uploaded files hang off this (documents are stored against
|
||||
* the application's entity id), so it's absent only on an application not yet persisted. */
|
||||
id?: number;
|
||||
applicationNo: string;
|
||||
applicationDate: string;
|
||||
applicantName: string;
|
||||
@@ -225,6 +228,12 @@ export interface LoanApplication {
|
||||
eligibleLoanAmount: number;
|
||||
coBorrowerRequired: boolean;
|
||||
coBorrowerName: string;
|
||||
/** Officer's voluntary opt-in to add a co-borrower even when `coBorrowerRequired` hasn't fired
|
||||
* on its own -- separate from that flag so a required co-borrower stays flagged even if this
|
||||
* is later unticked. */
|
||||
coBorrowerWanted: boolean;
|
||||
coBorrowerRelationship: string;
|
||||
coBorrowerMonthlyIncome: number;
|
||||
|
||||
// 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
|
||||
@@ -354,6 +363,14 @@ export interface LoanApplication {
|
||||
achConfirmationRef: string;
|
||||
emiActivationConfirmed: boolean;
|
||||
emiActivationDate: string;
|
||||
/** Set only when a real Fineract loan account was actually created at disbursement (Existing
|
||||
* Customer applications only, see loan-fineract-bridge.ts) -- null for every mock application
|
||||
* and for New Customer applications, which disburse mock-only. */
|
||||
fineractLoanId: number | null;
|
||||
fineractLoanAccountNo: string | null;
|
||||
/** Last error from attempting real loan creation, so the officer can see why and retry --
|
||||
* cleared on every fresh attempt, never fabricated as a success. */
|
||||
fineractLoanError: string | null;
|
||||
|
||||
currentStage: OriginationStage;
|
||||
/** The furthest stage this application has ever reached, as a STAGE_ORDER index -- kept
|
||||
@@ -936,6 +953,9 @@ function generateApplications(count: number): LoanApplication[] {
|
||||
eligibleLoanAmount,
|
||||
coBorrowerRequired: eligibleLoanAmount < requestedAmount,
|
||||
coBorrowerName: '—',
|
||||
coBorrowerWanted: false,
|
||||
coBorrowerRelationship: '',
|
||||
coBorrowerMonthlyIncome: 0,
|
||||
cicConsentSigned: false,
|
||||
cicChargeCollected: false,
|
||||
cicChargeMode: null,
|
||||
@@ -1011,6 +1031,9 @@ function generateApplications(count: number): LoanApplication[] {
|
||||
achConfirmationRef: '—',
|
||||
emiActivationConfirmed: false,
|
||||
emiActivationDate: '—',
|
||||
fineractLoanId: null,
|
||||
fineractLoanAccountNo: null,
|
||||
fineractLoanError: null,
|
||||
currentStage: 'APPLICATION',
|
||||
furthestStageIndex: 0,
|
||||
overallStatus: 'IN_PROGRESS',
|
||||
@@ -1388,9 +1411,15 @@ function generateApplications(count: number): LoanApplication[] {
|
||||
return apps;
|
||||
}
|
||||
|
||||
// Sized like a real cooperative bank's active sanctioning pipeline — enough to need filters
|
||||
// and pagination, not so many it stops reading as "an active queue".
|
||||
export const LOAN_APPLICATIONS: LoanApplication[] = generateApplications(140);
|
||||
/** Builds the demo book that seeds an empty pipeline on first use — sized like a real cooperative
|
||||
* bank's active sanctioning queue: enough to need filters and pagination, not so many it stops
|
||||
* reading as "an active queue".
|
||||
*
|
||||
* Called once, to POST to the seed endpoint. Applications are read back from the database after
|
||||
* that, so this is a one-time generator and no longer the live source of truth. */
|
||||
export function generateSeedApplications(count = 140): LoanApplication[] {
|
||||
return generateApplications(count);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Runtime actions — every button in the Application Detail screen calls one of these to mutate
|
||||
@@ -1787,7 +1816,7 @@ export function buildEmiSchedule(
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// New Application intake — the Pipeline screen's "New Application" form builds one of these and
|
||||
// appends it to LOAN_APPLICATIONS, exactly at the APPLICATION stage with everything else pending.
|
||||
// posts it to the database, exactly at the APPLICATION stage with everything else pending.
|
||||
// -------------------------------------------------------------------------
|
||||
export interface NewApplicationInput {
|
||||
channel: Channel;
|
||||
@@ -1839,6 +1868,9 @@ export function createApplication(input: NewApplicationInput, seq: number): Loan
|
||||
eligibleLoanAmount: 0,
|
||||
coBorrowerRequired: false,
|
||||
coBorrowerName: '—',
|
||||
coBorrowerWanted: false,
|
||||
coBorrowerRelationship: '',
|
||||
coBorrowerMonthlyIncome: 0,
|
||||
cicConsentSigned: false,
|
||||
cicChargeCollected: false,
|
||||
cicChargeMode: null,
|
||||
@@ -1914,6 +1946,9 @@ export function createApplication(input: NewApplicationInput, seq: number): Loan
|
||||
achConfirmationRef: '—',
|
||||
emiActivationConfirmed: false,
|
||||
emiActivationDate: '—',
|
||||
fineractLoanId: null,
|
||||
fineractLoanAccountNo: null,
|
||||
fineractLoanError: null,
|
||||
currentStage: 'APPLICATION',
|
||||
furthestStageIndex: 0,
|
||||
overallStatus: 'IN_PROGRESS',
|
||||
|
||||
Reference in New Issue
Block a user